尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
pandas去重实战:drop_duplicates与duplicated参数详解及避坑指南
我这几年代人做数据清洗最常听的一句话就是把重复的删掉就行。 说这话的时候大家都很轻松仿佛重复值是什么一眼就能看穿的东西。可真动手之后才发现drop_duplicates()一行代码确实能跑但跑完之后的结果到底对不对完全是另一回事。删除重复值这件事表面上是 pandas 的基础操作实际上牵扯到一个核心问题什么样的行在你的业务场景里才算重复是订单号相同就算重复还是用户ID加下单时间完全一致才算重复甚至更进一步同一用户下了一模一样的单到底该保留第一次的还是保留最新的这些决策直接决定了你去重之后的数据质量也决定了后面做的所有统计、分析和报表是不是靠谱。这篇文章我只讲一件事pandas 里去除重复值的完整打法。我会把drop_duplicates()和duplicated()的每一个参数聊透把什么叫重复讲明白把索引错乱、性能退化、NaN参与去重这类坑一个个排掉。内容适合刚接触 pandas 的初学者也适合已经写了半年一年数据处理脚本、但偶尔还会被重复值坑一把的同学。全文用订单数据和用户行为数据做示例代码都是可以直接拿去改着用的。1. 去重不是删行是回答什么叫相同先别急着写代码。我一直觉得pandas 的drop_duplicates()之所以容易用错是因为大家把它理解成了删掉长得一样的行于是参数随手一填就完事。实际上去重动作背后藏着一个业务定义你要认定哪些列相同才算同一条记录。举个例子。某次我在处理一份用户浏览日志字段包括user_id、page_url、visit_time、device。这份日志里出现了大量用户在一分钟内反复点击同一个链接的数据产品经理想统计独立访问量希望把这些短时间重复去掉。这时候如果直接import pandas as pd df pd.read_csv(user_log.csv) df df.drop_duplicates()那等于什么都没做——因为两次点击的visit_time精确到秒几乎不可能完全一致pandas 默认按所有列完全相等来判断结果一条都不会删。正确的做法是先想清楚业务上的重复定义。对于短时间重复点击这个场景应该按user_id和page_url来判定跟visit_time和device没关系df_dedup df.drop_duplicates(subset[user_id, page_url])这样才算把同一用户在同一页面上的重复访问去掉了。这里你可能会问那保留哪一行呢是保留第一次访问还是保留最后一次这又牵涉到下一个参数。但至少从判断标准上看subset决定了什么叫做重复。所以每次写去重代码之前先花十秒钟回答三个问题以哪些列作为唯一标识如果有多列是这几列都相同才算重复还是任意一列相同就算重复pandas 默认是都相同大多数业务也是这个逻辑重复的行里保留哪一条这三个问题的答案就是drop_duplicates()的subset和keep两个参数。别小看这两个参数后面所有复杂场景都是它们的组合。2. drop_duplicates 的参数语义不只是 keep first 和 keep last2.1 subset 的边界列少了一个结果差一个数量级subset参数接受一个列名列表。它的作用是告诉 pandas我只拿这些列做比较其他列我不管。我用一个很经典的订单表场景来说。假设有一份订单明细结构是这样的order_iduser_idproduct_idorder_timediscountA001U01P1002024-03-01 10:00:000.9A002U01P1002024-03-01 10:00:000.85A003U01P1002024-03-01 10:00:000.9这三行里user_id、product_id、order_time都相同但order_id和discount不同。它们算不算重复如果业务上认为同一用户在同一时间买了同一商品只算一次那么它们就是重复如果认为这是两笔独立的订单那它们就是正常业务数据。这不只是技术问题。我当时处理这类数据时业务方常用的口径是用户在同一秒内买同一个商品大概率是前端重复提交所以判定标准就是user_id product_id order_time。于是去重代码是df_orders_dedup df_orders.drop_duplicates( subset[user_id, product_id, order_time] )关键点是subset是业务唯一键的映射。你在数据分析里见过primary key的概念吧subset就是你在 pandas 里临时定义的一个复合主键。还有一个容易忽略的细节subset里列的先后顺序不影响判断结果。[user_id, product_id]和[product_id, user_id]的判断逻辑完全一样因为 pandas 是按多列的联合值来比较的而不是按顺序先后比较。这一点和某些数据库索引不同不必担心。2.2 keep 的真正含义保留哪一行取决于你的最新定义keep参数有三个可选值first、last、False。keepfirst保留相同记录里第一次出现的那一行。keeplast保留相同记录里最后一次出现的那一行。keepFalse把所有重复行全部删掉保留的全都是只出现一次的行。很多教程会这样解释但没告诉你的是keepfirst和keeplast并不是简单的保留第一行还是最后一行而是以数据当前的排列顺序为准。如果你想按时间最新的那条来保留就需要先排序否则keeplast保留的可能只是 DataFrame 里排序靠后的行而不是业务上最新的一条。举个我实际遇到的例子。业务方需要生成一份每个商品的当前有效配置数据来自一份历史上 30 天内每天更新一次的配置快照。同一个product_id会有多行每行带update_date我们要保留的是最新一次更新的配置。如果数据本身是按update_date从小到大排列的那直接写latest_cfg df_cfg.drop_duplicates( subset[product_id], keeplast )是没问题的。但如果原始数据没有按时间排甚至顺序是乱的那这个写法就危险了。正确姿势是先把时间排序再去重df_cfg_sorted df_cfg.sort_values( [product_id, update_date], ascending[True, True] ) latest_cfg df_cfg_sorted.drop_duplicates( subset[product_id], keeplast )先排序、再去重、再取last这三步是一个组合拳。我这里按update_date升序排排完后每一组重复记录里最后一行就是最新配置。你问为什么不能直接drop_duplicates之后再排序能但那样你筛选出来的行可能不是你想要的那条再去排序只是给错误结果排了个序而已。顺序问题一定要放在去重之前解决。2.3 keepFalse 的威力把重复行变成可追溯的异常keepFalse的价值很多人低估了。它的作用是把出现多次的记录全部剔除保留下来的每一行在指定的subset上都是唯一的。这个参数在清洗脏数据场景里特别好用。比如说你有一份用户基础信息表理论上user_id应该是唯一的但导入过程中出了问题某些user_id出现了两三次。如果你只保留其中一条万一保留了错误的那条后续发消息、打标签全都会出错。这时候更稳妥的策略是先把重复的用户全部揪出来单独做人审。dup_marks df_user.duplicated(subset[user_id], keepFalse) df_dup_users df_user[dup_marks]这段代码里duplicated(keepFalse)会把这组重复记录的所有行都标记为True然后用布尔索引把它们全取出来。这样你拿到的就是所有异常行而不是其中一行。等人工核对完决定哪条是有效数据之后再单独放回去。这种做法比默认的keepfirst更稳妥。默认的keepfirst会直接保留第一条如果第一条本身就是坏数据你连发现的机会都没有。做数据清洗宁可多跑一步检查也不要让脏数据悄悄漏过去。3. 不只是删行用 duplicated 做标记、统计和异常发现3.1 duplicated() 才是数据质量审计的核心工具很多同学用到drop_duplicates()就去干别的了但我会建议你在删之前先花三秒钟用duplicated()看一眼数据里到底有多少重复。duplicated()返回的是一个布尔型的 Series每一行如果是重复行即当前行之前出现过一次或多次就标记为True。它不是用来删数据的而是用来做标记的。这个标记功能在数据质量审计里极为有用。比如我在每次跑批任务之前都会生成一份重复率快报dup_count df.duplicated(subset[user_id, log_date]).sum() total_count len(df) dup_rate dup_count / total_count print(f待核查重复记录数: {dup_count}) print(f重复率: {dup_rate:.2%})重复率超过某个阈值我会先停下手里的分析回头查数据源。因为一条正常的数据流水重复率通常不会很高一旦超过 5% 或者 10%大概率不是业务问题而是上游同步任务重复执行了。你要是闷头去重等于把上游的 bug 掩盖掉了。3.2 把重复标记变成一列保留完整证据链有一种更实用的做法不去删行而是把是否重复的结果作为新的一列保存下来。这样后续分析时可以随时筛选也可以作为这条数据被清洗过的凭证。df[is_dup] df.duplicated(subset[user_id, order_id], keepFalse) df_suspicious df[df[is_dup]]加上keepFalse后是重复的每一行都会标成True。我把这些True行单独拉出来看的时候可以同时看到重复的每一行长什么样确认到底哪一列有差异为什么会被判成重复以及是否有合并的可能。这个证据链思路在正式项目里非常重要。很多人做数据清洗是闷头删删完之后老板问你删了什么为什么删答不上来。保留一个标记列随时能给出一个清单哪些行是重复的、判定依据是哪几列、保留的是哪一行。这一套下来数据团队和业务方的信任感会好很多。3.3 用分组统计识别同款但不同价的疑似重复还有一种情况比完全重复更隐蔽业务上高度疑似重复但没有哪几列是完全相等的。比如同一用户同一个收货地址在 10 分钟内连续下了两单都买了同一个商品。这种情况下order_id不同、order_time不同drop_duplicates()根本没法定义它们重复。但业务方会觉得这就是重复下单。遇到这种场景pandas 去重的常规参数不够用我一般配合分组统计来找出疑似记录suspect ( df.groupby([user_id, receiver, product_id], as_indexFalse) .agg( order_cnt(order_id, nunique), min_time(order_time, min), max_time(order_time, max) ) ) suspect suspect[ (suspect[order_cnt] 2) (suspect[max_time] - suspect[min_time] pd.Timedelta(minutes10)) ]这段代码先按用户、收货人和商品分组统计下单次数和最早最晚时间再筛出10 分钟内同人同地址同商品下过两单以上的记录。这些是疑似重复通常需要业务方确认是否只保留一单。我特别想强调这一点去重不永远是技术问题很多时候是口径问题。工具能帮你执行规则但规则本身得靠业务确认。所以我做这类任务的习惯是先跑duplicated()看全量重复情况再跑分组统计找疑似重复最后把两份结果汇总成一张问题清单再动手删除。这样每一步都有据可查。4. 去重之后的连锁反应索引、就地修改和性能4.1 索引错乱删完行后为什么不连续了drop_duplicates()删掉行之后DataFrame 的行索引并不会自动重新排列被删掉的行的索引位置会直接消失。比如一份 1000 行的数据去重后剩 800 行索引可能是0, 1, 3, 5, 6...中间缺了很多。这在后续做df.loc[某个索引]的时候会出大问题。最常见的就是你根据一个索引列表去另一个表里取数据结果索引对不上取回了一堆NaN。解决方案简单粗暴去重后立刻重置索引。df_clean df.drop_duplicates(subset[user_id]).reset_index(dropTrue)这里的dropTrue表示不要把原来的索引变成新列。如果你不加pandas 会把旧的索引保留为一个叫index的列你的 DataFrame 里就多出一列莫名其妙的数据。这个小细节很多人第一次都会遇到。4.2 inplaceTrue 真的省内存吗别被名字骗了关于inplaceTrue网上的说法很混乱。我的建议是没事别用。df.drop_duplicates(subset[user_id], inplaceTrue)这行代码确实能就地修改df。但 pandas 底层并不是完全不创建新对象很多inplace操作依然会先算出结果再替换内存上的好处没有想象中大。更麻烦的是inplaceTrue会让代码的链式调用和回溯变得很难检查——你没法清楚地知道当前这个 DataFrame 到底是什么状态。我自己的习惯是始终显式赋值。df_processed df.drop_duplicates(subset[user_id]).copy()这里多说一句copy()。drop_duplicates()返回的数据可能是原始数据的视图也可能是副本具体取决于 pandas 内部优化。如果你去重之后还要继续修改列为了避免SettingWithCopyWarning的警告提前.copy()是最省心的做法。这个警告不致命但留着很烦人而且说明你的代码可能在修改一个副本改了等于没改。4.3 上百万行的 DataFrame 去重为什么慢怎么提速当数据量到百万行以上drop_duplicates()有时会变得很慢。原因在于 pandas 需要把所有行进行两两比较或者内部构建哈希结构而这个过程中如果subset包含文本类列比如很长的 URL、备注文本哈希计算成本就会很高。几个实测有效的提速方案先删无用列再去重。如果分析根本不需要某些列先用df[[user_id, page_url]]取出子集再去重可以大幅减少比较的列数。把分类列转成 category 类型。对于一些取值种类有限的列转成category之后 pandas 的比较会走整数路径速度会快不少。df[user_id] df[user_id].astype(category) df_dedup df.drop_duplicates(subset[user_id, page_url])不过要小心转category之后如果后续要和其他 DataFrame 做合并可能遇到类别不一致的问题用完建议再转回object或者string。先排序再去重。对于部分数据先sort_values()把相同记录聚到一起再去重底层比较的局部性变好速度也会改善。但这不是所有场景都有效我在字符串列上试过有时反而更慢。建议拿真实数据做个基准测试再决定。用dask处理超大文件。如果数据量已经到了几十个 G单机 pandas 就不太合适了。dask 的drop_duplicates接口和 pandas 几乎一致可以先把dask.dataframe跑起来最后转回 pandas。这类场景我碰到的不多但每次碰到都是直接用 dask 解决不用硬扛。这些性能优化说白了是在跟 pandas 的内部实现打交道不同版本、不同数据特性下结论会不一样。所以我会建议先按默认方式跑一遍用time记录耗时再针对性地优化。不要一上来就把代码写得很复杂。5. 我踩过的那些坑NaN、空格、浮点数和类型陷阱5.1 NaN 参与去重时的微妙行为最典型的翻车现场是数据里有一列是空值而你恰好把它放进了subset。pandas 的规则是两个NaN在去重判断时会当成相等——也就是说同一用户 ID 下多行都是空值的时候这些行会被判定为重复。这在业务上可能不对。比如你统计用户收货地址时有些用户还没填地址地址列全是NaN但你依然希望保留每个用户的一条记录——不这里情况更复杂如果按[user_id, address]去重一个用户所有没填地址的记录会被合并成一条。如果业务想保留其中一条带具体时间信息的记录那么这种去重就会丢掉信息。我的处理方式非常直接把需要参与去重的字段先填充或者把空值字段排除在 subset 之外。df[address_filled] df[address].fillna(__EMPTY__) df_dedup df.drop_duplicates(subset[user_id, address_filled])暂时用一个统一占位符把空值标记出来既不会让多个空值直接合并又能一眼看出哪些是缺失数据。如果你确认缺失值不需要保留那也可以直接先dropna(subset[address])再做去重。关键在于做去重之前你对缺失值要有明确的策略而不是把判断全部交给 pandas 的默认行为。5.2 字符串里藏空格去重结果怎么看都怪某次我去重一份渠道名称表发现怎么去都去不干净。明明channel_name打印出来都叫官网注册但duplicated()就是认为不重复。排查到最后才发现一部分记录写的是官网注册另一部分写的是官网注册 末尾多了一个空格print 出来肉眼看毫无区别实际上是两个不同的字符串。从此我养成了一个习惯对字符串列做去重前先统一格式化。df[channel_name] df[channel_name].astype(str).str.strip().str.lower()strip()去掉首尾空格lower()把小写统一大小写不同造成的假不重复也就解决了。再遇到更复杂的比如全角和半角括号混用、中文和英文标点混用那还要做更细致的归一化但空格和大小写这两条是最常见的。想快速发现这类问题可以去重前跑一遍检查# 找出长度异常或者包含空格的唯一值 odd_vals df[df[channel_name].str.contains(r\s, regexTrue)][channel_name].unique() print(odd_vals)用正则把带空白的值全揪出来看一遍看得多了基本上就知道这种表格的脏集中在哪几类。5.3 浮点数的精度问题会让明显相同的值判为不同浮点数和去重的关系也是个大坑。举个最简单的例子0.1 0.2 0.3这个表达式在 Python 里是False因为浮点数在计算机里是二进制近似存储的。类似的如果你的数据里有一个amount字段是浮点型两行业务上都是0.3但一个是通过0.1 0.2算出来的一个是直接输入0.3它们底层存储的二进制值是不一样的。drop_duplicates(subset[amount])就会漏掉这个重复。处理这类问题的思路是不要直接对浮点列去重先格式化。比如金额保留两位小数可以转成整数分处理df[amount_cents] (df[amount] * 100).round().astype(int64) df_dedup df.drop_duplicates(subset[user_id, amount_cents])或者直接用round()统一精度df[amount_rounded] df[amount].round(6) df_dedup df.drop_duplicates(subset[user_id, amount_rounded])按多少位精度取决于业务但思路是一样的先把看起来差不多的数变成完全相等的数再做去重。这属于数据标准化的一部分做数据清洗时间长了你会发现很多去重问题本质上是类型和精度问题。5.4 类型不一致导致的假不重复浮点数之外还有一类问题是同一列里有的值是int有的是str。最典型的场景是从 Excel 导入数据后某些单元格被 pandas 读成了1这种整数某些被读成了1这种字符串。drop_duplicates(subset[customer_id])会认为1和1不是重复。我的排查方法很朴素去重之前先打印这一列的dtype再用value_counts()看看有没有异常值。print(df[customer_id].dtype) print(df[customer_id].value_counts(dropnaFalse).head(20))一旦发现类型混乱统一转成字符串df[customer_id] df[customer_id].astype(str).str.strip()转完之后再去重那些隐藏的重复就现出原形了。所有类型的坑总结成一句话就是让同一列的取值先处于同一个语义和存储水平再去谈去重。这个水平不统一用什么参数都是白搭。6. 一份可以直接抄作业的去重决策清单每次做数据清洗我会拿出下面这张表在脑子里过一遍。它不是为了教条而是为了在重复决策时提醒自己当前场景更偏向哪种逻辑。业务场景subset 怎么定keep 怎么选额外动作按用户去重保留首次记录[user_id]first先按时间升序排序按用户去重保留最新记录[user_id]last先按时间升序排序去除完全相同的数据行默认全列first先统一字符串空格和大小写漏斗统计独立用户数[user_id, event_date]first确认是否按天统计清除疑似重复下单[user_id, product_id]视业务而定先确认时间窗和重复判定口径把重复行全部剔出人工审核[user_id]用duplicated(keepFalse)标记保留标记列别删金额或分数类字段去重[user_id, score]视业务而定先 round 统一精度再比大批量日志去重[user_id, page_url, ts]first可考虑按时间排序或换 dask这张表不是标准答案每个项目的唯一键和保留策略都有差异。我之所以整理成表格是因为大多数时候你只需要把业务场景套进去再微调两三个细节去重代码基本不会跑偏。如果这篇里只能带走一样东西我希望是这一点删行之前先用量化的方式确认你处理的是重复问题而不是数据源质量问题。我用duplicated().sum()和duplicated(keepFalse)筛出可疑数据的时间远比直接跑drop_duplicates()的时间多。但正是这一步让最终交付的数据能经得住业务方的追问。最后分享一个我自己的小习惯正式脚本里我几乎不用drop_duplicates()直接覆盖原数据而是保留一步.assign(is_dup...)的中间状态把重复标记留下来。哪怕后面确认所有逻辑都没问题这列也不会对生产造成负担但万一哪天要回溯清洗过程这列就是最完整的现场记录。处理完记得reset_index(dropTrue)——这都是老手刻进肌肉记忆的细节了。
RELATED

