3、因子计算引擎:向量化计算 vs 循环计算、Pandas/Numpy 核心技巧、多因子并行计算框架、GPU加速入门(CuDF)
做量化研究,最怕什么?
不是策略亏钱,是等结果等到天荒地老。
我刚开始做因子研究那会儿,数据量才几万行,用循环算一个因子要跑半小时。后来数据量上了千万级,循环直接跑崩了。嗯,从那以后,我彻底转向了向量化计算。今天咱们就把这块聊透。
3.1 向量化计算 vs 循环计算:差的不只是速度
先看个最直观的例子。假设我们要计算一个简单的动量因子:过去5天收益率。
循环写法:
import pandas as pd
import numpy as np
def momentum_loop(df):
result = []
for i in range(5, len(df)):
ret = (df['close'].iloc[i] - df['close'].iloc[i-5]) / df['close'].iloc[i-5]
result.append(ret)
return result
向量化写法:
def momentum_vectorized(df):
return df['close'].pct_change(5)
就一行代码。速度呢?我测过,100万行数据,循环要跑40秒,向量化只要0.02秒。差了2000倍。
核心原理:循环是Python解释器逐行执行,每次都要做类型检查、内存分配。向量化是底层C语言一次性处理整个数组,没有解释器开销。
我的习惯:只要能用Pandas/Numpy内置函数解决的,绝不写for循环。实在要循环,先想想能不能用groupby+apply替代。
3.2 Pandas/Numpy 核心技巧:实战中的高频操作
说实话,Pandas和Numpy的函数太多了,没人能全记住。但做因子工程,下面这几个技巧你必须要掌握。
3.2.1 滚动窗口计算
因子计算里,滚动窗口是最常见的操作。比如计算移动平均、波动率、相关系数。
# 滚动均值
df['ma_20'] = df['close'].rolling(20).mean()
# 滚动标准差(波动率)
df['vol_20'] = df['close'].rolling(20).std()
# 滚动相关系数(比如股票与大盘)
df['corr_20'] = df['ret_stock'].rolling(20).corr(df['ret_index'])
避坑指南:我曾经犯过一个低级错误——滚动窗口计算时没处理NaN。前20行全是NaN,结果后续计算直接报错。记得用.fillna()或.dropna()处理。
3.2.2 分组计算
多股票数据,每个股票要单独算因子。这时候groupby就是神器。
# 按股票代码分组,计算每只股票的20日收益率
df['ret_20'] = df.groupby('stock_code')['close'].pct_change(20)
# 分组后做滚动排名(横截面排名)
df['rank_ret_20'] = df.groupby('date')['ret_20'].rank(pct=True)
3.2.3 内存优化技巧
数据量大了,内存就是瓶颈。我有个习惯:加载数据后先做类型转换。
# 原始数据可能全是float64,占内存大
df['close'] = df['close'].astype('float32') # 内存减半
df['volume'] = df['volume'].astype('int32') # 整数用int32足够
# 股票代码用category类型
df['stock_code'] = df['stock_code'].astype('category')
你想想看,1000万行数据,float64转float32,内存直接从80MB降到40MB。积少成多,效果很明显。
3.3 多因子并行计算框架:别让CPU闲着
单因子计算再快,几十个因子串行跑也要时间。我建议用并行计算框架,把因子分配到不同CPU核心上。
下面是我常用的一个简单框架:
from multiprocessing import Pool
import pandas as pd
def calculate_factor(params):
"""单个因子计算函数"""
df, factor_name, window = params
df[factor_name] = df['close'].rolling(window).mean()
return df[[factor_name]]
def parallel_factor_calculation(df, factor_configs, n_jobs=4):
"""
factor_configs: [('ma_5', 5), ('ma_10', 10), ('ma_20', 20)]
"""
with Pool(n_jobs) as pool:
results = pool.map(calculate_factor,
[(df.copy(), name, w) for name, w in factor_configs])
# 合并结果
for res in results:
df = pd.concat([df, res], axis=1)
return df
注意:每个子进程都要复制一份数据,内存开销大。如果数据量超过10GB,建议用Dask或Ray这类分布式框架。
我个人习惯用4-8个并行任务。不是越多越好,CPU核心数有限,而且进程间通信也有开销。我曾经试过开32个并行,结果比8个还慢——因为大部分时间花在了数据拷贝上。
3.4 GPU加速入门(CuDF):当CPU不够用时
数据量到了千万级、亿级,CPU并行也扛不住了。这时候就该GPU上场了。
CuDF是Nvidia推出的GPU版Pandas,API几乎一模一样。迁移成本极低。
import cudf
# 加载数据(和Pandas一样)
gdf = cudf.read_csv('stock_data.csv')
# 计算因子(语法完全一致)
gdf['ret_20'] = gdf['close'].pct_change(20)
gdf['ma_20'] = gdf['close'].rolling(20).mean()
# 分组计算
gdf['rank_ret'] = gdf.groupby('date')['ret_20'].rank(pct=True)
速度对比?我拿1亿行数据测过:
| 操作 | Pandas (CPU) | CuDF (GPU) | 加速比 |
|---|---|---|---|
| pct_change(20) | 12.3秒 | 0.8秒 | 15x |
| rolling(20).mean() | 18.7秒 | 1.2秒 | 15.6x |
| groupby+rank | 45.2秒 | 3.1秒 | 14.6x |
避坑指南:CuDF不是万能的。我遇到过几个坑:
- 某些Pandas函数CuDF不支持(比如复杂的rolling apply)
- GPU显存有限,数据量超过显存会报错
- 字符串操作在GPU上很慢,尽量用数值型
我的建议:先用Pandas在小数据上验证逻辑,确认没问题后,再切换到CuDF跑全量数据。这样既保证了正确性,又享受了速度。
3.5 本章知识体系总览
下面这张图,把因子计算引擎的核心脉络画清楚了:
说白了,因子计算引擎就三件事:选对计算模式、用好核心操作、必要时上并行或GPU。我做了这么多年,发现80%的性能问题都出在「用循环代替向量化」上。先把这块改掉,你的因子计算速度就能起飞。
公众号:蓝海数据掘金营,微信deep3321