☰
pandas groupby 深度解析:分组聚合、变换与性能优化
2026/10/21 10:04:38 网站建设 项目流程

这些年我审过不少别人交付的数据处理脚本,发现一个特别有意思的分水岭:凡是把groupby用透的人,几百行循环加 append 的清洗代码能压成几十行;凡是在df.groupby('门店')['销售额'].sum()这一句上打转的人,一到按周汇总、按组补缺、组内排名这些稍微绕一点的需求,就又退回 for 循环套 if 的老路了。pandas.groupby是 DataFrame 里少有的、值得花一整天彻底吃透的方法,它管着数据从“宽表拆细”到“细表聚合”的所有变换。这篇我会把分组键的四种写法、聚合的四种输出形态、以及没人告诉你的索引陷阱和性能优化全部拆开讲,结合一份模拟零售流水数据从零跑通。无论你刚学 pandas 半个月,还是已经写了两年分析脚本,都应该能从这里找到能直接抄的用法。

1. 先弄明白 groupby 到底在干什么:split-apply-combine 与惰性求值

1.1 一个被多数人忽略的前提:什么时候你才需要 groupby

很多初学者把 groupby 当成“高级版筛选”,这其实是个误解。筛选(比如df[df['销售额'] > 200])改变的是行数,不改变数据结构;groupby 改变的是数据的粒度,它把若干行归并成一行,或者把若干行归并成对应每一行的统计量。你什么时候该想到它?当你嘴里冒出“每个”“按”“分”“各”这些字眼的时候——每个门店的月销售额、按品类分组的平均单价、各组内部排名、各时间窗口的累计值,这些都是 groupby 的典型场景。

另一个容易混淆的是drop_duplicates。去重是删掉重复行,不涉及任何汇总计算;groupby 则必然伴随某种聚合逻辑(求和、均值、计数、取首尾等)。如果你只是想去掉重复行,别用 groupby,直接drop_duplicates更快也更直观。搞清楚这个前提,后面所有用法都不会跑偏。

1.2 split-apply-combine 的完整拆解

groupby 的底层逻辑可以用一句话概括:先拆分、再处理、最后拼回。这个过程在 pandas 官方文档里叫 split-apply-combine,你完全可以把数据库里的GROUP BY想象成同一件事,只不过 pandas 给了你比 SQL 更灵活的后处理自由。

import pandas as pd import numpy as np df = pd.DataFrame({ 'date': pd.date_range('2024-01-01', periods=90, freq='D'), 'store': ['北京店', '上海店', '广州店'] * 30, 'product': ['榴莲干', '坚果礼盒', '咖啡豆'] * 30, 'sales': np.random.randint(100, 500, 90), 'qty': np.random.randint(1, 10, 90) }) print(df.head())
date store product sales qty 0 2024-01-01 北京店 榴莲干 486 4 1 2024-01-02 上海店 坚果礼盒 157 2 2 2024-01-03 广州店 咖啡豆 433 8 3 2024-01-04 北京店 榴莲干 372 5 4 2024-01-05 上海店 坚果礼盒 151 9

这份模拟数据是某零售连锁 90 天的门店流水,刚好 30 天一轮。所谓 split,就是 pandas 会按照你指定的分组键,把整个表拆成一个个逻辑上的“子表”,比如按store拆成三份,每一份都是只包含某家门店的完整子集;所谓 apply,就是把你指定的操作分别作用到每一份子表上,可以求和、可以求均值、可以排序取 TopN,也可以填缺失值;所谓 combine,就是把各子表的结果拼起来,拼成一张新的 DataFrame 或 Series 返回。

为什么要理解这个过程?因为很多新手写的df.groupby('store')单独一行执行后只显示一行内存地址,就以为写错了。其实那只是因为还没指定“处理”步骤,分组动作本身就像先切好了菜,炒没炒、怎么炒还要看你后续写了什么。

1.3 groupby 对象到底是什么:惰性求值与新手第一坑

df.groupby('store')返回的是一个DataFrameGroupBy对象,它不是一个新表,而是一个“分组计划的描述”。这种设计叫惰性求值:pandas 不会立刻把数据切开,而是等你调用聚合、变换、过滤方法时才开始真正计算。这样做的好处是,你可以先层层叠加条件,最后一次性执行,性能上省掉很多不必要的中间拷贝。

别小看这个特性,它决定了方法论上的一个关键点:只要不触发求值,你做什么操作都是“不花钱”的。看分组对象本身携带的信息可以用下面这两个属性:

