2.3 数据去重:drop_duplicates 与 keep 的选择


2.3 数据去重:drop_duplicates 与 keep 的选择

本节摘要:重复记录让指标虚高、让连接膨胀,是清洗里最"隐形"的损耗。本节围绕 drop_duplicates 展开:整行重复与键重复的判别、keep 三种取值保哪条、去重后的索引空洞怎么补,以及"看起来不一样、其实一样"的伪不重复。承接 2.2 的异常处置,通往 2.4 的类型矫正。

「idempotent」这个词为什么总在去重现场出现

「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 数清楚。

收档清单

  • 先诊断:duplicated(keep=False) 把嫌疑行全揪出来,分类再动手;
  • keep 三态:first 保首现、last 保末现、False 全标;
  • 组合拳:保最新等于 sort_values 加 keep='last',方向必须对齐;
  • 规范化先行:strip 与统一大小写,消灭伪不重复;
  • 现场收干净:ignore_index=True 免留索引空洞。

重复清完,还有一类"型不对"的脏:数字躺在文本里、时间躺在一串字符里。2.4 请出 to_numeric 与 to_datetime 两位翻译,把错型的列安全送回正规类型。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U