相关推荐

钓鱼攻击防范指南:5类典型邮件诈骗案例与员工安全意识培训要点

钓鱼攻击防范指南:5类典型邮件诈骗案例与员工安全意识培训要点

做网络安全意识培训这几年,我开场一般会先问现场同事一个问题:如果周五下午四点,你的直属领导从企业邮箱发来一封邮件,让你马上安排一笔18.8万的付款,收款信息在附件里,还叮嘱你“合同后补、先付出去”&…

📅 2026/10/20 23:39:38
栈与队列:从原理到手写实现,彻底搞懂LIFO和FIFO

栈与队列:从原理到手写实现,彻底搞懂LIFO和FIFO

栈和队列大概是数据结构里最容易被低估的两个东西。很多人学的时候觉得“不就是先进后出和先进先出吗”,然后考试一过就扔了。但真正写过业务代码、调过性能瓶颈的人都知道,这两个结构几乎无处不在:浏览器的前进后退、编辑器的撤销重做、消息…

📅 2026/10/20 23:39:37
气动力辨识的Observer法:从EKF状态空间建模到工程避坑

气动力辨识的Observer法:从EKF状态空间建模到工程避坑

简介:基于Observer(状态观测器)法实现的气动力辨识程序,聚焦航空航天与飞行控制领域,面向需要从飞行器动态行为中估计升力、阻力、侧向力等难以直接测量参数的研究者或工程师,解决气动模型建立与参数辨识难…

