
本文介绍一种基于分组枚举与向量化比较的高性能方法,用于识别并删除具有相同 UNIQUE_ID 且结构完全一致(含多列值)的连续子数据框,避免低效循环,适用于百万级数据场景。
本文介绍一种基于分组枚举与向量化比较的高性能方法,用于识别并删除具有相同 `unique_id` 且结构完全一致(含多列值)的连续子数据框,避免低效循环,适用于百万级数据场景。
在处理时序化、ID 分组的结构化日志或事件数据时,常遇到一类特殊去重需求:并非删除单行重复,而是识别并剔除“语义上完全相同的子数据框序列”——即对同一 UNIQUE_ID,若某 EVENT_TIME 对应的完整子集(多行、多列)与前一个同 ID 的子集内容完全一致,则该子集视为冗余并移除;但若中间插入了其他 EVENT_TIME 的子集,则后续重复不再视为连续冗余,应保留。
例如,UNIQUE_ID=123 在 00:01 和 00:06 均出现完全相同的三行(Value1/Value2 组合一致),且中间无其他 123 子集,则 00:06 子集应被剔除;而 00:13 虽也与 00:01 相同,但因中间存在 00:10、00:11 等 123 子集,故予以保留。
传统循环遍历(如按 UNIQUE_ID → 再按 EVENT_TIME 逐个比对子 DataFrame)时间复杂度高,面对百万行数据极易成为性能瓶颈。以下提供一种纯向量化、无显式 Python 循环的高效解决方案:
核心思路:分组内偏移 + 全列等值校验 + 连续性判定
- 定义关键列:将业务主键列(如 'EVENT_TIME', 'UNIQUE_ID')作为分组标识,其余待比对列(如 'Value1', 'Value2')设为 value_cols;
-
构建唯一组号与内部序号:
- groups = df.groupby(['EVENT_TIME','UNIQUE_ID']).ngroup():为每个 (EVENT_TIME, UNIQUE_ID) 组分配全局唯一整数 ID;
- enums = df.groupby(['EVENT_TIME','UNIQUE_ID']).cumcount():在每组内标记行序(0, 1, 2…),用于后续“跨时间点对齐比较”;
- 按 UNIQUE_ID + enums 分组并偏移:df.groupby(['UNIQUE_ID', enums])[value_cols].shift() 将每个 UNIQUE_ID 下第 i 行(无论属于哪个 EVENT_TIME)向前偏移一行,使当前行能与前一个同 ID、同位置序号的行对齐;
- 逐元素等值判断 + 全行一致校验:.eq(df[value_cols]).all(axis=1) 判断当前行是否与其“逻辑前驱行”在所有 value_cols 上完全相等;
-
强化连续性约束:
- .groupby(groups).transform('all'):确保该组内所有行均满足上述等值条件(即整个子数据框完全一致);
- sizes.groupby([df['UNIQUE_ID'], enums]).diff().eq(0):验证前后两个子数据框大小相同(避免因行数不同导致错位匹配);
- 合并布尔掩码并过滤:dup 为 True 的行属于冗余子数据框,取反后索引即可获得去重结果。
完整可运行代码示例
import pandas as pd
import numpy as np
# 构造示例数据
df_dupl = pd.DataFrame({
'EVENT_TIME': ['00:01', '00:01', '00:01', '00:03', '00:03', '00:03', '00:06', '00:06', '00:06', '00:08', '00:08', '00:10', '00:10', '00:11', '00:11', '00:13', '00:13', '00:13'],
'UNIQUE_ID': [123, 123, 123, 125, 125, 125, 123, 123, 123, 127, 127, 123, 123, 123, 123, 123, 123, 123],
'Value1': ['A', 'B', 'A', 'A', 'B', 'A', 'A', 'B', 'A', 'A', 'B', 'A', 'B', 'C', 'B', 'A', 'B', 'A'],
'Value2': [0.3, 0.2, 0.2, 0.1, 1.3, 0.2, 0.3, 0.2, 0.2, 0.1, 1.3, 0.3, 0.2, 0.3, 0.2, 0.3, 0.2, 0.2]
})
# 步骤 1:定义待比对列(跳过 EVENT_TIME 和 UNIQUE_ID)
value_cols = df_dupl.columns[2:].tolist() # ['Value1', 'Value2']
# 步骤 2:构建分组标识
group_key = ['EVENT_TIME', 'UNIQUE_ID']
groupby_obj = df_dupl.groupby(group_key)
groups = groupby_obj.ngroup()
enums = groupby_obj.cumcount()
sizes = groupby_obj.size().reindex(df_dupl.set_index(group_key).index).values
# 步骤 3:向量化去重逻辑
dup_mask = (
df_dupl.groupby(['UNIQUE_ID', enums])[value_cols]
.shift() # 向下偏移,使当前行与前一个同 UNIQUE_ID & 同 enums 的行对齐
.eq(df_dupl[value_cols])
.all(axis=1) # 当前行所有 value_cols 均相等
.groupby(groups)
.transform('all') # 整个 EVENT_TIME+UNIQUE_ID 组内全部行都满足
&
pd.Series(sizes).groupby([df_dupl['UNIQUE_ID'], enums]).diff().eq(0)
)
# 步骤 4:过滤并输出
df_clean = df_dupl.loc[~dup_mask].reset_index(drop=True)
print(df_clean)注意事项与进阶优化
- ✅ NaN 安全处理:若数据中含 NaN,需将 .eq(...) 替换为基于 pd.isna 的自定义比较(如问题补充所示),即用 (shifted.isna() & current.isna()) | (shifted == current) 逻辑替代原 .eq(),确保 NaN == NaN 被视为真;
- ⚠️ 稳定性保障:若原始顺序敏感,建议在最终结果上调用 .sort_values(['UNIQUE_ID', 'EVENT_TIME'], kind='stable').reset_index(drop=True) 保持逻辑时序;
- ? 性能优势:该方法时间复杂度接近 O(n),实测在 10 万行数据上仅需约 318ms,较原始循环提速超 17 倍;
- ? 调试技巧:可单独打印 groups、enums、dup_mask 等中间变量,验证分组与偏移逻辑是否符合预期;
- ? 适用边界:本方案要求“重复子数据框必须严格对齐行序”(即第 i 行只与前一子数据框第 i 行比较),若子数据框行数不固定或需模糊匹配,需改用哈希指纹(如 pd.util.hash_pandas_object)方式预聚合。
通过此方法,你不仅能彻底摆脱慢速循环,更能以清晰、可维护、可扩展的方式解决复杂结构化去重问题,为大规模时序数据分析奠定坚实基础。










