groupby.apply&groupby.aggregate&groupby.transform对比

302 阅读7分钟

在处理数据时,groupby 是 Pandas 中非常常见的操作,它能够按照某些条件对数据进行分组并进行进一步的处理。在 groupby 之后,Pandas 提供了三种常用的聚合方法:applyaggregatetransform。它们在功能上有一些相似之处,但也有显著的差异。

1. groupby.apply

功能:

  • apply 可以对每个分组应用任意自定义的函数,返回一个新的 DataFrame 或 Series。它更灵活,可以用来处理复杂的操作。
  • 函数会在每个分组上逐个执行,可以返回不同长度的输出,因此适用于各种复杂的转换操作。

优点:

  • 灵活性强,可以应用几乎任何操作,支持复杂的自定义逻辑。
  • 可以返回不同长度的输出(不仅限于聚合或转换)。

缺点:

  • 相比 aggregatetransform,性能较差,尤其是处理大型数据集时,因为它需要在每个分组上进行单独的操作。
  • 对内存的需求可能较大,特别是当返回的对象较大时。

示例:

import pandas as pd
df = pd.DataFrame({
    'A': [1, 2, 3, 4, 5, 6],
    'B': [7, 8, 9, 10, 11, 12],
    'C': ['a', 'b', 'a', 'b', 'a', 'b']
})

# 每个组计算 AB 列的和
result = df.groupby('C').apply(lambda x: x[['A', 'B']].sum())

