☰
pandas groupby与apply详解:从分组机制到性能优化实战
2026/10/14 1:23:23 网站建设 项目流程

前一阵子有个朋友跑来找我,说自己在处理一份订单流水表时卡住了:要按用户分组,对每个用户过去一个月的下单次数、客单价、复购间隔分别做统计,还得分组做归一化,把结果合回原来的表里。他第一反应是写循环,一个用户一个用户去遍历,结果几百万行数据跑了好几分钟还没出结果。我跟他说,你这就是典型的“手握 pandas 却还在写 SQL 之外的三层 for 循环”的场景,groupby加apply两行就能说清楚的事,根本不用绕那么大弯子。

groupby和apply是 pandas 里被高频讨论、又容易被用飞的两个方法。一个负责“按什么分组”,一个负责“拿到分组后干什么”。单独看都不难,但组合在一起,能玩出的花样远比想象的多。这篇文章我打算从最底层的执行逻辑讲起,再拿几个我实际做过的场景出来拆解,最后聊一聊性能陷阱和常见报错的排查思路,把这两个方法一次讲透。

1. groupby 到底怎么分组:从数据结构到执行流程

1.1 拆分-应用-合并:理解一切的前提

groupby在执行时做的是三件事,英文世界有一个经典说法叫 Split-Apply-Combine。中文就是拆分、应用、合并。

第一步,按你指定的“分组键”把整个 DataFrame 拆成若干个小块。第二步,对每个小块执行你传进去的操作,可能是求和、均值、自定义函数,也可能什么都不做。第三步,把每个小块的结果拼装起来,再还给你一个结构化的结果。

这里最容易被误解的点在于:很多人觉得df.groupby('列名')返回的就是一个“已经分组完毕的表”,赋值给变量之后,直接打印会看到一串DataFrameGroupBy的对象描述,长这样:

<pandas.core.groupby.generic.DataFrameGroupBy object at 0x...>

它不是一张表,而是一个中间对象,官方叫GroupBy。你可以把它理解成一个“待命的分组工坊”——它已经记住了分组键的信息,也确定了数据的分块逻辑,但还没有真正执行任何分组运算。所有实际计算都发生在你调用了某个聚合方法(比如sum()、mean())或apply()之后。

形象的类比是外卖平台的订单分配逻辑。groupby('用户ID')相当于平台把所有订单先按骑手所属站点分好筐,每个筐贴好标签。但筐只是筐,里面的单子要干什么(统计金额 / 计算数量 / 做对比)是在后续步骤里才决定的。

1.2 GroupBy 对象里到底存了什么

对GroupBy对象不要有神秘感。它内部做了这么几件事:

  • 根据分组键计算每个行标签所属的组;
  • 建立 键值 -> 组内行索引 的映射关系;
  • 维护原始 DataFrame 的引用,但不复制数据。

因为不复制数据,所以groupby本身是很快的,真正消耗时间的是后续的聚合操作。这一点在设计大数据量处理流程时很重要:如果只需要分组后取最大值,df.groupby(...).max()一气呵成是最佳状态;如果你先groupby存成变量,再反复调用多个聚合方法,每次聚合都会重新走一遍分组计算。

另外,GroupBy 对象是可以被迭代的。遍历它时,每个元素是一个二元组:组名、子 DataFrame。

for name, group in df.groupby('类别'): print(name) print(group.head())

这在调试分组逻辑时非常有用——你可以先遍历看看每一组到底长什么样,是否符合预期。我在排查分组边界条件时,几乎必用这个方式。比如按月份分组,某些月份数据量为 0 时组会不会消失、边界日期归属哪一组,一迭代就全清楚了。

1.3 分组键可以有多少种传法

分组键不只有“列名”这一种传法。常见的几种方式:

  • 传单个列名:df.groupby('城市')
  • 传多个列名:df.groupby(['城市', '月份']),生成层级化索引
  • 传 Series:df.groupby(df['城市'].str[:2]),按城市前两字分组
  • 传数组 / 列表:长度必须与行数一致,df.groupby(['A组', 'B组', ...])
  • 传函数:作用于索引,比如df.groupby(lambda x: x.year)(当索引是时间,x 是索引标签)

最后一种方式我早期用得不

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询