承接前面《Pandas 过滤与条件查询》,本篇系统讲解 Pandas 缺失值识别、筛选、删除、填充、插值、标记缺失、缺失策略选型,区分
np.nan、pd.NA、None,配套业务场景方案,保持系列教程统一风格。
前置导入与示例数据
import pandas as pd
import numpy as np
df = pd.DataFrame({
"name": ["张三", "李四", None, "赵六", "孙七", "周八"],
"age": [22, np.nan, 30, 28, None, 32],
"city": ["杭州", "上海", "杭州", "", "上海", "北京"],
"salary": [8000, 12000, np.nan, 11000, 18000, 9500],
"join_date": ["2024-01-01", "2024-02-15", None, "2024-03-20", "2024-04-10", "2024-05-01"]
})
print(df)
一、先搞懂 Pandas 的缺失值
| 标识 | 说明 |
|---|---|
np.nan | 浮点数缺失,最常见,不等于任何值,np.nan == np.nan → False |
None | Python原生空值,在object/string列会转为pd.NA |
pd.NA | Pandas新版扩展类型(Int64、string、boolean)专用缺失,推荐优先使用 |
重点:空字符串
""不是缺失值,isna()识别不到,业务清洗时要单独处理。
二、识别缺失值 isna / notna
# 判断是否缺失,返回布尔矩阵
df.isna()
# 判断是否非缺失
df.notna()
# 统计每列缺失数量
df.isna().sum()
# 缺失占比
df.isna().mean()
# 筛选缺失行、非缺失行
# age为空的行
df.loc[df["age"].isna()]
# age不为空的行
df.loc[df["age"].notna()]
三、删除缺失值 dropna
语法:df.dropna(axis=0, how='any', thresh=None, subset=None)
axis=0:按行删除;axis=1:按列删除how='any':任意一个缺失就删;how='all':全部为缺失才删thresh=N:该行至少要有N个非空值才保留subset:只看指定列的缺失
# 任意一列缺失,删除整行(常用)
res = df.dropna()
# 只有指定列name、age为空才删除
res = df.dropna(subset=["name", "age"])
# 一行全部是缺失才删除
res = df.dropna(how="all")
# 至少4个非空字段才保留
res = df.dropna(thresh=4)
# 删除缺失过多的列
res = df.dropna(axis=1, thresh=4)
⚠️ 适合:缺失占比很低、删除后不影响样本;缺失多不要直接dropna,会丢失大量数据。
四、填充缺失值 fillna
语法:df.fillna(value, method=None, limit=None)
4.1 固定值填充
# 全部缺失填充0
df["salary"].fillna(0)
# 文本列填充未知
df["city"].fillna("未知")
# 字典,按列分别填充
fill_rule = {
"age": 0,
"city": "未知",
"salary": 0
}
df.fillna(fill_rule)
4.2 前后向填充(时序数据高频)
method="ffill"/pad:前向填充,用上一行的值向下填充method="bfill"/backfill:后向填充,用下一行的值向上填充
# 前向填充
df["age"].fillna(method="ffill")
# 后向填充
df["age"].fillna(method="bfill")
# limit:最多连续填充2个缺失
df["age"].fillna(method="ffill", limit=2)
4.3 统计值填充(均值、中位数、众数)
# 均值填充
df["age"].fillna(df["age"].mean())
# 中位数填充(抗异常值,推荐)
df["age"].fillna(df["age"].median())
# 众数填充(分类字段)
df["city"].fillna(df["city"].mode()[0])
数值字段优先中位数;分类字段优先众数。
五、插值 interpolate(时序数据优选)
fillna 只是简单复制,interpolate 是插值计算,适合连续时间序列
# 默认线性插值
df["salary"].interpolate()
# 时间索引插值(时间不等间隔时很有用)
# df["salary"].interpolate(method="time")
method可选:linear、nearest、spline、polynomial
六、标记缺失(新增标记列,不改动原数据)
很多机器学习场景,除了填充,还要记录这个位置原本是不是缺失
df["age_is_missing"] = df["age"].isna()
七、空字符串伪装缺失处理(业务高频坑)
原始Excel、CSV经常有空字符串 "",isna() 识别不到,需要先替换:
# 把空字符串、空白字符统一转为pd.NA
df["city"] = df["city"].replace("", pd.NA)
# 包含空格的空白值
df["city"] = df["city"].replace(r"^\s*$", pd.NA, regex=True)
八、缺失值替换 replace
# 把-999、-1这类业务标记的异常值转为缺失
df["salary"] = df["salary"].replace(-999, pd.NA)
九、缺失处理策略选型表
| 方案 | 适用场景 | 缺点 |
|---|---|---|
| dropna | 缺失占比很低(<5%),样本充足 | 丢失数据,样本少的时候慎用 |
| 固定值填充 | 分类字段,缺失代表“未知” | 引入人为偏差 |
| 均值/中位数填充 | 数值字段,分布稳定 | 压缩方差,破坏分布 |
| ffill/bfill | 时序、连续采集数据(传感器、流水) | 连续缺失多时误差大 |
| interpolate | 连续时序指标 | 不适合离散分类字段 |
| 保留缺失+新增标记列 | 机器学习建模 | 数据量变大 |
十、高频坑清单
- 空字符串
""不是缺失值,isna()检测不到,需要手动replace np.nan不能用== np.nan判断,必须用isna()- 填充后忘记赋值,
df["age"].fillna(0)不会修改原df,需要df["age"] = df["age"].fillna(0) - ffill/bfill 遇到开头/结尾缺失,无法填充,依然保留NA
- Int64(可空整数类型)和普通int区别:int列不能存pd.NA,会自动转float
- 分组场景不要直接全局均值填充,优先分组填充:
df.groupby("city")["age"].transform(lambda x: x.fillna(x.median()))
十一、分组填充实战(工业级常用)
按城市分组,用该城市内部年龄中位数填充,比全局填充更合理
df["age"] = df.groupby("city")["age"].transform(lambda s: s.fillna(s.median()))
十二、综合完整清洗模板
def clean_missing(df):
# 1. 空字符串转为缺失
df = df.replace(r"^\s*$", pd.NA, regex=True)
# 2. 新增缺失标记
df["age_miss"] = df["age"].isna()
# 3. 数值列分组中位数填充
df["age"] = df.groupby("city")["age"].transform(lambda s: s.fillna(s.median()))
df["salary"] = df["salary"].fillna(df["salary"].median())
# 4. 分类列填充未知
df["city"] = df["city"].fillna("未知")
return df
df_clean = clean_missing(df)
print(df_clean)
0 条笔记