4. 缺失值处理(下):均值/中位数填充、前向/后向填充、KNN插补与多重插补

上一节我们聊了删除法和简单填充。说实话,那些方法在ESG数据面前,经常不够用。为什么?因为ESG评分数据有个特点——缺失往往不是随机的。一家公司如果故意不披露碳排放数据,那它的碳排放大概率不会低。你直接删掉或填个均值,分析结果就会偏。

这一节,我们深入几种更“聪明”的填充方法。我个人习惯把它们分成两类:时序类填充跨样本类填充。前者适合面板数据的时间维度,后者适合截面数据的特征维度。

4.1 均值/中位数填充:最基础的“替补队员”

先说最简单的。均值填充,就是用该列所有非缺失值的平均值去填。中位数填充类似,只是用中位数。

什么时候用?

  • 数据分布接近正态,用均值
  • 数据有极端值(比如某公司ESG评分突然飙到100),用中位数更稳健
  • 缺失率很低(<5%),且缺失机制是随机缺失

我在项目中遇到过一家评级机构的数据,环境分(E)缺失了大概3%。我扫了一眼分布,基本对称,就直接用均值填充了。后来验证发现,对最终因子合成的影响不到0.5%。这种场景,没必要上复杂方法。

核心要点:均值/中位数填充会降低方差,导致数据“扁平化”。如果你后续要做回归分析,系数估计会偏向零。
import pandas as pd
import numpy as np

# 示例:ESG评分数据
df = pd.DataFrame({
    'E_score': [85, np.nan, 78, 92, np.nan, 88],
    'S_score': [70, 75, np.nan, 80, 82, 79],
    'G_score': [90, 88, 85, np.nan, 91, 87]
})

# 均值填充
df['E_score_filled'] = df['E_score'].fillna(df['E_score'].mean())
# 中位数填充
df['S_score_filled'] = df['S_score'].fillna(df['S_score'].median())

print(df)

4.2 前向/后向填充:时间序列的“就近原则”

ESG数据很多是面板数据——同一家公司,每年都有评分。这时候,缺失值往往有很强的时间连续性。比如某公司2022年E分缺失,但2021年和2023年都有。你想想看,它的E分大概率不会突然跳变。

前向填充(ffill):用上一个非缺失值填充下一个缺失值。
后向填充(bfill):用下一个非缺失值填充上一个缺失值。

我个人习惯:优先用前向填充。因为ESG评分通常是逐步调整的,上一期的值对下一期有很强的参考意义。后向填充容易引入“未来信息”,在回测时会造成数据泄露。

避坑指南:我曾经在构建ESG动量因子时,不小心用了后向填充。结果回测收益高得离谱,一查才发现——我用未来的数据填了过去的缺失值。这就是典型的数据泄露。切记:时间序列填充,永远只用历史信息
# 假设这是某公司连续5年的E评分
df_time = pd.DataFrame({
    'year': [2019, 2020, 2021, 2022, 2023],
    'E_score': [82, np.nan, 85, np.nan, 88]
})

# 前向填充
df_time['E_ffill'] = df_time['E_score'].ffill()
# 后向填充(谨慎使用)
df_time['E_bfill'] = df_time['E_score'].bfill()

print(df_time)

4.3 KNN插补:找“邻居”来帮忙

均值填充太粗糙,时序填充只适用于时间序列。那如果缺失值既没有时间规律,又跟其他变量高度相关呢?这时候,KNN插补就派上用场了。

KNN插补的核心思想很简单:找k个最相似的样本,用它们的值来估计缺失值。相似度通常用欧氏距离或曼哈顿距离衡量。

举个例子:公司A的E分缺失,但它的S分、G分、市值、行业都已知。KNN会找到跟A最像的k家公司,用它们的E分平均值来填充A的缺失值。

实战技巧:KNN插补对特征缩放很敏感。我建议先做标准化(StandardScaler),否则量级大的特征会主导距离计算。另外,k值一般选3-10,太小容易过拟合,太大又趋近于均值。
from sklearn.impute import KNNImputer
import numpy as np

# 模拟数据:E、S、G三列,部分缺失
data = np.array([
    [85, 70, 90],
    [np.nan, 75, 88],
    [78, np.nan, 85],
    [92, 80, np.nan],
    [np.nan, 82, 91],
    [88, 79, 87]
])

imputer = KNNImputer(n_neighbors=3)
data_filled = imputer.fit_transform(data)

print("填充后数据:\n", data_filled)

4.4 多重插补:最“严谨”的解法

前面几种方法,都有一个共同问题:它们把填充值当作真实值,忽略了填充本身的不确定性。说白了,你填进去的值是估计出来的,它应该有一个置信区间。多重插补(MICE)就是来解决这个问题的。

多重插补的流程大致是:

  1. 对缺失值进行多次(比如5次)独立插补,每次插补都引入随机噪声
  2. 生成多个完整数据集
  3. 在每个数据集上分别进行分析
  4. 合并分析结果,考虑插补间方差

这样做的好处是:最终的参数估计既包含了样本内方差,也包含了插补不确定性。在ESG因子研究中,如果你要构建一个投资组合,多重插补能让你更真实地评估策略的风险。

注意:多重插补计算量较大。我一般只在缺失率超过10%,或者要做严谨的统计推断(比如回归系数显著性检验)时才用。日常的因子合成,KNN或时序填充已经够用。
# 使用statsmodels的MICE实现(简化示例)
# 实际项目中建议用miceforest或sklearn的IterativeImputer
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer

imputer_mice = IterativeImputer(max_iter=10, random_state=42)
data_mice = imputer_mice.fit_transform(data)

print("多重插补结果:\n", data_mice)

4.5 方法选择决策框架

讲了这么多,到底该用哪个?我整理了一个简单的决策流程,你可以对照着选:

缺失值处理方法选择决策树 数据有缺失值 是时间序列数据? 是截面/面板数据? 前向填充 (ffill) 后向填充 (bfill) 缺失率 < 5%? 均值/中位数填充 缺失率 ≥ 5%? 需要统计推断? KNN插补 多重插补 (MICE) 起始节点 决策节点 最终方法

4.6 实战中的“组合拳”

真实项目中,我很少只用一种方法。ESG数据往往既有时间维度,又有截面维度。我的常用策略是:

  1. 先做时序填充:对每家公司,用前向填充补上短期缺失(比如连续缺失不超过2年)
  2. 再用KNN补剩余:对仍然缺失的值,用同行业、同规模公司的数据做KNN插补
  3. 最后做敏感性分析:用多重插补生成5个版本,看看关键因子的稳定性
一个小技巧:填充完成后,记得生成一个“缺失标记”变量(1表示原本缺失,0表示原本有值)。这个变量本身可能就是一个有用的因子——比如,一家公司频繁缺失ESG数据,可能意味着它的信息披露质量差,这本身就是一种风险信号。

好了,这一节的内容就到这里。缺失值处理没有银弹,关键是根据数据特点和你后续的分析目标来选。下一节我们会聊异常值检测,那又是另一个坑了。


蓝海数据掘金营,专注资料整理