RSS
菜单
全部文章快讯开发科技深度热点

第 7 章 缺失值与重复值处理

内容摘要

现实世界的数据几乎都有"脏"的地方:空值、重复行、异常格式。本篇文章是数据清洗的核心章节,系统讲解**缺失值的检测、删除、填充、插值**,以及**重复值的识别与处理**。

现实世界的数据几乎都有"脏"的地方:空值、重复行、异常格式。本篇文章是数据清洗的核心章节,系统讲解缺失值的检测、删除、填充、插值,以及重复值的识别与处理。

目录

1. 缺失值的表示

2. 缺失值检测

3. 删除缺失值:dropna

4. 填充缺失值:fillna

5. 插值法:interpolate

6. 其他缺失值处理技巧

7. 重复值处理

8. 数据清洗实战案例

9. 常见坑与注意事项

10. 本章小结与练习


1. 缺失值的表示

pandas 中缺失值有几种表示方式:

| 表示 | 说明 |

|------|------|

| np.nan | 最常用,float 类型(注意:会强制列变成 float) |

| None | Python 内置空值,在 pandas 中一般视为 NaN |

| pd.NA | pandas 2.0 引入的通用缺失值(推荐新代码使用) |

| NaT | 时间类型缺失值 |

| 空字符串 "" | 不是缺失值,需手动处理 |

import pandas as pd
import numpy as np

s = pd.Series([1, None, np.nan, pd.NA, 5])
print(s)
# 0       1
# 1    <NA>
# 2     NaN
# 3    <NA>
# 4       5
# dtype: object

# 统一检测
print(s.isna())
# 0    False
# 1     True
# 2     True
# 3     True
# 4    False
注意:np.nan != np.nan(NaN 不等于自己),所以判断缺失只能用 isna()/isnull(),不能用 ==。

2. 缺失值检测

df = pd.DataFrame({
    "A": [1, 2, None, 4],
    "B": [None, "x", "y", "z"],
    "C": [1.5, 2.5, 3.5, 4.5],
})

# 元素级检测(True/False 矩阵)
print(df.isna())

# 每列缺失个数
print(df.isna().sum())
# A    1
# B    1
# C    0

# 每行缺失个数
print(df.isna().sum(axis=1))
# 0    1
# 1    1
# 2    1
# 3    0

# 缺失占比
print(df.isna().mean())

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

# 无缺失的行
print(df[df.notna().all(axis=1)])

常用技巧:

# 缺失值总览
missing = df.isna().sum()
missing = missing[missing > 0].sort_values(ascending=False)
print(missing)

# 找出缺失率超过 50% 的列,考虑删除
drop_cols = missing[missing / len(df) > 0.5].index
print(drop_cols)

3. 删除缺失值:dropna

# 删除所有含缺失值的行(默认)
df.dropna()

# 删除所有列都缺失的行
df.dropna(how="all")

# 至少要有 3 个非空值才保留(否则删除)
df.dropna(thresh=3)

# 只检查指定列
df.dropna(subset=["A", "B"])

# 按列方向删除(删除含缺失的列)
df.dropna(axis=1)

# 原地修改
df.dropna(inplace=True)   # 官方推荐 df = df.dropna()

| 参数 | 作用 |

|------|------|

| how="any"(默认) | 只要有一个缺失就删 |

| how="all" | 全部缺失才删 |

| thresh=n | 至少 n 个非空值才保留 |

| subset=[列] | 只在这些列中检查 |

| axis=0/1 | 按行/按列删 |


4. 填充缺失值:fillna

4.1 用常数填充

# 统一填 0
df.fillna(0)

# 按列填充不同值
df.fillna({"A": 0, "B": "未知"})

# 填字符串
df["B"] = df["B"].fillna("未知")

4.2 用统计量填充

# 用均值 / 中位数 / 众数填充
df["A"] = df["A"].fillna(df["A"].mean())
df["A"] = df["A"].fillna(df["A"].median())
df["B"] = df["B"].fillna(df["B"].mode()[0])   # 众数(可能多个,取第一个)

# 数值列统一用均值填充
for col in df.select_dtypes(include="number").columns:
    df[col] = df[col].fillna(df[col].mean())

