4. 因子预处理:去极值、标准化、中性化处理,我踩过的坑与经验

因子预处理,说白了就是给原始数据“洗澡”。

你辛辛苦苦挖出来的因子,如果不做这步处理,直接扔进模型里,结果往往惨不忍睹。我刚开始做量化那会儿,就吃过这个亏——因子收益率看着挺高,一上实盘就崩。后来才发现,是几个极端值把整个信号带偏了。

今天咱们就把去极值、标准化、中性化这三个核心步骤掰开揉碎讲清楚。我会把我在项目中踩过的坑,以及后来总结的经验,一并分享给你。

核心逻辑:因子预处理的目标是让数据更“干净”、更“稳定”。去极值处理异常点,标准化统一量纲,中性化剔除冗余信息。三者缺一不可。

因子预处理三步走 原始因子 含极端值、量纲不一 去极值 去极值后 剔除异常点 标准化 标准化后 均值为0,标准差为1 中性化处理(可选) 剔除市值、行业等影响 注:中性化通常在标准化之后进行,也可在标准化之前

4.1 去极值:别让一颗老鼠屎坏了一锅粥

什么是极值?就是那些明显偏离正常范围的异常数据。比如某个股票突然涨停,因子值瞬间飙到天上。这种数据如果不处理,会严重干扰后续的标准化和模型训练。

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

方法 原理 适用场景
MAD法 基于中位数和绝对中位差 数据分布不对称时
百分位法 设定上下百分位阈值 简单粗暴,快速处理
标准差法 超过n倍标准差则截断 数据近似正态分布时

我个人习惯用MAD法。为什么?因为它对异常值更鲁棒。中位数本身就不容易被极端值影响,所以MAD法在处理金融数据时特别稳。

我的经验:千万别用均值±3倍标准差去截断。金融数据往往有厚尾特征,3倍标准差会砍掉太多有效信息。我一般用5倍标准差,或者MAD法配合3倍MAD阈值。

import numpy as np
import pandas as pd

def winsorize_mad(factor, n=3):
    """
    MAD法去极值
    factor: Series或1D数组
    n: MAD倍数,默认3
    """
    median = np.median(factor)
    mad = np.median(np.abs(factor - median))
    upper = median + n * mad
    lower = median - n * mad
    return np.clip(factor, lower, upper)

# 示例
factor = pd.Series([1, 2, 3, 4, 5, 100])  # 100是极端值
cleaned = winsorize_mad(factor)
print(cleaned)
# 输出: [1, 2, 3, 4, 5, 8.5]  # 100被截断到8.5左右

注意:去极值不是删除数据,而是截断。把极端值拉到阈值边界上,保留样本数量。我曾经见过有人直接把极端值删掉,结果导致截面数据不完整,后面合并时各种报错。

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

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

最常用的标准化方法是Z-score:

def standardize_zscore(factor):
    """
    Z-score标准化
    输出:均值为0,标准差为1
    """
    mean = np.mean(factor)
    std = np.std(factor)
    return (factor - mean) / std

嗯,这里要注意。Z-score假设数据是正态分布。但金融数据往往不是。所以标准化之后,你最好看一眼分布形状。如果偏态严重,可以考虑用Rank标准化——就是把因子值排序后映射到[0,1]区间。

我踩过的坑:有一次我同时处理了100多个因子,全部用Z-score标准化。结果回测时发现,有几个因子的IC序列波动极大。排查了半天才发现,原来是这些因子本身分布就畸形,标准化后反而放大了噪声。后来我改用Rank标准化,效果稳定多了。

Rank标准化的代码也很简单:

def standardize_rank(factor):
    """
    Rank标准化
    输出:均匀分布在[0,1]区间
    """
    rank = factor.rank()
    return (rank - 1) / (len(rank) - 1)

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

我的建议:中性化之前,一定要把市值取对数。因为市值分布是右偏的,取对数后更接近正态,回归效果更好。另外,行业哑变量记得做one-hot编码,并且去掉一个类别避免多重共线性。

你可能会问:中性化是不是必须的?

不一定。如果你的策略本身就是做多小盘股,那市值暴露反而是你想要的。但如果你追求的是纯粹的alpha,那中性化就是必须的。我个人习惯是:先中性化再选因子,这样选出来的因子更“干净”。

我曾经犯过的错:有一次我中性化之后,发现因子IC大幅下降,差点把整个因子库都废了。后来仔细一查,原来是我把行业哑变量搞错了——有些行业只有一两只股票,回归时这些哑变量几乎解释了所有方差。所以,对于样本量太少的行业,建议合并到“其他”类别里。

4.4 预处理顺序:先做哪一步?

这个问题我当年纠结了很久。网上各种说法都有。我自己的经验是:

  1. 先去极值——避免极端值影响后续的均值和标准差计算
  2. 再标准化——统一量纲
  3. 最后中性化——因为中性化依赖回归,标准化后的数据回归效果更好

当然,也有人先中性化再标准化。这个顺序不是绝对的。但有一点是确定的:去极值一定要放在最前面。否则一个极端值就能把整个中性化回归模型带偏。

总结一下:因子预处理不是走过场,它是整个多因子策略的基石。去极值、标准化、中性化,每一步都有它的道理。我见过太多人因为预处理没做好,导致后面模型怎么调都不对。所以,别嫌麻烦,把这步做扎实了,后面的路会顺很多。

好了,关于因子预处理,我就讲这么多。这些经验都是我在实盘中一点点踩坑踩出来的。希望你能少走一些弯路。


专注资料整理