3. 因子计算引擎设计:因子计算框架、并行策略与存储格式
因子计算引擎是因子库的“生产车间”。一个设计良好的引擎需要解决三个核心问题:用什么工具计算(框架选型)、如何加速计算(并行策略)、计算完存在哪里(存储格式)。本章从实战角度拆解这三部分,给出可直接落地的方案。
3.1 因子计算框架选型:Pandas / NumPy / Dask
不同数据规模和计算复杂度对应不同的框架选择。下表从核心维度对比三种主流框架:
| 维度 | Pandas | NumPy | Dask |
|---|---|---|---|
| 适用数据量 | 单机内存可容纳(通常 < 10GB) | 单机内存可容纳(纯数值,通常 < 20GB) | 超内存 / 分布式(TB 级) |
| 计算粒度 | DataFrame 级(行/列/分组) | 数组级(矩阵运算) | 分区级(分块并行) |
| 典型因子场景 | 横截面因子、时间序列因子、分组排序 | 矩阵类因子(协方差、特征值、滚动窗口) | 全市场高频因子、多资产并行计算 |
| 学习成本 | 低 | 中 | 中高 |
| 与量化生态兼容性 | 极高(Alphalens、pyfolio 原生支持) | 高(作为底层数组) | 中(需适配) |
实战建议:初期因子库(100~500 个因子,全市场 3000~5000 只股票,5 年日频数据)使用 Pandas 完全足够。当单因子计算涉及大量矩阵运算(如协方差矩阵类因子)时,将核心计算下沉到 NumPy 可提升 5~10 倍速度。当数据量超过单机内存或需要分布式计算时,再引入 Dask。
以下是一个典型的 Pandas + NumPy 混合计算示例:
import pandas as pd
import numpy as np
# 假设 df 为多级索引 DataFrame: (date, asset) 列包含 close, volume, returns
def calc_momentum_factor(df: pd.DataFrame, window: int = 20) -> pd.Series:
"""计算动量因子:过去 window 日收益率,使用 NumPy 加速滚动计算"""
# 按资产分组,提取收益率序列
returns = df['returns'].values # 转为 NumPy 数组加速
# 使用 NumPy 的 stride_tricks 实现高效滚动窗口
shape = (returns.shape[0] - window + 1, window)
strides = (returns.strides[0], returns.strides[0])
rolling_returns = np.lib.stride_tricks.as_strided(returns, shape=shape, strides=strides)
# 计算累积收益率(动量因子值)
momentum = np.prod(1 + rolling_returns, axis=1) - 1
# 对齐回原 DataFrame 索引
result = pd.Series(momentum, index=df.index[window-1:])
return result
3.2 并行计算策略
因子计算天然适合并行:不同因子之间无依赖、不同资产之间可独立计算、不同时间窗口可拆分。常见的并行策略有三种:
- 任务并行(因子级):每个因子作为一个独立任务,分配到不同 CPU 核心或机器上计算。适合因子数量多、单因子计算量适中的场景。
- 数据并行(资产级):将全市场资产拆分为多个分组(如行业分组),每组独立计算所有因子。适合单因子计算量较大、资产数量多的场景。
- 混合并行:先按资产分组,组内再按因子并行。适合大规模因子库(1000+ 因子 × 5000+ 资产)。
工程实现要点:
- 使用
joblib.Parallel+backend='loky'实现多进程并行,避免 GIL 限制。 - 设置
prefer='threads'当计算以 NumPy 运算为主(释放 GIL)时使用线程并行,减少内存开销。 - 使用
dask.delayed或dask.dataframe实现分布式并行,适合集群环境。 - 注意内存管理:并行任务数 × 单任务内存 < 可用内存,否则触发 OOM。
下面是一个使用 joblib 实现因子级并行的典型代码:
from joblib import Parallel, delayed
import pandas as pd
def compute_single_factor(df: pd.DataFrame, factor_name: str, params: dict) -> pd.Series:
"""计算单个因子(示例)"""
if factor_name == 'momentum':
return calc_momentum_factor(df, window=params.get('window', 20))
elif factor_name == 'volatility':
return df['returns'].rolling(params.get('window', 20)).std()
# ... 其他因子
return pd.Series(dtype=float)
# 因子配置列表
factor_configs = [
('momentum', {'window': 20}),
('momentum', {'window': 60}),
('volatility', {'window': 20}),
('volatility', {'window': 60}),
# ... 共 200 个因子配置
]
# 并行计算所有因子
results = Parallel(n_jobs=8, verbose=10)(
delayed(compute_single_factor)(df, name, params)
for name, params in factor_configs
)
# 合并为因子 DataFrame
factor_df = pd.concat(results, axis=1, keys=[name for name, _ in factor_configs])
3.3 因子存储格式:HDF5 vs Parquet
因子计算完成后,存储格式直接影响后续读取速度、磁盘占用和生态兼容性。HDF5 和 Parquet 是量化领域最主流的两种列式存储格式。
| 对比维度 | HDF5(通过 PyTables) | Parquet |
|---|---|---|
| 存储结构 | 分层组(Group)+ 数据集(Dataset),类似文件系统 | 列式 + 行组(Row Group),自描述 schema |
| 压缩效率 | 中等(支持 zlib/lzo/bzip2) | 高(默认 snappy,支持 zstd/gzip/brotli) |
| 读取速度(全列) | 快(连续存储) | 快(列式裁剪) |
| 读取速度(子集列) | 慢(需读取整个数据集) | 极快(只读取需要的列) |
| 元数据管理 | 内置(属性表、索引) | 依赖外部 schema 或目录结构 |
| 生态兼容性 | Python 生态好,跨语言较差 | 跨语言(Python/Java/R/Spark 原生支持) |
| 典型量化场景 | 单机因子研究、IC 分析、回测数据缓存 | 生产环境因子库、数据管道、分布式系统 |
避坑指南:
- HDF5 单文件写入不支持并发(多进程同时写入会损坏文件),必须使用进程锁或单进程写入。
- Parquet 写入时建议设置
row_group_size为 100000~500000 行,平衡压缩率和读取性能。 - 因子数据通常按 (date, asset) 排序后存储,Parquet 的排序属性(sorting columns)可加速范围查询。
- 不要将 HDF5 用于超过 50GB 的单文件,此时 Parquet 的分区存储(按日期/行业分区)优势明显。
以下是两种格式的读写代码示例:
import pandas as pd
import numpy as np
# 生成示例因子数据
dates = pd.date_range('2020-01-01', '2024-12-31', freq='D')
assets = [f'A{i:04d}' for i in range(3000)]
index = pd.MultiIndex.from_product([dates, assets], names=['date', 'asset'])
factor_data = pd.DataFrame({
'momentum_20d': np.random.randn(len(index)),
'volatility_20d': np.random.randn(len(index)),
'size': np.random.randn(len(index)),
}, index=index).reset_index()
# ---- HDF5 存储 ----
with pd.HDFStore('factors.h5', mode='w', complevel=5, complib='zlib') as store:
store.put('factors', factor_data, format='table', data_columns=True, index=False)
# 读取指定日期范围
with pd.HDFStore('factors.h5', mode='r') as store:
subset = store.select('factors', where='date >= "2024-01-01" & date < "2024-02-01"')
# ---- Parquet 存储 ----
factor_data.to_parquet('factors.parquet', compression='zstd', row_group_size=250000)
# 读取指定列和日期范围(列裁剪 + 谓词下推)
subset = pd.read_parquet(
'factors.parquet',
columns=['date', 'asset', 'momentum_20d'],
filters=[('date', '>=', '2024-01-01'), ('date', '<', '2024-02-01')]
)
3.4 实战建议:引擎设计清单
在搭建因子计算引擎时,建议按以下清单逐项确认:
- 框架选型:初期用 Pandas,核心矩阵运算下沉到 NumPy,数据量超限时引入 Dask。
- 并行策略:因子数量多 → 任务并行;资产数量多 → 数据并行;两者都多 → 混合并行。
- 存储格式:单机研究用 HDF5(方便元数据管理);生产环境用 Parquet(列裁剪 + 跨语言兼容)。
- 增量计算:设计因子计算时考虑增量更新(如只计算最新交易日),避免全量重算。
- 错误处理:每个因子计算包裹 try-except,失败时记录日志并返回 NaN,不影响其他因子。
- 版本管理:因子计算代码和参数使用 Git 管理,因子值存储时附带计算版本号。
关键指标监控:引擎运行时应记录每个因子的计算耗时、内存峰值、输出行数。当某个因子计算时间超过中位数 5 倍时,触发告警并检查是否存在死循环或数据倾斜。