4. Pandas进阶与时间序列分析:时间序列索引、重采样、滚动窗口计算、shift与pct_change操作
做量化回测,说白了就是在跟时间打交道。你想想看,股票价格、成交量、财务数据,哪个不是按时间排列的?Pandas的时间序列功能,就是咱们处理这些数据的利器。我个人习惯把这一章叫做「量化分析的基本功」,因为后面所有的策略逻辑,几乎都建立在这些操作之上。
4.1 时间序列索引:让数据带上时间戳
先说说最基础的东西——时间索引。没有时间索引的数据,就像没有标签的文件夹,你根本不知道哪个数据在前、哪个在后。
核心要点:时间索引让数据有了「时间维度」,你可以按日期切片、按月份筛选、按年份聚合。
创建时间索引其实很简单。我刚开始做量化时,经常从CSV文件里读数据,然后手动设置索引。后来发现,直接用pd.to_datetime()转换一下就行。
import pandas as pd
import numpy as np
# 创建一个时间序列
dates = pd.date_range('2024-01-01', periods=10, freq='D')
data = pd.Series(np.random.randn(10), index=dates)
print(data)
# 按日期切片
print(data['2024-01-03':'2024-01-07'])
嗯,这里要注意:时间索引的切片是包含两端的,跟普通索引不一样。我曾经在这个坑里摔过一次——回测时少算了一天数据,结果策略收益凭空多了5%。后来排查了半天才发现是切片边界的问题。
4.2 重采样:把数据「揉」成你想要的频率
重采样,说白了就是改变数据的时间频率。日线数据转周线、分钟数据转小时线,这些都是重采样的典型应用。
我个人习惯把重采样分成两类:
- 降采样:高频转低频(比如日线转周线)
- 升采样:低频转高频(比如月线转日线,需要插值)
做量化时,降采样用得最多。比如你拿到的是日线数据,但策略是周线级别的,那就得重采样。
# 生成日线数据
dates = pd.date_range('2024-01-01', periods=30, freq='D')
price = pd.Series(np.cumsum(np.random.randn(30)) + 100, index=dates)
# 降采样:日线转周线,取每周最后一个交易日收盘价
weekly_close = price.resample('W').last()
print(weekly_close)
# 也可以计算每周均值
weekly_mean = price.resample('W').mean()
小技巧:重采样时,W代表周,M代表月,Q代表季度。如果你想要「自然周」而不是「日历周」,可以用W-MON指定从周一开始算一周。
我记得有一次做因子分析,需要把日频因子数据对齐到月频收益率上。当时直接用resample('M').last()取了月末值,结果发现因子值和收益率对不上——因为有些月份最后一天不是交易日。后来改用resample('M').apply(lambda x: x.iloc[-1])才搞定。
4.3 滚动窗口计算:滑动的时间窗口
滚动窗口,就是在一个固定大小的窗口里做计算,然后窗口不断向前滑动。均线、波动率、动量指标,这些量化里常用的东西,都离不开滚动窗口。
你想想看,计算20日均线,不就是取最近20天的收盘价求平均吗?这就是滚动窗口的典型应用。
# 计算滚动均值和滚动标准差
window = 5
rolling_mean = price.rolling(window=window).mean()
rolling_std = price.rolling(window=window).std()
# 布林带:均值 ± 2倍标准差
upper_band = rolling_mean + 2 * rolling_std
lower_band = rolling_mean - 2 * rolling_std
避坑指南:滚动窗口计算时,前window-1个数据会是NaN。我曾经在回测框架里忘了处理这个,结果策略在刚开始运行时疯狂开仓,因为NaN被当成了0。嗯,那次的回测结果简直没法看。
滚动窗口还有很多变体:
rolling().apply():自定义函数,比如计算滚动相关系数rolling().corr():滚动相关系数矩阵expanding():扩展窗口,从数据开始一直累积到当前
4.4 shift与pct_change:时间序列的「位移」与「变化」
这两个操作,可以说是量化分析里最常用的两个函数了。一个用来获取历史数据,一个用来计算收益率。
4.4.1 shift:把数据「推」到过去或未来
shift()的作用很简单:把整列数据向上或向下移动。正数表示向下移动(取前一期数据),负数表示向上移动(取后一期数据)。
# 获取前一天的收盘价
price_shifted = price.shift(1)
# 计算当日收益率:(当日价格 - 前一日价格) / 前一日价格
returns = (price - price.shift(1)) / price.shift(1)
为什么需要shift?因为在量化策略里,你只能用「已知」的数据。比如今天收盘后,你只能知道昨天的收益率,不能知道今天的。shift就是用来做这种「时间对齐」的。
关键理解:shift(1)相当于把数据「滞后」一期。在回测中,这能避免「未来函数」——也就是用到了未来数据的问题。
4.4.2 pct_change:一键计算收益率
其实上面的收益率计算,用pct_change()一行就能搞定。这个函数就是专门用来计算百分比变化的。
# 计算日收益率
daily_returns = price.pct_change()
# 计算周收益率(5个交易日)
weekly_returns = price.pct_change(periods=5)
# 计算对数收益率
log_returns = np.log(price / price.shift(1))
我个人习惯用pct_change()计算简单收益率,用np.log()计算对数收益率。简单收益率适合做组合权重计算,对数收益率适合做统计分析(因为对数收益率近似正态分布)。
我曾经在构建因子库时,需要计算过去20天的累计收益率。一开始我写了个循环,后来发现直接用pct_change(20)就搞定了。嗯,Pandas的向量化操作,比循环快了几十倍。
4.5 综合案例:构建一个简单的动量因子
把上面这些知识点串起来,咱们来构建一个最简单的动量因子——过去N天的累计收益率。
# 生成模拟价格数据
dates = pd.date_range('2024-01-01', periods=100, freq='D')
price = pd.Series(np.cumsum(np.random.randn(100)) + 100, index=dates)
# 计算动量因子:过去20天的累计收益率
momentum = price.pct_change(periods=20)
# 计算滚动波动率(作为风险调整)
volatility = price.pct_change().rolling(window=20).std()
# 风险调整后的动量因子
risk_adjusted_momentum = momentum / volatility
# 查看结果
result = pd.DataFrame({
'price': price,
'momentum': momentum,
'volatility': volatility,
'risk_adjusted_momentum': risk_adjusted_momentum
})
print(result.tail(10))
实战经验:动量因子在A股市场有显著的「反转效应」——短期动量(1-3个月)可能有效,但长期动量(6-12个月)往往反转。我建议你在构建因子时,多试试不同的时间窗口。
4.6 本章知识体系
下面这张图,把本章的核心知识点串了起来。你可以把它当作一个「思维导图」,方便回顾。
这张图展示了时间序列分析的四个核心模块。它们之间是递进关系:先有时间索引,才能做重采样;有了重采样后的数据,才能做滚动窗口计算;而shift与pct_change则是计算收益率和构建因子的基础工具。
好了,这一章的内容就到这里。时间序列操作是量化分析的基石,多练练这些基础操作,后面构建策略时会顺手很多。