4. 因子预处理:去极值、标准化、中性化处理,我踩过的坑与经验
因子预处理,说白了就是给原始数据“洗澡”。
你辛辛苦苦挖出来的因子,如果不做这步处理,直接扔进模型里,结果往往惨不忍睹。我刚开始做量化那会儿,就吃过这个亏——因子收益率看着挺高,一上实盘就崩。后来才发现,是几个极端值把整个信号带偏了。
今天咱们就把去极值、标准化、中性化这三个核心步骤掰开揉碎讲清楚。我会把我在项目中踩过的坑,以及后来总结的经验,一并分享给你。
核心逻辑:因子预处理的目标是让数据更“干净”、更“稳定”。去极值处理异常点,标准化统一量纲,中性化剔除冗余信息。三者缺一不可。
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 预处理顺序:先做哪一步?
这个问题我当年纠结了很久。网上各种说法都有。我自己的经验是:
- 先去极值——避免极端值影响后续的均值和标准差计算
- 再标准化——统一量纲
- 最后中性化——因为中性化依赖回归,标准化后的数据回归效果更好
当然,也有人先中性化再标准化。这个顺序不是绝对的。但有一点是确定的:去极值一定要放在最前面。否则一个极端值就能把整个中性化回归模型带偏。
总结一下:因子预处理不是走过场,它是整个多因子策略的基石。去极值、标准化、中性化,每一步都有它的道理。我见过太多人因为预处理没做好,导致后面模型怎么调都不对。所以,别嫌麻烦,把这步做扎实了,后面的路会顺很多。
好了,关于因子预处理,我就讲这么多。这些经验都是我在实盘中一点点踩坑踩出来的。希望你能少走一些弯路。