github star gitee star atomgit star PyPI Downloads AI 编程 AI 交流群

大家好,我是正在实战各种AI项目的程序员晚枫。

今天学习数据清洗中最重要的话题——缺失值处理

真实世界的数据往往不完整,如何处理缺失值直接影响分析结果的准确性。我将分享5种常用策略,帮你应对各种场景。


认识缺失值

创建示例数据

1
2
3
4
5
6
7
8
9
10
11
12
13
import pandas as pd
import numpy as np

df = pd.DataFrame({
'姓名': ['张三', '李四', '王五', '赵六', '钱七'],
'年龄': [25, np.nan, 30, np.nan, 35],
'薪资': [15000, 12000, np.nan, 10000, np.nan],
'部门': ['技术', '销售', '技术', np.nan, '销售'],
'入职日期': pd.to_datetime(['2020-03-15', '2019-07-20',
'2018-11-08', '2021-01-10', np.nan])
})

print(df)

检测缺失值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 判断是否为缺失值
print(df.isnull())

# 每列缺失值数量
print(df.isnull().sum())

# 每行缺失值数量
print(df.isnull().sum(axis=1))

# 缺失值比例
print(df.isnull().mean() * 100)

# 有缺失值的行
print(df[df.isnull().any(axis=1)])

# 完全没缺失值的行
print(df.dropna())

策略1:删除缺失值

适用场景:缺失值很少(<5%),且随机分布。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 删除包含任何缺失值的行
df_clean = df.dropna()

# 删除整行都是缺失值的行(通常没用)
df_clean = df.dropna(how='all')

# 删除某列有缺失值的行
df_clean = df.dropna(subset=['薪资'])

# 删除缺失值超过2个的行
df_clean = df.dropna(thresh=3) # 至少要有3个非缺失值

# 删除缺失值过多的列(比如超过50%)
threshold = len(df) * 0.5
df_clean = df.dropna(axis=1, thresh=threshold)

注意:删除会丢失信息,谨慎使用!


策略2:填充固定值

适用场景:知道缺失值代表什么含义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 填充0
df['薪资'].fillna(0, inplace=True)

# 填充字符串
df['部门'].fillna('未知', inplace=True)

# 填充特定值
df['年龄'].fillna(18, inplace=True)

# 不同列填充不同值
values = {'年龄': df['年龄'].median(),
'薪资': 0,
'部门': '未知'}
df.fillna(value=values, inplace=True)

策略3:统计值填充(最常用)

适用场景:数值型数据,缺失是随机的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 均值填充
df['薪资'].fillna(df['薪资'].mean(), inplace=True)

# 中位数填充(对异常值更稳健)
df['年龄'].fillna(df['年龄'].median(), inplace=True)

# 众数填充(适合类别数据)
mode_dept = df['部门'].mode()[0]
df['部门'].fillna(mode_dept, inplace=True)

# 按组分填充(更精确)
# 用同部门的平均薪资填充
df['薪资'] = df.groupby('部门')['薪资'].transform(
lambda x: x.fillna(x.mean())
)

策略4:前后值填充

适用场景:时间序列数据。

1
2
3
4
5
6
7
8
9
10
11
12
# 前向填充(用前一个有效值)
df['销量'].fillna(method='ffill', inplace=True)

# 后向填充(用后一个有效值)
df['销量'].fillna(method='bfill', inplace=True)

# 限制填充次数(避免连锁填充)
df['销量'].fillna(method='ffill', limit=1, inplace=True)

# 先向前再向后
df['销量'].fillna(method='ffill', inplace=True)
df['销量'].fillna(method='bfill', inplace=True)

策略5:插值法

适用场景:数值有规律变化的数据。

1
2
3
4
5
6
7
8
9
# 线性插值
df['温度'].interpolate(method='linear', inplace=True)

# 多项式插值
df['温度'].interpolate(method='polynomial', order=2, inplace=True)

# 时间序列插值
df.set_index('日期', inplace=True)
df['销量'].interpolate(method='time', inplace=True)

实战:完整清洗流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import pandas as pd
import numpy as np

