现实世界的数据几乎都有"脏"的地方:空值、重复行、异常格式。本篇文章是数据清洗的核心章节,系统讲解缺失值的检测、删除、填充、插值,以及重复值的识别与处理。
目录
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 等类型转换武器。
文章回复
0 条公开回复