4. 因子计算与实现:因子计算框架设计、向量化计算、因子去极值、标准化

好,咱们进入实战环节。

前面聊了那么多因子的逻辑和分类,现在终于要动手了。因子计算这块,说白了就是两件事:算得对算得快。我见过太多人,因子逻辑没问题,但代码写得一塌糊涂,跑一次要几个小时,回测一次要等一宿。嗯,这其实不是能力问题,是框架没搭好。

4.1 因子计算框架设计

我个人习惯,先把架子搭起来。别一上来就写循环,先想清楚数据怎么流。

一个成熟的因子计算框架,至少要有三层:

  • 数据层:负责清洗、对齐、去极值、填充缺失值。这是地基。
  • 计算层:负责具体的因子逻辑。比如动量、波动率、估值类因子。
  • 输出层:负责标准化、中性化、合并、存储。

为什么要分层?我在项目中遇到过,一个因子跑出来全是NaN,查了半天发现是原始数据里有除零错误。如果数据层统一处理了,这种问题根本不会出现。

核心原则: 数据层只做数据的事,计算层只做计算的事。别混在一起。

举个例子,一个简单的动量因子,框架设计大概是这样的:

# 伪代码示意
class FactorEngine:
    def __init__(self, data):
        self.raw_data = data  # 数据层
        self.clean_data = None
        self.factors = {}     # 计算层
        self.result = None    # 输出层

    def data_preprocess(self):
        # 去极值、填充、对齐
        pass

    def calc_momentum(self, window=20):
        # 计算动量因子
        pass

    def standardize(self):
        # 标准化
        pass

你想想看,如果以后要加一个新因子,只需要在计算层加一个方法,数据层和输出层完全不用动。这就是框架的价值。

4.2 向量化计算

这是最容易被忽视的地方。

很多新手写因子,喜欢用 for 循环,一行一行算。对于1000只股票、500个交易日的数据,循环一次可能要几分钟。而向量化计算,同样的逻辑,几毫秒就搞定了。

为什么会这样?因为Python的循环是解释执行的,而NumPy和Pandas的向量化操作底层是C语言,速度差了几百倍。

我刚开始做量化时,写过一个波动率因子,用循环跑了40分钟。后来改成向量化,3秒出结果。嗯,从那以后我再也没写过因子循环。

避坑指南: 我曾经在计算多因子协方差矩阵时,用了嵌套循环,结果跑了一夜没出结果。后来改成 df.corr() 向量化版本,10秒搞定。记住:能用矩阵运算,就别写循环。

来看一个具体的例子。计算过去20天的收益率标准差(波动率因子):

import pandas as pd
import numpy as np

# 假设 prices 是 DataFrame,index=日期,columns=股票代码
# ❌ 错误做法:循环
def calc_vol_loop(prices, window=20):
    vol = pd.DataFrame(index=prices.index, columns=prices.columns)
    for col in prices.columns:
        for i in range(window, len(prices)):
            vol.iloc[i, col] = prices.iloc[i-window:i, col].pct_change().std()
    return vol

# ✅ 正确做法:向量化
def calc_vol_vectorized(prices, window=20):
    returns = prices.pct_change()
    vol = returns.rolling(window).std()
    return vol

你看,向量化版本就一行代码。而且Pandas的 rolling 函数内部做了大量优化,比手写循环快得多。

4.3 因子去极值

因子计算出来之后,第一件事不是标准化,而是去极值。

为什么?因为金融数据里经常有极端值。比如某只股票一天涨了50倍(停牌后复牌),这个数据如果不处理,会严重扭曲后续的标准化和模型训练。

常用的去极值方法有三种:

方法 原理 适用场景
MAD法 基于中位数绝对偏差 数据分布不对称时
百分位法 截断上下1%/5%分位数 简单粗暴,快速过滤
标准差法 超过均值±3σ的替换为边界值 数据近似正态分布时

我个人最常用的是MAD法。为什么?因为中位数对极端值不敏感,比均值更稳健。我在项目中遇到过,用标准差法去极值,结果因为一个极端值把均值拉偏了,导致去完极值后数据反而更奇怪了。

注意: 去极值不是删除数据,而是把极端值替换成边界值。千万别直接删行,否则时间序列会断掉。

MAD法的实现代码:

def winsorize_mad(series, n=5):
    """
    基于MAD的去极值
    n: 阈值,通常取3-5
    """
    median = series.median()
    mad = (series - median).abs().median()
    upper = median + n * mad
    lower = median - n * mad
    return series.clip(lower, upper)

嗯,这里要注意:MAD法里的 n 值怎么选?我一般取5。取3的话太严格,会把很多正常值也截掉。取7又太宽松,极端值去不干净。5是个比较折中的值。

4.4 标准化

去完极值,下一步就是标准化。

标准化的目的,是把不同量纲的因子拉到同一个尺度上。比如市盈率可能是几十倍,换手率可能是百分之几,如果不标准化,模型会天然偏向数值大的因子。

常用的标准化方法:

  • Z-score标准化:减去均值,除以标准差。结果服从均值为0、标准差为1的分布。
  • Min-Max标准化:缩放到[0,1]区间。适合有明确上下界的因子。
  • Rank标准化:把数值转为排序百分位。对极端值最鲁棒。

我建议,大部分情况下用Z-score。为什么?因为它保留了数据的分布形状。Min-Max会把数据压扁,Rank会丢失数值间的距离信息。

但有一个例外:如果因子里有很多异常值(即使去完极值后),用Rank更安全。我在做小市值因子时,就吃过这个亏——Z-score标准化后,模型几乎只关注了那几只极端小市值的股票,回测曲线漂亮,实盘一塌糊涂。

经验之谈: 标准化之前,一定要先检查因子的分布。画个直方图看看,如果是长尾分布,优先用Rank标准化。

Z-score标准化的实现:

def standardize_zscore(factor):
    """
    Z-score标准化
    注意:要按截面(同一时间点)做,不能跨时间
    """
    mean = factor.mean()
    std = factor.std()
    return (factor - mean) / std

这里有个细节:标准化一定要按截面做。什么意思?就是每个交易日,对所有股票的因子值做一次标准化。不能把过去一年的数据拉在一起算均值和标准差,因为市场环境在变,不同时期的因子分布是不一样的。

我曾经犯过这个错,把全历史数据一起标准化,结果因子值在牛熊转换时完全失真。嗯,后来改成逐日截面标准化,问题就解决了。

4.5 整体流程总结

来,我们把整个流程串起来,画一张图:

因子计算与处理流程 原始数据 行情/财务/另类数据 数据预处理 对齐 · 填充 · 去极值 因子计算 向量化 · 滚动窗口 去极值 MAD / 百分位 / 标准差 标准化 Z-score / Rank / Min-Max 标准化因子 数据流方向

你看,整个流程是线性的:原始数据 → 预处理 → 计算因子 → 去极值 → 标准化。每一步都有明确的目标,不能跳步,也不能乱序。

最后说一句:因子计算不是一次性的工作。你可能会反复调整因子逻辑、去极值参数、标准化方法。所以框架设计一定要灵活,方便你快速迭代。我现在的习惯是,每个因子都写成一个独立的函数,参数全部暴露出来,方便调参。

嗯,这一章就到这里。记住:框架先行,向量化提速,去极值保稳健,标准化统一尺度。把这四步做好,你的因子计算就稳了。

专注资料整理