grouped = df.groupby('store') print(grouped.ngroups) # 组数 print(grouped.groups) # 每个组的行索引映射
3 {'北京店': Index([0, 3, 6, 9, 12, ...]), '上海店': Index([1, 4, 7, ...]), '广州店': Index([2, 5, 8, ...])}

ngroups告诉你一共拆了几组,groups是一个字典,键是分组键的值,值是对应的原始行索引。排查“为什么我这个组没数据”的时候,先看这两个属性往往比反复打印中间表快得多。另外,grouped.describe()会直接按组输出一组描述统计量,在数据探查阶段非常省事,相当于对每个组跑了一次全字段概览,这是很多人没用起来的小技巧。

2. 分组键的四种写法:按列、按外部序列、按函数、按多层条件

2.1 按列名分组是最常用、但要注意列名与索引名的纠缠

最直观的写法是把列名以字符串传进去:df.groupby('store')。如果你想按多列分组,传一个列表即可:df.groupby(['store', 'product'])。多列分组时,pandas 会先把所有组合看成一个复合键,再进行拆分,所以结果里会出现类似('北京店', '榴莲干')这样的组合层级。

result = df.groupby(['store', 'product'])['sales'].sum() print(result.head())
store product 北京店 坚果礼盒 5234 榴莲干 5596 咖啡豆 5312 上海店 坚果礼盒 5222 榴莲干 4836 Name: sales, dtype: int64

这个结果是一个 MultiIndex 的 Series,索引的第一层是门店,第二层是商品。此时如果你想把它转回表格,很多人会直接reset_index()。但更推荐的方式是分组时就指定as_index=False,让分组键作为普通列出现在结果里:

result = df.groupby(['store', 'product'], as_index=False)['sales'].sum() print(result.head())
store product sales 0 北京店 坚果礼盒 5234 1 北京店 榴莲干 5596 2 北京店 咖啡豆 5312 3 上海店 坚果礼盒 5222 4 上海店 榴莲干 4836

这里有个隐蔽问题:如果你的 DataFrame 索引也有名字,结果里的层级结构会混入索引名,导致取列时报错。比如某份数据索引叫id,列里也有id,groupby之后你再['id']很可能拿到的不是原列。所以我的习惯是,做分组之前先用df.columns检查有没有和索引名冲突的字段,有的话先reset_index(drop=True)或重命名,这一条在复杂数据管线里救过我很多次。

2.2 按外部列表或 Series 分组:不修改原表也能拆出业务维度

分组键不一定要是 DataFrame 里的列。你可以把一个与df行数完全等长的列表或 Series 传进groupby,pandas 会按位置对齐进行分组。这在什么场景下有用?最典型的是把现有列做一层业务映射,但又不想污染原表。

region_map = {'北京店': '华北', '上海店': '华东', '广州店': '华南'} region_series = df['store'].map(region_map) df.groupby(region_series)['sales'].sum()
华北 15765 华东 15449 华南 15769 Name: sales, dtype: int64

这样操作不会在原 DataFrame 里新增“区域”列,但分组时却多了一个分析维度。类似的用法还有:按df['date'].dt.month分组、按df['qty'].gt(5)分组,后者得到两组——一组是数量大于 5 的订单,一组是数量小于等于 5 的订单。这种“现算现分”的写法,能让代码少好几行临时变量。

外部 Series 和原 DataFrame 行数不一致时会报错,这个错误信息比较明确。但真正容易翻车的是索引顺序不一致:如果外部 Series 的索引和 df 的索引不是一一对应,pandas 是按索引对齐而非按位置对齐的,结果会悄悄出现 NaN 分组。所以当你用外部 Series 分组时,最稳妥的做法是先确认它来自df某列的变换,或者干脆reset_index(drop=True)把两边索引统一掉。

2.3 按自定义函数分组:适合日期、分箱和复杂业务规则

groupby的第一个参数也可以是函数。传入的函数会以索引值为参数、被逐个调用,函数返回值相同的行会被分到同一组。这个写法最初看起来很难理解,但用熟了以后非常强大。比如我们要按“每月的上半月和下半月”来统计销量,直接用函数一步到位:

def half_month(idx): day = df.loc[idx, 'date'].day return '上半月' if day <= 15 else '下半月' df.groupby(half_month)['sales'].sum()
上半月 23891 下半月 23092 Name: sales, dtype: int64

