
本文介绍如何在 pandas DataFrame 中识别并仅移除末尾连续重复的行组(基于指定列),保留所有前面的重复块(包括中间重复段),实现“截断尾部重复块、保留首次完整出现”的逻辑,适用于日志截断、会话归档等场景。
本文介绍如何在 pandas dataframe 中识别并**仅移除末尾连续重复的行组**(基于指定列),保留所有前面的重复块(包括中间重复段),实现“截断尾部重复块、保留首次完整出现”的逻辑,适用于日志截断、会话归档等场景。
在实际数据分析中,我们常需处理具有时间序或自然顺序的 DataFrame(如日志、传感器读数、用户行为流),其中某些记录可能在末尾连续重复出现多次(例如系统重复上报最后几条状态)。此时,若使用 drop_duplicates(keep=’first’) 会全局去重,误删中间已出现过的合法重复;而 keep=’last’ 则只留最后一个——均不符合“仅清理尾部冗余、保留历史完整性”的需求。
正确思路是:将数据按指定列(如 ‘name’, ‘age’)分组为连续块(run-Length encoding),识别最后一个重复块,并仅排除该块中除首行外的所有后续行(即截断尾部连续重复段)。核心在于区分“全局重复”与“尾部连续重复”。
以下代码实现了这一逻辑:
import pandas as pd df = pd.DataFrame({ 'id': [1,2,3,4,5,6,7,8,9,10], 'name': ['mary','mary','mary','tom','tom','john','sarah','tom','tom','tom'], 'age': [30,30,30,25,25,28,36,25,25,25] }) # 指定用于判断重复的列(忽略 'id') cols = ['name', 'age'] # 步骤1:生成连续组标识(cumulative group ID) # df[cols].shift() 将每列下移一行,ne() 判断是否与上一行不同 → 每组起始行为 True # any(axis=1) 表示任意列变化即视为新组开始,cumsum() 累计求和生成组ID grp = df[cols].ne(df[cols].shift()).any(axis=1).cumsum() # 步骤2:构造布尔掩码 —— 仅保留组ID不等于最大组ID的行(即排除最后一组全部?不对!) # 实际关键:用 grp.shift().ne(grp.max()) —— 检查当前行的前一组ID是否 ≠ 最大组ID # 这样最后一组的首行(其 shift() 是倒数第二组ID)仍为 True,但后续行因 shift() == grp.max() 而为 False cond = grp.shift().ne(grp.max()) out = df[cond].reset_index(drop=True) print(out)
输出结果:
id name age 0 1 mary 30 1 2 mary 30 2 3 mary 30 3 4 tom 25 4 5 tom 25 5 6 john 28 6 7 sarah 36 7 8 tom 25
✅ 符合预期:保留了前两段 ‘mary’ 和 ‘tom’ 的完整块,也保留了 ‘john’、’sarah’,仅截断了末尾连续三次出现的 ‘tom’+’25’ 块,仅留下该块的首行(id=8)。
关键原理说明
- df[cols].ne(df[cols].shift()) 逐行比较当前值与上一行是否不同,返回布尔 DataFrame;
- .any(axis=1) 将每行任一列为 True 视为“组切换点”;
- .cumsum() 将布尔序列转为递增整数组 ID(如 [1,1,1,2,2,3,4,5,5,5]);
- grp.shift() 使组 ID 序列下移,首行为 NaN,便于对齐判断“当前行是否属于最后一组的延续”;
- grp.shift().ne(grp.max()) 返回 True 当且仅当当前行的前一组不是最后一组 —— 即:最后一组的首行(其 shift() 是倒数第二组 ID)仍被保留,而该组后续行因 shift() 等于 grp.max() 而被过滤。
注意事项
- 此方法依赖行序,确保 DataFrame 已按业务逻辑(如时间戳、索引)正确排序;
- 若需基于时间列排序,务必先执行 df.sort_values(‘timestamp’, ignore_index=True);
- cols 必须为需要参与重复判定的列,不可包含唯一标识列(如 id、log_id);
- 若最后一组仅有一行,则 cond 全为 True,结果等价于原 DataFrame;
- 性能友好,全程向量化操作,无需 apply 或循环,适合大规模数据。
该方案精准解决了“保留所有历史重复块,仅清理尾部连续冗余”的典型工程需求,是 drop_duplicates 的重要补充策略。