# 原始数据
df = pd.DataFrame({
'姓名': ['张三', '李四', '王五', '赵六', '钱七', '孙八'],
'年龄': [25, np.nan, 30, 28, np.nan, 35],
'薪资': [15000, 12000, np.nan, 10000, np.nan, 18000],
'部门': ['技术', '销售', '技术', np.nan, '销售', '技术'],
'工作年限': [2, np.nan, 5, 3, np.nan, 6]
})

print("原始缺失情况:")
print(df.isnull().sum())

# 步骤1:删除缺失严重的行(缺失超过3个)
df = df[df.isnull().sum(axis=1) <= 2]

# 步骤2:年龄用中位数填充
df['年龄'].fillna(df['年龄'].median(), inplace=True)

# 步骤3:薪资按部门均值填充
df['薪资'] = df.groupby('部门')['薪资'].transform(
lambda x: x.fillna(x.mean())
)

# 步骤4:部门用众数填充
df['部门'].fillna(df['部门'].mode()[0], inplace=True)

# 步骤5:工作年限根据年龄估算(假设22岁毕业)
df['工作年限'].fillna(df['年龄'] - 22, inplace=True)

print("\n清洗后缺失情况:")
print(df.isnull().sum())
print("\n清洗后的数据:")
print(df)

最佳实践建议

✅ 应该做的

  1. 先分析缺失模式:是随机的还是有规律的?
  2. 记录清洗过程:保留原始数据,新建清洗后的列
  3. 可视化检查:用图表看缺失值分布
  4. 对比验证:填充前后统计指标的变化

❌ 不应该做的

  1. 无脑删除:可能丢失重要信息
  2. 全部填0:会扭曲数据分布
  3. 不记录操作:无法复现和审计
  4. 忽视业务逻辑:要结合实际情况选择策略

性能对比:不同缺失值处理方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pandas as pd
import numpy as np

df = pd.DataFrame({
'A': [1, np.nan, 3, np.nan, 5, 6, 7, 8, 9, 10] * 10000,
'B': [np.nan, 2, 3, 4, np.nan, 6, 7, 8, np.nan, 10] * 10000,
'C': range(100000)
})

# 方式1:删除缺失值
%timeit df.dropna() # 快但丢数据

# 方式2:填充固定值
%timeit df.fillna(0) # 最快

# 方式3:填充均值
%timeit df.fillna(df.mean()) # 中等

# 方式4:插值法
%timeit df.interpolate() # 最慢但最精确

进阶用法

分组填充

1
2
3
4
5
6
7
# 按组填充缺失值(每组填自己的均值)
df['salary'] = df.groupby('dept')['salary'].transform(
lambda x: x.fillna(x.mean())
)

# 前向填充(适合时间序列)
df['price'] = df.groupby('stock_id')['price'].fillna(method='ffill')

插值法详解

1
2
3
4
5
6
7
8
9
10
11
# 线性插值
df['value'] = df['value'].interpolate(method='linear')

# 时间序列插值
df['price'] = df['price'].interpolate(method='time')

# 多项式插值
df['value'] = df['value'].interpolate(method='polynomial', order=2)

# 限制连续填充数量
df['value'] = df['value'].interpolate(limit=3) # 最多连续填3个

避坑指南

❌ 坑1:fillna没有inplace

1
2
3
4
5
6
7
8
9
# 错误:以为修改了原数据
df.fillna(0) # 没生效!
print(df.isnull().sum()) # 还是有缺失值

# 正确方式1:赋值回去
df = df.fillna(0)

# 正确方式2:inplace=True
df.fillna(0, inplace=True)

❌ 坑2:非标准缺失值

1
2
3
4
5
6
7
8
9
# 有时候缺失值不是NaN,而是字符串
df = pd.DataFrame({'value': ['1', '-', 'N/A', 'null', '5']})
# 这些都会被当成有效字符串!

# 读取时指定缺失值
df = pd.read_csv('data.csv', na_values=['-', 'N/A', 'null', 'NA', ''])

# 或者读取后替换
df = df.replace(['-', 'N/A', 'null', 'NA'], np.nan)

实战案例:清洗房产交易数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import pandas as pd
import numpy as np

