You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于键列与值列的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结果会按键排序,打乱原顺序)。

边界情况处理

  1. 保留初始行:
    当前实现会过滤掉每个键的第一行(因为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]
  1. 处理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]
  1. 值列变化的判定逻辑:
    当前实现使用all(axis=1),即只有所有值列都变化时才保留行。如果需求是任意一个值列变化即保留,需将all改为any:
mask = df[val_columns].ne(shifted_vals).any(axis=1)

遗漏点确认

  • 行顺序依赖:当前实现和优化后的方法都依赖于同组内行的原始顺序,确保shift(1)是同键的上一行。若原始DataFrame的行顺序不代表时间/事件顺序,需先按键和时间列排序。
  • 键列类型:确保键列的类型一致(如字符串/数值),避免分组时因类型差异导致错误分组。

内容的提问来源于stack exchange,提问作者Sahil Puri

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 13:24:55