4、因子构建方法论:从原始数据到因子值的标准化流程

做量化投资这些年,我最大的体会就是:因子构建的成败,往往不在选什么因子,而在数据处理。你想想看,原始数据就像刚从矿里挖出来的矿石,杂质多、分布乱、量纲不统一。直接拿去做回测,结果基本是废的。

这一章,我就带你走一遍因子构建的标准化流程。说白了就三步:去极值 → 标准化 → 中性化。每一步都有坑,我踩过不少,今天一并告诉你。

步骤一 去极值 MAD / 百分位法 步骤二 标准化 Z-score / 排序分 步骤三 中性化 回归残差法 原始数据 → 干净因子值 → 可比截面 → 纯净信号

4.1 去极值:别让异常值毁了你的因子

先说说去极值。为什么非做不可?

我举个例子你就明白了。假设你在算市盈率因子,大部分股票PE在10到40之间。突然来了一只亏损股,PE是-5000。如果你不做处理,这只股票会把整个因子的均值和标准差拉到离谱的位置。后面做标准化、做回归,全都会变形。

我个人习惯用两种方法:

方法一:MAD法(中位数绝对偏差)

这个方法比传统的3σ法稳健得多。为什么?因为均值本身会被极端值影响,而中位数不会。

def winsorize_mad(series, n=5):
    """
    基于MAD的去极值
    n: 阈值倍数,我一般取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又太宽松,极端值没去掉。当然,具体要看你的因子分布——偏态严重的因子,我建议n取小一点。

方法二:百分位法

这个方法更直观。直接切掉头尾1%或5%的数据。

def winsorize_percentile(series, lower=0.01, upper=0.99):
    """
    百分位去极值
    """
    q_low = series.quantile(lower)
    q_high = series.quantile(upper)
    return series.clip(q_low, q_high)
注意:千万不要对截面数据整体做百分位截断!我曾经犯过这个错——把整个面板数据的所有值一起排序截断,结果不同时间点的截断阈值完全不一样,因子在时间序列上出现了跳跃。正确的做法是:每个截面(每个时间点)单独做去极值

4.2 标准化:让因子站在同一起跑线上

去完极值,接下来就是标准化。这一步的目的是让不同量纲的因子可以互相比较。

你想想看,市值因子的数值是几十亿到几千亿,换手率因子是0.1%到10%。如果不标准化,你根本没法把它们放在同一个模型里。

常用的标准化方法有两种:

方法 公式 适用场景 我的评价
Z-score标准化 (x - μ) / σ 因子近似正态分布 最常用,但受极端值影响大
排序分标准化 rank(x) / N 因子分布未知或偏态 稳健,但丢失了幅度信息
def standardize_zscore(series):
    """Z-score标准化"""
    return (series - series.mean()) / series.std()

def standardize_rank(series):
    """排序分标准化,结果在[0,1]之间"""
    return series.rank(pct=True)

我个人更倾向于排序分标准化。为什么?因为金融数据很少有严格正态分布的。你去做个偏度检验就知道了,大部分因子都有明显的偏态。排序分虽然丢失了数值间的距离信息,但换来了稳健性——这在多因子模型里特别重要。

核心原则:标准化一定要在去极值之后做。顺序不能乱。先去掉极端值,再标准化,这样Z-score才不会因为极端值而失真。

4.3 中性化:剥离你不想要的风格暴露

这一步是最容易被忽视的,但恰恰是最关键的。

什么叫中性化?说白了就是:把因子中与某些已知风格(如市值、行业)相关的部分剥离掉

举个例子。你构建了一个"营收增长率"因子。但你发现这个因子和市值高度相关——小市值公司的增长率普遍更高。如果你不做中性化,你的因子其实是在"做多小市值、做空大市值"。这到底是增长因子的效果,还是市值因子的效果?说不清楚。

我常用的方法是回归残差法:

import statsmodels.api as sm

def neutralize_factor(factor, market_cap, industry_dummies):
    """
    对因子做市值和行业中性化
    factor: 待中性化的因子值
    market_cap: 对数市值
    industry_dummies: 行业哑变量矩阵
    """
    X = pd.concat([market_cap, industry_dummies], axis=1)
    X = sm.add_constant(X)
    
    model = sm.OLS(factor, X).fit()
    residual = model.resid
    
    return residual

回归之后,残差就是剥离了市值和行业影响后的"纯净因子"。这个纯净因子才是你真正想要的信号。

避坑指南:我曾经在回测中直接用了原始因子,结果绩效好得吓人。后来一查,发现因子在银行股上暴露特别大,而那段时间银行股正好在涨。去掉行业暴露后,因子收益直接腰斩。所以记住:不做中性化的因子,你永远不知道它赚的是风格的钱还是alpha的钱

4.4 完整的因子处理流水线

把上面三步串起来,就是一个完整的因子处理流程:

def factor_pipeline(factor_series, market_cap, industry_dummies):
    """
    因子处理全流程
    """
    # 第一步:去极值
    cleaned = winsorize_mad(factor_series, n=5)
    
    # 第二步:标准化
    standardized = standardize_rank(cleaned)
    
    # 第三步:中性化
    neutralized = neutralize_factor(standardized, market_cap, industry_dummies)
    
    # 最后再标准化一次(中性化后分布可能变化)
    final_factor = standardize_zscore(neutralized)
    
    return final_factor
一个小技巧:中性化之后,我习惯再做一次标准化。因为回归残差的均值为0,但标准差不一定为1。再标准化一次,可以保证所有因子的尺度统一,方便后续做因子合成或模型输入。

嗯,到这里因子构建的标准化流程就讲完了。总结下来就三句话:去极值保稳健,标准化消量纲,中性化提纯度。每一步都有它的道理,少一步都不行。

我在实际项目中,会把这套流程封装成一个类,每次新因子进来,自动走一遍流水线。这样既保证了处理的一致性,也方便后续的因子监控和归因分析。


专注资料整理