缺失值处理:数据清洗的第一道坎

说实话,做量化投研这几年,我遇到最多的坑就是缺失值。你想想看,行情数据断点、财务数据延迟、因子计算时除数为零……各种情况都会导致数据出现空洞。如果不处理好,模型训练出来就是个笑话。

我个人习惯,拿到数据的第一件事就是检查缺失值。这就像做饭前先洗菜,省不掉。

识别缺失值:先看看问题有多大

在Python里,pandas用NaN表示缺失值。但实际数据中,缺失值可能伪装成None0甚至-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()是我每天必敲的命令。

核心原则:先看缺失比例,再决定处理方式。缺失超过50%的列,我通常会直接删除。

删除法(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股的数据。你看,删除法其实隐含了选择偏差。

避坑指南:我曾经因为滥用dropna,导致时间序列出现断层。回测时信号无法连续生成,白白浪费了两天时间。记住:时间序列数据慎用行删除。

填充法(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好了不少。因为线性插值考虑了前后两个点的趋势,更接近真实价格路径。

选择指南:
- 数据波动小 → 线性插值
- 数据有趋势 → 多项式插值
- 数据有周期性 → 考虑用季节分解后再插值

知识体系总览

下面这张图,是我梳理的缺失值处理全流程。你可以把它当作操作手册。

缺失值处理知识体系 识别缺失值 判断缺失比例 & 数据类型 删除法 dropna 填充法 fillna 插值法 interpolate • 删除含缺失的行 • 删除含缺失的列 • 按阈值删除 • 固定值填充 • 均值/中位数填充 • 向前/向后填充 • 线性插值 • 时间插值 • 多项式插值 核心原则:先识别 → 再判断 → 最后选择策略

实战建议:别让缺失值毁了你的模型

说了这么多,最后给你几条实在的建议:

  • 先看数据来源:如果是交易所数据,缺失可能是停牌,用ffill更合理。如果是财务数据,缺失可能是未披露,用均值填充更稳妥。
  • 别一股脑全删:我见过有人直接df.dropna(),结果数据少了三分之一。回测结果漂亮,实盘一塌糊涂——因为删掉的全是极端行情。
  • 记录你的处理方式:每次填充或删除,最好在代码里加个注释。不然三个月后你自己都看不懂当时为什么这么处理。
  • 交叉验证:如果时间允许,试试不同的缺失值处理方法,看看对最终结果的影响有多大。如果影响很大,说明你的模型对数据质量太敏感,需要改进。
我的个人习惯:对于时间序列数据,优先用ffill+limit。对于截面数据,优先用中位数填充。实在不行再考虑删除。插值法虽然漂亮,但计算量大,适合离线分析,不适合实盘。

好了,缺失值处理就聊到这儿。记住一句话:数据清洗不是炫技,而是为了让你模型更稳健。别为了追求完美而过度处理,有时候保留一点缺失,反而更真实。

专注资料整理