Pandas分组与聚合(groupby

拆分-应用-合并的数据分析范式

一、认识 groupby:拆分-应用-合并

在数据分析中,分组聚合是最常见的操作模式。Pandas 的 groupby 方法实现了经典的 "拆分-应用-合并"(Split-Apply-Combine)范式。这一范式由 Hadley Wickham 在其 2011 年的论文中系统阐述,现已成为数据分析领域的标准方法。

拆分-应用-合并三步流程:

  • 拆分: 根据指定的键(一个或多个列)将 DataFrame 拆分为多个组
  • 应用: 对每个组独立应用函数(聚合、转换、过滤等)
  • 合并: 将各组的处理结果合并为最终输出

直接看一个最简单的例子。假设我们有销售数据,需要按部门计算平均销售额:

import pandas as pd # 构造示例数据 df = pd.DataFrame({ '部门': ['技术部', '技术部', '市场部', '市场部', '运营部', '运营部'], '员工': ['张三', '李四', '王五', '赵六', '钱七', '孙八'], '销售额': [100, 150, 200, 180, 120, 160], '年龄': [28, 35, 30, 42, 26, 38] }) # 按部门分组,计算平均销售额 result = df.groupby('部门')['销售额'].mean() print(result)
部门 运营部 140.0 市场部 190.0 技术部 125.0 Name: 销售额, dtype: float64

这个例子虽然简单,却完整展示了三步流程。拆分阶段,Pandas 将六行数据按部门拆成三个独立的小组;应用阶段,对每个组的 销售额 列分别调用 mean();合并阶段,将三个结果拼成一个 Series。从代码风格来看,groupby 使得数据分析非常接近于 SQL 的 GROUP BY 操作,但更加灵活。

对比 SQL:

Pandas groupby 完全覆盖 SQL 的 GROUP BY 功能,同时支持 Python 函数自定义分组逻辑、转换操作保持原始行数、以及 apply 自由度极高的扩展。这些是 SQL 难以实现的。

二、创建分组对象

groupby 方法返回一个 DataFrameGroupBy 对象。这个对象本身并不计算任何结果,只是记录了"如何分组"的元信息。直到你调用聚合、转换或过滤方法时,才会真正触发计算。这种惰性求值设计可以链式组合多个操作。

2.1 单列分组

最基础的形式:按照一个列的值进行分组。分组键可以是列名(字符串),也可以是 Series。

# 按单个列名分组 grouped = df.groupby('部门') # 查看分组对象类型 print(type(grouped)) # <class 'pandas.core.groupby.generic.DataFrameGroupBy'>

2.2 多列分组

传入列名列表,创建基于多列的分层分组索引。这对于有多维度层次的数据非常有用。

# 创建包含多维度分类的数据 df2 = pd.DataFrame({ '城市': ['上海', '上海', '北京', '北京', '上海', '北京'], '季度': ['Q1', 'Q2', 'Q1', 'Q2', 'Q1', 'Q2'], '收入': [100, 150, 120, 130, 110, 140] }) # 按城市和季度两列分组 grouped_multi = df2.groupby(['城市', '季度']) result = grouped_multi['收入'].sum() print(result)
城市 季度 北京 Q1 120 Q2 270 上海 Q1 210 Q2 150 Name: 收入, dtype: int64

多列分组会产生一个 MultiIndex,后续可以通过 .unstack() 将内层索引转为列,形成透视表的效果。

2.3 自定义分组函数

当分组逻辑不能简单地由列值确定时,可以传入一个函数。函数会被应用到索引(index)或指定的列上,返回值为分组键。

import numpy as np # 按数值区间分组:使用 pd.cut df3 = pd.DataFrame({ 'score': [55, 72, 88, 43, 91, 67, 82, 59], 'name': ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] }) # 自定义分组:按分数段分组 bins = [0, 60, 75, 85, 100] labels = ['不及格', '及格', '良好', '优秀'] df3['等级'] = pd.cut(df3['score'], bins=bins, labels=labels) grouped = df3.groupby('等级') print(grouped['score'].agg(['count', 'mean']))
count mean 等级 不及格 2 49.0 及格 2 69.5 良好 2 85.0 优秀 2 89.5

另一种常见场景是按索引的函数分组,例如按年份分组时间序列数据:

# 按索引的函数分组 dates = pd.date_range('2025-01-01', periods=6, freq='ME') df4 = pd.DataFrame({'value': [100, 200, 150, 300, 250, 180]}, index=dates) # 分组函数:按年份分组 grouped_year = df4.groupby(lambda x: x.year) print(grouped_year['value'].sum()) # 输出: 2025 1180

2.4 level 层级分组

当 DataFrame 已经有 MultiIndex(多层行索引)时,可以通过 level 参数指定按哪个层级分组,而无需额外指定列名。

# 创建多层索引 DataFrame arrays = [['A', 'A', 'B', 'B', 'C', 'C'], ['X', 'Y', 'X', 'Y', 'X', 'Y']] index = pd.MultiIndex.from_arrays(arrays, names=['类别', '子类']) df5 = pd.DataFrame({'数值': [10, 20, 30, 40, 50, 60]}, index=index) print(df5) # 数值 # 类别 子类 # A X 10 # Y 20 # B X 30 # Y 40 # C X 50 # Y 60 # 按第一层级(类别)分组求和 result_level = df5.groupby(level='类别').sum() print(result_level) # 数值 # 类别 # A 30 # B 70 # C 110

选择合适的分组方式:

  • 数据列中有分类值:使用列名分组(最常用)
  • 多维度分类:使用多列分组(列表形式)
  • 分组逻辑复杂:使用自定义函数或 pd.cut
  • 已有 MultiIndex 的 DataFrame:使用 level 参数

三、聚合操作(Aggregation)

聚合是最常用的"应用"操作。它将对每个组的数据进行计算,并为每个组返回一个标量值(或一组标量值)。聚合操作通过 .agg() 方法或直接调用聚合函数实现。

3.1 单个聚合函数

可以直接在分组对象后调用聚合函数,如 sum()mean()count() 等:

df = pd.DataFrame({ '组别': ['A', 'A', 'B', 'B', 'C'], '数值1': [10, 20, 30, 40, 50], '数值2': [1, 2, 3, 4, 5] }) # 直接调用聚合方法 print(df.groupby('组别').sum()) # 求和 print(df.groupby('组别').mean()) # 均值 print(df.groupby('组别').std()) # 标准差 print(df.groupby('组别').min()) # 最小值 print(df.groupby('组别').max()) # 最大值 print(df.groupby('组别').count()) # 非空计数

3.2 多个聚合函数(agg 列表形式)

使用 .agg() 方法可以一次性应用多个聚合函数。传入函数名的字符串列表即可:

result = df.groupby('组别')['数值1'].agg(['sum', 'mean', 'std', 'min', 'max']) print(result)
sum mean std min max 组别 A 30 15.0 7.071068 10 20 B 70 35.0 7.071068 30 40 C 50 50.0 NaN 50 50

如果需要对不同的列应用不同的聚合函数,可以传入字典:

result_dict = df.groupby('组别').agg({ '数值1': ['sum', 'mean'], '数值2': ['min', 'max'] }) print(result_dict)
数值1 数值2 sum mean min max 组别 A 30 15.0 1 2 B 70 35.0 3 4 C 50 50.0 5 5

3.3 自定义聚合函数

当内置函数不够用时,可以传入自定义函数。自定义函数接收一个 Series(或 DataFrame)作为输入,返回一个标量值。

# 自定义聚合:计算值域范围 def value_range(s): return s.max() - s.min() # 自定义聚合:计算变异系数(CV) def cv(s): return s.std() / s.mean() if s.mean() != 0 else np.nan # 应用自定义聚合 result_custom = df.groupby('组别')['数值1'].agg([value_range, cv]) print(result_custom)
value_range cv 组别 A 10 0.471405 B 10 0.202031 C 0 NaN

需要注意的是,自定义函数会带来一定的性能损失。如果内置函数能满足需求,优先使用内置函数。自定义函数也可以结合 np.* 函数使用:

# 使用 NumPy 函数 result_np = df.groupby('组别')['数值1'].agg([np.sum, np.mean, np.std]) print(result_np)

3.4 命名聚合(Named Aggregation)

Pandas 0.25+ 版本引入了命名聚合功能,通过 **kwargs 语法为聚合结果指定有意义的列名,比字典方式更清晰。

# 命名聚合:为每个结果列指定名称 result_named = df.groupby('组别').agg( 总和=('数值1', 'sum'), 均值=('数值1', 'mean'), 最小值=('数值2', 'min'), 最大值=('数值2', 'max'), 数值比=('数值1', lambda x: x.iloc[-1] / x.iloc[0]) ) print(result_named)
总和 均值 最小值 最大值 数值比 组别 A 30 15.0 1 2 2.0 B 70 35.0 3 4 1.0 C 50 50.0 5 5 1.0

命名聚合的语法为:新列名=(来源列, 聚合函数)。这种方式既避免了多级列名的问题,又使得代码可读性大大增强。

agg 参数快速参考

参数形式示例适用场景
字符串.agg('sum')单一函数,简单场景
函数对象.agg(np.sum)NumPy 或自定义函数
列表.agg(['sum','mean'])多个函数作用于同一列
字典.agg({'col1':'sum','col2':'mean'})不同列用不同函数
命名聚合.agg(新列=('col','sum'))需要自定义列名

四、转换操作(Transform)

转换与聚合的关键区别在于:聚合为每个组返回一个标量值,导致结果行数等于组数;而转换为每个组返回一个与原始组大小相同的对象,结果行数与原始 DataFrame 相同。这使得 transform 非常适合用于创建新特征、填充缺失值或计算组内比例。

4.1 transform 保持形状

下面的例子展示了 transform 如何保持原始行数,而聚合会减少行数:

df = pd.DataFrame({ '团队': ['A', 'A', 'B', 'B', 'B'], '销售额': [100, 200, 150, 250, 300] }) # 聚合:每个组返回一行 agg_result = df.groupby('团队')['销售额'].mean() print(agg_result) # 团队 # A 150.0 # B 233.3 # 转换:保持原始行数 df['团队均值'] = df.groupby('团队')['销售额'].transform('mean') print(df)
团队 销售额 团队均值 0 A 100 150.0 1 A 200 150.0 2 B 150 233.3 3 B 250 233.3 4 B 300 233.3

4.2 组内标准化(Z-score 归一化)

transform 非常适合组内计算,例如:计算每个销售额相对于其所在团队均值的偏离程度。

# 组内 Z-score 标准化 def z_score_group(x): return (x - x.mean()) / x.std() df['销售额_Z'] = df.groupby('团队')['销售额'].transform(z_score_group) print(df)
团队 销售额 团队均值 销售额_Z 0 A 100 150.0 -0.707107 1 A 200 150.0 0.707107 2 B 150 233.3 -0.872872 3 B 250 233.3 0.218218 4 B 300 233.3 0.654654

在实际业务场景中,这种组内标准化的意义很大。比如评估不同城市门店的销售表现时,直接比较绝对销售额可能不公允(一线城市天然比三线城市高),但比较组内 Z-score 可以反映各门店在其所处市场中的真实相对表现。

4.3 组内缺失值填充

另一个高频场景:按组填充缺失值,用该组有效值的均值或中位数填补。

# 包含缺失值的数据 df_miss = pd.DataFrame({ '组': ['X', 'X', 'X', 'Y', 'Y', 'Y'], '值': [10, np.nan, 30, 40, np.nan, 60] }) print(df_miss) # 组 值 # 0 X 10.0 # 1 X NaN # 2 X 30.0 # 3 Y 40.0 # 4 Y NaN # 5 Y 60.0 # 用组内均值填充缺失值 df_miss['值_填充'] = df_miss.groupby('组')['值'].transform( lambda x: x.fillna(x.mean()) ) print(df_miss)
组 值 值_填充 0 X 10.0 10.0 1 X NaN 20.0 2 X 30.0 30.0 3 Y 40.0 40.0 4 Y NaN 50.0 5 Y 60.0 60.0

可以看到,X 组的缺失值被填充为 (10+30)/2=20,Y 组的缺失值被填充为 (40+60)/2=50。这种方式比全局均值填充更加合理,因为它考虑了组内差异。

transform 典型应用场景:

  • 计算组内排名、组内百分比
  • 按组标准化/归一化数据
  • 按组填充缺失值
  • 计算相对于组均值的差值
  • 计算组内累计和

五、过滤操作(Filter)

过滤根据组的整体属性决定是否保留该组的所有行。它接受一个函数(返回布尔值),函数接收整个组 DataFrame,如果返回 True 则保留,否则丢弃。

过滤与布尔索引的区别在于:布尔索引基于行级别的条件筛选,而 filter 则基于组级别的条件。例如"只保留样本量大于 3 的组",这是一个组级别的条件,无法用行级别的布尔索引实现。

df_filter = pd.DataFrame({ '组别': ['A', 'A', 'B', 'B', 'B', 'C', 'C', 'C', 'C'], '分数': [60, 70, 80, 85, 90, 50, 55, 65, 60] }) print("原始数据:", df_filter.groupby('组别').size().to_dict()) # {'A': 2, 'B': 3, 'C': 4} # 只保留样本量 > 2 的组 filtered = df_filter.groupby('组别').filter(lambda x: len(x) > 2) print(filtered)
组别 分数 2 B 80 3 B 85 4 B 90 5 C 50 6 C 55 7 C 65 8 C 60

A 组只有 2 条记录,不满足条件,因此被整体移除。更复杂的过滤条件也可以组合:

# 只保留组内均值大于 70 且样本量大于 2 的组 filtered2 = df_filter.groupby('组别').filter( lambda x: x['分数'].mean() > 70 and len(x) > 2 ) print(filtered2) # 只有 B 组满足条件(均值=85,样本量=3)
组别 分数 2 B 80 3 B 85 4 B 90

在实际应用中,filter 常用于数据清洗阶段,例如去除异常组、保留有足够样本的类别等。在金融数据分析中,经常用 filter 去除交易笔数不足的股票:

# 实际场景:只保留交易天数 >= 200 的股票 # stocks_grouped = stock_data.groupby('股票代码') # stocks_200 = stocks_grouped.filter(lambda x: x['交易日期'].nunique() >= 200)

filter vs query/布尔索引

filter 作用于分组后的组的整体属性。如果你只需要根据行的值做筛选,用 .query() 或布尔索引比 filter 更高效。两者定位不同,不要混淆。

六、分组对象的内置方法

GroupBy 对象自身提供了许多有用的属性和方法,帮助你了解分组的细节信息。

6.1 groups 属性

返回一个字典,键为组名,值为该组对应的原始索引标签。这在调试和理解分组结构时非常有用。

df = pd.DataFrame({ '城市': ['上海', '北京', '上海', '广州', '北京'], '销量': [100, 200, 150, 180, 220] }) grouped = df.groupby('城市') # groups 属性 print(grouped.groups) # {'上海': [0, 2], '北京': [1, 4], '广州': [3]}

6.2 get_group() 方法

直接获取指定组的所有数据行。这在分步调试时非常有用。

# 获取指定组的数据 shanghai = grouped.get_group('上海') print(shanghai)
城市 销量 0 上海 100 2 上海 150

6.3 describe() 方法

对每个组生成描述性统计。这是快速了解各组数据分布的利器。

# 各组描述统计 desc = df.groupby('城市')['销量'].describe() print(desc)
count mean std min 25% 50% 75% max 城市 上海 2.0 125.0 35.355 100 112.5 125.0 137.5 150 北京 2.0 210.0 14.142 200 205.0 210.0 215.0 220 广州 1.0 180.0 NaN 180 180.0 180.0 180.0 180

6.4 ngroups 属性

返回分组的数量。在循环处理各分组时,常常需要知道有多少个组。

print(f"共有 {grouped.ngroups} 个组") # 共有 3 个组

6.5 其他常用方法

# size():每个组的行数 print(df.groupby('城市').size()) # 上海 2 # 北京 2 # 广州 1 # first() / last():每组第一行 / 最后一行 print(df.groupby('城市').first()) # nth(n):每组第 n 行(0-based) print(df.groupby('城市').nth(0)) # head(n) / tail(n):每组前 n 行 / 后 n 行 print(df.groupby('城市').head(1))

分组对象方法速查

方法/属性返回值用途
groups字典查看组名到索引的映射
get_group()DataFrame获取特定组的数据
describe()DataFrame各组描述性统计
ngroups整数分组数量
size()Series每组行数
first/last/nthDataFrame选择特定位置的行
head/tailDataFrame每组前/后 N 行

七、Apply 高级应用

.apply() 是 groupby 中最灵活的方法,也是性能开销最大的方法。它可以在每个分组上执行任意操作,返回一个 DataFrame、Series 或标量值。apply 的设计哲学是"只要你能用函数表达的逻辑,apply 都能做"。

7.1 apply 返回 DataFrame

# 对每个组应用自定义处理,返回 DataFrame def process_group(g): """为每组添加排名和累计占比""" g = g.sort_values('销量', ascending=False) g['排名'] = range(1, len(g) + 1) g['累计'] = g['销量'].cumsum() return g df_apply = pd.DataFrame({ '组别': ['A', 'A', 'A', 'B', 'B', 'B'], '产品': ['P1', 'P2', 'P3', 'P1', 'P2', 'P3'], '销量': [100, 200, 150, 300, 100, 200] }) result_apply = df_apply.groupby('组别').apply(process_group) print(result_apply)
组别 产品 销量 排名 累计 0 A P1 100 3 100 1 A P2 200 1 200 2 A P3 150 2 350 3 B P1 300 1 300 4 B P2 100 3 100 5 B P3 200 2 300

7.2 apply 返回标量

# 计算每个组的数据量级 def group_magnitude(g): """返回组的总量和均值乘积(一个有趣的指标)""" return g['销量'].sum() * g['销量'].mean() magnitudes = df_apply.groupby('组别').apply(group_magnitude) print(magnitudes) # 组别 # A 135000.0 # B 180000.0 # dtype: float64

7.3 apply 的索引问题

apply 的一个常见坑点是返回结果的索引。默认情况下,apply 会保留原始索引并添加分组键作为外层索引。可以通过设置 group_keys=False 控制索引行为。

# group_keys=False 避免多层索引 result_no_keys = df_apply.groupby('组别', group_keys=False).apply(process_group) print(result_no_keys) # 此时不会在索引中添加组别层级

apply vs agg vs transform

  • agg: 每个组返回一个标量值(或一组标量值),结果行数 = 组数
  • transform: 每个组返回同形状的对象,结果行数 = 原始行数
  • apply: 最灵活,可返回任意形状,但性能最差。当 agg 和 transform 能满足需求时,优先使用它们

7.4 apply 实战:组内线性回归

apply 的强大之处在于可以整合第三方库的计算。下面是在每个组内拟合线性回归的例子:

from scipy import stats # 为每个组拟合线性回归,返回斜率 def fit_slope(g): x = g['x'] y = g['y'] slope, intercept, r_val, p_val, std_err = stats.linregress(x, y) return pd.Series({'斜率': slope, 'R方': r_val**2, 'p值': p_val}) df_reg = pd.DataFrame({ '组': ['A']*5 + ['B']*5, 'x': [1,2,3,4,5, 1,2,3,4,5], 'y': [2,4,6,8,10, 3,5,7,9,11] }) result_reg = df_reg.groupby('组').apply(fit_slope) print(result_reg) # 斜率 R方 p值 # 组 # A 2.0 1.0 0.0 # B 2.0 1.0 0.0

八、GroupBy 性能优化

当数据量较大时,groupby 操作可能成为瓶颈。以下优化策略可以有效提升性能。

8.1 使用分类数据类型(CategoricalDtype)

如果分组键的重复值很多,将其转为 categorical 类型可以显著加速 groupby。

# 将分组列转为分类类型 df['组别'] = df['组别'].astype('category') # 后续 groupby 操作会更快 # 批量测试示例 import time n = 100000 df_large = pd.DataFrame({ 'cat': np.random.choice(['A', 'B', 'C', 'D', 'E'], size=n), 'val': np.random.randn(n) }) t0 = time.time() _ = df_large.groupby('cat')['val'].mean() t1 = time.time() print(f"默认分组: {t1-t0:.4f}s") # 转为 categorical df_large['cat'] = df_large['cat'].astype('category') t0 = time.time() _ = df_large.groupby('cat')['val'].mean() t1 = time.time() print(f"category 分组: {t1-t0:.4f}s")

8.2 避免在 agg/transform 中使用 lambda

Python 原生 lambda 函数在大量数据时性能较差。尽量使用内置函数或向量化方法。

# 不推荐 result_slow = df.groupby('组别')['数值'].agg(lambda x: x.max() - x.min()) # 推荐 result_fast = df.groupby('组别')['数值'].agg(np.ptp) # peak to peak # 如果必须用自定义函数,考虑 numba 加速 # 或者使用 groupby().apply() 替换为 groupby().transform() + 向量化操作

8.3 只选择需要的列

groupby 之前只选择需要的列,可以减少分组对象处理的数据量。

# 不推荐:对整个 DataFrame 分组后取列 result_bad = df.groupby('组别')[['数值1']].mean() # 推荐:先选择需要的列,再分组 result_good = df[['组别', '数值1']].groupby('组别').mean() # 两种写法结果相同,但后者减少了分组时的数据量

8.4 使用 sort=False 加速

默认情况下 groupby 会对结果按分组键排序。如果不需要排序结果,设置 sort=False 可以提升性能。

# 关闭排序可以加速 result_unsorted = df.groupby('组别', sort=False)['数值1'].sum()

性能优化优先级总结

  1. 仅选择必要的列: 减少数据传输量
  2. 使用分类类型: 特别适合重复值多的场景
  3. 优先使用内置或 NumPy 函数: 比 Python 自定义函数快数倍
  4. transform 优先于 apply: transform 有 C 级别的优化
  5. sort=False: 不需要排序时有用
  6. 必要时使用 numba: 自定义复杂函数时考虑

九、完整实战案例

下面通过一个综合案例,将上述所有知识点串联起来。假设我们有一份电商订单数据,需要完成一系列分析任务。

import pandas as pd import numpy as np # 构造电商订单数据 np.random.seed(42) dates = pd.date_range('2025-01-01', periods=100, freq='D') orders = pd.DataFrame({ '订单日期': np.random.choice(dates, 500), '城市': np.random.choice(['北京', '上海', '广州', '深圳', '杭州'], 500), '品类': np.random.choice(['电子产品', '服装', '食品', '日用品', '书籍'], 500), '金额': np.random.uniform(10, 1000, 500).round(2), '用户ID': np.random.randint(1000, 1100, 500) }) # 偶尔产生缺失值 orders.loc[np.random.choice(500, 20), '金额'] = np.nan print(f"数据概况:{orders.shape[0]} 行, {orders.shape[1]} 列") print(orders.head())
数据概况:500 行, 5 列 订单日期 城市 品类 金额 用户ID 0 2025-01-27 广州 服装 835.74 1036 1 2025-03-19 上海 日用品 174.31 1002 2 2025-02-14 深圳 日用品 427.10 1057 3 2025-01-11 杭州 食品 340.38 1045 4 2025-02-13 北京 电子产品 66.63 1054

任务一:各城市销售额汇总

# 聚合:每个城市的总销售额和订单数 city_stats = orders.groupby('城市')['金额'].agg( 总销售额='sum', 平均销售额='mean', 订单数='count', 标准差='std' ).round(2) print("各城市销售统计:") print(city_stats.sort_values('总销售额', ascending=False))
各城市销售统计: 总销售额 平均销售额 订单数 标准差 城市 北京 55783.10 454.58 107 294.99 上海 42776.70 411.31 104 282.14 杭州 41436.67 387.26 107 294.63 深圳 38557.19 377.03 100 303.04 广州 38520.81 378.64 100 288.96

任务二:各品类在各城市的销售表现

# 多列分组:品类 x 城市 category_city = orders.groupby(['品类', '城市'])['金额'].agg( 销售额='sum', 订单数='count' ).round(2) print(category_city.head(10))
品类 城市 日用品 上海 3766.63 北京 6296.40 广州 3733.06 深圳 4559.23 杭州 4896.44 服装 上海 5689.93 北京 7139.29 广州 5648.03 深圳 4926.30 杭州 4337.77

任务三:计算每个订单金额相对于其所在城市的占比

# transform:计算每个城市总金额,然后求占比 city_total = orders.groupby('城市')['金额'].transform('sum') orders['城市占比'] = (orders['金额'] / city_total * 100).round(2) print(orders[['城市', '金额', '城市占比']].head(10))
城市 金额 城市占比 0 广州 835.74 2.17 1 上海 174.31 0.41 2 深圳 427.10 1.11 3 杭州 340.38 0.82 4 北京 66.63 0.12

任务四:只保留有足够订单数据的城市-品类组合

# filter:只保留订单笔数 >= 10 的城市-品类组合 filtered_orders = orders.groupby(['城市', '品类']).filter( lambda x: len(x) >= 10 ) print(f"过滤前:{len(orders)} 行") print(f"过滤后:{len(filtered_orders)} 行")
过滤前:500 行 过滤后:462 行

任务五:按月份统计销售趋势

# 按月分组:使用 pd.Grouper orders_by_month = orders.set_index('订单日期').groupby( pd.Grouper(freq='ME') )['金额'].agg(月销售额='sum', 订单数='count') print(orders_by_month.head(6))
月销售额 订单数 订单日期 2025-01-31 35921.29 152 2025-02-28 30693.27 129 2025-03-31 31767.22 121 2025-04-30 29637.82 98

任务六:用户消费分层

# 按用户分组,计算消费特征 user_features = orders.groupby('用户ID').agg( 消费总金额=('金额', 'sum'), 消费次数=('金额', 'count'), 平均客单价=('金额', 'mean'), 最近消费日期=('订单日期', 'max') ) # 添加消费分层标签 def label_user(g): total = g['消费总金额'].iloc[0] if total >= 3000: return '高价值' elif total >= 1500: return '中价值' else: return '低价值' user_features['用户分层'] = user_features.groupby('用户ID').apply(label_user) print(user_features.head(10))
消费总金额 消费次数 平均客单价 最近消费日期 用户分层 用户ID 1000 2796.83 6 466.14 2025-04-25 中价值 1001 1439.60 5 287.92 2025-04-17 低价值 1002 5010.14 10 501.01 2025-04-13 高价值 1003 2850.97 8 356.37 2025-04-27 中价值

这个综合案例展示了 groupby 的多种操作如何协同工作。从聚合看整体,经 transform 算占比,借 filter 做清洗,最终靠 apply 做用户分层分析。每个步骤环环相扣,构成了完整的数据分析流水线。

十、核心要点总结

Pandas groupby 核心要点

  • 拆分-应用-合并范式是 groupby 的底层哲学,理解它是掌握一切操作的前提
  • agg(聚合):每个组返回标量值,行数减少。支持多函数、不同列不同函数、命名聚合等灵活形式
  • transform(转换):保持原始行数,用于组内计算(标准化、填充缺失值、计算占比)
  • filter(过滤):基于组的整体属性做行筛选,用于数据清洗阶段的异常组剔除
  • apply(应用):最灵活但性能最差。能解决 agg/transform/filter 无法覆盖的问题
  • 性能优化:分类类型、仅选必要列、优先内置函数、sort=False,这些小技巧在大数据量时效果显著

方法选择优先级:

  1. 内置方法(sum, mean, count 等):性能最佳,优先使用
  2. agg(含命名聚合):需要多个聚合函数或自定义聚合时使用
  3. transform:需要保持形状的组内运算
  4. filter:基于组属性的行筛选
  5. apply:上述方法无法满足需求时的最后选择

groupby 是 Pandas 中最核心、最强大的功能之一。熟练掌握它意味着你能够用 Python 高效地完成绝大多数数据分组分析任务,无论是简单的分组汇总,还是复杂的分组特征工程。建议读者在实际项目中多加练习,特别是将 agg、transform、filter 配合使用,组合出强大的分析流水线。