对 GroupBy 使用 apply()
将跨多行的函数传递给 groupby().apply(),计算 agg() 无法表达的复杂组级摘要。
对 GroupBy 使用 apply() 是 CoddyKit 上的免费 Pandas & NumPy Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Pandas & NumPy Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Pandas & NumPy Academy 课程共包含 4 节课。
GroupBy 为何需要 apply()
内置的 agg() 方法可以处理 sum、mean 和 count 等简单聚合,即每个组输出一个标量。但有些分组级计算需要查看整个组的子 DataFrame,而不只是单个列。groupby().apply(func) 会将完整的组 DataFrame 传递给您的函数并汇总结果,从而实现 agg() 无法表达的复杂汇总。
import pandas as pd
import numpy as np
df = pd.DataFrame({
'region': ['North', 'North', 'South', 'South', 'North'],
'product': ['A', 'B', 'A', 'A', 'C'],
'revenue': [100, 250, 180, 90, 300]
})
print(df)每组返回一个标量
当传递给 groupby().apply() 的函数返回一个标量时,结果是一个按组键建立索引的 Series,与 agg() 生成的结果相同。当标量需要基于多列进行逻辑处理时,这种形式非常有用。例如,计算每组收入最高产品的收入与该组总收入的比值,就无法通过单个 agg() 列规范来表达。
def top_product_share(group):
top = group['revenue'].max()
total = group['revenue'].sum()
return top / total
share = df.groupby('region').apply(top_product_share)
print(share)每组返回一个 Series
当函数返回一个 pd.Series 时,结果会包含一个 MultiIndex:外层是组键,内层是 Series 的索引。这适用于在一次 apply 调用中计算每组的多个统计量,生成一张汇总表,其中每个组包含多行指标。
def group_stats(group):
return pd.Series({
'total': group['revenue'].sum(),
'top_product': group.loc[group['revenue'].idxmax(), 'product'],
'n_products': group['product'].nunique()
})
result = df.groupby('region').apply(group_stats)
print(result)每组返回一个 DataFrame
当函数返回一个 pd.DataFrame 时,groupby().apply() 会将所有子 DataFrame 按行拼接起来。这是功能最强的形式:您可以在每个组内筛选或转换行,并返回修改后的子集。例如,保留每个区域内按收入排名前 2 的产品。
def top2_per_region(group):
return group.nlargest(2, 'revenue')
top2 = df.groupby('region').apply(top2_per_region)
print(top2.reset_index(drop=True))计算组内 Z 分数
groupby().apply() 的一个常见用途是进行组内标准化。不要相对于所有订单进行收入归一化(这样会混合不同区域),而应计算每个区域内收入的 Z 分数。北部的 Z 分数为 2,表示“高于北部平均值 2 个标准差”——这比高于全局平均值 2 个标准差更有意义。
def within_group_zscore(group):
mean = group['revenue'].mean()
std = group['revenue'].std()
group = group.copy()
group['revenue_z'] = (group['revenue'] - mean) / std
return group
df_z = df.groupby('region').apply(within_group_zscore).reset_index(drop=True)
print(df_z)自定义聚合:Winsorised 平均值
Winsorised 平均值 会在求平均前截断极端值,因此对于偏态分布,它比简单平均值更稳健。这种自定义聚合无法通过标准的 agg() 函数表达,但使用 groupby().apply() 很容易实现:在每个组内按第 5 和第 95 百分位进行截断,然后对截断后的值计算平均值。
def winsorised_mean(group, lower_pct=0.05, upper_pct=0.95):
col = group['revenue']
lo = col.quantile(lower_pct)
hi = col.quantile(upper_pct)
clipped = col.clip(lo, hi)
return clipped.mean()
wins_mean = df.groupby('region').apply(winsorised_mean)
print('Winsorised mean by region:')
print(wins_mean)每组的自定义滚动窗口
直接应用于完整 DataFrame 的滚动窗口会忽略组边界。如果在交错包含两个产品的时间序列上计算 3 行滚动平均值,窗口就会错误地跨越产品边界。将滚动窗口放在 groupby().apply() 内部执行,可以确保每次滚动计算都限制在所属组内——例如,计算每个区域的 3 个月滚动收入平均值。
def group_rolling_mean(group, window=3):
group = group.sort_values('order_date').copy()
group['rolling_revenue'] = group['revenue'].rolling(window, min_periods=1).mean()
return group
# df_ts has order_date column
# df_rolled = df_ts.groupby('region').apply(group_rolling_mean).reset_index(drop=True)
print('Rolling window per group applied inside apply()')include_groups 参数(Pandas 2.2+)
在 Pandas 2.2 及更高版本中,如果函数接收到的 DataFrame 中包含 groupby 键,groupby().apply() 会发出 FutureWarning。传入 include_groups=False,即可从传递给函数的子 DataFrame 中排除 groupby 列。这样既能避免警告,也能避免在函数体内意外操作键列。
def revenue_only(group):
# group does not include 'region' column when include_groups=False
return group['revenue'].sum()
# result = df.groupby('region').apply(revenue_only, include_groups=False)
print('include_groups=False avoids FutureWarning in Pandas 2.2+')使用 reset_index 合并 apply() 结果
当 groupby().apply() 为每个组返回一个 DataFrame 时,结果会包含一个以组键为外层级别的 MultiIndex。使用 reset_index(drop=True) 可以将索引还原为普通整数范围。或者使用 reset_index(level=0) 将组键提升为一列,使其在输出中明确显示。
result = df.groupby('region').apply(top2_per_region)
print('With MultiIndex:')
print(result.head())
print('\nAfter reset_index:')
print(result.reset_index(drop=True))何时优先使用 transform() 而不是 apply()
groupby().apply() 用于复杂的分组级计算,其返回结果的形状不同于输入。groupby().transform() 用于添加与原索引对齐的新列,例如将组平均值广播回每一行以进行归一化。当您需要与原 DataFrame 形状相同的结果时,请使用 transform;当结果形状不同时,请使用 apply。
# transform: adds group mean back to each row
df['group_mean_revenue'] = df.groupby('region')['revenue'].transform('mean')
# apply: computes one scalar per group
group_totals = df.groupby('region')['revenue'].apply(sum)
print(df[['region', 'revenue', 'group_mean_revenue']])性能提示:简单聚合应避免使用 apply()
对于简单操作,groupby().apply() 明显慢于 groupby().agg(),因为 apply() 会为每个组创建一个完整的 Python 对象。对于求和、求平均值和计数,请始终使用 agg()。只有在确实需要完整组 DataFrame 时才使用 apply(),例如进行多列条件逻辑、自定义统计或组内筛选。
# SLOW: apply for simple sum
slow = df.groupby('region').apply(lambda g: g['revenue'].sum())
# FAST: native agg
fast = df.groupby('region')['revenue'].sum()
print('Both produce the same result:')
print(fast.equals(slow))快速检查
检验您对本课数据分析概念的理解。
课程回顾
本课您学会了:从 groupby().apply() 返回标量、Series 和 DataFrames、计算组内 Z 分数和自定义稳健统计量,以及在分组级操作中选择 apply()、agg() 和 transform()。接下来我们将探索 map() 和 applymap() 在 Series 与 DataFrames 上执行逐元素操作的方法。
用 AI 导师学习 Python — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 30
- 课程
- 120
常见问题解答
「对 GroupBy 使用 apply()」课时是免费的吗?
是的 — 「对 GroupBy 使用 apply()」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Pandas & NumPy Academy 课程的其余内容,请升级到 CoddyKit PRO。 Pandas & NumPy Academy 课程共包含 4 节课。
「对 GroupBy 使用 apply()」这节课中我会学到什么?
将跨多行的函数传递给 groupby().apply(),计算 agg() 无法表达的复杂组级摘要。 你通过在浏览器中直接运行的动手代码来练习 Pandas & NumPy Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Pandas & NumPy Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Pandas & NumPy Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「对 GroupBy 使用 apply()」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Pandas & NumPy Academy 课中编写并运行代码吗?
能。每节 Pandas & NumPy Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 对列和行使用 apply()
- 对 GroupBy 使用 apply()
- 用于逐元素操作的 map() 和 applymap()
- 使用 pipe() 链式调用方法