本文介绍如何在 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),再筛选出非最后一组的所有行
。以下是推荐解法:
输出:
✅
原理详解
:
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 中处理有序重复模式的惯用范式。
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