4.3 用前后值填充(时间序列常用)

s = pd.Series([1, np.nan, np.nan, 4, np.nan])

# 前向填充:用上一个非空值
print(s.ffill())
# 0    1.0
# 1    1.0
# 2    1.0
# 3    4.0
# 4    4.0

# 后向填充:用下一个非空值
print(s.bfill())
# 0    1.0
# 1    4.0
# 2    4.0
# 3    4.0
# 4    NaN

# 限制填充数量(最多向前填 1 个)
print(s.ffill(limit=1))
# 0    1.0
# 1    1.0
# 2    NaN
# 3    4.0
# 4    4.0

4.4 分组填充(高级)

# 每个分组内用自己的均值填充
df["A"] = df.groupby("组别")["A"].transform(lambda x: x.fillna(x.mean()))

4.5 保留填充痕迹

# 记录哪些值是被填充的(方便审计)
was_na = df["A"].isna()
df["A"] = df["A"].fillna(df["A"].mean())
df["A_was_filled"] = was_na

5. 插值法:interpolate

interpolate() 用数学方法估计缺失值,适合有规律的时间序列/连续数据。

s = pd.Series([10, np.nan, np.nan, 40, np.nan, 70])

# 线性插值(默认)
print(s.interpolate())
# 0    10.0
# 1    20.0
# 2    30.0
# 3    40.0
# 4    55.0
# 5    70.0

# 时间插值(按时间间隔比例)
df_time = pd.DataFrame({
    "时间": pd.to_datetime(["2025-01-01", "2025-01-02", "2025-01-05"]),
    "值": [100, np.nan, 400],
}).set_index("时间")
print(df_time["值"].interpolate(method="time"))
# 2025-01-01    100.0
# 2025-01-02    200.0   <- 按时间比例插值
# 2025-01-05    400.0

# 多项式 / 样条插值
s.interpolate(method="polynomial", order=2)
s.interpolate(method="spline", order=3)

# 限制插值范围(只插中间,两端不填)
s.interpolate(limit_area="inside")

| method | 说明 |

|--------|------|

| linear(默认) | 线性插值 |

| time | 按时间间隔比例插值 |

| polynomial | 多项式插值(需 order) |

| spline | 样条插值(需 order) |

| nearest | 最近邻 |

插值只对数值列有意义,且数据要有一定规律才可信。

6. 其他缺失值处理技巧

# 6.1 空字符串替换为 NaN
df = df.replace("", np.nan)

# 6.2 特定标记替换为 NaN
df = df.replace(["N/A", "NULL", "-"], np.nan)

# 6.3 统一为 pd.NA
df = df.astype("string").replace("<NA>", pd.NA)

# 6.4 行列全部为空的检查
df.dropna(axis=1, how="all")       # 删掉整列都是空值的列
df.dropna(how="all")               # 删掉整行都是空值的行

7. 重复值处理

7.1 检测重复

df_dup = pd.DataFrame({
    "姓名": ["张三", "李四", "张三", "王五", "张三"],
    "城市": ["北京", "上海", "北京", "广州", "北京"],
    "年龄": [25, 30, 25, 28, 26],
})

# 整行是否重复
print(df_dup.duplicated())
# 0    False
# 1    False
# 2     True   <- 与第 0 行完全一样
# 3    False
# 4    False   <- 城市、姓名同,但年龄不同

# 重复行个数
print(df_dup.duplicated().sum())     # 1

# 只看指定列是否重复(姓名+城市相同即算重复)
print(df_dup.duplicated(subset=["姓名", "城市"]))
# 0    False
# 1    False
# 2     True
# 3    False
# 4     True   <- 姓名城市与第 0 行重复

# 保留首次出现的判断(keep 参数)
df_dup.duplicated(keep="first")    # 默认:第一次出现不算重复
df_dup.duplicated(keep="last")     # 最后一次出现不算重复
df_dup.duplicated(keep=False)      # 所有重复的都标 True

7.2 删除重复

# 整行去重(保留第一次出现的)
df_clean = df_dup.drop_duplicates()
print(df_clean)
#    姓名  城市  年龄
# 0  张三  北京  25
# 1  李四  上海  30
# 3  王五  广州  28
# 4  张三  北京  26

