4. 数据缺失值处理(上):缺失值检测与删除策略
大家好,欢迎来到第四章。今天咱们聊聊数据清洗里最常碰到的“坑”——缺失值。
说实话,我做了这么多年金融数据,还没见过哪份数据是干干净净、一个空值都没有的。你想想看,交易所的数据偶尔断流,财报数据可能漏填,用户问卷更是经常空着不答。所以,怎么处理这些“窟窿”,就成了咱们数据工程师的基本功。
这一章我们先讲上半部分:怎么发现缺失值,怎么统计它们,以及最粗暴但有时候也最有效的办法——直接删掉。
4.1 缺失值检测:isnull() 与 notnull()
先说说最基础的两个函数:isnull() 和 notnull()。
说白了,isnull() 就是问“这个位置是不是空的?”,返回 True 或 False。notnull() 正好相反,问“这个位置是不是有值?”。
我个人习惯,拿到数据第一件事就是跑 df.isnull().sum()。这行代码能告诉你每一列有多少个缺失值。嗯,这里要注意,sum() 会把 True 当成 1 来加,所以结果就是缺失值的数量。
import pandas as pd
import numpy as np
# 模拟一份金融数据
data = {
'股票代码': ['000001', '000002', '000003', '000004', '000005'],
'收盘价': [10.5, 11.2, np.nan, 9.8, 12.1],
'成交量': [10000, np.nan, 15000, 12000, np.nan],
'市盈率': [15.2, 14.8, 16.1, np.nan, 13.5]
}
df = pd.DataFrame(data)
# 检测缺失值
print(df.isnull())
print('---')
print(df.isnull().sum())
输出结果会告诉你:收盘价有1个缺失,成交量有2个,市盈率有1个。一目了然。
我在项目中遇到过一种情况:数据量特别大,几百万行,跑 isnull() 会有点慢。这时候我建议先抽样检测,或者只看关键字段。别一上来就全表扫描,浪费时间。
df.info() 也能快速看到每列的非空值数量。它比 isnull().sum() 多显示数据类型和内存占用,我经常两个一起用。
4.2 缺失值统计与可视化
光知道数字还不够。你得知道缺失值长什么样,分布在哪。
我一般会做两件事:一是算缺失率,二是画图。
4.2.1 缺失率计算
缺失率 = 缺失值数量 / 总行数。这个比例很关键。如果一列缺失率超过 50%,我基本会考虑直接扔掉这列。但如果只有 1%-2%,那补全或者删除都行。
# 计算每列的缺失率
missing_rate = df.isnull().mean()
print(missing_rate)
# 筛选出缺失率超过50%的列
high_missing = missing_rate[missing_rate > 0.5]
print('高缺失率列:', high_missing.index.tolist())
输出结果:
股票代码 0.0
收盘价 0.2
成交量 0.4
市盈率 0.2
dtype: float64
高缺失率列: []
你看,成交量缺失率 40%,虽然高,但还没到 50%。这种列我会再考虑一下,不一定直接删。
4.2.2 可视化展示
画图能让你一眼看出缺失值的分布模式。我常用的是 missingno 这个库,专门为缺失值可视化设计的。
import missingno as msno
import matplotlib.pyplot as plt
# 缺失值矩阵图
msno.matrix(df)
plt.show()
# 缺失值柱状图
msno.bar(df)
plt.show()
矩阵图里,白色条带就是缺失值的位置。如果白色条带集中在某几行,说明这些样本有问题。如果白色条带集中在某几列,说明这些字段采集有问题。
我曾经处理过一份期货数据,发现所有缺失值都集中在某个时间段。后来一查,原来是那段时间交易所系统升级,数据没录进去。你看,可视化能帮你找到根因。
4.3 删除缺失值:dropna() 策略与实战
好,检测完了,统计完了,也画图了。接下来怎么办?
最直接的办法:删掉。
dropna() 就是干这个的。但怎么删,有讲究。
4.3.1 基本用法
# 删除所有包含缺失值的行
df_clean = df.dropna()
print(df_clean)
输出:
股票代码 收盘价 成交量 市盈率
0 000001 10.5 10000.0 15.2
你看,原来5行数据,删完只剩1行。为什么?因为只要某行有一个缺失值,整行就被删了。这太激进了。
4.3.2 按列删除
有时候你只想删掉那些“坏”列,而不是“坏”行。
# 删除所有包含缺失值的列
df_clean_col = df.dropna(axis=1)
print(df_clean_col)
输出:
股票代码
0 000001
1 000002
2 000003
3 000004
4 000005
只剩股票代码一列了。其他列都有缺失值,全被删了。这更激进,基本不可用。
4.3.3 设置阈值:thresh 参数
这才是实战中常用的。你可以设置一个阈值,比如“某行至少有3个非空值才保留”。
# 保留至少有3个非空值的行
df_thresh = df.dropna(thresh=3)
print(df_thresh)
输出:
股票代码 收盘价 成交量 市盈率
0 000001 10.5 10000.0 15.2
1 000002 11.2 NaN 14.8
3 000004 9.8 12000.0 NaN
4 000005 12.1 NaN 13.5
你看,第2行(索引2)因为只有收盘价和市盈率两个非空值,被删掉了。其他行至少有三个非空值,保留了下来。
这个 thresh 参数,说白了就是“容忍度”。你容忍一行里最多有几个缺失值?我一般设成列数的一半。比如有10列,我就设 thresh=5。
4.3.4 按子集删除:subset 参数
有时候你只关心某些关键字段。比如在金融数据里,股票代码和收盘价绝对不能为空。其他字段可以空着。
# 只检查关键字段
df_subset = df.dropna(subset=['股票代码', '收盘价'])
print(df_subset)
输出:
股票代码 收盘价 成交量 市盈率
0 000001 10.5 10000.0 15.2
1 000002 11.2 NaN 14.8
3 000004 9.8 12000.0 NaN
4 000005 12.1 NaN 13.5
第2行(索引2)因为收盘价是 NaN,被删掉了。其他行虽然成交量或市盈率是空的,但关键字段都在,所以保留。
dropna() 不加任何参数,结果把 80% 的数据都删了。后来才发现,原来有一列“备注”字段几乎全是空的。从那以后,我每次用 dropna() 之前,都会先看看每列的缺失率,再决定用哪个参数。
4.4 实战策略总结
说了这么多,到底什么时候该删?我总结了几条经验:
| 场景 | 建议策略 | 原因 |
|---|---|---|
| 缺失率 < 5% | 直接删除行 | 影响很小,删了省事 |
| 缺失率 5%-30% | 用 thresh 或 subset 删除 | 保留有价值的数据 |
| 缺失率 > 50% | 删除整列 | 这列信息量太少,留着也没用 |
| 关键字段缺失 | 必须删除该行 | 关键字段空了,这行数据就没意义了 |
当然,这只是经验值。具体怎么设,还得看你的业务场景。比如做风控模型,缺失率 1% 我都可能删行。但做统计分析,缺失率 20% 我也可能留着,后面用插补法补全。
4.5 本章知识体系
下面这张图,帮你理清本章的核心逻辑:
嗯,这一章的内容就到这。删除缺失值是最简单的方法,但也是最容易出错的方法。记住:删之前一定要先检测、先统计、先可视化。下一章我们讲更高级的玩法——缺失值填充,到时候你会看到,有些缺失值其实是可以“救”回来的。
isnull()、dropna() 方法,统计缺失率并尝试用不同策略删除。看看哪种策略保留的数据最多,同时还能保证质量。
公众号:蓝海资料掘金营,微信deep3321