缺失值处理:缺失值识别、删除法、填充法、插值法
做金融数据清洗这么多年,我敢说缺失值处理是大家最常碰到的「老朋友」。你想想看,无论是交易数据、行情数据还是客户信息,几乎每张表里都有那么几个空值。今天我就把这套实战经验掰开揉碎讲给你听。
一、缺失值识别——先找到「敌人」在哪
处理缺失值的第一步,不是急着填,而是先搞清楚哪些字段有缺失、缺失了多少。我个人习惯用 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%的字段,我建议直接考虑删除,否则填充出来的数据反而会引入噪声。
二、删除法——简单粗暴但有效
删除法分两种:删除行和删除列。什么时候用?我总结了三句话:
- 缺失值极少(<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)}")
三、填充法——均值/中位数/众数/前向填充
填充法是我用得最多的方法。说白了,就是用某个「合理值」去替代空值。但选什么值,大有讲究。
3.1 均值填充 vs 中位数填充
均值对异常值敏感,中位数则稳健得多。我举个例子:
# 均值填充
df['年龄_均值'] = df['年龄'].fillna(df['年龄'].mean())
# 中位数填充
df['年龄_中位数'] = df['年龄'].fillna(df['年龄'].median())
print("均值:", df['年龄'].mean())
print("中位数:", df['年龄'].median())
你看,如果数据里混入了一个「年龄=200」的异常值,均值会被拉高,但中位数几乎不受影响。所以对于收入、年龄这类可能有异常值的字段,我建议用中位数。
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[['日期', '收盘价', '收盘价_线性插值', '收盘价_多项式插值']])
线性插值假设数据在两点之间是直线变化,适合平稳序列。多项式插值则能捕捉曲线趋势,适合有明显周期性的数据。
五、知识体系总览
下面这张图是我自己总结的缺失值处理决策流程,你照着走基本不会出错:
六、总结与建议
说了这么多,最后给你几条实在的建议:
- 先分析缺失模式:用
isnull().sum()和heatmap可视化,看看缺失是随机的还是有规律的 - 不要盲目填充:如果缺失值本身包含信息(比如「不填收入」可能意味着低收入),填充反而会丢失信号
- 交叉验证效果:填充后,用
describe()对比填充前后的分布,确保没有引入偏差 - 保留原始字段:我习惯把填充后的数据放在新列,而不是直接覆盖原列。这样万一填错了还能回滚