本节摘要:体检发现问题后,本节系统处理两类最普遍的脏数据——缺失值与重复值。缺失值讲透"为什么缺决定怎么填"的决策逻辑,覆盖删除、均值中位数填充、前后向填充与插值;重复值讲清识别口径与删除策略,避免误删合法数据。
阅读完本节,你应当能够:
同样的缺失值,处理方式完全不同:客户没填手机号,可以留空或填"未知";金额在传输中丢了一截,用均值填可能合理;温度传感器某小时没读数,用前后时刻的值填最自然。脱离"为什么缺"谈"怎么填"都是耍流氓。本节的核心不是背 API,而是建立"原因 → 策略"的决策树。
处理前先做决策,决策树比背 API 重要得多:
💡 关键直觉:判断成因最实用的方法——看缺失值集中在某列还是某几行。集中在一列多是系统性缺失,散落在各行多是随机缺失。
重复有两种:完全重复(整行一模一样)几乎可以确定是错误;部分重复(关键字段相同但其他字段不同)可能是同一实体的多次记录。去重前先问:按什么口径算"同一个"?订单按订单号、客户按手机号、交易按时间戳——subset 参数就是用来指定这个口径的。
| 场景 | 策略 | 代码 |
|---|---|---|
| 随机缺失、比例小 | 删行 | df.dropna(subset=["金额"]) |
| 数值列、右偏分布 | 填中位数 | df["金额"].fillna(df["金额"].median()) |
| 数值列、近似对称 | 填均值 | df["金额"].fillna(df["金额"].mean()) |
| 时间序列 | 前后向填充 | df["值"].ffill() / .bfill() |
| 连续型数据 | 插值 | df["值"].interpolate() |
| 类别列 | 填众数或特殊值 | df["门店"].fillna("未知") |
| 整列缺失超八成 | 删整列 | df.drop(columns=["备注"]) |
import pandas as pd import numpy as np ts = pd.Series([100, np.nan, np.nan, 160, 190]) print(ts.ffill()) # 前向填充: [100,100,100,160,190] print(ts.bfill()) # 后向填充: [100,160,160,160,190] print(ts.interpolate()) # 线性插值: [100,120,140,160,190]
⚠️ 常见坑:插值只适合连续型数据(温度、价格、销售),类别或计数数据插值会造出"不存在的小数"。时序数据用
ffill比用均值更符合"数据连续性"的直觉。
df["金额"] = df["金额"].fillna(df["金额"].median()) print("处理后剩余缺失:", df["金额"].isnull().sum())
每次处理完都验证一遍剩余缺失量——目标通常是 0,若仍有残留,说明填充范围没覆盖全,回头查。
# 识别 df.duplicated() # 逐行布尔 df.duplicated().sum() # 重复行数 df[df.duplicated()] # 查看重复行 # 删除 df.drop_duplicates() # 整行去重 df.drop_duplicates(subset=["订单号"]) # 按订单号去重(保留第一条) df.drop_duplicates(subset=["订单号"], keep="last") # 保留最后一条
⚠️ 常见坑:
keep参数默认保留第一条。如果"最新记录才有效"(比如客户地址更新过),要显式写keep="last",否则删掉的是新数据。
df = pd.DataFrame({ "订单号": ["A001", "A001", "A002", "A003"], "金额": [199, 199, np.nan, 89], "门店": ["北京", "北京", "上海", "广州"], }) # 1. 按订单号去重,保留第一条 df = df.drop_duplicates(subset=["订单号"]) # 2. 缺失金额用中位数填充 df["金额"] = df["金额"].fillna(df["金额"].median()) # 3. 验证 print(df) print("缺失:", df.isnull().sum().sum(), "重复:", df.duplicated().sum())
三步走完,表的缺失与重复归零,且每一步都有明确理由。
清洗决策(填均值还是删行)直接影响后续分析结论。建议在代码旁注释"为什么这么填",或在项目文档里记录数据字典与清洗规则。这样三个月后数据更新了,你能照着规则重跑,而不是重新发明一遍。
| 填充方法 | 适合 | 不适合 | 理由 |
|---|---|---|---|
| 固定值 0 | 明确"无即无" | 金额、评分 | 会扭曲分布 |
| 均值 | 对称分布 | 右偏、有异常 | 被大值带偏 |
| 中位数 | 右偏、有异常 | 分布极不规则 | 抗离群稳健 |
| 众数 | 类别列 | 数值列 | 保持类别分布 |
| ffill/bfill | 时间序列 | 无顺序数据 | 符合连续性 |
| interpolate | 连续数值 | 类别数据 | 插出小数 |
⚠️ 常见坑:把时间序列的缺失值填均值,等于"用全期的平均水平替换局部",会抹掉趋势信息。时间序列优先 ffill 或插值,这是分析正确性层面的选择,不只是口味问题。
"重复"常常由多个字段联合定义,比如"同一客户 + 同一天 + 同一商品"才算重复下单:
df.drop_duplicates(subset=["客户号", "日期", "商品"], keep="first")
多键去重更精确,但也要防过度:键设得太宽(把订单号也加进去)就永远不重复,键设得太窄会误删合法记录。判断标准是业务口径:什么组合代表"同一件事"。
清洗是否有效,用数据说话:
before = df.shape df = df.drop_duplicates(subset=["订单号"]) df["金额"] = df["金额"].fillna(df["金额"].median()) print("清洗前:", before) print("清洗后:", df.shape) print("剩余缺失:", df.isnull().sum().sum()) print("剩余重复:", df.duplicated(subset=["订单号"]).sum())
把"清洗前 → 清洗后 → 剩余问题"打印出来,既是自查也是交付物——告诉别人这份数据经历过什么、现在是什么状态。
缺失和重复清完了,下一节处理"看起来有值但不对"的问题——类型转换、类别编码与数值标准化。