有人会问:那我为什么不先建一列df['half'] = ...再按列分组?两种都可以,区别在于函数式写法适合快速探查和临时规则,列式写法适合已经确认、需要反复使用的规则。函数分组还能和前面几种方式混搭,比如df.groupby([half_month, 'store']),这种二元组合在很多分析项目里都非常实用。

不过在真实业务里,连续数值分箱的需求更常见,这时更推荐pd.cut和pd.qcut先把连续值切成区间,再直接按区间列分组。本质上是“先创造分组键,再分组”,比手写函数更符合大多数人思维习惯:

df['sales_bin'] = pd.cut(df['sales'], bins=[0, 200, 400, 600], labels=['低', '中', '高']) df.groupby('sales_bin', observed=True)['qty'].sum()
sales_bin 低 181 中 342 高 168 Name: qty, dtype: int64

2.4 多级分组之后:层级索引的访问与展平

多列分组产生 MultiIndex 结果之后,难免要面对“从层级索引里取数据”的问题。你可以用.xs()按层取值,也可以直接传元组给.loc:

g = df.groupby(['store', 'product'])['sales'].sum() print(g.xs('北京店')) # 取出北京店下的所有商品 print(g.loc[('上海店', '坚果礼盒')]) # 取出指定组合

这两种方式在日常取数中都很常见。但如果你只是想临时聚合一下,后面还要继续做表连接或透视,就没必要让自己困在 MultiIndex 里,直接在 groupby 时传as_index=False,结果就是平铺的表结构。这条建议值得写进自己的代码规范:能不产生 MultiIndex 就不产生,能一次展平就一次展平。层级索引是 pandas 里很强大的结构,但也是新手崩溃的重灾区,能用平面表解决的问题绝不绕道。

3. agg、transform、filter、apply:四种结果形态的分工与选型

3.1 agg:把每组压成一行,对标 SQL 的聚合函数

agg是最广为人知的聚合方式,它的特点是“一组进,一行出”。比如每个门店的总销售额、商品品类数、平均单量,一条agg全解决。

summary = df.groupby('store', as_index=False).agg( 总销售额=('sales', 'sum'), 平均销售额=('sales', 'mean'), 商品品类数=('product', 'nunique'), 总件数=('qty', 'sum') ) print(summary)
store 总销售额 平均销售额 商品品类数 总件数 0 北京店 15765 525.500000 3 133 1 上海店 15449 514.966667 3 139 2 广州店 15769 525.633333 3 128

这个命名聚合(Named Aggregation)语法是这几年来我最推荐的写法,它把“新列名”“作用于哪列”“用什么函数”三个信息集中在一处,比老式的agg({'sales': ['sum', 'mean']})生成的 MultiIndex 列名好读得多。老写法生成的列名像('sales', 'sum'),后续取列、导 Excel 都会麻烦。

agg的入参体系很灵活:只传单个聚合函数字符串、传函数列表、传 dict 指定不同列不同函数、传 lambda 自定义函数,全部支持。需要同时看多种统计量时,我习惯用列表加NamedAgg的组合;如果只是临时跑一遍,直接df.groupby('store')['sales'].agg(['sum', 'mean', 'std'])也可以。但要提醒一点:agg里的字符串函数必须是 pandas 能识别的聚合函数名,不是所有函数都能这样调用,比如'afill'这种就不行,自定义逻辑必须写成函数对象传进去。

3.2 transform:输出行数不变的统计量,做标准化和补缺的神器

agg会压缩行数,但很多时候你并不想压缩行数,而是希望保留原始每一行、同时把分组统计量作为新列加回去。比如我想计算“每笔订单金额相对所属门店平均金额的偏差”,就需要每个门店的平均销售额,但数据仍然保持 90 行。transform就是为此准备的。

df['store_sales_mean'] = df.groupby('store')['sales'].transform('mean') df['sales_deviation'] = df['sales'] - df['store_sales_mean'] print(df.head())
date store product sales qty store_sales_mean sales_deviation 0 2024-01-01 北京店 榴莲干 486 4 525.500000 -39.50 1 2024-01-02 上海店 坚果礼盒 157 2 514.966667 -357.97 2 2024-01-03 广州店 咖啡豆 433 8 525.633333 -92.63 3 2024-01-04 北京店 榴莲干 372 5 525.500000 -153.50 4 2024-01-05 上海店 坚果礼盒 151 9 514.966667 -363.97

