
本文介绍如何先按某一列(如 '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])。
掌握这一技巧,即可灵活实现“以组态终值为锚点”的多级分组,在用户行为分析、时序会话聚类、状态机日志归并等场景中极具实用价值。










