缺失值处理:缺失值识别、删除法、填充法、插值法

做金融数据清洗这么多年,我敢说缺失值处理是大家最常碰到的「老朋友」。你想想看,无论是交易数据、行情数据还是客户信息,几乎每张表里都有那么几个空值。今天我就把这套实战经验掰开揉碎讲给你听。

一、缺失值识别——先找到「敌人」在哪

处理缺失值的第一步,不是急着填,而是先搞清楚哪些字段有缺失、缺失了多少。我个人习惯用 isnull() 配合 sum() 快速扫一眼。

import pandas as pd
import numpy as np

# 模拟一份金融数据
df = pd.DataFrame({
    '客户ID': [1, 2, 3, 4, 5],
    '年龄': [25, np.nan, 30, 35, np.nan],
    '月收入': [8000, 12000, np.nan, 15000, 20000],
    '信用评分': [650, 720, np.nan, np.nan, 780]
})

# 缺失值识别
print(df.isnull().sum())

输出结果一目了然:年龄缺2个,月收入缺1个,信用评分缺2个。嗯,这里要注意——缺失比例超过30%的字段,我建议直接考虑删除,否则填充出来的数据反而会引入噪声。

实战经验:我在某银行做风控数据清洗时,发现「家庭年收入」字段缺失率高达45%。当时团队想用均值填充,我拦住了——因为高收入人群更倾向于不填这个字段,均值填充会严重低估真实收入。最后我们直接删除了这个字段,改用「公积金缴纳额」做替代特征。

二、删除法——简单粗暴但有效

删除法分两种:删除行和删除列。什么时候用?我总结了三句话:

  • 缺失值极少(<5%):直接删除行,不影响整体分布
  • 缺失值极多(>30%):直接删除列,别犹豫
  • 关键字段缺失:比如「客户ID」为空,这行数据基本废了,删
# 删除包含缺失值的行
df_drop_row = df.dropna()

# 删除指定列(比如信用评分缺失太多)
df_drop_col = df.drop(columns=['信用评分'])

print(f"删除行后数据量: {len(df_drop_row)}")
print(f"删除列后字段数: {len(df_drop_col.columns)}")
避坑指南:我曾经在清洗某券商交易数据时,直接用了 dropna() 删掉了所有含空值的行,结果发现数据量从100万条骤降到60万条。后来一查,是因为「交易对手方」字段在跨境交易中经常为空,但其他字段都是完整的。所以删除前一定要分析缺失模式,别一刀切。

三、填充法——均值/中位数/众数/前向填充

填充法是我用得最多的方法。说白了,就是用某个「合理值」去替代空值。但选什么值,大有讲究。

3.1 均值填充 vs 中位数填充

均值对异常值敏感,中位数则稳健得多。我举个例子:

# 均值填充
df['年龄_均值'] = df['年龄'].fillna(df['年龄'].mean())

# 中位数填充
df['年龄_中位数'] = df['年龄'].fillna(df['年龄'].median())

print("均值:", df['年龄'].mean())
print("中位数:", df['年龄'].median())

你看,如果数据里混入了一个「年龄=200」的异常值,均值会被拉高,但中位数几乎不受影响。所以对于收入、年龄这类可能有异常值的字段,我建议用中位数

实战经验:在清洗某P2P平台的借款人数据时,「月收入」字段有大量缺失。我一开始用均值填充,结果发现填充后的收入分布严重右偏——因为少数高收入者拉高了均值。后来改用中位数填充,分布就合理多了。

3.2 众数填充——分类字段的救星

对于「性别」「职业」「地区」这类分类变量,均值和中位数都不适用。这时候用众数(出现频率最高的值)填充最合理。

# 众数填充
df['信用评分_众数'] = df['信用评分'].fillna(df['信用评分'].mode()[0])

print("众数:", df['信用评分'].mode()[0])

但要注意:如果众数占比过高(比如超过80%),说明这个字段本身区分度就不高,填充后意义不大。

3.3 前向填充——时间序列的标配

金融数据里大量存在时间序列,比如股票每日收盘价。如果某天数据缺失,用前一天的数值填充往往比均值更合理。

# 模拟时间序列数据
ts_data = pd.DataFrame({
    '日期': pd.date_range('2024-01-01', periods=10, freq='D'),
    '收盘价': [100, 102, np.nan, 105, 107, np.nan, 110, 112, 115, np.nan]
})

# 前向填充
ts_data['收盘价_ffill'] = ts_data['收盘价'].fillna(method='ffill')

print(ts_data)

为什么会这样?因为金融时间序列通常具有「惯性」——今天的价格和昨天不会差太远。前向填充正好利用了这一点。

小技巧:前向填充时,如果开头就有缺失值,可以用 bfill()(后向填充)先处理开头部分,再整体前向填充。我经常这么干。

四、插值法——更「聪明」的填充

插值法比简单填充更高级。它不是用一个固定值去填,而是根据周围数据点的趋势「猜」出一个值。常用的有线性插值和多项式插值。

# 线性插值
ts_data['收盘价_线性插值'] = ts_data['收盘价'].interpolate(method='linear')

# 多项式插值(阶数越高越灵活,但也越容易过拟合)
ts_data['收盘价_多项式插值'] = ts_data['收盘价'].interpolate(method='polynomial', order=2)

print(ts_data[['日期', '收盘价', '收盘价_线性插值', '收盘价_多项式插值']])

线性插值假设数据在两点之间是直线变化,适合平稳序列。多项式插值则能捕捉曲线趋势,适合有明显周期性的数据。

实战经验:我在处理某基金净值数据时,发现连续3天缺失。如果用前向填充,净值曲线会变成「平台」,看起来很不自然。改用线性插值后,曲线平滑过渡,回测结果也更接近真实情况。

五、知识体系总览

下面这张图是我自己总结的缺失值处理决策流程,你照着走基本不会出错:

缺失值处理决策流程 识别缺失值 缺失比例? <5% 删除行 5%-30% 数据类型? 数值型 均值/中位数 /插值法 分类型 众数填充 >30% 删除列

六、总结与建议

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

  1. 先分析缺失模式:用 isnull().sum()heatmap 可视化,看看缺失是随机的还是有规律的
  2. 不要盲目填充:如果缺失值本身包含信息(比如「不填收入」可能意味着低收入),填充反而会丢失信号
  3. 交叉验证效果:填充后,用 describe() 对比填充前后的分布,确保没有引入偏差
  4. 保留原始字段:我习惯把填充后的数据放在新列,而不是直接覆盖原列。这样万一填错了还能回滚
最后说一句:缺失值处理没有银弹。均值填充、中位数填充、插值法各有适用场景。我的建议是——多试几种方法,用业务逻辑去判断哪个结果更合理。毕竟,数据清洗的最终目的是服务业务,而不是炫技。

专注资料整理