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])
因子计算并行策略架构图 原始数据(日频 OHLCV) 并行调度器(joblib / Dask Scheduler) 策略 1:任务并行 因子 A → Worker 1 因子 B → Worker 2 因子 C → Worker 3 策略 2:数据并行 资产组 1 → 全因子 资产组 2 → 全因子 资产组 3 → 全因子 策略 3:混合并行 资产组 1 → 因子 A/B 资产组 2 → 因子 A/B 资产组 3 → 因子 C/D 因子值矩阵(date × asset × factor)

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')]
)
因子存储格式选择决策流程 因子数据准备就绪 数据规模是否超过单机内存? 否 → 单机研究为主 推荐:HDF5(PyTables) 是 → 生产/分布式环境 推荐:Parquet(分区存储)

3.4 实战建议:引擎设计清单

在搭建因子计算引擎时,建议按以下清单逐项确认:

  1. 框架选型:初期用 Pandas,核心矩阵运算下沉到 NumPy,数据量超限时引入 Dask。
  2. 并行策略:因子数量多 → 任务并行;资产数量多 → 数据并行;两者都多 → 混合并行。
  3. 存储格式:单机研究用 HDF5(方便元数据管理);生产环境用 Parquet(列裁剪 + 跨语言兼容)。
  4. 增量计算:设计因子计算时考虑增量更新(如只计算最新交易日),避免全量重算。
  5. 错误处理:每个因子计算包裹 try-except,失败时记录日志并返回 NaN,不影响其他因子。
  6. 版本管理:因子计算代码和参数使用 Git 管理,因子值存储时附带计算版本号。
关键指标监控:引擎运行时应记录每个因子的计算耗时、内存峰值、输出行数。当某个因子计算时间超过中位数 5 倍时,触发告警并检查是否存在死循环或数据倾斜。