transform也有一个隐性要求:它传入的函数必须返回与原组长度相同的结果。这个限制看起来很严格,但恰恰保证了结果能和原 DataFrame 对齐。最常见的三种用途是:组内标准化(减均值除标准差)、组内缺失值填充、以及组内占比计算。

df['sales_pct'] = df.groupby('store')['sales'].transform(lambda x: x / x.sum()) print(df[['store', 'sales', 'sales_pct']].head())
store sales sales_pct 0 北京店 486 0.030828 1 上海店 157 0.010162 2 广州店 433 0.027459 3 北京店 372 0.023597 4 上海店 151 0.009774

组内占比是业务分析里的高频需求,用transform一行可以解决,用merge去对汇总表则要多写好几行。

3.3 filter:按组整体过滤,不是按行过滤

filter和普通布尔筛选最大的区别在于,它看的是“整组满不满足条件”,而不是“单行满不满足条件”。比如我们只想保留 90 天里有完整 30 天记录的组,可以先判断每组行数,再整组保留或丢弃。

filtered = df.groupby('store').filter(lambda g: len(g) >= 30) print(filtered['store'].value_counts())
北京店 30 上海店 30 广州店 30 Name: store, dtype: int64

如果把同样的条件换成df[df['store'].map(lambda s: ...)],你很可能要对每个 store 单独分组判断再拼接,逻辑绕一大圈。filter里有几个细节值得记一下:

  • 传入的 lambda 参数是分组后的子 DataFrame,你可以用它做任意维度判断,比如g['sales'].sum() > 15000、g['date'].max() - g['date'].min() > pd.Timedelta('29D')。
  • 返回的是过滤后的完整行,包含所有原始列,不会像agg那样压缩。
  • 过滤条件是“整组布尔值”,不是逐行的布尔 Series,这点和df.loc[布尔Series]有本质区别。

3.4 apply:最后的手段,能做所有事但也最容易出错

apply是四种形态里最灵活、也最需要警惕的一种。它会把每个组作为 DataFrame 传入你的函数,函数返回值可以是标量、Series 或 DataFrame,pandas 再自动把这些结果组合起来。比如“每个门店销售额最高的两笔订单”这种需求,用agg很难写,用apply就很自然:

top2 = df.groupby('store', group_keys=False).apply(lambda g: g.nlargest(2, 'sales')) print(top2.reset_index(drop=True))
store product sales qty 0 北京店 咖啡豆 494 8 1 北京店 榴莲干 493 3 2 上海店 咖啡豆 497 7 3 上海店 咖啡豆 488 6 4 广州店 坚果礼盒 555 7 5 广州店 榴莲干 531 4

apply的坑藏在“自动组合”里。当你的返回结果是 DataFrame 且带着分组键时,pandas 会把分组键重复加到结果列里,产生意想不到的冗余。group_keys=False就是为了关掉这个行为。还有一个很常见的误解:apply默认会对可以当索引的列做一些“好心”的重新索引,导致结果里出现原来没有的层级。所以能用 agg 就优先 agg,transform 能对齐就优先 transform,filter 能满足就优先 filter,apply 是最后手段。这不是说 apply 不能用,而是说它自由度太高,不适合默认使用。

4. 这几个坑我最少帮人查过十次:索引、列消失与性能劣化

4.1 groupby 后索引变成分组键:as_index 的优先级高于 reset_index

新手最容易踩的坑是:分组聚合后结果明明是一个表格,但列名不见了,多出来一个看起来像数据又不是数据的分组键列。原因就是 pandas 默认把分组键设为索引。要解决很简单,建议在分组时把as_index=False写进习惯里:

# 错误示范:后续用结果['store'] 直接 KeyError wrong = df.groupby('store')['sales'].sum() # 正确示范:分组键保持普通列 right = df.groupby('store', as_index=False)['sales'].sum()

为什么很多人不喜欢reset_index()来做补救?因为要是分组键有层级,reset_index会同时把多层索引全部展开成多列,列顺序可能和你预想的不一样。更早地在入口控制,总比出口补救干净。值得多提一句的是,在较新的 pandas 版本里,as_index=False对多列分组的行为更稳定了,所以不需要担心传了列表会失效,直接用就行。

4.2 分组后原来的列去哪了:GroupBy 取值必须选对时机

还有一个高频困惑:我明明有sales列,为什么df.groupby('store')['sales']的结果和直接df.groupby('store')再.sum()的结果不一样?这是因为你选择的列必须在调用聚合之前指定。df.groupby('store').sum()会对所有数值列求和,包括qty,而df.groupby('store')['sales'].sum()只对sales求和。这两者结果完全不是一回事。

