
本文介绍如何在 Pandas DataFrame 中识别并仅保留每组「逻辑重复块」的首次出现部分,剔除尾部连续重复行(忽略唯一标识列如 id),适用于日志去重、会话截断等场景。
本文介绍如何在 pandas dataframe 中识别并仅保留每组「逻辑重复块」的首次出现部分,剔除尾部连续重复行(忽略唯一标识列如 `id`),适用于日志去重、会话截断等场景。
在实际数据分析中,我们常遇到一种特殊重复模式:数据按时间或顺序排列,同一业务逻辑记录(如用户行为、会话状态)可能连续多次出现,而我们希望仅保留该重复块的首次完整出现,后续连续重复块则整体舍弃——这与 drop_duplicates(keep='first') 的全局去重不同,也不同于 duplicated() 的逐行标记。本例即典型:以 name 和 age 为业务键,id 仅为序号;tom/25 在索引 3–4 首次成组出现,之后在索引 7–9 再次连续出现,目标是保留第一次 tom/25 组(行3–4),但只保留第二次 tom/25 组的首行(行7),舍弃其后连续重复行(行8–9)。
实现这一逻辑的关键在于:将连续相同的业务行划分为独立组(run-length grouping),再筛选出非最后一组的所有行。以下是推荐解法:
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:生成连续重复组编号
# df[cols].shift() 向下错位 → 与原值比较是否变化 → any(axis=1) 判断任一列变化 → cumsum() 累计求和形成组ID
grp = df[cols].ne(df[cols].shift()).any(axis=1).cumsum()
# 步骤2:构造布尔掩码:当前组不是最大组号(即排除最后一个连续重复块)
cond = grp != grp.max()
# 步骤3:过滤
result = df[cond].reset_index(drop=True)
print(result)输出:
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
✅ 原理详解:
- df[cols].ne(df[cols].shift()) 返回布尔 DataFrame,标记每行相对于上一行是否发生变更;
- .any(axis=1) 将每行任意列为 True 视为“变化点”;
- .cumsum() 对变化点累积计数,使每个连续相同块获得唯一组号(如 mary/30→1,首个 tom/25→2,john/28→3,sarah/36→4,末段 tom/25→5);
- grp != grp.max() 即排除组号为 5 的所有行,精准截断尾部重复块。
⚠️ 注意事项:
- 此方法依赖数据顺序性,确保业务上连续重复具有语义意义(如时间序列、日志流);
- 若需保留尾部块的首行而非整块剔除(如本例中保留行7但不要行8–9),当前解法已满足;若需更复杂策略(如保留每块首行),可改用 groupby(grp).head(1);
- 列选择 cols 必须准确反映业务去重维度,避免遗漏关键字段导致误合并;
- 性能优异,全程向量化操作,适用于百万级数据。
总结:面对「保留首次重复块、截断尾部连续重复」这一非标准去重需求,不应强行套用 drop_duplicates,而应通过 shift + ne + cumsum 构建连续组标识,再结合组号逻辑过滤——这是 Pandas 中处理有序重复模式的惯用范式。










