4、因子数据处理:数据对齐与重采样、缺失值处理、去极值与标准化、中性化处理
各位好,我是老K。今天咱们聊聊因子数据处理。说实话,这步才是量化策略的「分水岭」。你因子算得再漂亮,数据没处理好,回测结果就是一堆垃圾。我在Qlib项目里踩过不少坑,今天把这些经验掰开揉碎讲给你听。
4.1 数据对齐与重采样:别让时间戳骗了你
先问个问题:你拿到日频数据,是不是直接拿来就用?
我刚开始做因子时也这样。结果回测收益曲线漂亮得不行,实盘一跑就崩。后来发现,问题出在数据对齐上。
举个例子。你算了个「昨日收盘价/20日均线」因子。但你的价格数据是16:00收盘,而你的因子计算脚本是17:00跑的。中间这1小时,数据源可能已经更新了。你算出来的因子,其实包含了未来信息。
数据对齐的核心原则:因子值必须基于「已知信息」,不能偷看未来。
在Qlib里,数据对齐靠的是D(日频)和M(分钟频)这些频率标识。我个人习惯用D做日频因子,然后用.resample()做重采样。
重采样场景:把5分钟数据重采样成日数据。注意,不是简单取平均。我一般用「最后一条数据」作为日频值,因为那才是收盘时刻的真实状态。
# Qlib风格的数据对齐示例
import qlib
from qlib.data import D
# 加载日频数据
daily_data = D.features(["close", "volume"], ["SH600519"], start_time="2020-01-01", end_time="2020-12-31")
# 重采样:从分钟到日
# 注意:这里用last()而不是mean()
daily_resampled = daily_data.resample("1D").last()
我曾经踩过的坑:用mean()做重采样,结果把开盘价、盘中价、收盘价混在一起。因子信号完全失真。记住:金融数据重采样,默认用last()或ohlc(),别偷懒。
4.2 缺失值处理:不是填个0就完事了
缺失值处理,说白了就是「数据里有个洞,你怎么补」。但补法不对,因子就废了。
我见过最粗暴的做法:df.fillna(0)。嗯,填0。你想想看,一个市盈率因子,缺失值填0,那这只股票瞬间变成「估值极低」的香饽饽。回测里它被疯狂买入,实盘里你根本买不到——因为数据源根本没这只股票。
正确的处理思路:
- 时间序列缺失:用前向填充(
ffill)或插值。比如某天停牌,用前一天数据。 - 截面缺失:用行业均值填充。比如某只股票没算出来ROE,用同行业其他股票的均值代替。
- 大量缺失:直接删除。如果某只股票有80%的数据都缺失,别补了,删掉它。
# 缺失值处理示例
import pandas as pd
# 前向填充:适合时间序列
df["factor"] = df["factor"].ffill()
# 行业均值填充:适合截面数据
df["factor"] = df.groupby("industry")["factor"].transform(lambda x: x.fillna(x.mean()))
# 删除缺失过多的股票
df = df.dropna(thresh=0.8 * len(df.columns))
我的小技巧:处理缺失值时,先看看缺失率。如果某只股票缺失率超过50%,我直接扔掉。别心疼数据,垃圾进垃圾出。
4.3 去极值与标准化:让因子站在同一起跑线
去极值和标准化,这俩经常一起做。为什么?
你想想看,一个市盈率因子,茅台可能50倍,某ST股可能-100倍。这俩放在一起,ST股直接把均值拉偏了。去极值就是把这些「离谱值」拉回来。
去极值的方法:
- MAD法:中位数绝对偏差。稳健,不受极端值影响。
- 百分位法:比如把1%和99%以外的值截断。
- 3σ法:超过3倍标准差的值拉回边界。
我个人偏爱MAD法。为什么?因为它对异常值不敏感。3σ法容易被一个极端值带偏。
# MAD法去极值
import numpy as np
def mad_winsorize(series, n=5):
median = series.median()
mad = np.abs(series - median).median()
upper = median + n * mad
lower = median - n * mad
return series.clip(lower, upper)
# 标准化:Z-score
def standardize(series):
return (series - series.mean()) / series.std()
标准化后的因子:均值为0,标准差为1。这样不同量纲的因子才能放在一起比较。比如市盈率和市净率,一个几十倍,一个几倍,不标准化没法合并。
4.4 中性化处理:剔除你不想要的「偏见」
中性化,说白了就是「把因子里的杂质去掉」。最常见的杂质是行业和市值。
举个例子。你算了个「动量因子」,发现银行股普遍动量强。但这不是因为银行股本身动量好,而是因为银行股盘子大、波动小。你真正想捕捉的是「个股自身的动量」,而不是「行业带来的动量」。
中性化的做法:用回归把行业和市值的影响剔除。
# 行业市值中性化示例
import statsmodels.api as sm
def neutralize(factor, industry_dummies, market_cap):
# 构建回归模型
X = pd.concat([industry_dummies, np.log(market_cap)], axis=1)
X = sm.add_constant(X)
y = factor
# 回归,取残差
model = sm.OLS(y, X).fit()
residual = model.resid
return residual
我曾经犯过的错:中性化时忘了取对数市值。结果市值大的股票残差依然很大,中性化等于没做。记住:市值分布是偏态的,取对数后才接近正态。
4.5 本章知识体系
下面这张图,是我自己总结的因子数据处理流程。你照着这个顺序做,基本不会出错。
嗯,这套流程我用了好几年。每次做因子研究,都是这个顺序。你照着来,至少能保证数据层面不出大问题。
最后提醒一句:因子数据处理不是一次性工作。每次回测前,都要重新做一遍。别想着「上次处理过了,这次直接用」。市场在变,数据在变,你的处理逻辑也得跟着变。
公众号:蓝海数据掘金营,微信deep3321