数据清洗与预处理:处理缺失值、复权处理、去极值、标准化
各位同学,欢迎来到第四章。说实话,这一章才是真正决定你模型成败的关键。
很多人喜欢把时间花在选股因子、调参优化上,但数据没洗干净,后面全是白搭。我见过太多人,模型跑出来回测曲线漂亮得不行,一上实盘就崩——查到最后,原来是复权数据没处理好。嗯,这种坑我踩过不止一次。
4.1 缺失值处理:别让空值毁了你的策略
金融数据里,缺失值太常见了。停牌、节假日、数据源故障,都会导致空值。我个人习惯,拿到数据第一件事就是检查缺失率。
核心原则:缺失率低于5%,可以填充;超过20%,建议直接剔除该特征。
常用的填充方法有这么几种:
- 前向填充(ffill):用上一个有效值填充。适合日频数据,比如停牌后复牌,用停牌前价格。
- 后向填充(bfill):用下一个有效值填充。适合数据开头有缺失的情况。
- 插值法:线性插值或时间插值。适合少量缺失,且数据趋势明显。
- 行业均值填充:用同行业股票的均值填充。适合基本面因子。
import pandas as pd
import numpy as np
# 示例:日频收益率数据
df = pd.DataFrame({
'date': pd.date_range('2024-01-01', periods=10, freq='D'),
'stock_a': [1.0, np.nan, 1.02, 1.03, np.nan, 1.05, 1.06, np.nan, 1.08, 1.09],
'stock_b': [0.98, 0.99, 1.0, np.nan, 1.02, 1.03, np.nan, 1.05, 1.06, 1.07]
})
# 前向填充
df_ffill = df.fillna(method='ffill')
# 线性插值
df_interp = df.interpolate(method='linear')
print("原始数据:\n", df)
print("\n前向填充后:\n", df_ffill)
注意:千万不要用全局均值填充金融时间序列!我曾经在回测里这么干过,结果把停牌期间的异常值当成了正常信号,策略直接失效。金融数据有很强的时序依赖性,均值填充会破坏这种结构。
4.2 复权处理:还原真实的收益率
复权处理,说白了就是消除分红、送股、配股对价格的影响。你想想看,一只股票今天除权除息,价格跳空低开,但你的账户资产其实没变。如果不复权,技术指标全乱套。
我个人习惯用后复权数据做回测。为什么?因为后复权能真实反映持有期的总收益,包括分红再投资。
| 复权方式 | 特点 | 适用场景 |
|---|---|---|
| 前复权 | 调整历史价格,保持最新价格不变 | 技术分析、画图 |
| 后复权 | 调整历史价格,保持上市首日价格不变 | 收益率计算、回测 |
| 不复权 | 真实成交价,有跳空缺口 | 实盘交易、订单执行 |
# 模拟复权处理逻辑
def adjust_price(df, factor_col='adj_factor'):
"""
简单复权处理
factor_col: 复权因子列
"""
df['adj_close'] = df['close'] * df[factor_col]
df['adj_open'] = df['open'] * df[factor_col]
return df
# 实际项目中,我会用akshare或tushare直接获取复权数据
# 比如:ak.stock_zh_a_hist(symbol="000001", adjust="qfq")
小技巧:如果你用的是日频数据,建议同时保留复权价和原始价。复权价用于计算因子,原始价用于模拟交易成本。我有个策略就是因为没区分这两者,回测收益虚高了20%。
4.3 去极值:别让异常值绑架你的模型
金融数据里,极端值太多了。比如某天突然涨停,或者财报里出现一个异常高的市盈率。这些极端值会严重扭曲均值和标准差,影响后续标准化。
常用的去极值方法有三种:
- MAD法(中位数绝对偏差):稳健性最好,适合有大量异常值的数据。
- 百分位法:直接截断上下1%或5%的数据。简单粗暴,但有效。
- 标准差法:超过3倍标准差的值替换为边界值。适合正态分布的数据。
def winsorize_mad(series, n=5):
"""
MAD法去极值
n: 中位数上下n倍MAD
"""
median = series.median()
mad = np.median(np.abs(series - median))
upper = median + n * mad
lower = median - n * mad
return series.clip(lower, upper)
def winsorize_percentile(series, lower=0.01, upper=0.99):
"""
百分位法去极值
"""
q_low = series.quantile(lower)
q_high = series.quantile(upper)
return series.clip(q_low, q_high)
# 示例
np.random.seed(42)
data = pd.Series(np.random.randn(1000) * 10 + 50)
data.iloc[0] = 500 # 人为加入极端值
cleaned_mad = winsorize_mad(data)
cleaned_pct = winsorize_percentile(data)
print(f"原始数据均值: {data.mean():.2f}, 标准差: {data.std():.2f}")
print(f"MAD法均值: {cleaned_mad.mean():.2f}, 标准差: {cleaned_mad.std():.2f}")
print(f"百分位法均值: {cleaned_pct.mean():.2f}, 标准差: {cleaned_pct.std():.2f}")
我的经验:对于市值因子、换手率这类偏态分布的数据,MAD法比标准差法更靠谱。我曾经用标准差法处理换手率,结果把正常的活跃股都截断了,策略选出来的全是僵尸股。
4.4 标准化:让不同量纲的因子站在同一起跑线
标准化,说白了就是把不同量纲的数据拉到同一个尺度。市盈率可能是几十倍,换手率可能是百分之几,不标准化的话,模型会天然偏向数值大的特征。
常用的标准化方法:
- Z-score标准化:(x - mean) / std。假设数据服从正态分布,处理后均值为0,标准差为1。
- Min-Max标准化:(x - min) / (max - min)。将数据映射到[0,1]区间,适合有边界的数据。
- Rank标准化:将数据排序后转为百分位数。最稳健,不受极端值影响。
from sklearn.preprocessing import StandardScaler, MinMaxScaler
from scipy.stats import rankdata
def zscore_normalize(series):
"""Z-score标准化"""
return (series - series.mean()) / series.std()
def minmax_normalize(series):
"""Min-Max标准化"""
return (series - series.min()) / (series.max() - series.min())
def rank_normalize(series):
"""Rank标准化:转为0-1之间的百分位数"""
ranks = rankdata(series)
return (ranks - 1) / (len(series) - 1)
# 示例:不同量纲的因子
factors = pd.DataFrame({
'pe': [15, 30, 50, 100, 200],
'turnover': [0.01, 0.05, 0.10, 0.20, 0.50],
'volume': [1e6, 5e6, 1e7, 5e7, 1e8]
})
print("原始数据:\n", factors)
print("\nZ-score标准化后:\n", factors.apply(zscore_normalize))
print("\nRank标准化后:\n", factors.apply(rank_normalize))
建议:在量化选股中,我强烈推荐Rank标准化。为什么?因为金融数据很少服从正态分布,Z-score会受极端值影响。Rank标准化只关心排序,不受量纲和分布影响,稳健性最好。
4.5 完整的数据清洗流水线
在实际项目中,我会把上述步骤封装成一个流水线。你想想看,每次手动操作多麻烦,还容易出错。
class DataCleaner:
"""数据清洗流水线"""
def __init__(self, method_missing='ffill', method_outlier='mad',
method_norm='rank'):
self.method_missing = method_missing
self.method_outlier = method_outlier
self.method_norm = method_norm
def clean(self, df):
"""执行完整清洗流程"""
# 1. 缺失值处理
if self.method_missing == 'ffill':
df = df.fillna(method='ffill')
elif self.method_missing == 'interp':
df = df.interpolate()
# 2. 去极值
if self.method_outlier == 'mad':
df = df.apply(winsorize_mad)
elif self.method_outlier == 'percentile':
df = df.apply(winsorize_percentile)
# 3. 标准化
if self.method_norm == 'rank':
df = df.apply(rank_normalize)
elif self.method_norm == 'zscore':
df = df.apply(zscore_normalize)
return df
# 使用示例
cleaner = DataCleaner()
clean_data = cleaner.clean(factors)
print("清洗完成:\n", clean_data)
重要提醒:清洗参数(比如MAD的n值、百分位的阈值)必须在训练集上确定,然后应用到测试集。我见过有人在整个数据集上做标准化,结果回测过拟合得一塌糊涂。记住:测试集不能偷看未来的信息。
4.6 本章知识体系
下面这张图,是我梳理的数据清洗与预处理核心流程。你可以把它当作一个检查清单,每次处理数据时对照着来。
好了,这一章的内容就到这里。数据清洗看起来琐碎,但它是整个量化投资体系的基石。我建议你每次拿到新数据,都按照这个流程走一遍,养成习惯。下一章我们会讨论因子构建,到时候这些清洗好的数据就能派上大用场了。
公众号:蓝海资料掘金营,微信deep3321