4、因子计算与标准化:因子计算公式、去极值、标准化(Z-score)、中性化处理
好,咱们进入正题。因子算出来了,原始数据摆在那,能直接用吗?不能。我早年刚入行时,就吃过这个亏——把原始因子值直接扔进模型,结果回测曲线漂亮得不像话,实盘一跑就崩。后来才明白,原始因子里藏着大量噪声和偏差,必须经过一套标准化的“洗菜”流程。
说白了,因子标准化就是给数据“洗澡”。洗掉脏东西(极值),统一度量衡(Z-score),再剔除不该有的偏见(中性化)。这一套做完,因子才能用。
4.1 因子计算公式:先搞清楚你在算什么
因子计算本身不复杂,但容易出错。我见过最离谱的,是把市盈率的倒数算成了市净率。嗯,这里要注意,因子公式一定要和业务逻辑对应上。
常见的因子公式就那么几类:
- 估值类:比如 EP( earnings yield )= 1 / PE。为什么用 EP 而不是 PE?因为 PE 的分布太偏了,EP 更接近正态。
- 动量类:比如过去 12 个月收益率(剔除最近 1 个月)。这个公式里有个坑——要不要复权?我个人习惯用后复权价格,不然分红会扭曲动量信号。
- 质量类:比如 ROE = 净利润 / 净资产。注意分母用期初还是期末?我建议用期初,不然新上市的公司数据会出问题。
举个例子,一个简单的动量因子计算:
# 假设 df 是日频价格数据,包含复权收盘价
df['momentum'] = df['close'].pct_change(252) # 年化收益率
# 剔除最近 20 个交易日,避免短期反转干扰
df['momentum_clean'] = df['close'].pct_change(20, 252) # 这个写法不对
# 正确做法:用 shift 偏移
df['momentum'] = df['close'].pct_change(252) - df['close'].pct_change(20)
你看,公式写错一个参数,结果就完全不一样。我在项目中遇到过,有人把 shift 和 pct_change 的周期搞反了,回测出来动量因子居然是负收益。排查了两天才找到原因。
4.2 去极值:别让异常值毁了你的因子
因子数据里经常有极端值。比如某只股票因为停牌,复牌后一天涨了 500%,动量因子直接爆表。这种极值如果不处理,Z-score 标准化时会把正常数据压成“一坨”,你想想看,那还有啥区分度?
去极值的方法,我常用的有三种:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| MAD 法 | 基于中位数绝对偏差,稳健性强 | 因子分布不对称时首选 |
| 百分位法 | 直接截断 1% 和 99% 分位数 | 简单粗暴,适合快速验证 |
| 标准差法 | 超过均值 ±3σ 的截断 | 因子近似正态分布时可用 |
我个人最常用的是 MAD 法。为什么?因为中位数不受极值影响,比均值稳健得多。我曾经用标准差法处理一个偏态分布的因子,结果截断后数据反而更奇怪了。
def winsorize_mad(series, n=5):
"""
基于 MAD 的去极值
n=5 表示超过 5 倍 MAD 的视为极值
"""
median = series.median()
mad = (series - median).abs().median()
upper = median + n * mad
lower = median - n * mad
return series.clip(lower, upper)
4.3 标准化(Z-score):让因子站在同一起跑线
去完极值,接下来就是标准化。Z-score 是最常用的方法:
Z = (X - μ) / σ
标准化之后,因子均值为 0,标准差为 1。这样不同量纲的因子才能放在一起比较。比如市盈率倒数(EP)和动量因子,一个在 0.01 量级,一个在 0.5 量级,不标准化的话,动量因子会主导模型。
但这里有个细节:均值和标准差用哪个截面算?
- 截面标准化:每个时间点,用当期所有股票的均值和标准差。这是最常用的,能保证每个时间截面上因子分布一致。
- 时序标准化:每只股票,用其历史均值和标准差。这个用得少,因为会引入未来信息(除非你严格用滚动窗口)。
我建议用截面标准化。原因很简单——我们做的是截面选股,不是时序预测。每个时间点上,我们希望因子能区分出哪些股票好、哪些差。
def zscore_cross_sectional(series):
"""
截面 Z-score 标准化
注意:已经去极值后再做
"""
mean = series.mean()
std = series.std()
return (series - mean) / std
4.4 中性化处理:剔除你不想要的偏见
标准化之后,因子就干净了吗?不一定。你想想看,市值大的股票是不是天然 ROE 更高?行业不同,市盈率是不是天然有差异?这些“偏见”如果不剔除,因子选出来的股票可能只是大市值或某个行业的,而不是真正有 alpha 的。
中性化处理,就是把这些偏见剔除掉。最常用的方法是回归残差法:
- 把因子值作为因变量,需要中性化的变量(如市值、行业)作为自变量。
- 做线性回归,得到残差。
- 残差就是中性化后的因子值。
举个例子,对动量因子做市值和行业中性化:
import statsmodels.api as sm
def neutralize_factor(factor, market_cap, industry_dummies):
"""
因子中性化
factor: 原始因子值(已标准化)
market_cap: 对数市值
industry_dummies: 行业哑变量(one-hot 编码)
"""
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
做完中性化之后,因子值和市值、行业的相关性应该接近于 0。你可以用相关系数矩阵验证一下。
4.5 完整流程:一张图说清楚
好了,上面讲了四个步骤:计算、去极值、标准化、中性化。它们不是孤立的,而是一条流水线。我画了一张流程图,帮你理清逻辑:
你看,这个流程是串行的。先算因子,再去极值,然后标准化,最后中性化。顺序不能乱。我见过有人先标准化再去极值,结果极值被标准化后“隐藏”在 ±3 以内,根本发现不了。
4.6 实战中的几个坑
最后,分享几个我踩过的坑,希望能帮你省点时间:
- 坑一:中性化时忘记加截距项。不加截距,残差的均值不为 0,会导致因子整体偏移。我有一回回测收益异常高,查了半天发现是截距项漏了。
- 坑二:去极值和标准化顺序搞反。先标准化再去极值,极值会被“压缩”到正常范围内,但实际上它还是异常值。正确的做法是先截断,再标准化。
- 坑三:中性化变量选择过多。有些人把 20 个行业、市值、换手率、波动率全加进去,结果因子残差几乎全是噪声。记住,中性化是“剔除偏见”,不是“剔除所有信息”。
好了,这一章的内容就到这。下一章我们会聊因子组合的构建方法,到时候这些标准化后的因子就能派上用场了。
公众号:蓝海资料掘金营,微信deep3321