量化人的 pandas 最小技能包
本课目标:掌握量化研究中出现频率最高的一小撮 pandas 操作——读数据、对齐、算收益率、滚动统计、分组统计,以及贯穿始终的矢量化思维。
pandas 功能极多,但量化入门真正高频使用的只有一小部分。这一课不求全面,只求把这个”最小技能包”练到不假思索。
把行情读进 DataFrame
第一步永远是把上一课存好的数据读进来,并把日期设为索引:
import pandas as pd
df = pd.read_parquet("data/600519_hfq.parquet")df["date"] = pd.to_datetime(df["date"])df = df.set_index("date").sort_index()三个动作缺一不可:日期转成 datetime 类型、设为索引、按时间排序。后面所有基于时间的操作(切片、滚动、对齐)都依赖这个前提。设好索引后,df.loc["2024"] 就能直接取出 2024 年的所有数据,df.loc["2024-01":"2024-06"] 取出上半年,这种按时间切片的能力会反复用到。
读入之后养成”先看一眼再动手”的习惯:df.head() 看前几行、df.dtypes 确认每列类型、df.index.is_monotonic_increasing 确认时间已排好序。三行代码十秒钟,能避免后面莫名其妙的错误结果。
多标的对齐:索引是灵魂
研究多只股票时,最容易出错的环节是对齐。正确的姿势是把数据整理成”日期 × 标的”的宽表,例如收盘价表:
# prices: 行是交易日,列是股票代码,值是后复权收盘价prices = pd.concat( {code: read_close(code) for code in codes}, axis=1)concat 按索引自动对齐:某只股票停牌的日期,对应位置自动变成 NaN,而不是把不同日期的数据错位拼在一起。让 pandas 用索引对齐,永远优于自己按行号硬拼——按行号拼接多只股票的数据,只要有一只停过牌,后面的数据就全部错位,而且不会报错。
顺便认识两种数据形态:上面这种”日期 × 标的”的宽表适合做时间序列计算;另一种长表是每行一条记录(日期、代码、字段值),适合做分组统计。两者用 stack / unstack 或 pivot / melt 互相转换,量化研究里两种形态都会频繁用到,遇到别扭的计算时先想想是不是数据形态不对。
pct_change:一行算出收益率
日收益率 = 今天收盘价 / 昨天收盘价 - 1。pandas 一行搞定:
returns = prices.pct_change() # 每列独立计算日收益率cum = (1 + returns).cumprod() - 1 # 累计收益率第二行值得多看一眼:累计收益不能把日收益率直接相加,因为收益是复利叠乘的关系——每天的收益作用在前一天的净值之上。(1 + returns).cumprod() 表达的正是”净值逐日连乘”,这个模式在计算净值曲线时会一遍遍出现。
pct_change 对宽表的每一列独立计算,天然支持多标的。注意第一行必然是 NaN(没有前一天),后续计算要么 dropna,要么确认所用函数能忽略 NaN。
和它配套的另一个高频函数是 shift。prices.shift(1) 把整列数据往后挪一天,于是 prices / prices.shift(1) - 1 与 pct_change() 等价。更重要的用途是错开信号与执行:signal.shift(1) 表示”用昨天的信号决定今天的动作”,这是回测中防止用到未来数据的关键一招,先把这个函数记住。
rolling:均线与滚动波动率
技术指标和风险指标大量依赖滚动窗口:
ma20 = prices.rolling(20).mean() # 20 日均线vol20 = returns.rolling(20).std() # 20 日收益波动率ann_vol = vol20 * (252 ** 0.5) # 粗略年化(约 252 个交易日)rolling(20) 的含义是”过去 20 个交易日(含当天)“。窗口不满 20 天的开头部分结果是 NaN,这是正常的,不要强行填充。除了 mean 和 std,滚动对象上还能接 max、min、sum 等方法——比如 prices.rolling(60).max() 就是”过去 60 日最高价”,突破类策略的核心组件一行就写完了。
这里埋一个伏笔:用当天收盘价算出的均线,只能用于指导明天的交易——信号和执行之间必须隔一天,否则就用了未来数据。这是回测课的重点。
groupby:分组统计
横截面研究离不开分组。比如把长表格式的数据按股票分组、按月份分组:
# long: 长表,列包含 code / date / retby_code = long.groupby("code")["ret"].agg(["mean", "std", "count"])
# 按月统计每个月的平均日收益by_month = long.groupby(long["date"].dt.to_period("M"))["ret"].mean()groupby 的思考模板是三步:按什么分组(split)、对每组做什么计算(apply)、结果如何合并(combine)。以后做因子分层回测——按因子值把股票分成五组、比较各组收益——本质就是一次 groupby。
一个实用细节:agg 里可以同时给出多个统计量,一次算完;如果要对分组结果做”组内排名”或”组内标准化”这类保持原有行数的计算,用 transform 而不是 agg。这两个方法的区别(聚合成一行 vs 逐行返回)值得花十分钟亲手试一遍。
矢量化思维:告别 for 循环
从普通 Python 转向量化研究,最大的思维转变是矢量化。同样是算 20 日均线,两种写法:
# 慢:逐行 for 循环ma = []for i in range(len(close)): ma.append(close[max(0, i - 19): i + 1].mean())
# 快:一次性对整列操作ma = close.rolling(20).mean()矢量化写法不只是快几十倍,更重要的是表达意图清晰、不容易写错边界条件。上面的循环版本就藏着一个容易忽略的问题:开头不足 20 天时它算的是”有多少算多少”的均值,而 rolling 版本诚实地返回 NaN——后者才是研究中想要的行为。
看到 for 循环逐行处理 DataFrame 时,先停下来问自己:这件事能不能用 rolling、shift、pct_change、groupby 的组合表达?入门阶段绝大多数场景答案是能。真正需要逐行状态依赖的逻辑(比如复杂的持仓状态机)再用循环不迟。
最后给一条学习建议:不要试图背 API。把本课的五类操作亲手敲三遍,遇到不会的再查文档,两周后这些函数就会变成肌肉记忆。工具的目标是”够用且熟练”,花哨的技巧留给以后按需补充。
顺带一句,本系列只讲研究方法,不构成投资建议。
本课小结
- 读入数据后先做三件事:日期转类型、设索引、排序。
- 多标的数据靠索引自动对齐,不要按行号硬拼。
pct_change算收益率、rolling做滚动统计、groupby做分组统计,覆盖入门期大部分需求。- 滚动窗口开头的 NaN 是正常现象;信号与执行之间要隔一天,防止使用未来数据。
- 优先矢量化,for 循环留给真正有状态依赖的逻辑。
动手练习:用上一课下载的股票数据,计算日收益率、20 日均线和 20 日年化波动率,然后用 groupby 按年份统计每年的平均日收益率和波动率,输出成一张小表。全程不使用 for 循环。