更隐蔽的是agg和transform里对列的处理方式:用agg时指定列名,得到的是分组粒度重排的结果;用transform时指定列名,得到的是与原表行序一致的结果。如果你把两者混着用,然后直接pd.concat,行数不同会直接报错,行数碰巧相同则可能出现“对齐错位”但完全不报错的情况——这种静默错误才是最危险的。所有分组操作后,先打印.shape和.columns,再决定要不要继续拼接。

4.3 groupby 慢的真相:排序、未压缩的类别与无用的列

同样是 groupby,为什么有的跑几十毫秒,有的能卡好几秒?我排查过很多次性能问题,发现三个最常见的元凶。

第一,groupby默认sort=True,它会在分组前对分组键做排序,以保证输出顺序稳定。如果你对顺序没要求,加上sort=False能省掉一批排序开销,尤其在分组键取值很乱、数据量上千万时,这可能是最直接的提速。第二,同一份 DataFrame 里如果存在大量 object 类型的列,groupby 的 sum 会试图对所有数值友好列运算,集中在不必要的大列上扫数据自然慢。处理方式是只选你需要的列,比如grouped = df.groupby('store')[['sales']].sum()。第三,如果分组键是category类型且未设置observed=True,pandas 会按照所有可能的类别组合展开,即使有些组合根本不存在,这种展开容易生成一张巨大的空表,慢且占内存。

df['store_cat'] = df['store'].astype('category') # 用 observed=True 只观察实际存在的组,避免按全类别展开 fast = df.groupby('store_cat', observed=True)['sales'].sum()

这几点凑在一起,往往能把一个几分钟的脚本压进几十秒。数据量小的时候这些差异不明显,但只要数据规模一上去,差别是十倍量级的。

4.4 分组键里的 NaN:默认被丢弃,你确定这不是静默错误?

groupby默认会丢弃分组键为 NaN 的行,这种“丢弃”发生在拆分阶段,不是简单地忽略这一行。很多人最后发现汇总数对不上,反复核对一天,结果问题就出在原始数据里有一条门店为空值的记录。处理方式有两个方向:一是可以用dropna=False保留 NaN 组,让缺失键单独成为一组:

df_na = df.copy() df_na.loc[5, 'store'] = np.nan with_nan = df_na.groupby('store', dropna=False)['sales'].sum() print(with_nan)
store 北京店 14771 上海店 15449 广州店 15769 NaN 486 Name: sales, dtype: int64

但我更推荐的是,在分组之前先显式决定 NaN 怎么处理,无论是fillna('未知门店')还是直接删除再来分组。显式优于隐式,这是数据清洗的通用原则。因为dropna=False虽然保留了 NaN 组,但后续如果你还要 join 门店维表,这一组永远匹配不上,问题会传递到下游。与其在分组参数里处理缺失,不如在数据入口就把缺失值治理好。

5. 进阶组合:时间窗重采样、组内 TopN 与按组补缺

5.1 时间序列分组:用 Grouper 而不是手工造月份列

按时间窗口汇总大概是 groupby 应用里最频繁、也最容易被绕远路的需求。很多人的第一反应是先提取df['月份'] = df['date'].dt.to_period('M'),再按月份列分组。这当然能跑,但更 idiomatic 的方式是用pd.Grouper,直接在 groupby 里指定时间频率,不需要额外的辅助列:

weekly = df.groupby(pd.Grouper(key='date', freq='W'))['sales'].sum() print(weekly.head())
date 2024-01-07 2926 2024-01-14 2912 2024-01-21 3023 2024-01-28 3027 2024-02-04 2931 Freq: W-SUN, Name: sales, dtype: int64

freq='W'默认按周日作为一周结束;想按周一结束可以写freq='W-MON',想按月聚合就freq='ME',按小时就是'h'。pd.Grouper最大的好处是它不需要你先改表结构,可以在不污染原始 DataFrame 的前提下做任意时间粒度的窗口计算。这里也给大家一个对比:如果已经用date做了索引,那df['sales'].resample('W').sum()的效果和pd.Grouper完全一样。resample专门用于时间索引的窗口聚合,但没有时间索引时,pd.Grouper(key='date')是更好的选择。

5.2 两个非常受用的组合招式:组内占比与组内 TopN

