Pandas按条件删除id分组下连续重复outcome行的实现方法
问题分析
你原有代码的核心问题有两个:
- 计算连续块的
shift操作没有按id分组,会把上一个id的最后一条记录和下一个id的第一条记录做比较,导致跨id的块标签计算错误 - 没有实现规则中「yes后面第一个出现的no需要保留」的逻辑,所有连续块统一取尾行,和需求冲突
实现方案
这里提供两种符合pandas规范的实现方式,可根据数据量选择:
方案1:分组自定义函数(逻辑清晰易维护,适合中小数据量)
直接按id分组后逐块处理,完全匹配规则:
import pandas as pd # 预处理:确保日期格式正确、按id和日期升序排序(已排序可跳过排序步骤) df['date'] = pd.to_datetime(df['date'], dayfirst=True) df = df.sort_values(['id', 'date']).reset_index(drop=True) def filter_id_group(g): # 生成当前id下的连续outcome块标签 g['block'] = (g['outcome'] != g['outcome'].shift()).cumsum() result_rows = [] prev_block_outcome = None # 遍历每个连续块 for _, block in g.groupby('block'): curr_outcome = block['outcome'].iloc[0] if curr_outcome == 'yes': # yes块取最后一条 result_rows.append(block.iloc[-1]) else: # no块判断前一个块是不是yes if prev_block_outcome == 'yes': # 前一个是yes,取当前no块的第一条 result_rows.append(block.iloc[0]) else: # 其他情况取no块最后一条 result_rows.append(block.iloc[-1]) prev_block_outcome = curr_outcome return pd.DataFrame(result_rows) # 分组处理并去掉辅助列 updated_df = df.groupby('id', group_keys=False).apply(filter_id_group).drop(columns='block').reset_index(drop=True) # 可选:把日期转回你需要的字符串格式 updated_df['date'] = updated_df['date'].dt.strftime('%d/%m/%Y')
方案2:向量化操作(性能更高,适合百万级以上大数据量)
完全用pandas原生向量化操作实现,避免循环:
import pandas as pd # 预处理同上 df['date'] = pd.to_datetime(df['date'], dayfirst=True) df = df.sort_values(['id', 'date']).reset_index(drop=True) # 1. 按id生成连续outcome块标签 df['block'] = df.groupby('id')['outcome'].transform(lambda x: (x != x.shift()).cumsum()) # 2. 关联每个块的属性:当前块outcome、前一个块的outcome block_meta = df.groupby(['id', 'block'])['outcome'].first().reset_index(name='curr_block_out') block_meta['prev_block_out'] = block_meta.groupby('id')['curr_block_out'].shift() df = df.merge(block_meta, on=['id', 'block'], how='left') # 3. 按规则筛选行 # 条件1:yes块取最后一行 cond_yes = (df['curr_block_out'] == 'yes') & (df.groupby(['id', 'block']).cumcount(ascending=False) == 0) # 条件2:前块是yes的no块取第一行 cond_no_after_yes = (df['curr_block_out'] == 'no') & (df['prev_block_out'] == 'yes') & (df.groupby(['id', 'block']).cumcount() == 0) # 条件3:其他no块取最后一行 cond_normal_no = (df['curr_block_out'] == 'no') & (df['prev_block_out'] != 'yes') & (df.groupby(['id', 'block']).cumcount(ascending=False) == 0) updated_df = df[cond_yes | cond_no_after_yes | cond_normal_no].drop(columns=['block', 'curr_block_out', 'prev_block_out']).reset_index(drop=True) # 可选日期格式转换 updated_df['date'] = updated_df['date'].dt.strftime('%d/%m/%Y')
输出结果
两种方案得到的结果完全和预期一致:
id date outcome 0 3 04/09/2019 no 1 3 30/10/2019 yes 2 3 03/05/2020 no 3 5 26/12/2019 no 4 5 03/06/2020 yes 5 6 27/10/2019 no 6 6 14/04/2020 yes 7 6 24/04/2020 no
内容的提问来源于stack exchange,提问作者Ze0ruso
相关产品推荐
相关产品推荐