# 模拟房产数据(含各种缺失情况)
np.random.seed(42)
n = 5000
df = pd.DataFrame({
'house_id': range(10001, 10001 + n),
'area': np.where(np.random.rand(n) > 0.1, np.random.uniform(50, 200, n), np.nan),
'price': np.where(np.random.rand(n) > 0.15, np.random.uniform(50, 500, n) * 10000, np.nan),
'rooms': np.where(np.random.rand(n) > 0.05, np.random.choice([1,2,3,4,5], n), np.nan),
'floor': np.where(np.random.rand(n) > 0.08, np.random.randint(1, 35, n), np.nan),
'district': np.where(np.random.rand(n) > 0.03, np.random.choice(['浦东', '静安', '徐汇', '黄浦', '长宁'], n), np.nan),
'year': np.where(np.random.rand(n) > 0.12, np.random.randint(1990, 2025, n), np.nan)
})

print("=== 缺失值报告 ===")
print(df.isnull().sum())
print(f"\n缺失率:")
print((df.isnull().mean() * 100).round(1))

# 处理策略
# area: 用同区域均值填充
df['area'] = df.groupby('district')['area'].transform(lambda x: x.fillna(x.mean()))

# price: 用area × 单价估算
avg_unit_price = (df['price'] / df['area']).median()
df['price'] = df['price'].fillna(df['area'] * avg_unit_price)

# rooms: 用众数填充
df['rooms'] = df['rooms'].fillna(df['rooms'].mode()[0])

# floor: 用中位数填充
df['floor'] = df['floor'].fillna(df['floor'].median())

# district: 3%直接删除
df = df.dropna(subset=['district'])

# year: 用中位数填充
df['year'] = df['year'].fillna(df['year'].median())

print(f"\n清洗后缺失值: {df.isnull().sum().sum()}")
print(f"清洗后数据量: {len(df)} 行")

缺失值处理决策树

1
2
3
4
5
6
7
8
9
10
11
12
发现缺失值

├── 缺失比例 < 5%? → 删除缺失行

├── 缺失比例 5-30%?
│ ├── 数值型 → 填充均值/中位数
│ ├── 分类型 → 填充众数/"未知"
│ └── 时间序列 → 插值法

├── 缺失比例 30-70%? → 谨慎处理,考虑是否值得保留

└── 缺失比例 > 70%? → 删除该列(信息量太少)

缺失值可视化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import matplotlib.pyplot as plt
import seaborn as sns

# 方法1:热力图
plt.figure(figsize=(12, 6))
sns.heatmap(df.isnull(), cbar=False, yticklabels=False, cmap='viridis')
plt.title('缺失值分布')
plt.show()

# 方法2:条形图
missing = df.isnull().sum()
missing[missing > 0].plot(kind='barh', figsize=(10, 6))
plt.title('各列缺失值数量')
plt.show()

# 方法3:使用missingno库
import missingno as msno
msno.matrix(df) # 缺失值矩阵
msno.bar(df) # 缺失值条形图
msno.heatmap(df) # 缺失值相关性

下节预告

下一课我们将学习处理重复值,继续数据清洗的旅程。

👉 继续阅读:Pandas数据清洗-处理重复值


💬 加入学习交流群

扫码加入Python学习交流群,和数千名同学一起进步:

👉 点击加入交流群

群里不定期分享:

  • 数据分析实战案例
  • Python学习资料
  • 求职面试经验
  • 行业最新动态

推荐:AI Python数据分析实战营

🎁 限时福利:送《利用Python进行数据分析》实体书

👉 点击了解详情


课程导航

上一篇: Pandas数据筛选与查询

下一篇: Pandas数据清洗-处理重复值


PS:数据清洗占数据分析工作的70%。耐心处理好缺失值,后续分析才能准确。



📚 推荐教材

主教材《Excel+Python 飞速搞定数据分析与处理(图灵出品)》

💬 联系我

平台账号/链接
微信扫码加好友
微博@程序员晚枫
知乎@程序员晚枫
抖音@程序员晚枫
小红书@程序员晚枫
B 站Python 自动化办公社区

主营业务:AI 编程培训、企业内训、技术咨询

🎓 AI 编程实战课程

想系统学习 AI 编程?程序员晚枫的 AI 编程实战课 帮你从零上手!