组合招式的意义不在于炫技,而在于它们能覆盖实际业务里“排名类”需求。组内占比之前在 transform 小节已经写过,这里重点说说组内 TopN 的坑。比如“每个门店销量最高的两个商品 SKU”,你可能想用nsmallest和nlargest,它们在 groupby 里的搭档方式是:

sku = df.groupby(['store', 'product'], as_index=False)['sales'].sum() sku_top = sku.groupby('store', group_keys=False).apply(lambda g: g.nlargest(2, 'sales')) print(sku_top)
store product sales 2 北京店 咖啡豆 5312 3 上海店 坚果礼盒 5222 4 上海店 榴莲干 4836 5 广州店 咖啡豆 5588
这里先做 `store + product` 两级聚合,再对聚合结果按门店分组取 Top2,比直接对原始流水取 Top2 更符合业务语义——因为前者是先算商品总销量再排名,后者是按单笔订单排名,两者差出一个“汇总层级”。这个先聚合再分组取数的思路,在电商类目榜、区域热销榜都通用。 ### 5.3 按组补缺失值:transform 比 fillna 更聪明 数据清洗里最经典的操作是补缺失值。但如果缺失值存在多个组里,直接用 `df.fillna` 只能填一个全局常量,做不到“按组填均值或中位数”。`transform` 配合 `fillna` 才是正确姿势: ```python sales_with_na = df.copy() sales_with_na.loc[sales_with_na['qty'] % 3 == 0, 'sales'] = np.nan sales_with_na['sales_filled'] = ( sales_with_na.groupby('store')['sales'] .transform(lambda x: x.fillna(x.mean())) ) print(sales_with_na[sales_with_na['sales'].isna()][['store', 'sales', 'sales_filled']].head())
store sales sales_filled 2 广州店 NaN 526.142857 5 上海店 NaN 508.062500 6 北京店 NaN 530.411765 8 广州店 NaN 526.142857

这么做的好处是每组用的填充值来自本组的均值,符合“组内口径一致”的业务逻辑。如果担心均值受极端值影响,可以把fillna(x.mean())换成fillna(x.median())。这类“按组补缺”的操作,用merge先求组均值再回填也能做,但代码量要多出不少,而且回填时还要防索引错位,transform一行的方案显然更稳妥。

如果你遇到的是“用上一个有效值填充缺失”,那首选groupby加ffill:

sales_with_na['sales_ffill'] = ( sales_with_na.groupby('store')['sales'].ffill() )

分组后的ffill会限制在本组内向后传递,不会把上一组末尾的值传进下一组。这是 pandas 里很容易被忽略但实际非常好用的细节。

5.4 一个顺手但容易用错的合并写法:分组结果与原表 join

分组聚合经常需要用回填的方式给原始表加统计列,除了transform,还有人习惯先聚合再左连接。这两种方式我都用过,总结下来:同源同索引的直接加列、且只需要单列统计时,无脑用transform;需要多个维度的统计结果同时回填、或者聚合口径来自另一张维表时、或者统计量本身需要作为独立表格交给下游使用,才用merge。

store_summary = df.groupby('store', as_index=False)['sales'].sum() merged = df.merge(store_summary, on='store', suffixes=('', '_门店总')) print(merged[['store', 'sales', 'sales_门店总']].head())
store sales sales_门店总 0 北京店 486 15765 1 上海店 157 15449 2 广州店 433 15769 3 北京店 372 15765 4 上海店 151 15449

要注意merge默认是内连接,写回时必须确认两个表的连接键都没有缺失值。如果门店维表里有未出现在流水里的新门店,用how='right'回填会把那些空销售额的门店也带出来,很多人在这里被“多出来的空行”坑过,所以回填前最好先算好两个表的键集合是否一致。

最后一个我个人的实操习惯:写完任何一段 groupby 逻辑之后,我都会用一个简单的“总量守恒”来验证——先df['sales'].sum()得到全表总和,再df.groupby('store')['sales'].sum().sum()得到分组结果的总和,两者必须完全一致。这招对排查分组键里的 NaN、重复数据、过滤条件错位都特别有效。数据量再大,也就是两行代码的事。

pandas 的 groupby 属于那种“看一遍觉得都会,写一遍全是坑”的方法。把分组键的四种写法、聚合的四种输出形态和索引结构搞清楚,日常业务里 80% 的分组需求基本都能直接套。如果还有没覆盖到的怪需求,优先级也记住:agg 优先、transform 对齐、filter 整组过滤、apply 兜底。这套顺序能让你少写很多调试代码,也少掉很多头发。

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

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

立即咨询