📅 2026/10/20 23:34:24
MORE NEWS

更多资讯

📰

语音情感识别实战:双向LSTM+MFCC全流程解析

简介:这是一套基于双向LSTM(BLSTM)的语音情感识别算法项目源码,面向从事语音识别、情感计算研究的工程师与初学者,旨在提供可直接运行的实战基线。传统方案依赖手工特征,而本项目采用BLSTM同时捕捉语音序列…

📰

NPOI v2.2.1 实战:C#/WPF 大数据量 Excel 导入导出

简介:NPOI v2.2.1是面向.NET开发者的开源Office文档处理库,支持通过C#、VB.NET等语言创建、读取和修改Word、Excel文件,无需安装Microsoft Office即可操作。它适合在WPF应用中集成数据导入导出、报表生成、邮件合并与文件转换等功能&#xff…

📰

智能工作流助手的Skill机制:从技能存放到热更新与维护的工程实践

WorkBuddy 的 Skill 机制,是我认为这套工具里最值得认真琢磨的一块。最近我们团队把它从备选工具正式扶正,从跑定时任务、收消息通知,到接提数、报表和流程审批的活,全都往上面压。用多了之后我最大的感触是:一个智能工…

📰

Python日期时间处理全指南:从time到zoneinfo,避开时区与夏令时陷阱

1. 先把"日期时间"这件事拆开:模块划分与核心认知讲真,Python 里日期时间看起来是最不需要上心的模块之一,import datetime就是一行的事。但这些年我见过太多项目栽在它手上,不是不会用,而是没搞明白这套东西…

📰

C语言指针完全指南:从内存地址到野指针与函数指针

1. 指针不是洪水猛兽:它只是一个装着地址的变量很多人学到指针就卡住了,"指针""指针变量""指向"这些词绕来绕去,越绕越晕。我身边不止一个朋友在学到这一章时直接放弃了C语言,说"指针太难了&a…

📰

C#调用OpenVINO+ByteTrack实现工业级多目标追踪

简介:本资源是一个面向C#开发者与计算机视觉初学者的实战型目标检测与追踪Demo,聚焦于在Windows平台下用C#集成OpenVINO推理引擎部署YOLOv8模型,并结合ByteTrack实现多目标实时追踪,适用于智能安防、工业巡检等需低延迟响应的边缘…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