缺失值处理:删除法、填充法、插值法、向前/向后填充
做市业务的数据清洗,说白了就是跟「脏数据」打架。而缺失值,是这场架里最常见的对手。
我记得刚入行那会儿,接手了一套高频做市系统的回测数据。跑完策略一看收益曲线,漂亮得不像真的。后来一查,好家伙,某天的中间价字段全是空值,策略直接跳过了那天。你说这要是上线了,得亏多少钱?
所以今天咱们就聊聊缺失值怎么处理。我个人的习惯是:先看缺失比例,再决定方案。别一上来就删,也别无脑填充。
一、删除法:最粗暴,但有时最有效
删除法分两种:删除行和删除列。
删除行:如果某条记录的缺失字段太多,或者关键字段(比如买卖报价、成交量)缺失了,直接扔掉。我一般设个阈值——比如缺失超过30%的字段,这条记录就不要了。
删除列:如果某个字段的缺失率超过80%,我会考虑直接删掉这列。为什么?因为剩下的20%数据很难代表整体,强行填充反而引入偏差。
# 删除缺失比例超过30%的行
df_clean = df.dropna(thresh=len(df.columns) * 0.7)
# 删除缺失比例超过80%的列
df_clean = df.dropna(axis=1, thresh=len(df) * 0.2)
二、填充法:均值、中位数、众数
填充法是最常用的。但选哪个统计量,得看数据分布。
| 统计量 | 适用场景 | 做市业务举例 |
|---|---|---|
| 均值 | 数据近似正态分布,无极端值 | 订单处理延迟(通常集中在某个范围) |
| 中位数 | 数据有偏或存在异常值 | 买卖价差(经常被大单拉偏) |
| 众数 | 分类变量或离散值 | 订单类型(市价单/限价单) |
我个人习惯:对于做市数据里的数值型字段,优先用中位数。为什么?因为做市数据里经常有极端值——比如某笔大单把价差瞬间拉大到正常值的10倍。均值会被这种值带偏,中位数稳得多。
# 按交易对分组,填充中位数
df['spread'] = df.groupby('symbol')['spread'].transform(
lambda x: x.fillna(x.median())
)
三、插值法:让数据更平滑
插值法适合时间序列数据。说白了,就是用前后数据「猜」中间的值。
做市业务里,我最常用的是线性插值。比如某笔订单的成交时间戳缺失了,用前后两条记录的时间取中间值,基本八九不离十。
但要注意:插值法假设数据是连续变化的。如果市场出现跳空(比如突发新闻导致价格瞬间跳变),插值出来的值可能完全不对。
# 线性插值
df['price'] = df['price'].interpolate(method='linear')
# 时间加权插值(更精确)
df['price'] = df['price'].interpolate(method='time')
四、向前/向后填充:做市数据的「惯性」
向前填充(ffill)和向后填充(bfill),在量化做市里特别实用。
向前填充:用上一个有效值填充当前缺失值。比如某笔订单的「订单簿快照」字段缺失了,用上一笔的快照填充——因为订单簿在毫秒级变化不大。
向后填充:用下一个有效值填充。比如某笔成交的「对手方ID」缺失了,用下一笔同价格的对手方ID填充——这招我在处理交易所数据时用过,效果不错。
我曾经遇到过一个坑:用向前填充处理「买卖方向」字段。结果某笔订单的方向缺失,向前填充用了上一笔的「买」,但实际应该是「卖」。从那以后,我对于分类变量的填充都会先做一次逻辑校验。
# 向前填充
df['orderbook'] = df['orderbook'].ffill()
# 向后填充
df['counterparty'] = df['counterparty'].bfill()
# 限制填充步数(防止填充太久远的数据)
df['price'] = df['price'].ffill(limit=3)
五、实战选择:到底用哪个?
我总结了一个简单的决策流程,画成图给大家参考:
嗯,这个图基本概括了我的思路。但实际项目中,我还会考虑一个因素:缺失是否随机。
如果缺失是随机的(比如网络抖动丢了几条数据),填充法没问题。但如果缺失是有偏的(比如大行情时数据总缺失),那就要小心了——这时候缺失本身可能包含了信息。
- 缺失少、关键字段 → 删除行
- 缺失多、非关键字段 → 删除列
- 时间序列 → 插值或向前向后填充
- 数值型 → 中位数(稳健)
- 分类型 → 众数
最后说一句:没有万能的方法。我每次处理缺失值,都会先做一次缺失值分析——看看缺失的分布、跟其他字段的关系。有时候,缺失值本身就是一个特征,值得单独建模。
好了,这一章就到这儿。下一章咱们聊聊异常值检测,那又是另一场硬仗。