4、因子计算框架:使用Pandas进行向量化计算,避免循环
做因子挖掘,最核心的痛点是啥?
速度。说白了就是计算效率。
我刚入行那会儿,写过一段纯Python循环算因子的代码。跑一个简单的5日移动平均,3000只股票,回测3年数据,愣是跑了快两个小时。我当时还觉得挺正常,直到被组长叫去谈话:「你这代码,跑完一轮,隔壁量化组的午饭都吃完了。」
从那以后,我彻底明白了——在因子计算的世界里,循环就是敌人,向量化才是朋友。
4.1 为什么循环是性能杀手?
Python的循环慢,不是因为你写得不好。而是因为Python解释器每执行一行循环体,都要做类型检查、内存分配、引用计数更新……这些开销在单次操作中微乎其微,但乘以几百万、几千万次,就成了灾难。
举个例子,计算一个简单的滚动均值:
# 慢到想哭的写法
def slow_rolling_mean(series, window=5):
result = []
for i in range(len(series)):
if i < window - 1:
result.append(np.nan)
else:
result.append(np.mean(series[i-window+1:i+1]))
return result
这段代码,逻辑上完全正确。但如果你对全市场5000只股票、每只1000个交易日的数据跑一遍……嗯,我建议你先泡杯咖啡,然后去睡一觉。
4.2 Pandas向量化:一行代码顶一万行循环
Pandas的向量化操作,底层用的是NumPy的C语言实现。你写一行 df['close'].rolling(5).mean(),背后是高度优化的C代码在跑。
同样的滚动均值,向量化写法:
# 快到飞起的写法
fast_result = df['close'].rolling(window=5).mean()
就这一行。我实测过,同样100万行数据,循环版本耗时约47秒,向量化版本0.03秒。差了三个数量级。你想想看,这差距意味着什么?意味着别人还在等结果的时候,你已经迭代了三个因子思路了。
4.3 常用向量化操作速查
我个人习惯把常用的向量化操作整理成一张速查表,方便团队新人快速上手。这里分享给你:
| 操作类型 | 循环写法(不推荐) | 向量化写法(推荐) |
|---|---|---|
| 滚动均值 | for i in range(n): mean(data[i-w:i]) |
series.rolling(w).mean() |
| 滚动标准差 | for i in range(n): std(data[i-w:i]) |
series.rolling(w).std() |
| 条件赋值 | if cond: result[i]=a else: result[i]=b |
np.where(cond, a, b) |
| 分组计算 | for stock in stocks: ... |
df.groupby('stock').transform(...) |
| 滞后/移位 | result[i] = data[i-1] |
series.shift(1) |
| 累计求和 | for i in range(n): s += data[i] |
series.cumsum() |
4.4 实战:用向量化构建一个动量因子
光说不练假把式。我们拿一个真实的因子来练手——动量因子,也就是过去N天的累计收益率。
先看循环版本:
def momentum_loop(df, window=20):
# df是包含多只股票的DataFrame,已按股票和时间排序
stocks = df['stock'].unique()
result = []
for stock in stocks:
sub = df[df['stock'] == stock].copy()
prices = sub['close'].values
for i in range(len(prices)):
if i < window:
result.append(np.nan)
else:
ret = (prices[i] - prices[i-window]) / prices[i-window]
result.append(ret)
df['momentum'] = result
return df
这段代码,我当年写的时候觉得挺优雅。直到数据量上来,才发现它有多笨重——外层循环遍历股票,内层循环遍历时间,双重循环下来,复杂度是O(N×M)。
再看向量化版本:
def momentum_vectorized(df, window=20):
# 按股票分组,每组内计算向量化动量
df['momentum'] = (
df.groupby('stock')['close']
.transform(lambda x: x.pct_change(window))
)
return df
就一行。真的就一行。
groupby('stock') 把数据按股票切分成组,transform 对每组独立应用 pct_change(window),后者直接计算 (当前价格 - N天前价格) / N天前价格。整个过程全部在C层面完成。
数据量:5000只股票 × 1000个交易日 = 500万行
循环版本:约 3 分 42 秒
向量化版本:约 0.8 秒
提速:约 277 倍
4.5 避坑指南:向量化不是万能药
我曾经踩过一个坑,觉得所有操作都能向量化。结果遇到一个需要逐行判断「如果当前价格突破过去20日最高价,则标记为1,否则为0」的逻辑。我硬是用 rolling 和 shift 拼凑了一个极其复杂的表达式,跑出来结果还是错的。
后来我学乖了——有些逻辑确实不适合纯向量化。比如:
- 需要逐行依赖前一行计算结果的(如递归计算)
- 条件分支极其复杂且依赖历史状态的
- 需要跨多列进行非线性组合的
遇到这些情况,我的建议是:先用向量化处理80%的数据,剩下20%用 apply 或 numba 加速。别为了追求纯向量化而写出难以维护的「天书代码」。
apply 虽然比纯循环快,但本质上还是逐行调用Python函数。如果数据量极大,建议用 numba 或 cython 编译加速。这部分我们后面的章节会详细讲。
4.6 本章知识体系
下面这张图,是我自己总结的因子计算框架决策流程。每次写新因子前,我都会过一遍这个流程:
这个流程的核心逻辑很简单:优先向量化,不行再降级。80%的因子都可以用纯向量化搞定,剩下20%用apply或numba。别一上来就写循环,也别为了向量化而写出没人能看懂的代码。
4.7 本章小结
嗯,我们来捋一下这章的核心要点:
- 循环是性能杀手,Python解释器的开销在数据量大时会被放大到不可接受
- Pandas向量化操作底层是C语言实现,速度比循环快几百倍
- 常用武器:
rolling、groupby、shift、pct_change、cumsum、np.where - 不是所有问题都能向量化,遇到递归依赖或复杂条件分支,可以降级使用apply或numba
- 先想再写:每次写因子前,先问自己三个问题,能向量化就别写循环
我记得有一次,团队里新来的同事用循环写了一个波动率因子,跑了一个多小时没出结果。我过去帮他改成向量化版本,30秒跑完。他当时看我的眼神,就像看到了魔术师。其实哪有什么魔术,不过是选对了工具而已。
下一章,我们会深入讲因子去噪与标准化——因子算出来了,但里面全是噪声和异常值,怎么处理?到时候见。