第四节:因子预处理——去极值、标准化、中性化
各位好,我是老张。今天咱们聊聊因子预处理。
说实话,很多做量化的人容易忽略这一步。他们觉得,因子算出来,直接扔进模型就完事了。我刚开始也是这么干的,结果呢?回测曲线漂亮得不行,一到实盘就崩。后来我才明白,问题出在因子预处理上。
因子预处理,说白了就是给原始因子数据「洗个澡」。把脏东西洗掉,把数据弄整齐,让模型能好好干活。今天咱们重点讲三个步骤:去极值、标准化、中性化。
核心观点:因子预处理不是可选项,而是必选项。不做预处理的因子,就像没洗过的菜,你敢直接下锅吗?
一、去极值:把「妖怪」揪出来
什么叫极值?就是那些离谱的数据点。比如某个股票的市盈率突然变成10000倍,或者某个因子值比平均值高出几十个标准差。这些数据点,说白了就是「妖怪」。
我在项目中遇到过这样的情况:一个因子本来选股效果不错,但因为某只小盘股的数据异常,导致整个因子排名被带偏。后来一查,是数据源的问题。嗯,从那以后,去极值就成了我的标准流程。
常用的去极值方法有三种:
- MAD法(中位数绝对偏差法):我个人最推荐的方法。它用中位数代替均值,对异常值更鲁棒。
- 百分位法:直接砍掉上下1%或5%的数据。简单粗暴,但有效。
- 标准差法:超过3倍标准差的数据被截断。这个方法对正态分布的数据效果好,但金融数据往往不是正态的。
给你看个代码示例,我平时用的MAD法:
import numpy as np
import pandas as pd
def winsorize_mad(factor, n=5):
"""
MAD法去极值
factor: 因子值序列
n: 阈值倍数,默认5倍MAD
"""
median = np.median(factor)
mad = np.median(np.abs(factor - median))
upper = median + n * mad
lower = median - n * mad
# 截断处理
factor_clipped = np.clip(factor, lower, upper)
return factor_clipped
# 使用示例
factor_series = pd.Series([1.2, 1.5, 1.3, 100.0, 1.4, 1.1, 1.6])
cleaned = winsorize_mad(factor_series)
print(cleaned)
# 输出:1.2, 1.5, 1.3, 截断值, 1.4, 1.1, 1.6
小技巧:MAD法的阈值n,我一般取5。如果数据噪声大,可以降到3。但别太激进,否则会把有效信息也砍掉。
二、标准化:让因子站在同一起跑线
标准化,就是把不同量纲的因子拉到同一个尺度上。你想想看,市盈率可能是几十,市净率可能是几,换手率可能是百分之几。如果不做标准化,模型会天然偏向数值大的因子。
常用的标准化方法:
| 方法 | 公式 | 适用场景 |
|---|---|---|
| Z-score标准化 | (x - mean) / std | 数据近似正态分布时 |
| 中位数标准化 | (x - median) / mad | 数据有异常值时 |
| Rank标准化 | 排序后映射到[0,1] | 因子值分布不规则时 |
我个人习惯用Z-score标准化,但前提是已经做过去极值。否则,一个异常值就能把均值和标准差带偏,标准化就白做了。
def standardize_zscore(factor):
"""
Z-score标准化
"""
mean = np.mean(factor)
std = np.std(factor)
factor_std = (factor - mean) / std
return factor_std
# 注意:标准化前一定要去极值!
factor_cleaned = winsorize_mad(factor_series)
factor_standardized = standardize_zscore(factor_cleaned)
避坑指南:我曾经犯过一个错误——在截面数据上做标准化时,忘了按时间分组。结果把不同时间点的数据混在一起标准化,导致因子失效。记住:标准化一定要在每个时间截面上独立进行。
三、中性化:剔除「噪音」干扰
中性化,是因子预处理中最关键也最容易被忽视的一步。它的目的是剔除因子中与市值、行业等因素相关的部分。
为什么要这么做?举个例子:小市值股票天然有更高的收益。如果你的因子恰好在小市值股票上表现好,那到底是因子有效,还是小市值效应在起作用?
中性化的核心思路:用回归把「噪音」部分剥离掉。
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)
y = factor
# 回归
model = sm.OLS(y, X).fit()
residuals = model.resid
# 残差就是中性化后的因子
return residuals
这段代码的逻辑很简单:把因子对市值和行业做回归,取残差。残差就是剔除掉市值和行业影响后的「纯净」因子。
关键点:中性化后的因子,与市值和行业的相关性接近于0。这样我们才能确认,因子的选股能力来自因子本身,而不是来自市值或行业暴露。
四、知识体系总览
说了这么多,咱们用一张图来总结一下因子预处理的完整流程:
五、实战中的注意事项
最后,分享几个我在实战中踩过的坑:
- 顺序问题:先做去极值,再做标准化,最后做中性化。顺序乱了,效果会打折扣。
- 分组处理:每个时间截面独立处理,不要把不同时间的数据混在一起。
- 行业分类:中性化时用的行业分类要统一。我见过有人用申万一级行业做中性化,但因子计算时用的是中信行业,结果对不上。
- 市值处理:市值一般取对数后再做中性化,因为市值分布是偏态的。
我的习惯:每次做完因子预处理,我都会检查一下因子与市值、行业的相关性。如果相关性还很高,说明中性化没做好,需要重新调整。
好了,因子预处理就讲到这里。记住:预处理做得好,因子选股差不了。别嫌麻烦,这一步值得花时间。
公众号:蓝海资料掘金营,微信deep3321