apply() 是最直接的选择,它将每个分组作为独立 dataframe 或 series 传入,支持任意逻辑处理(如滚动均值、字符串拼接、返回多列),但返回标量、series 或 dataframe 会分别生成 series、广播结果或拼接 dataframe,且不保证输出结构一致。

用 apply() 对分组结果做任意逻辑处理
分组后想跑自定义函数(比如计算滚动均值、拼接字符串、返回多列),apply() 是最直接的选择。它把每个分组当作一个独立 DataFrame 或 Series 传入,你在函数里怎么操作都行。
注意:它默认不保证返回结构与输入一致——返回单个标量、Series、DataFrame 都可以,但会影响最终输出形状。
- 如果函数返回标量(如
len(x)),结果是Series,索引为分组键 - 如果返回
Series且长度与分组行数一致(如x["col"].cumsum()),结果会广播回原位置,保持原始索引 - 如果返回
DataFrame(如pd.DataFrame({"a": x.col * 2})),结果会被拼成一个大DataFrame,但列名必须明确,否则容易丢数据
示例:df.groupby("category").apply(lambda g: g.sort_values("score").head(2)) —— 每组取分数最高的两条记录,返回带双层索引的 DataFrame。
agg() 和 transform() 的分工边界在哪
agg() 用于聚合(降维),每组输出一个值;transform() 用于广播(升维),每组输出与原组等长的结果,且强制对齐到原始索引。
常见误用:想给每行加一个组内最大值,却用了 agg("max"),结果得到的是压缩后的 Series,无法直接赋值回原 DataFrame。
- 用
transform():适合“每行都填上它所在组的统计量”,如df["group_max"] = df.groupby("id")["value"].transform("max") - 用
agg():适合“只保留每组一条摘要”,如df.groupby("id").agg({"value": ["mean", "std"]}) - 混合用法可行:
df.groupby("id").agg({"value": "mean", "name": lambda x: "|".join(x)}),但注意后者若返回非标量,可能触发apply回退逻辑
自定义函数里修改原始数据会出问题吗
会。Pandas 分组操作默认使用视图或副本机制不透明,直接在 apply 或 transform 的函数里改 inplace=True 或赋值给 g["col"],行为不可靠:有时生效,有时报 SettingWithCopyWarning,有时静默失败。
- 安全做法:始终返回新对象,例如
return g.assign(new_col=g.x / g.x.sum()) - 避免
g.loc[:, "col"] = ...或g["col"] = ...这类就地修改 - 如果逻辑复杂必须分步,先
copy()再操作,再返回,例如g = g.copy(); g["tmp"] = ...; return g
尤其注意 transform() 的函数里绝不要改输入对象——它依赖返回值做广播对齐,改了也没用,还可能污染后续计算。
性能敏感时该避开哪些写法
分组 + 自定义函数是 Pandas 最易慢的场景之一。以下写法在万行以上数据中明显拖慢:
- 在
apply()里调用 Python 循环遍历g.iterrows()—— 改用向量化方法,如g["x"].diff().fillna(0) - 反复访问
g.shape[0]或len(g)多次 —— 提前存为变量 - 返回包含大量重复索引的
DataFrame,后续再reset_index()—— 直接构造好索引再返回 - 用
apply()做本可用agg()完成的事,比如只是求和、计数 ——agg底层走 Cython,快一个数量级
真需要高性能又逻辑复杂?考虑先 groupby(...).indices 拿到各组行号,用 NumPy 索引批量处理,再拼回去。
实际用的时候,先问自己:这个操作是要压缩、扩展,还是原样变形?选对入口函数比优化函数体更重要。另外,所有自定义函数在空分组下是否健壮(比如 g.empty == True),这点经常被忽略。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











