本节摘要:重复记录让指标虚高、让连接膨胀,是清洗里最"隐形"的损耗。本节围绕 drop_duplicates 展开:整行重复与键重复的判别、keep 三种取值保哪条、去重后的索引空洞怎么补,以及"看起来不一样、其实一样"的伪不重复。承接 2.2 的异常处置,通往 2.4 的类型矫正。
「idempotent」,中文叫幂等——同一操作执行多次与执行一次,结果相同。数据管道最看重这个性质,而去重正是幂等性的守门员:清洗脚本跑第二遍,数据不能再多出一行。一次重试、一次补传、一次系统重放,都会让上游表里多出几批一模一样的记录。所以择菜间把去重单独设一道工序:先判哪些算重复,再定保哪一条,最后把现场收拾干净。往前承接 2.2 的异常安置,往后把规整的料交给 2.4 做类型矫正。
整行重复:所有列完全一致,多半是重放或补传造成,直接去重即可。键重复:订单号相同但其余列有差异——可能是状态流转的快照(同一单先支付后退款),也可能是数据打架,前者要"保最新",后者要人工裁决。两类混着处理必出事故:对状态快照表做整行判断,会发现"没有重复",其实键早就撞了。动手前先跑一遍诊断,是去重的第一纪律。
**df.duplicated(subset=None, keep='first')**:返回布尔面具,标记"这一行是不是重复出现"。subset 指定按哪些列判重——不写就是全列;keep='first' 保留首次出现、后续标 True;keep='last' 反过来;keep=False 把所有重复出现的行都标 True(一条不留),适合"先把嫌疑人全揪出来看看"的诊断阶段。**df.drop_duplicates(subset=None, keep='first', ignore_index=False)**:按同样的逻辑直接删;ignore_index=True 顺手重排索引,省掉事后 reset_index 的一步。
import pandas as pd df = pd.DataFrame({ "订单号": ["A1", "A2", "A1", "A3", "A2"], "状态": ["支付", "支付", "支付", "创建", "退款"], "金额": [99.0, 59.0, 99.0, 45.0, 59.0], }) # 诊断:按订单号判重,把所有嫌疑行都标出来 print(df[df.duplicated(subset=["订单号"], keep=False)]) # 订单号 状态 金额 # 0 A1 支付 99.0 # 2 A1 支付 99.0 <- 整行重复,重放痕迹 # 1 A2 支付 59.0 # 4 A2 退款 59.0 <- 键重复,状态流转快照
诊断结果分两类:A1 是整行重复,随便保哪条都行;A2 是状态快照,业务上要保最新状态(退款)。正确做法是先按时间排好序,再 keep='last' 保最新——排序与去重是组合拳。
# 给快照表补一个时间列(真实场景来自上游表) df["更新时间"] = pd.to_datetime([ "2024-05-01 10:00", "2024-05-01 10:05", "2024-05-01 10:00", "2024-05-01 09:30", "2024-05-01 11:00", ]) # 第一步:整行重复直接去 df = df.drop_duplicates().reset_index(drop=True) # 第二步:键重复保最新——先按时间排,再 keep="last" df = (df.sort_values("更新时间") .drop_duplicates(subset=["订单号"], keep="last") .reset_index(drop=True)) print(df) # 订单号 状态 金额 更新时间 # 0 A3 创建 45.0 2024-05-01 09:30:00 # 1 A1 支付 99.0 2024-05-01 10:00:00 # 2 A2 退款 59.0 2024-05-01 11:00:00
注意行序:keep='last' 配 sort_values 之后,结果按时间升序排列;要恢复业务顺序,最后再 sort_values("订单号") 一次。去重不是一步函数调用,而是"诊断、分类、排序、去重、复位"的完整流程。
去重前先问一句:业务要的是"每类留一条"还是"数清有几类"?前者才是 drop_duplicates 的活;后者用 value_counts 或 4.1 的 size 直接数,不该动数据本身。"重复出现"在某些业务里就是信息本身——刷单检测、重试分析,数重复比删重复有价值。分清这两种诉求,再去重就不容易下手过重。
翻车一:伪不重复。" A102"(带前导空格)与"A102"、"alice@mail.com"与"ALICE@MAIL.COM",肉眼一样,机器眼里是不同行。去重前先做规范化:文本列 strip 去空白、统一大小写,再判重。**翻车二:索引空洞。**drop_duplicates 默认保留原索引,删过的表索引不连续,之后按位置取数容易错位——养成 ignore_index=True 或事后 reset_index(drop=True) 的习惯。**翻车三:keep 的方向搞反。**没排序就 keep='last',保住的可能是最早那条旧状态;keep 的方向必须和排序方向绑在一起检查。
要"保最新"还有个聚合路线:groupby 之后按更新时间取最大行,或用 idxmax 定位再 loc 取回——语义更显式,适合写成团队通用函数。纯 Python 的列表去重保序,用 dict.fromkeys(items) 一行搞定,它靠字典键的唯一性实现了有序去重。至于"允许重复但要计数"的场景,别去重,用 1.2 提过的 Counter 或 groupby + size 数清楚。
重复清完,还有一类"型不对"的脏:数字躺在文本里、时间躺在一串字符里。2.4 请出 to_numeric 与 to_datetime 两位翻译,把错型的列安全送回正规类型。