基于键列与值列的Pandas DataFrame去重方案优化问询
需求:保留Pandas DataFrame中同一键对应值发生变化的行
需求说明
现有Pandas DataFrame包含三类列:
- 键列(Key Columns):用于分组的标识列
- 值列(Value Columns):需要监控变化的列
- 其他列(Other Columns):无关列
需求为:仅保留同一键分组下,值列与组内上一行相比发生变化的行。
测试数据生成代码
import pandas as pd import numpy as np import string import random N_keys = 3 N_rows = 20 key_columns = ['key1', 'key2' ] val_columns = ['val1', 'val2',] oth_columns = ['oth1', 'oth2'] random.seed(42) np.random.seed(42) keys = [ tuple([random.choice(string.ascii_letters) for _ in range(len(key_columns))]) for _ in range(N_keys)] last_val_dict = {} data = [] for _ in range(N_rows): # 随机选择一个键 k = random.choice(keys) if k not in last_val_dict: v = [random.choice(string.ascii_letters) for _ in range(len(val_columns))] else: # 50%概率沿用之前的值,50%概率生成新值 if random.random() > 0.5: v = [random.choice(string.ascii_letters) for _ in range(len(val_columns))] else: v = last_val_dict[k] last_val_dict[k] = v o = [np.random.randn() for _ in range(len(oth_columns))] data.append({col:val for col, val in zip( key_columns+val_columns+oth_columns, list(k)+v+o )}) df = pd.DataFrame(data) # 手动修改一行的值用于测试 df.loc[5, 'val1'] = 'I' df.loc[5, 'val2'] = 'f'
测试数据示例
key1 key2 val1 val2 oth1 oth2 0 O h i V 0.496714 -0.138264 1 O h I f 0.647689 1.523030 (*) 需保留,因(O,h)的值从(i,V)变为(I,f) 2 r p B c -0.234153 -0.234137 3 O h I f 1.579213 0.767435 (*) 需移除,因(O,h)的值保持为(I,f) 4 O h b J -0.469474 0.542560 5 O h I f -0.463418 -0.465730 (*) 需保留,因(O,h)的值从(b,J)变为(I,f) 6 b V o C 0.241962 -1.913280 7 r p B c -1.724918 -0.562288 8 O h k S -1.012831 0.314247 9 b V o C -0.908024 -1.412304 10 O h k S 1.465649 -0.225776 11 b V o C 0.067528 -1.424748 12 b V o C -0.544383 0.110923 13 b V Z c -1.150994 0.375698 14 r p B c -0.600639 -0.291694 15 O h y f -0.601707 1.852278 16 r p B c -0.013497 -1.057711 17 r p x K 0.822545 -1.220844 18 O h c Q 0.208864 -1.959670 19 O h f o -1.328186 0.196861
当前实现逻辑
df_new = pd.concat( [key_df[ (key_df[val_columns] != key_df[val_columns].shift(1)).all(axis=1) ] \ for keys, key_df in df.groupby(key_columns) ] ,axis=0)
优化建议与遗漏点检查
性能优化
当前实现通过循环分组后concat子DataFrame的方式,在数据量较大时效率较低。推荐使用向量化的分组shift操作直接生成掩码,避免concat:
# 对每个键分组后,将值列向下偏移一行 shifted_vals = df.groupby(key_columns)[val_columns].shift() # 生成掩码:当前行值列与上一行(同组)完全不同则保留 mask = df[val_columns].ne(shifted_vals).all(axis=1) # 过滤得到结果 df_new = df[mask]
该方法全程使用Pandas内置的向量化操作,性能更优,且结果保留原DataFrame的行顺序(当前实现的concat结果会按键排序,打乱原顺序)。
边界情况处理
- 保留初始行:
当前实现会过滤掉每个键的第一行(因为shift后第一行为NaN,比较结果为False)。如果需求需要保留每个键的初始状态行,可修改掩码:
shifted_vals = df.groupby(key_columns)[val_columns].shift() mask = df[val_columns].ne(shifted_vals).all(axis=1) # 将每个组的第一行设为保留 mask |= df.groupby(key_columns).cumcount() == 0 df_new = df[mask]
- 处理NaN值:
默认的ne()比较中,NaN != NaN会返回False,导致连续的NaN值行会被误判为无变化。如果需要将NaN视为与任何值(包括NaN)不同,可修改比较逻辑:
shifted_vals = df.groupby(key_columns)[val_columns].shift() # 比较规则:值不等 或 一方为NaN另一方不为NaN diff = df[val_columns].ne(shifted_vals) | (df[val_columns].isna() != shifted_vals.isna()) mask = diff.all(axis=1) df_new = df[mask]
- 值列变化的判定逻辑:
当前实现使用all(axis=1),即只有所有值列都变化时才保留行。如果需求是任意一个值列变化即保留,需将all改为any:
mask = df[val_columns].ne(shifted_vals).any(axis=1)
遗漏点确认
- 行顺序依赖:当前实现和优化后的方法都依赖于同组内行的原始顺序,确保
shift(1)是同键的上一行。若原始DataFrame的行顺序不代表时间/事件顺序,需先按键和时间列排序。 - 键列类型:确保键列的类型一致(如字符串/数值),避免分组时因类型差异导致错误分组。
内容的提问来源于stack exchange,提问作者Sahil Puri
相关产品推荐
相关产品推荐

