因子构建基础:数据清洗与预处理、去极值与标准化、中性化处理
说实话,因子构建这事儿,80%的时间都花在数据准备上。我刚开始做量化那会儿,总觉得写策略、调参数才是核心,结果被数据坑了无数次。后来才明白——数据质量不过关,再牛的因子也是白搭。
这一章,咱们就聊聊因子构建前必须做的三件事:数据清洗与预处理、去极值与标准化、中性化处理。这三步走扎实了,后面的工作才能站得住脚。
一、数据清洗与预处理——别让脏数据毁了你的因子
数据清洗,说白了就是给原始数据「洗澡」。我见过太多人拿到数据直接开干,结果回测漂亮得一塌糊涂,实盘一跑就崩。为什么?因为数据里有鬼。
常见的坑有这么几个:
- 缺失值:某只股票某天停牌了,数据是NaN。你不管它,因子计算就断链。
- 异常值:比如某只小盘股突然因为一笔错单,换手率飙到1000%。这种数据不处理,因子排名直接失真。
- 数据对齐:不同数据源的时间戳不一致,或者复权方式没统一。我踩过这个坑——用前复权算因子,结果回测收益翻倍,实盘亏成狗。
我的处理原则:
- 缺失值:行业均值填充,或者直接剔除(看样本量)。我个人习惯用前向填充,因为金融数据有延续性。
- 异常值:先看业务逻辑。比如市盈率为负,是亏损还是数据错误?别一刀切。
- 对齐:统一用后复权,时间戳精确到毫秒级。
# 一个简单的数据清洗示例(Python伪代码)
import pandas as pd
import numpy as np
# 前向填充缺失值
df['factor'] = df['factor'].fillna(method='ffill')
# 剔除市盈率为负且绝对值过大的异常值
df = df[(df['pe'] > 0) & (df['pe'] < 1000)]
# 按日期和股票代码对齐
df = df.sort_values(['date', 'code']).reset_index(drop=True)
💡 我的小技巧:每次清洗完数据,我都会画个分布图看一眼。如果因子分布突然出现「断崖」或者「尖峰」,那八成是清洗逻辑有问题。
二、去极值与标准化——让因子站在同一起跑线
数据洗干净了,接下来就是去极值和标准化。这两个操作经常一起做,但目的不同。
去极值,是为了干掉那些「离谱」的数据点。你想想看,如果某只股票的因子值是其他股票的100倍,那它在多因子模型里就会「一家独大」,其他因子全被淹没了。
常用的去极值方法有三种:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| MAD法 | 基于中位数绝对偏差,稳健性强 | 因子分布有厚尾时首选 |
| 百分位法 | 直接截断上下1%/5%分位数 | 简单粗暴,适合快速验证 |
| 标准差法 | 超过均值±3倍标准差则截断 | 数据近似正态分布时好用 |
我个人最常用的是MAD法。为什么?因为它对异常值不敏感。标准差法容易被极端值「带偏」,而MAD法基于中位数,稳得很。
# MAD法去极值示例
def winsorize_mad(series, n=5):
median = series.median()
mad = (series - median).abs().median()
upper = median + n * mad
lower = median - n * mad
return series.clip(lower, upper)
# 百分位法去极值
def winsorize_percentile(series, lower=0.01, upper=0.99):
return series.clip(
series.quantile(lower),
series.quantile(upper)
)
标准化,则是把不同量纲的因子拉到同一个尺度。比如市盈率是几十倍,换手率是百分之几,不标准化的话,模型会「偏爱」数值大的因子。
最常见的标准化是Z-score:减去均值,除以标准差。这样处理后,因子均值为0,标准差为1。
⚠️ 我曾经踩过的坑:标准化一定要在截面(同一时间点)上做,而不是在时序上做。如果你把某只股票过去一年的因子做标准化,那相当于把历史信息泄露到了当前截面——回测会严重过拟合。
# 截面标准化(按日期分组)
def standardize_cross_section(df, factor_col):
df['zscore'] = df.groupby('date')[factor_col].transform(
lambda x: (x - x.mean()) / x.std()
)
return df
三、中性化处理——剥离已知风险,提取纯净alpha
中性化处理,是因子构建中最「高级」的一步。说白了,就是把你因子里的「杂质」去掉。
举个例子:你发现小市值股票的某个因子表现特别好。但问题是,小市值本身就是一个强因子。你怎么知道你的因子是独立贡献,还是沾了小市值的光?
中性化就是干这个的——把因子对市值、行业等已知风险因素的暴露「回归掉」,剩下的残差才是你真正的alpha信号。
具体做法:
- 把因子值作为因变量,市值、行业等作为自变量,做截面回归。
- 取回归的残差作为中性化后的因子。
- 残差 = 原始因子 - 市值效应 - 行业效应 - ...
# 中性化处理示例(市值+行业)
import statsmodels.api as sm
def neutralize_factor(df, factor_col, market_cap_col, industry_dummies):
# 对每个日期做截面回归
def regress(group):
y = group[factor_col]
X = group[[market_cap_col] + industry_dummies]
X = sm.add_constant(X)
model = sm.OLS(y, X).fit()
group['neutralized'] = model.resid
return group
df = df.groupby('date').apply(regress)
return df
中性化之后,你的因子就和市值、行业不相关了。这时候你再去做多因子组合,才能保证每个因子都在「各司其职」。
💡 我的经验:中性化不是越多越好。有些人恨不得把100个变量都回归掉,结果因子残差变成了白噪声。我一般只中性化市值和行业,最多再加个波动率。记住,中性化的目的是「去噪」,不是「去信号」。
小结一下
数据清洗、去极值标准化、中性化处理,这三步是因子构建的「地基」。地基不稳,楼盖得再高也得塌。
我见过太多人跳过这些步骤,直接上机器学习模型。结果模型在训练集上跑得飞起,一到验证集就崩。为什么?因为数据里的异常值和噪声被模型「记住」了,而不是「学会」了。
所以,别嫌这些步骤麻烦。你花在数据预处理上的每一分钟,都会在后面的建模和实盘中加倍回报给你。
公众号:蓝海资料掘金营,微信deep3321