
本文介绍如何先按某一列(如 ‘y’)分组,再基于每组中指定列(如 ‘x’)的最后一个值进行聚合分组,从而实现动态、无需预知分组键的嵌套式分组逻辑。
在 pandas 数据分析中,有时需要按“组内某个特征的最终状态”进行再分组——例如:按用户会话(y)分组后,进一步将所有以相同操作(x 的最后一行值)结尾的会话归为一类。这种需求无法通过单层 groupby(‘y’) 直接满足,而需构造一个基于组内末行值的新分组键。
以下以原始数据为例:
import pandas as pd df = pd.DataFrame({ 'x': ['a', 'b', 'c', 'c', 'e', 'f', 'd', 'a', 'b', 'c', 'c', 'e', 'f', 'd'], 'y': ['a', 'a', 'a', 'a', 'b', 'b', 'b', 'f', 'f', 'f', 'f', 'g', 'g', 'g'], })
目标是:
- 先按 ‘y’ 分组(共 4 组:y=’a’, ‘b’, ‘f’, ‘g’);
- 提取每组中 ‘x’ 列的最后一个值(即 y=’a’ 组末行为 ‘c’,y=’b’ 组末行为 ‘d’,依此类推);
- 再按这些“组末 x 值”(如 ‘c’, ‘d’)统一聚类,得到两个大组。
✅ 推荐方案:使用 groupby().transform(‘last’)
最清晰、可读性强且健壮的方式是借助 transform(‘last’) 构造辅助列:
# 步骤1:按 'y' 分组,提取每组 'x' 的最后一个值,广播到该组所有行 last_x_per_y = df.groupby('y')['x'].transform('last') # 步骤2:按此新列再次分组 for last_val, group in df.groupby(last_x_per_y): print(f"nGroup where last 'x' in 'y'-group is: '{last_val}'") print(group)
输出:
Group where last 'x' in 'y'-group is: 'c' x y 0 a a 1 b a 2 c a 3 c a 7 a f 8 b f 9 c f 10 c f Group where last 'x' in 'y'-group is: 'd' x y 4 e b 5 f b 6 d b 11 e g 12 f g 13 d g
? transform(‘last’) 保证了结果长度与原 DataFrame 一致,且自动对齐——这是实现“组内特征广播”的关键。
⚡ 高性能替代方案(适用于 y 值唯一连续成块)
若 ‘y’ 列天然构成不重叠、连续的逻辑块(如日志中按会话 ID 有序排列),可跳过 groupby,用向量化操作提速:
# 方案A:利用 drop_duplicates + map(推荐,语义清晰) mapper = df.drop_duplicates('y', keep='last').set_index('y')['x'] last_x = df['y'].map(mapper) # 方案B:用 mask + bfill(更紧凑,但可读性略低) last_x = df['x'].mask(df['y'].duplicated(keep='last')).bfill() # 后续分组不变 for k, grp in df.groupby(last_x): print(f"nGroup for last x = '{k}':") print(grp)
? 通用化:支持任意组内聚合逻辑
若需基于其他规则(如首值、众数、自定义函数)生成分组键,只需替换 transform 中的参数:
# 例如:用每组 'x' 的第一个值分组 last_x = df.groupby('y')['x'].transform('first') # 或用自定义逻辑(如倒数第二行,需确保长度≥2) last_x = df.groupby('y')['x'].transform(lambda s: s.iloc[-2] if len(s) >= 2 else s.iloc[0])
? 输出为字典便于后续处理
如需将结果存为字典(键为分组值,值为子 DataFrame),一行即可:
grouped_dict = dict(list(df.groupby(last_x_per_y))) # grouped_dict['c'] → 包含所有以 'c' 结尾的 y-group 的行
⚠️ 注意事项
- y 是否真正独立? 若 ‘y’ 值重复出现但语义不同(如 [‘a’,’a’,’b’,’b’,’a’,’a’] 应视为 3 个独立会话而非 2 个),需改用序列分组:
y_group_id = df['y'].ne(df['y'].shift()).cumsum() # 生成连续块ID last_x = df.groupby(y_group_id)['x'].transform('last') - 空组/单行组安全:transform(‘last’) 对单行组天然安全;若用 iloc[-1] 自定义函数,建议加长度判断。
- 性能提示:transform(‘last’) 在底层高度优化,通常优于 apply(Lambda x: x.iloc[-1])。
掌握这一技巧,即可灵活实现“以组态终值为锚点”的多级分组,在用户行为分析、时序会话聚类、状态机日志归并等场景中极具实用价值。