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 本章知识体系总览

下面这张图,把因子计算引擎的核心脉络画清楚了:

因子计算引擎核心架构 原始行情数据 向量化计算 Pandas/Numpy内置函数 循环计算 for/while(不推荐) GPU加速 CuDF/CuML 滚动窗口计算 分组计算 内存优化 并行计算框架 多进程Pool Dask/Ray GPU显存管理 数据类型转换 API兼容性 因子矩阵(用于后续建模)

说白了,因子计算引擎就三件事:选对计算模式、用好核心操作、必要时上并行或GPU。我做了这么多年,发现80%的性能问题都出在「用循环代替向量化」上。先把这块改掉,你的因子计算速度就能起飞。


公众号:蓝海数据掘金营,微信deep3321