缺失值处理:数据清洗的第一道坎
说实话,做量化投研这几年,我遇到最多的坑就是缺失值。你想想看,行情数据断点、财务数据延迟、因子计算时除数为零……各种情况都会导致数据出现空洞。如果不处理好,模型训练出来就是个笑话。
我个人习惯,拿到数据的第一件事就是检查缺失值。这就像做饭前先洗菜,省不掉。
识别缺失值:先看看问题有多大
在Python里,pandas用NaN表示缺失值。但实际数据中,缺失值可能伪装成None、0甚至-9999。我建议先统一识别。
import pandas as pd
import numpy as np
# 模拟一份有缺失的行情数据
df = pd.DataFrame({
'date': ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04'],
'close': [100.5, np.nan, 102.3, np.nan],
'volume': [10000, 12000, np.nan, 15000],
'ma5': [np.nan, 101.2, 101.8, np.nan]
})
# 快速查看缺失情况
print(df.isnull().sum())
# close 2
# volume 1
# ma5 2
我在项目中遇到过一种情况:某只股票连续停牌,导致一整段行情全是NaN。如果没提前识别,后续计算收益率时会直接报错。所以,isnull().sum()是我每天必敲的命令。
删除法(dropna):简单粗暴,但别乱用
dropna()是最直接的方法。把有缺失的行或列直接扔掉。但你要想清楚:删掉的数据可能包含重要信息。
# 删除任何包含缺失值的行
df_clean = df.dropna()
print(df_clean)
# 只剩0行!因为每一行都有缺失
# 更精细的控制:只删除全部缺失的行
df_clean = df.dropna(how='all')
# 按列删除:某列缺失超过2个就删掉整列
df_clean = df.dropna(thresh=2, axis=1)
我记得有一次做因子回测,删掉了5%的缺失行后,回测结果突然变好了。后来一查,原来删掉的全是ST股的数据。你看,删除法其实隐含了选择偏差。
填充法(fillna):用合理值填补空缺
填充法比删除法更温和。你可以用固定值、统计值或者前后值来填充。
# 用0填充
df['volume'].fillna(0, inplace=True)
# 用均值填充(常见于截面数据)
df['close'].fillna(df['close'].mean(), inplace=True)
# 用中位数填充(对异常值更鲁棒)
df['ma5'].fillna(df['ma5'].median(), inplace=True)
我个人习惯,对于成交量这种非负数据,用中位数填充比均值更安全。为什么?因为成交量偶尔会有极端大值,均值会被拉偏,中位数则稳得多。
df.fillna({'close': df['close'].mean(), 'volume': 0})
向前向后填充:时间序列的专属武器
量化数据大多是时间序列。这时候,向前填充(ffill)和向后填充(bfill)就派上用场了。
向前填充,就是用上一个非缺失值填充当前缺失。比如股票停牌恢复后,用停牌前的价格填充——虽然不精确,但至少逻辑通顺。
# 向前填充:用上一个有效值填充
df['close'].fillna(method='ffill', inplace=True)
# 向后填充:用下一个有效值填充
df['close'].fillna(method='bfill', inplace=True)
# 限制填充步数:最多向前填充2个
df['close'].fillna(method='ffill', limit=2, inplace=True)
我在做高频数据时遇到过一个问题:某只股票在盘中突然没有成交,连续5笔都是NaN。如果用ffill,会一直沿用之前的报价,但实际上价格可能已经变了。所以我会设置limit=3,超过3笔缺失就不再填充,宁可空着也不误导模型。
插值法(interpolate):更平滑的填充方式
插值法比简单填充更聪明。它会根据数据点的趋势,在缺失位置插入一个合理的值。说白了,就是让曲线更平滑。
# 线性插值:默认方式
df['close'].interpolate(inplace=True)
# 时间插值:考虑时间间隔
df['close'].interpolate(method='time', inplace=True)
# 多项式插值:适合有趋势的数据
df['close'].interpolate(method='polynomial', order=2, inplace=True)
我记得有一次处理分钟级别的期货数据,缺失值刚好在开盘和收盘之间。用线性插值填充后,回测结果比用ffill好了不少。因为线性插值考虑了前后两个点的趋势,更接近真实价格路径。
- 数据波动小 → 线性插值
- 数据有趋势 → 多项式插值
- 数据有周期性 → 考虑用季节分解后再插值
知识体系总览
下面这张图,是我梳理的缺失值处理全流程。你可以把它当作操作手册。
实战建议:别让缺失值毁了你的模型
说了这么多,最后给你几条实在的建议:
- 先看数据来源:如果是交易所数据,缺失可能是停牌,用ffill更合理。如果是财务数据,缺失可能是未披露,用均值填充更稳妥。
- 别一股脑全删:我见过有人直接
df.dropna(),结果数据少了三分之一。回测结果漂亮,实盘一塌糊涂——因为删掉的全是极端行情。 - 记录你的处理方式:每次填充或删除,最好在代码里加个注释。不然三个月后你自己都看不懂当时为什么这么处理。
- 交叉验证:如果时间允许,试试不同的缺失值处理方法,看看对最终结果的影响有多大。如果影响很大,说明你的模型对数据质量太敏感,需要改进。
好了,缺失值处理就聊到这儿。记住一句话:数据清洗不是炫技,而是为了让你模型更稳健。别为了追求完美而过度处理,有时候保留一点缺失,反而更真实。