第4章:数据清洗与预处理
数据预处理,说白了就是给原始数据「洗澡」。我做了这么多年量化,见过太多人拿着脏数据就跑模型,结果回测漂亮得像神话,实盘一跑就现原形。嗯,这一章我们就把数据洗干净。
4.1 缺失值处理
股票数据里缺失值太常见了。停牌、新股上市、数据源抽风,都会导致空值。我个人习惯,拿到数据第一件事就是看缺失率。
核心原则:缺失率超过50%的因子,直接扔掉。别犹豫,补了也是噪声。
4.1.1 删除法
最简单粗暴,但也最有效。适用于缺失值占比很小的情况。
import pandas as pd
import numpy as np
# 删除含有缺失值的行
df_clean = df.dropna()
# 删除缺失率超过30%的列
df_clean = df.dropna(thresh=len(df) * 0.7, axis=1)
我的经验:回测时用删除法没问题,但实盘交易时,当天缺失的数据你没法删——你得实时处理。所以实盘我一般用填充法。
4.1.2 填充法
常用的填充方式有几种:
- 均值/中位数填充:简单,但会压缩方差
- 前向填充:用上一个交易日的数据,适合停牌场景
- 行业均值填充:用同行业股票的均值,我比较推荐这个
# 行业均值填充
df['pe_ratio'] = df.groupby('industry')['pe_ratio'].transform(
lambda x: x.fillna(x.mean())
)
# 前向填充(适合停牌)
df['volume'] = df.groupby('stock_code')['volume'].ffill()
注意:千万不要用未来数据填充!我曾经见过有人用后一天的收盘价填充前一天的缺失值,回测结果漂亮得离谱,实盘直接崩了。
4.2 异常值检测
异常值就是那些离谱的数据点。比如某只股票市盈率突然变成10000倍,或者换手率飙到99%。你想想看,这明显是数据录入错误或者极端事件。
4.2.1 3σ原则
假设数据服从正态分布,超过均值±3倍标准差的就是异常值。这个方法简单,但股票数据往往不服从正态分布,所以效果一般。
def detect_outliers_3sigma(series):
mean = series.mean()
std = series.std()
lower = mean - 3 * std
upper = mean + 3 * std
return (series < lower) | (series > upper)
4.2.2 四分位距法(IQR)
这个方法更稳健,不受极端值影响。我比较常用这个。
def detect_outliers_iqr(series):
Q1 = series.quantile(0.25)
Q3 = series.quantile(0.75)
IQR = Q3 - Q1
lower = Q1 - 1.5 * IQR
upper = Q3 + 1.5 * IQR
return (series < lower) | (series > upper)
避坑指南:我曾经在市值因子上用3σ法,结果把茅台这种大市值股票标记为异常值,直接删掉了。后来改用IQR法,才保留了这些真正的优质股。
4.3 数据标准化
不同因子的量纲不一样。市盈率可能是几十,换手率是百分之几,市值是几十亿。不标准化的话,模型会天然偏向数值大的因子。
4.3.1 Z-score标准化
把数据变成均值为0、标准差为1的分布。这是最常用的方法。
from scipy import stats
def zscore_standardize(series):
return (series - series.mean()) / series.std()
# 或者直接用scipy
df['pe_zscore'] = stats.zscore(df['pe_ratio'])
4.3.2 排序标准化
把数据映射到0到1之间。这个方法对异常值不敏感,我特别喜欢在组合构建时用。
def rank_standardize(series):
ranks = series.rank()
return (ranks - 1) / (len(ranks) - 1)
关键点:标准化一定要在训练集上计算参数,然后应用到测试集。否则会造成数据泄露,回测结果虚高。
4.4 中性化处理
中性化,说白了就是剔除掉你不想要的「风格暴露」。比如市值因子和行业因子,很多时候我们想剥离它们的影响,只看个股自身的alpha。
4.4.1 市值中性化
把因子值对市值做回归,取残差作为新的因子值。这样新因子就和市值无关了。
import statsmodels.api as sm
def market_neutralize(factor, market_cap):
X = sm.add_constant(market_cap)
model = sm.OLS(factor, X).fit()
residual = model.resid
return residual
4.4.2 行业中性化
行业内做标准化,让每个行业的因子均值为0、标准差为1。这样不同行业之间就可以直接比较了。
def industry_neutralize(df, factor_col, industry_col):
df['factor_neutral'] = df.groupby(industry_col)[factor_col].transform(
lambda x: (x - x.mean()) / x.std()
)
return df
重要提醒:中性化不是万能的。过度中性化会抹掉真实的alpha信号。我见过有人把因子对100个行业虚拟变量做回归,结果残差全是噪声,模型完全失效。
知识体系总览
下面这张图,是我自己梳理的数据预处理流程。每次做新项目,我都会先过一遍这个框架。
整个流程走下来,数据才算真正「能用」。我见过太多人跳过这些步骤,直接跑模型,结果就是过拟合、回测失效。嗯,数据预处理虽然枯燥,但它是整个量化投资体系的基石。
总结一下:缺失值先看缺失率,高就删、低就补;异常值用IQR法更稳健;标准化选Z-score或排序法;中性化剔除市值和行业干扰。每一步都有坑,但踩过之后,你的模型会感谢你。