股票数据分析这个活儿说大不大说小也不小。很多人第一次接触量化或者行情复盘都是从打开行情软件、手动翻K线开始的翻着翻着就发现几百上千只标的靠眼睛看根本看不过来更别提算均线、算波动率、做回测了。这时候pandas就派上用场了——它不是什么高深的东西本质就是一张会算数的Excel表只不过这张表能一次性处理几十万行数据还能把重复的统计动作固化成几行代码。我这次要聊的就是怎么用pandas把一堆原始行情数据整理成能直接拿来做分析、做复盘、做策略验证的结构化结果。适合两类人看一类是刚学会pandas基础语法、但不知道怎么往真实业务上套的另一类是做投研、运营或者数据岗手里有行情数据却一直用Excel硬扛的。整篇内容围绕数据获取、清洗、指标计算、分组统计、可视化衔接这几个环节展开每一步我都会说清楚为什么这么设计、参数怎么定、坑在哪里。1. 整体设计思路与方案选型1.1 为什么选pandas而不是别的工具先把选型这件事说透。做股票数据分析工具无非几类Excel、数据库SQL、pandas这类编程库、还有专门的量化平台。Excel的问题在于数据量一过十万行就开始卡而且每次更新数据都要手动复制粘贴重复劳动太多SQL擅长存储和聚合但做时间序列的滚动计算、多表对齐、缺失值插补这些操作写起来非常别扭量化平台封装得太好反而让人看不清中间过程出了问题不知道怎么排查。pandas刚好卡在中间它跑在内存里几十万行数据毫无压力它的DataFrame结构天然适合带时间索引的表格数据它的rolling、resample、groupby这些方法几乎就是为行情分析量身定做的。我个人的判断标准很简单凡是需要按时间顺序做滚动计算或者按标的做分组统计的活儿优先用pandas。股票分析恰好两样都占。比如算20日均线本质是按时间滚动求均值比如算每只标的的年化波动率本质是按标的分组做统计。这两类操作在pandas里都是一行链式调用的事换成别的工具要么写循环要么写窗口函数维护成本高不少。1.2 数据管道的整体分层我把整个分析流程拆成四层这个分层不是拍脑袋定的而是照着数据从脏到净、从原始到可用的自然顺序来的层级职责典型操作产出物采集层拿到原始行情读取CSV/接口拉取原始DataFrame清洗层统一格式、处理缺失与异常类型转换、去重、补缺规整的宽表/长表计算层派生指标均线、收益率、波动率带指标的结果表呈现层分组汇总与图表衔接groupby聚合、导出汇总表/图表数据这么分层的好处是每一层都可以单独测试。清洗层出问题不会污染计算逻辑计算层改了公式也不用回头动采集代码。我见过不少人把所有逻辑塞在一个几百行的脚本里改一个指标要通读全文调试成本极高。分层之后每层输入输出都是明确的DataFrame用几个assert就能验证数据形状和取值范围排查效率完全不是一个量级。1.3 长表与宽表的取舍这是新手最容易纠结的点。长表是每行一条记录字段大概是日期、代码、开盘、收盘、成交量宽表是日期做索引每只标的的收盘价占一列。两种形态各有用途长表适合存储和分组计算因为groupby按代码分组非常自然宽表适合做多标的对齐和时间序列运算比如算两只标的的相关性宽表直接两列一算就行长表还得先pivot。我的做法是采集和清洗阶段统一用长表进入计算阶段前根据具体任务决定要不要pivot。算单只标的的均线长表过滤一下就行算多标的收益相关性先pivot成宽表再算。不要一开始就纠结用哪种让数据结构跟着操作走而不是让操作迁就数据结构。2. 数据清洗的核心细节与实操要点2.1 字段类型转换别让字符串毁了计算原始行情数据最常见的坑就是类型不对。从CSV读进来的价格字段经常是字符串成交量可能带单位后缀日期可能是20240105这种整数格式。pandas的read_csv虽然会自动推断类型但推断结果不一定靠谱尤其是价格列里混了空值或者异常符号的时候整列会被推断成object类型后面一做算术就报错。我的习惯是读入时就把dtype显式指定价格用float64成交量用int64代码用str注意不能用int否则000001会变成1前导零丢失。日期列单独处理用pd.to_datetime转换并且显式指定format参数避免不同日期格式混在一起时解析出错。import pandas as pd df pd.read_csv( quotes.csv, dtype{ code: str, open: float64, high: float64, low: float64, close: float64, volume: int64, }, parse_dates[trade_date], )这里有个细节值得说parse_dates比读完再to_datetime效率高因为它在解析阶段就完成了转换省了一次全表遍历。数据量大的时候这个差别很明显几十万行能省下好几秒。注意代码字段一定要用字符串类型。用整数存代码前导零会丢后续按代码匹配、合并的时候会对不上这个坑非常隐蔽排查起来很费时间。2.2 缺失值与停牌的区分处理行情数据里的缺失值分两种处理方式完全不同。一种是停牌某只标的某天没有交易这种缺失是合理的不应该填补填补了反而制造假数据另一种是数据源漏采本该有数据却没有这种需要补或者标记出来。区分方法很直接看当天该标的是否有其他字段有值。如果整行都缺失大概率是停牌或者未上市如果只有个别字段缺失那就是采集问题。我的处理策略是停牌产生的缺失保留为NaN计算指标时用min_periods参数控制最小样本数比如算20日均线时设min_periods10样本不足10个就不出结果避免用少量数据算出误导性的均线。df[ma20] ( df.groupby(code)[close] .transform(lambda s: s.rolling(20, min_periods10).mean()) )这里用groupby加transform是关键。直接对整列做rolling会把不同标的的数据串在一起算第一只标的的最后几行会和第二只标的的开头几行混着算结果完全错误。transform保证滚动计算在每只标的内部独立进行返回的结果还能对齐回原表这是pandas做分组滚动计算的标准写法务必记牢。2.3 异常值识别涨跌幅是最好的筛子价格数据里的异常值最有效的识别手段是日涨跌幅。正常交易的标的单日涨跌幅有明确边界超出边界的基本可以判定为数据错误比如小数点错位、复权处理不当、或者数据源本身有问题。计算涨跌幅用pct_change同样要按标的分组df[ret] ( df.groupby(code)[close] .transform(lambda s: s.pct_change()) )算出涨跌幅后用分位数或者固定阈值筛异常。我一般先看分布用describe看最大最小值如果出现单日涨跌超过合理范围的值就标记出来人工核对。这里不建议直接删除因为有些异常是真实的比如重大事项复牌删了会丢失信息。更好的做法是新增一个标记列把可疑记录标出来后续分析时可以选择性排除。异常类型识别方法处理策略价格小数点错位涨跌幅绝对值异常大标记人工核对后修正或剔除成交量为负直接判断volume 0置为NaN最高价低于最低价high low整行标记为可疑日期重复duplicated检查保留最新一条2.4 复权处理不复权等于白算这一条是股票分析里最容易被忽略、但影响最大的细节。不复权的价格数据遇到分红送股会出现价格跳空算出来的收益率和均线全是错的。比如某标的10送10除权日价格直接腰斩不复权数据会显示单日跌50%但实际上持有人权益没变。处理方式有两种一是直接用数据源提供的复权因子自己算二是直接取已经复权好的数据。我倾向于后者省事且不容易出错。如果只能拿到不复权数据加复权因子计算方式是前复权价格 不复权价格 × 复权因子 / 最新复权因子。这个公式的核心逻辑是把历史价格按比例调整到当前口径保证时间序列连续。提示做任何跨除权日的收益率计算之前先确认数据是否复权。这个检查花不了几秒钟但能避免后面所有结论建立在错误数据上。3. 核心指标计算与实操流程3.1 均线系统滚动窗口的参数逻辑均线是最基础的派生指标但参数怎么定有讲究。常见的5日、10日、20日、60日分别对应一周、两周、一个月、一个季度的交易日数量。选这些数字不是玄学而是跟交易节奏对齐5日对应短线资金的调仓周期20日对应月度级别的趋势判断60日对应中期趋势。计算上rolling的window参数就是窗口大小min_periods控制最小有效样本数。我前面提过设成窗口的一半左右比较合理样本太少均线没有统计意义样本要求太严又会导致前期大量NaN。实际写的时候把多个周期的均线一次性算出来用循环或者字典推导都行windows [5, 10, 20, 60] for w in windows: df[fma{w}] ( df.groupby(code)[close] .transform(lambda s, ww: s.rolling(w, min_periodsmax(2, w // 2)).mean()) )注意lambda里用默认参数ww绑定当前循环变量否则闭包会捕获循环结束后的最终值所有均线算出来都是同一个窗口的结果。这个坑在Python里非常经典我第一次写的时候也踩过排查了半天才发现是闭包问题。3.2 收益率与波动率年化换算的关键系数日收益率算完只是第一步真正有比较价值的是年化波动率。计算逻辑是日收益率的标准差 × 年化系数。年化系数取多少取决于标的的交易天数。A股一年大约242到244个交易日美股大约252个加密货币是365。这个系数不能随便取取错了波动率会系统性偏高或偏低。import numpy as np TRADING_DAYS 242 vol ( df.groupby(code)[ret] .std() .mul(np.sqrt(TRADING_DAYS)) .rename(annual_vol) )为什么是开根号而不是直接乘因为波动率衡量的是收益率的离散程度方差随时间线性增长标准差作为方差的平方根自然按时间的平方根缩放。这是金融时间序列的基本性质理解了这一点年化系数的选择就不会拍脑袋了。除了波动率最大回撤也是常用指标。计算方式是先算累计净值(1 ret).cumprod()再算历史最高净值cummax两者相除减一就是当前回撤取最小值就是最大回撤。这个指标反映的是最坏情况下的亏损幅度比波动率更直观。3.3 分组聚合用groupby做横截面统计单只标的的指标算完下一步是做横截面比较比如每个行业里波动率最高的标的、每个交易日涨幅前10的标的。这类需求用groupby加agg就能搞定关键是想清楚分组键和聚合函数。summary ( df.groupby(code) .agg( avg_ret(ret, mean), vol(ret, std), max_close(close, max), min_close(close, min), days(close, count), ) .reset_index() )agg里用命名聚合的写法新列名(原列名, 函数)比先聚合再改列名清晰得多。聚合完记得reset_index否则code会变成索引后续合并或者导出的时候容易出问题。如果要做多级分组比如按行业和日期两个维度统计groupby传列表就行。这时候结果会是多级索引用reset_index展开或者用unstack转成宽表看后续怎么用。3.4 时间重采样从日频到周频月频有时候日频数据太细需要聚合成周频或者月频看趋势。pandas的resample就是干这个的前提是索引必须是DatetimeIndex。用法是先设时间索引再按频率重采样df df.set_index(trade_date).sort_index() weekly ( df.groupby(code) .resample(W) .agg({open: first, high: max, low: min, close: last, volume: sum}) .reset_index() )重采样的聚合规则要跟字段语义匹配开盘价取第一个收盘价取最后一个最高最低取极值成交量求和。这个规则搞错了聚合出来的K线就是错的。另外resample的频率字符串要写对W是周M是月Q是季新版pandas对M有弃用提示建议用ME表示月末。注意resample之前一定要先sort_index。数据没按时间排序的话重采样结果会错乱而且不会报错属于静默错误非常危险。4. 常见问题排查与避坑实录4.1 SettingWithCopyWarning赋值警告的根源这个警告几乎每个pandas用户都遇到过。根源是链式索引先切片再赋值pandas分不清你是在操作原表还是副本。比如df[df[code]X][ma20] ...这种写法就会触发警告而且赋值可能根本没生效。正确做法是用.loc一次性完成行列定位df.loc[df[code] X, ma20] df.loc[df[code] X, close].rolling(20).mean()或者更稳妥的方式先算出完整的结果列再整体赋值避免在切片上做原地修改。我的经验是只要看到这个警告就说明写法有问题不要用copy()糊弄过去copy()只是消除警告逻辑上的链式索引问题还在。4.2 分组滚动计算的性能问题数据量大的时候groupby加transform加lambda的组合会明显变慢因为lambda是逐组调用的Python函数没法向量化。几十万行、几百只标的的时候可能要跑好几秒甚至十几秒。优化思路有两个一是能用内置方法就不用lambda比如pct_change、diff这些都有分组版本二是数据量特别大时考虑用numpy手写滚动计算或者把数据按标的分片并行处理。我实测下来几百只标的、几年日频数据这个量级pandas原生写法完全够用没必要过早优化。真正需要优化的是那种几千只标的、十几年分钟频数据的场景那时候才需要考虑换工具。问题现象可能原因排查方向均线结果全为NaN分组键错误或min_periods过大检查groupby列名、窗口参数收益率出现极端值未复权导致除权跳空确认复权状态重采样结果错乱索引未排序检查sort_index内存占用暴涨宽表列数过多或object类型检查dtype、考虑用category4.3 内存占用宽表的隐形代价宽表用起来方便但列数一多内存占用会飙升。几千只标的的收盘价宽表如果全是float64内存占用是行数×列数×8字节。十万行、三千列就是2.4GB再加上中间计算产生的临时对象很容易把内存吃满。控制内存的手段价格用float32代替float64精度足够内存减半代码列用category类型重复值多的时候压缩效果明显及时用del删除不用的中间变量。这些手段单独看收益不大组合起来能省一半以上内存。df[code] df[code].astype(category) df[close] df[close].astype(float32)4.4 实操心得先小样本验证再全量跑这一条是我踩坑踩出来的。刚上手的时候喜欢直接拿全量数据跑结果一个公式写错等了半天才出结果一看全是错的又得重跑。后来养成习惯先用一只标的、几十行数据跑通逻辑确认指标数值合理再上全量。小样本验证花不了一分钟但能省下大量等待和排查时间。验证的时候重点看几个东西均线在数据开头是不是NaN应该是收益率第一行是不是NaN应该是波动率数量级对不对日频年化一般在0.2到0.5之间超出这个范围大概率有问题。这几个检查点过一遍基本能排除大部分低级错误。5. 从计算结果到可视化衔接5.1 导出结构化结果供图表使用pandas算完的结果最终要喂给图表库或者报表系统。导出的时候注意两点索引要重置否则导出CSV会多一列无名索引列名要语义化ma20比col_5清晰得多后续对接图表配置的时候不容易搞混。result summary.sort_values(vol, ascendingFalse).reset_index(dropTrue) result.to_csv(summary.csv, indexFalse, encodingutf-8-sig)encodingutf-8-sig是为了兼容部分表格软件打开中文列名时的编码问题这个细节在实际交付时很实用。5.2 指标结果的合理性校验导出前做一轮合理性校验能挡住大部分数据问题。校验点包括波动率是否在合理区间、最大回撤是否小于等于0、均线是否在价格的最大最小值之间、交易日数量是否与时间跨度匹配。这些检查用几行assert就能写完但能避免把错误结果交付出去。assert (summary[vol] 0).all(), 波动率出现负值 assert (summary[days] 0).all(), 存在无交易记录的标的我个人的习惯是任何要交付或者要拿来做决策的数据导出前必须过一遍校验。校验不过就停下来查原因不要带着疑问往下走。这个习惯看起来保守但实际帮我挡掉过好几次数据源异常导致的问题。5.3 后续扩展方向这套流程跑通之后往上扩展的空间很大。比如把单标的指标扩展成多因子打分用rank做横截面排序比如把日频重采样成周频做趋势跟踪的信号验证比如把计算结果接到回测框架里做策略验证。核心的清洗和计算逻辑不用动只需要在计算层加新的派生列在呈现层加新的聚合维度。我在实际使用中的体会是pandas做股票分析难点从来不在语法而在对数据本身的理解——知道什么数据该清洗、什么缺失该保留、什么指标该怎么算才符合业务逻辑。语法查文档十分钟能学会数据敏感度得靠一个个项目磨出来。所以别急着堆代码先把手里这份数据的来龙去脉搞清楚后面的计算自然水到渠成。