# 保留最后一次出现的
df_dup.drop_duplicates(keep="last")

# 按指定列去重
df_dup.drop_duplicates(subset=["姓名", "城市"])

# 原地去重
df_dup.drop_duplicates(inplace=True)

7.3 查看重复的详细信息

# 所有重复行
print(df_dup[df_dup.duplicated(keep=False)])

# 每行重复次数(累计)
print(df_dup.groupby(df_dup.columns.tolist()).size().sort_values(ascending=False))

8. 数据清洗实战案例

把本章知识串起来,处理一份"脏"数据:

import pandas as pd
import numpy as np

# 模拟一份脏数据
raw = pd.DataFrame({
    "订单号": ["A001", "A001", "A002", None, "A004"],
    "客户": ["小明", "小明", "小红", "小刚", ""],
    "金额": [100, 100, np.nan, 250, 300],
    "日期": ["2025-01-01", "2025-01-01", "2025-01-02", "2025-01-03", "2025-01-04"],
})

def clean_orders(df):
    df = df.copy()                      # 1. 拷贝,避免修改原数据

    df["订单号"] = df["订单号"].fillna("UNKNOWN")    # 2. 订单号缺失填充

    df["客户"] = df["客户"].replace("", "未知")      # 3. 空字符串替换

    df["金额"] = df["金额"].fillna(df["金额"].median())  # 4. 金额缺失用中位数填充

    df = df.drop_duplicates(subset=["订单号", "客户", "金额"])  # 5. 去重

    df["日期"] = pd.to_datetime(df["日期"])          # 6. 日期标准化

    df = df.sort_values("金额", ascending=False)     # 7. 按金额排序

    return df.reset_index(drop=True)    # 8. 重置索引

print(clean_orders(raw))
  订单号  客户     金额        日期
0  A001  小明  100.0 2025-01-01
1  A004  未知  300.0 2025-01-04
2  A002  小红  250.0 2025-01-02
3  UNKNOWN 小刚 250.0 2025-01-03

9. 常见坑与注意事项

| 坑 | 现象 | 解决办法 |

|----|------|----------|

| 用 == np.nan 判断 | 永远 False | 用 isna()/isnull() |

| NaN 让列变 float | int 列出现 NaN 后变成 float64 | 如确需整数,填充后再 astype |

| 空字符串不是缺失 | isna() 检测不到 | 先 replace("", np.nan) |

| fillna 默认不原地 | 填充后原表没变 | 重新赋值 df = df.fillna(...) |

| 盲目删行 | 数据量骤减 | 先算缺失占比,再决定删/填 |

| 时间序列 ffill 用错 | 边界仍留空 | 配合 limit、limit_area |


10. 本章小结与练习

小结

  • 检测:isna()/isnull()、isna().sum()、isna().mean();
  • 删除:dropna(how=, thresh=, subset=);
  • 填充:常数 fillna(0)、统计量 fillna(mean)、前后值 ffill/bfill、分组填充;
  • 插值:interpolate(method="linear/time/polynomial");
  • 重复:duplicated(keep=) 检测、drop_duplicates(subset=) 删除;
  • 清洗套路:拷贝 → 填充/替换 → 去重 → 类型转换 → 排序 → 重置索引。

练习题

1. 构造含缺失值的 DataFrame,分别用 0、均值、中位数填充并对比。

2. 用 ffill 和 bfill 处理一个时间序列,观察结果差异。

3. 对含重复行的数据用 drop_duplicates(subset=["a","b"]) 去重。

4. 编写一个函数:输入 DataFrame,自动填充数值列缺失(用均值)、删除缺失率超 50% 的列。

5. 用 interpolate(method="time") 处理按日期采样的缺失数据。


下一篇预告:第 8 章 数据类型与转换 —— 理解 dtype,掌握 astype、to_numeric 等类型转换武器。
— 全文完 —回到顶部 ↑
下载推广海报

文章推广海报

《第 7 章 缺失值与重复值处理》完整推广海报
DISCUSSION

文章回复

0 条公开回复
未登录回复需要审核后公开
还没有回复,欢迎参与讨论。