4. 缺失值处理(下):填充缺失值
上一节我们聊了怎么识别和删除缺失值。说实话,删除是最简单粗暴的办法,但很多时候我们舍不得扔数据。比如一个因子序列里就缺了那么一两个点,直接删掉一整段,太浪费了。
这一节,我带你看看更精细的做法——填充。说白了,就是用某种合理的值把空缺补上。我做了这么多年量化,可以负责任地告诉你:填充方法选对了,因子质量能上一个台阶;选错了,反而会引入噪声。
核心原则:填充不是瞎猜,而是基于数据本身的规律做合理推断。你填充的值,要尽量接近那个「本应存在」的真实值。
4.1 固定值填充:fillna 的基本用法
最直接的方法,就是用某个固定值去填。Pandas 里用 fillna() 搞定。
import pandas as pd
import numpy as np
# 构造一个有缺失值的因子序列
factor = pd.Series([1.2, np.nan, 1.5, np.nan, 1.8, 2.0])
print(factor)
# 用 0 填充
filled_zero = factor.fillna(0)
print(filled_zero)
# 用均值填充
filled_mean = factor.fillna(factor.mean())
print(filled_mean)
我个人习惯,在因子计算里很少用 0 填充。为什么?因为因子值如果是 0,往往代表某种极端状态,比如流动性为 0、波动率为 0,这跟「缺失」完全是两码事。你想想看,把缺失填成 0,模型会以为这个股票当天真的出了什么大事,其实只是数据没采集到而已。
我的经验:固定值填充只适合两种情况:一是你知道缺失值理论上就该是这个值(比如某些开关量);二是你只是想临时占个位,后面还会做进一步处理。
4.2 前向填充与后向填充
时间序列数据里,有个很自然的想法:用上一个交易日的值来填今天的缺失。这就是前向填充(forward fill)。反过来,用下一个交易日的值填,就是后向填充(backward fill)。
# 前向填充:用上一个非缺失值填充
ffilled = factor.ffill()
print(ffilled)
# 后向填充:用下一个非缺失值填充
bfilled = factor.bfill()
print(bfilled)
我在处理高频因子时经常用前向填充。比如某只股票在 10:30 那一分钟没有成交,用 10:29 的因子值来替代,逻辑上说得通——市场状态不会在 1 分钟内发生剧变。
但要注意一个坑:如果缺失值出现在序列开头,前向填充就无能为力了。 同样,后向填充也处理不了结尾的缺失。我一般会组合使用:先前向,再后向,或者反过来。
# 组合填充:先前向,再后向
combined = factor.ffill().bfill()
print(combined)
我曾经踩过的坑:有一次做因子回测,用了前向填充处理停牌日的缺失值。结果发现,连续停牌 5 天以上的股票,因子值一直沿用停牌前的数据,复牌后第一天因子突然跳变,导致回测信号异常。后来我加了限制:连续缺失超过 3 个的,不填充,直接标记为无效。
4.3 插值法:interpolate
前向填充说白了就是「复制粘贴」,不够精细。如果你希望缺失值能根据前后趋势平滑过渡,那就用插值法。
Pandas 的 interpolate() 提供了多种插值方式,我最常用的是线性插值。
# 线性插值
linear_interp = factor.interpolate(method='linear')
print(linear_interp)
# 时间插值(适用于时间索引)
date_factor = pd.Series(
[1.2, np.nan, 1.5, np.nan, 1.8, 2.0],
index=pd.date_range('2024-01-01', periods=6, freq='D')
)
time_interp = date_factor.interpolate(method='time')
print(time_interp)
线性插值假设缺失值前后的变化是线性的。对于日频因子,这个假设通常可以接受。但如果是高频数据,比如分钟级,我建议用 method='quadratic' 或 method='cubic',能更好地捕捉非线性变化。
| 插值方法 | 适用场景 | 我的建议 |
|---|---|---|
| linear | 日频数据,变化平稳 | 默认首选 |
| time | 时间索引,间隔不均匀 | 处理节假日缺失时很好用 |
| quadratic/cubic | 高频数据,趋势明显 | 注意过拟合风险 |
| polynomial | 有明确多项式趋势 | 慎用,容易过度拟合 |
一个小技巧:插值前先看看缺失值前后的数据点数量。如果缺失段太长(比如连续 10 个以上),插值结果基本就是瞎猜了。我一般设定一个阈值,超过 5 个连续缺失就不做插值。
4.4 使用模型预测填充
前面几种方法都只利用了单变量自身的信息。如果你有多个相关的因子,或者有横截面数据,那就可以用模型来预测缺失值。
举个例子:你有一个动量因子,缺失了某只股票某天的值。但你有同一天其他 100 只股票的动量因子值,还有这只股票的其他因子(比如市值、换手率)。这时候,你可以用这些信息来预测缺失值。
from sklearn.linear_model import LinearRegression
import pandas as pd
import numpy as np
# 构造示例数据:假设有 3 个因子,其中因子1有缺失
data = pd.DataFrame({
'factor1': [1.2, np.nan, 1.5, np.nan, 1.8, 2.0],
'factor2': [0.5, 0.6, 0.7, 0.8, 0.9, 1.0],
'factor3': [2.1, 2.3, 2.5, 2.7, 2.9, 3.1]
})
# 分离缺失和非缺失部分
missing = data[data['factor1'].isna()]
complete = data[data['factor1'].notna()]
# 用完整数据训练模型
X_train = complete[['factor2', 'factor3']]
y_train = complete['factor1']
model = LinearRegression()
model.fit(X_train, y_train)
# 预测缺失值
X_missing = missing[['factor2', 'factor3']]
predicted = model.predict(X_missing)
print(predicted)
这个方法在横截面因子中特别有效。比如你算了一个行业内的相对强弱因子,某只股票缺失了,用同行业其他股票的数据来预测,结果往往比简单插值更合理。
我曾经犯过的错:有一次我用全市场数据训练模型来填充某只小盘股的缺失值。结果模型预测出来的值跟真实值差很远。后来发现,小盘股和大盘股的数据分布完全不同。从那以后,我坚持在同类股票内部做模型填充——同行业、同市值区间,效果会好很多。
4.5 如何选择填充方法?
说了这么多,到底该用哪种?我整理了一个决策思路,你可以参考:
- 先看缺失比例:如果缺失超过 30%,别填充了,直接考虑删除或重新计算因子。
- 再看缺失模式:随机缺失用插值或均值填充;连续缺失用前向/后向填充;结构性缺失(比如停牌)用模型预测。
- 最后看数据特性:时间序列用前向或插值;横截面用模型预测;面板数据(既有时间又有截面)可以结合两者。
我的个人习惯:对于日频因子,我通常先用前向填充处理 1-2 天的缺失,然后用线性插值处理中间段的缺失,最后用模型预测处理那些孤立的大段缺失。三步走下来,大部分情况都能覆盖。
4.6 本章知识体系
下面这张图总结了缺失值填充的完整思路,你可以保存下来,做因子计算时对照着看。
填充缺失值没有银弹。我的建议是:先试简单的,不行再上复杂的。 前向填充和线性插值能解决 80% 的问题,剩下的 20% 才需要模型上场。别一上来就搞机器学习,杀鸡用牛刀不说,还可能引入偏差。
好了,这一节的内容就到这里。记住,数据清洗不是目的,让因子更干净、更稳定才是。填充方法选对了,你的因子策略就赢在了起跑线上。
公众号:蓝海资料掘金营,微信deep3321