Pandas & NumPy Academy · 课时

对 GroupBy 使用 apply()

将跨多行的函数传递给 groupby().apply(),计算 agg() 无法表达的复杂组级摘要。

第 2 / 4 课13 个步骤

对 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 反馈 — 无需本地设置。

此课程中的所有课时

  1. 对列和行使用 apply()
  2. 对 GroupBy 使用 apply()
  3. 用于逐元素操作的 map() 和 applymap()
  4. 使用 pipe() 链式调用方法
← 返回 Pandas & NumPy Academy