3、缺失值处理:缺失值检测方法、删除策略、填充策略
做行情数据清洗,最头疼的问题是什么?
我个人的答案是:缺失值。
你想想看,行情数据是实时流进来的,网络抖动、交易所宕机、数据源断连……任何一个环节出问题,你的数据表里就会多出几个空荡荡的格子。我刚开始做量化回测时,就因为没处理好缺失值,回测曲线漂亮得不行,实盘一跑直接崩了。后来一查,原来是某天的开盘价是NaN,程序自动跳过了那天——嗯,教训深刻。
核心观点:缺失值不是「没有数据」,而是「有问题的数据」。你必须先判断它为什么缺失,再决定怎么处理。
3.1 缺失值检测方法
检测缺失值,说白了就是找出数据表里那些「空着的坑」。我常用的方法有这几种:
- isnull() / isna() 直接扫描:Pandas里最基础的方法。对整个DataFrame调用
df.isnull().sum(),就能看到每列有多少个空值。 - 可视化检测:用
missingno库画个矩阵图,缺失值会显示为白色条纹。我在项目中一眼就能看出哪些时间段数据断流了。 - 统计描述法:
df.describe()里如果某列的count远小于总行数,那肯定有缺失。这个方法虽然笨,但很实用。
import pandas as pd
import missingno as msno
# 加载行情数据
df = pd.read_csv('market_data.csv')
# 方法1:统计每列缺失数量
print(df.isnull().sum())
# 方法2:可视化缺失模式
msno.matrix(df)
我的小技巧:别只看总数。我曾经遇到一个情况,某只股票每天缺失5%的数据,看起来不多,但连续缺失3天就导致策略信号完全失效。所以我会按时间窗口分组统计缺失率,比如按小时、按天。
3.2 删除策略
删除是最简单粗暴的方法。但什么时候该删?什么时候不该删?我踩过不少坑。
3.2.1 行删除
行删除就是直接扔掉有缺失值的行。适用于:
- 缺失比例很小(比如低于5%)
- 缺失是随机发生的,没有规律
- 你不需要保持时间序列的连续性
# 删除任何包含缺失值的行
df_clean = df.dropna()
# 只删除某列缺失的行
df_clean = df.dropna(subset=['close_price'])
注意:我曾经在回测中大量使用行删除,结果发现回测结果比实盘好很多。为什么?因为删除的行往往是市场剧烈波动的时候——那些才是真正考验策略的时刻。你删掉了最难的行情,回测当然漂亮。所以,行删除要谨慎。
3.2.2 列删除
列删除就是直接扔掉整列。适用于:
- 某列缺失率超过70%
- 该列对业务没有实际意义
- 该列是衍生指标,可以从其他列计算得到
# 删除缺失率超过70%的列
threshold = 0.7
df_clean = df.loc[:, df.isnull().mean() < threshold]
我个人习惯是:先看业务意义,再看缺失率。如果某列是「成交量加权均价」,但缺失了80%,我肯定删掉。但如果某列是「开盘价」,哪怕只缺失1%,我也要想办法填充——因为开盘价太重要了。
3.3 填充策略
填充比删除更精细。说白了,就是用合理的值去填补那些空位。
3.3.1 前向填充
前向填充就是用上一个有效值填充当前缺失值。在行情数据里非常常用——因为价格通常是连续的,上一秒的价格往往是最合理的估计。
# 前向填充
df['close_price'] = df['close_price'].ffill()
我在项目中遇到过一个问题:某只股票停牌了,前向填充会把停牌前的价格一直往后填。这其实不对,因为停牌期间的价格不应该被「延续」。所以我会先标记停牌时间段,再决定是否填充。
3.3.2 后向填充
后向填充就是用下一个有效值填充当前缺失值。适用于:你知道缺失值之后的数据是准确的,比如数据延迟到达。
# 后向填充
df['close_price'] = df['close_price'].bfill()
避坑指南:我曾经在分钟级数据上同时使用前向和后向填充——先ffill再bfill。结果呢?中间一段缺失的数据被「两头夹击」填平了,看起来完美无缺。但仔细一想,这其实是引入了未来信息。回测时看着没问题,实盘时根本拿不到未来的数据。所以,千万别在回测中使用后向填充。
3.3.3 插值法
插值法比简单填充更智能。它根据缺失值前后的数据点,拟合出一条曲线,然后估算缺失点的值。
# 线性插值
df['close_price'] = df['close_price'].interpolate(method='linear')
# 时间插值(考虑时间间隔)
df['close_price'] = df['close_price'].interpolate(method='time')
你想想看,行情数据的时间间隔是不均匀的——上午9:30到11:30是交易时间,中间还有午休。如果用线性插值,它会假设价格在午休期间也在变化,这显然不合理。所以我一般用method='time',它会根据实际时间间隔来插值。
3.4 基于业务逻辑的填充
这是我最看重的方法。说白了,就是利用你对市场的理解来填充缺失值。
举个例子:
- 涨跌停板:如果某只股票涨停了,当天的收盘价缺失,可以用涨停价填充。因为涨停时价格就是那个价。
- 除权除息:除权日当天价格会跳空,缺失值不能用前向填充,而应该用复权后的价格。
- 非交易时间:中午11:30到13:00的缺失值,不应该填充任何价格——因为市场就是没交易。
# 基于业务逻辑的填充示例
def fill_by_logic(df):
# 涨停日:用涨停价填充收盘价缺失
df.loc[df['is_limit_up'], 'close_price'] = df['limit_up_price']
# 非交易时间:保持NaN
df.loc[(df['time'] >= '11:30') & (df['time'] < '13:00'), 'close_price'] = np.nan
return df
我的经验:没有一种填充方法能通吃所有场景。我通常的做法是:先用业务逻辑填充特殊场景(涨跌停、除权等),剩下的缺失值用前向填充或插值法处理。最后再检查一遍,确保没有引入未来信息。
知识体系总览
下面这张图是我自己梳理的缺失值处理流程,你可以照着这个思路来:
这张图的核心逻辑很简单:先看缺失率,再看业务场景,最后看数据特性。每一步都有对应的处理方法。
最后说一句:缺失值处理没有银弹。我见过有人把所有缺失值都填0,结果模型学到了一堆假规律。也见过有人把所有缺失行都删掉,结果样本量不够模型训练。关键是要理解你的数据、理解你的业务,然后选择合适的方法。
公众号:蓝海资料掘金营,微信deep3321