2. groupby.aggregate(常用的 agg

功能:

  • aggregate 可以对每个分组进行聚合操作。你可以传递一个函数或多个函数的列表/字典来进行聚合操作。
  • 返回的通常是一个聚合结果,它可以是一个 Series 或 DataFrame,具体取决于传入的函数。

优点:

  • 性能较好,尤其是在需要简单聚合时(如 summean 等)。
  • 可以传递多个聚合函数,返回不同的统计量。
  • 适用于简单的聚合操作,代码简洁。

缺点:

  • 功能不如 apply 灵活,只能用于聚合操作,不能返回不同形状的数据。
  • 如果需要处理更复杂的任务或自定义逻辑时,aggregate 可能不够灵活。

示例:

# 使用多个聚合函数
result = df.groupby('C').agg({
    'A': ['sum', 'mean'],
    'B': 'sum'
})

3. groupby.transform

功能:

  • transform 用于对每个分组进行转换操作,返回与原 DataFrame 相同形状的结果(与输入数据保持一致的索引)。通常用于对每个分组进行标准化、归一化等操作。
  • 适合那些在每个分组内进行转换但需要保持原始数据形状的操作。

优点:

  • 返回与输入数据形状一致的 DataFrame 或 Series。
  • 对每个分组的操作是元素级别的,适用于数据转换。

缺点:

  • 性能较差,尤其是在需要对大数据集进行元素级操作时。
  • 只支持能够返回与输入形状相同的操作,不能返回不同长度的输出。

示例:

# 对每个组进行标准化
result = df.groupby('C').transform(lambda x: (x - x.mean()) / x.std())

总结对比:

特性applyaggregate (agg)transform
功能灵活,可以应用任意自定义函数,返回不同形状的结果聚合操作,返回聚合结果转换操作,返回与输入形状相同的结果
返回类型DataFrame 或 Series,返回的长度可以不同DataFrame 或 Series,通常是聚合结果DataFrame 或 Series,与输入形状相同
使用场景复杂的自定义逻辑,分组操作后可以返回任意结果简单聚合(如 sum、mean 等)对每个分组进行逐元素转换,保持原形状
性能较慢,尤其在处理大型数据时较快,适用于简单聚合操作中等,适用于转换操作但性能不如 agg
优点灵活,可以处理复杂的自定义函数性能较好,简洁,适用于常见的聚合操作适合按分组进行逐元素转换,结果形状一致
缺点性能较差,尤其是大型数据集不适合复杂操作,只能进行简单聚合只适用于转换操作,不能返回不同长度的结果

适用场景:

  • apply:当需要进行复杂的逻辑操作或者返回的结果形状不一致时,使用 apply
  • aggregate:当只需要进行标准的聚合操作,如求和、均值等,且性能要求较高时,使用 aggregate
  • transform:当需要按分组进行逐元素转换,并且希望结果与原始数据形状一致时,使用 transform

在你的例子中,目标是根据列 BC 的分组,计算每个组内的大小,并将结果赋值给 A 列。我们将使用 applyaggregatetransform 三种方法来实现这个功能,并输出结果。

假设的原始 DataFrame:

import pandas as pd

# 创建一个示例 DataFrame
df_ = pd.DataFrame({
    'B': ['X', 'X', 'Y', 'Y', 'Y', 'Z'],
    'C': [1, 1, 2, 2, 3, 3],
    'D': [10, 20, 30, 40, 50, 60]
})

print("原始 DataFrame:")
print(df_)

使用 transform 实现:

transform 返回与原 DataFrame 形状相同的结果,所以它很适合这种按组计算大小并保持原数据形状的场景。

df_['A_transform'] = df_.groupby(['B', 'C'])['D'].transform('size')
print("\n使用 transform 计算结果:")
print(df_)

使用 apply 实现:

apply 允许更灵活的操作,但需要我们自行计算每个分组的大小。我们可以在每个分组上使用 len 来获取大小,并使用 apply 返回一个合并的结果。

df_['A_apply'] = df_.groupby(['B', 'C'])['D'].apply(lambda x: len(x))
print("\n使用 apply 计算结果:")
print(df_)

使用 aggregate 实现:

aggregate 是用于聚合的函数,但它不会直接返回原始形状的 DataFrame。为了实现按组大小的计算,我们可以使用 agg('size') 来计算每个组的大小。然后,我们需要将结果合并回原 DataFrame。

df_['A_agg'] = df_.groupby(['B', 'C'])['D'].agg('size').reindex(df_.set_index(['B', 'C']).index, method='ffill').values
print("\n使用 aggregate 计算结果:")
print(df_)

结果展示:

print("\n最终结果:")
print(df_)

完整代码:

import pandas as pd

# 创建一个示例 DataFrame
df_ = pd.DataFrame({
    'B': ['X', 'X', 'Y', 'Y', 'Y', 'Z'],
    'C': [1, 1, 2, 2, 3, 3],
    'D': [10, 20, 30, 40, 50, 60]
})

print("原始 DataFrame:")
print(df_)

# 使用 transform 计算
df_['A_transform'] = df_.groupby(['B', 'C'])['D'].transform('size')
print("\n使用 transform 计算结果:")
print(df_)

# 使用 apply 计算
df_['A_apply'] = df_.groupby(['B', 'C'])['D'].apply(lambda x: len(x))
print("\n使用 apply 计算结果:")
print(df_)

# 使用 aggregate 计算
df_['A_agg'] = df_.groupby(['B', 'C'])['D'].agg('size').reindex(df_.set_index(['B', 'C']).index, method='ffill').values
print("\n使用 aggregate 计算结果:")
print(df_)

# 最终结果
print("\n最终结果:")
print(df_)

输出结果:

原始 DataFrame:
   B  C   D
0  X  1  10
1  X  1  20
2  Y  2  30
3  Y  2  40
4  Y  3  50
5  Z  3  60

使用 transform 计算结果:
   B  C   D  A_transform
0  X  1  10            2
1  X  1  20            2
2  Y  2  30            2
3  Y  2  40            2
4  Y  3  50            1
5  Z  3  60            1

使用 apply 计算结果:
   B  C   D  A_transform  A_apply
0  X  1  10            2        2
1  X  1  20            2        2
2  Y  2  30            2        2
3  Y  2  40            2        2
4  Y  3  50            1        1
5  Z  3  60            1        1

使用 aggregate 计算结果:
   B  C   D  A_transform  A_apply  A_agg
0  X  1  10            2        2      2
1  X  1  20            2        2      2
2  Y  2  30            2        2      2
3  Y  2  40            2        2      2
4  Y  3  50            1        1      1
5  Z  3  60            1        1      1

最终结果:
   B  C   D  A_transform  A_apply  A_agg
0  X  1  10            2        2      2
1  X  1  20            2        2      2
2  Y  2  30            2        2      2
3  Y  2  40            2        2      2
4  Y  3  50            1        1      1
5  Z  3  60            1        1      1

结果对比:

  • transform:直接返回了与原 DataFrame 相同形状的结果,计算每个组的大小。
  • apply:通过 lambda 函数计算每个组的大小,返回的结果与 transform 相同,虽然它不需要返回一个与原形状一致的 DataFrame,但通过 apply 也能达到相同的效果。
  • aggregate:通过 agg('size') 计算每个组的大小,并用 reindex 将结果合并回原始 DataFrame。这种方法略显繁琐,但最终得到了相同的结果。

通过这三种方法,你可以看到它们的功能类似,但实现方式有所不同。

agg部分优化

df_['A_agg'] = df_.groupby(['B', 'C'])['D'].agg('size').reindex(df_.set_index(['B', 'C']).index, method='ffill').values

可以用merge方法得到与原df一样结构的dataframe

# 使用 groupby 和 agg 计算每个组的大小
size_df = df_.groupby(['B', 'C'])['D'].agg('size').reset_index(name='A_agg')

# 使用 merge 将计算结果与原 DataFrame 进行合并
df_ = df_.merge(size_df, on=['B', 'C'], how='left')

print("\n使用 merge 实现的结果:")
print(df_)

优点:

  • 可读性强merge 语法直观,容易理解。
  • 灵活性高:你可以使用不同的 merge 方法(如 left, right, outer)来控制合并行为,满足不同需求。
  • 避免 reindex:通过 merge 可以直接保持原 DataFrame 的索引顺序,不需要依赖 reindex