Pandas Groupby 完整使用指南 – wiki基地


Pandas Groupby 完整使用指南:数据分组与聚合的艺术

在数据分析的广阔天地里,我们经常需要按不同的类别或维度对数据进行分组统计、计算或转换。设想一下,你有一份包含数百万条销售记录的数据集,你可能想知道:
* 每个地区的总销售额是多少?
* 每种产品的平均价格是多少?
* 每个月销量最好的产品是什么?
* 不同用户群体的购买习惯有何差异?

这些问题的核心都在于一个操作:分组 (Grouping)。将具有相同特征的数据行归为一组,然后对每一组独立地进行某种计算。在 Pandas 中,实现这一强大功能的关键就是 groupby() 方法。

groupby() 是 Pandas 中最强大和灵活的工具之一,它实现了数据处理中一个非常重要的范式:Split-Apply-Combine(分裂-应用-组合)。理解并熟练掌握 groupby() 的使用,能极大地提升你的数据处理效率和分析能力。

本文将带你深入探索 Pandas groupby() 的方方面面,从基本概念到高级用法,力求为你提供一份完整的指南。

1. Split-Apply-Combine 范式

在深入代码之前,理解 groupby() 背后的核心思想——Split-Apply-Combine 范式——至关重要。

  1. Split (分裂): 根据某个或多个键(key),将原始数据(通常是一个 DataFrame)分裂成多个独立的数据块。每个数据块包含原 DataFrame 中所有属于同一组的数据行。
  2. Apply (应用): 对每个独立的数据块应用一个函数。这个函数可以是聚合函数(如求和、平均值)、转换函数(如填充缺失值、标准化)或过滤函数(如选择满足条件的组)。
  3. Combine (组合): 将对每个数据块应用函数后得到的结果组合成一个新的数据结构,通常是一个 Series、DataFrame 或 MultiIndex Series/DataFrame。

groupby() 方法主要负责分裂这一步,它返回一个 GroupBy 对象。接下来的应用组合则通过在 GroupBy 对象上调用不同的方法(如 agg(), transform(), filter(), apply() 等)来实现。

2. groupby() 方法的基本使用

df.groupby(by=None, axis=0, level=None, as_index=True, sort=True, group_keys=True, squeeze=False, observed=False, dropna=True)

groupby() 方法有很多参数,但最常用的是 by

  • by: 用于确定分组的键。它可以是以下几种形式:
    • 单个列名(字符串)
    • 多个列名组成的列表(列表)
    • 一个 Series 或数组(与 DataFrame 索引长度相同)
    • 一个字典或函数(用于将索引值或列值映射到组名)
    • 索引级别名称或编号(用于 MultiIndex)
  • axis: 指定沿着哪个轴进行分组。0'index' 表示按行分组(默认),1'columns' 表示按列分组。在实际应用中,绝大多数情况下是按行分组(axis=0)。
  • as_index: 默认为 True。如果为 True,分组键将成为结果 DataFrame 的索引(对于多列分组,结果索引将是 MultiIndex)。如果为 False,分组键将作为结果 DataFrame 的普通列。在进行聚合操作时,设置 as_index=False 可以避免创建 MultiIndex,结果更扁平,有时更方便后续处理。
  • sort: 默认为 True。是否对分组键进行排序。通常保留默认值即可。
  • dropna: 默认为 True。如果为 True,在分组键中值为 NaN 的行将被排除。

示例数据构建:

为了演示,我们先创建一个示例 DataFrame。

“`python
import pandas as pd
import numpy as np

创建示例 DataFrame

data = {‘Category’: [‘A’, ‘B’, ‘A’, ‘C’, ‘B’, ‘A’, ‘C’, ‘B’, ‘A’, ‘C’],
‘SubCategory’: [‘X’, ‘Y’, ‘X’, ‘Z’, ‘Y’, ‘X’, ‘Z’, ‘Y’, ‘X’, ‘Z’],
‘Value1’: np.random.randint(10, 100, 10),
‘Value2’: np.random.rand(10) * 100,
‘Date’: pd.to_datetime([‘2023-01-15’, ‘2023-01-20’, ‘2023-02-10’, ‘2023-02-18’,
‘2023-03-05’, ‘2023-03-25’, ‘2023-04-01’, ‘2023-04-10’,
‘2023-05-01’, ‘2023-05-20’])}
df = pd.DataFrame(data)

print(“原始 DataFrame:”)
print(df)
print(“-” * 30)
“`

按单列分组:

“`python

按 ‘Category’ 列分组

grouped_by_category = df.groupby(‘Category’)

print(“按 ‘Category’ 分组后的 GroupBy 对象:”)
print(grouped_by_category)
print(“-” * 30)

GroupBy 对象本身没有直接的可见输出,需要进一步操作

我们可以查看分组的数量

print(f”分组数量: {len(grouped_by_category)}”)

也可以查看具体的分组名和每组包含的索引

for name, group in grouped_by_category:
print(f”组名: {name}”)
print(f”组内数据索引: {group.index.tolist()}”)
# print(group) # 打印每组的完整数据
print(“-” * 30)
“`

按多列分组:

“`python

按 ‘Category’ 和 ‘SubCategory’ 列分组

grouped_by_multi_cols = df.groupby([‘Category’, ‘SubCategory’])

print(“按 [‘Category’, ‘SubCategory’] 分组后的 GroupBy 对象:”)
print(grouped_by_multi_cols)
print(f”分组数量: {len(grouped_by_multi_cols)}”)
print(“-” * 30)
“`

按函数/字典分组:

假设我们想按年份分组 Date 列。

“`python

按日期年份分组

grouped_by_year = df.groupby(df[‘Date’].dt.year)

print(“按年份分组后的 GroupBy 对象:”)
print(grouped_by_year)
print(f”分组数量: {len(grouped_by_year)}”)
print(“-” * 30)

也可以使用 lambda 函数分组索引(如果索引是 datetime 类型的)

grouped_by_index_year = df.set_index(‘Date’).groupby(lambda x: x.year)

“`

按索引级别分组 (针对 MultiIndex):

如果 DataFrame 的索引是 MultiIndex,可以使用 level 参数按某个索引级别进行分组。

“`python

创建一个带 MultiIndex 的 DataFrame

df_multiindex = df.set_index([‘Category’, ‘SubCategory’])

按第一个索引级别(’Category’)分组

grouped_by_level0 = df_multiindex.groupby(level=0)
print(“按第一个索引级别分组后的 GroupBy 对象:”)
print(grouped_by_level0)
print(f”分组数量: {len(grouped_by_level0)}”)

按第二个索引级别(’SubCategory’)分组

grouped_by_level1 = df_multiindex.groupby(level=’SubCategory’)
print(“按第二个索引级别分组后的 GroupBy 对象:”)
print(grouped_by_level1)
print(f”分组数量: {len(grouped_by_level1)}”)
print(“-” * 30)
“`

3. Apply Phase: 在 GroupBy 对象上进行操作

groupby() 返回的 GroupBy 对象是一个“惰性”对象,它只描述了如何分组,但数据本身并没有被立即分组或计算。实际的计算发生在对 GroupBy 对象调用某个方法时。这就是 Split-Apply-Combine 中的 ApplyCombine 步骤。

主要的操作类型包括:

  • Aggregation (聚合): 计算每个组的总和、平均值、计数等,将每组的多行数据压缩成一行结果。
  • Transformation (转换): 对每个组应用一个函数,但结果的形状与原组的形状相同,用于在组内进行数据转换(如标准化、填充缺失值)。
  • Filtration (过滤): 根据组的某些特征,丢弃或保留整个组。
  • Application (应用): 对每个组应用任意的函数,这是最灵活的方式。

3.1 Aggregation (聚合)

聚合是 groupby() 最常见的用途。你可以在 GroupBy 对象上直接调用常见的聚合函数,或者使用 agg() / aggregate() 方法进行更复杂的聚合。

常用的聚合函数:

  • sum(): 求和
  • mean(): 平均值
  • median(): 中位数
  • count(): 计算非 NaN 值的数量
  • size(): 计算组的大小(行数),包括 NaN 值
  • min(): 最小值
  • max(): 最大值
  • std(): 标准差
  • var(): 方差
  • first(): 组内的第一个值
  • last(): 组内的最后一个值
  • nth(n): 组内的第 n 个值
  • nunique(): 计算唯一值的数量

示例:使用常用的聚合函数

“`python

按 ‘Category’ 计算 Value1 的总和

sum_by_category = df.groupby(‘Category’)[‘Value1’].sum()
print(“按 Category 分组,Value1 求和:”)
print(sum_by_category)
print(“-” * 30)

按 ‘Category’ 计算 Value2 的平均值

mean_by_category = df.groupby(‘Category’)[‘Value2’].mean()
print(“按 Category 分组,Value2 求平均值:”)
print(mean_by_category)
print(“-” * 30)

按 ‘Category’ 和 ‘SubCategory’ 分组计算 Value1 的最大值

max_by_multi_cols = df.groupby([‘Category’, ‘SubCategory’])[‘Value1’].max()
print(“按 Category 和 SubCategory 分组,Value1 求最大值:”)
print(max_by_multi_cols)
print(“-” * 30)

计算每组的行数 (包括 NaN)

size_by_category = df.groupby(‘Category’).size()
print(“按 Category 分组,计算每组行数 (size):”)
print(size_by_category)
print(“-” * 30)

计算每组 Value1 的非 NaN 数量

count_by_category = df.groupby(‘Category’)[‘Value1’].count()
print(“按 Category 分组,计算 Value1 非 NaN 数量 (count):”)
print(count_by_category)
print(“-” * 30)

注意:size() 和 count() 的区别

size() 计算每组的行数,与列无关

count() 计算指定列在每组中非 NaN 值的数量

如果 Value1 列有 NaN,count() 结果会小于 size()

df_nan = pd.DataFrame({‘Category’: [‘A’, ‘B’, ‘A’, ‘B’], ‘Value’: [1, 2, np.nan, 4]})
print(“包含 NaN 的示例 DataFrame:”)
print(df_nan)
print(df_nan.groupby(‘Category’).size())
print(df_nan.groupby(‘Category’)[‘Value’].count())
print(“-” * 30)
“`

使用 agg() / aggregate() 进行多重聚合

agg() (或 aggregate()) 方法允许你对一个或多个列应用一个或多个聚合函数。这是聚合操作中最强大和灵活的方式。

  • 对一个列应用多个函数: 传递一个函数名或函数列表。

“`python

按 Category 分组,对 Value1 计算总和、平均值、最大值和最小值

agg_single_col_multi_funcs = df.groupby(‘Category’)[‘Value1’].agg([‘sum’, ‘mean’, ‘max’, ‘min’])
print(“对 Value1 应用多个聚合函数:”)
print(agg_single_col_multi_funcs)
print(“-” * 30)
“`

  • 对多个列应用同一个函数: 在选择多列后应用函数名。

“`python

按 Category 分组,对 Value1 和 Value2 都计算平均值

agg_multi_cols_single_func = df.groupby(‘Category’)[[‘Value1’, ‘Value2’]].mean()
print(“对 Value1 和 Value2 应用同一个聚合函数:”)
print(agg_multi_cols_single_func)
print(“-” * 30)
“`

  • 对多个列应用不同的函数: 传递一个字典,键是列名,值是函数名或函数列表。

“`python

按 Category 分组,对 Value1 计算总和和平均值,对 Value2 计算最大值和标准差

agg_multi_cols_multi_funcs = df.groupby(‘Category’).agg({
‘Value1’: [‘sum’, ‘mean’],
‘Value2’: [‘max’, ‘std’]
})
print(“对不同列应用不同的聚合函数:”)
print(agg_multi_cols_multi_funcs)
print(“-” * 30)

结果会是 MultiIndex 列,如果需要扁平化,可以在agg后使用 .reset_index() 或修改列名

“`

  • 使用 NamedAgg 或元组指定输出列名: 对于复杂的聚合,直接使用函数名会导致多级列名,可以使用 NamedAgg(Pandas 0.25+)或 (列名, 函数) 元组来自定义输出列名。

“`python

使用元组指定输出列名

agg_named_cols_tuple = df.groupby(‘Category’).agg(
total_value1=(‘Value1’, ‘sum’),
avg_value2=(‘Value2’, ‘mean’),
value1_value2_ratio=(‘Value1’, lambda x: x.sum() / x.mean()) # 也可以使用 lambda 函数
)
print(“使用元组指定输出列名:”)
print(agg_named_cols_tuple)
print(“-” * 30)

使用 pd.NamedAgg (更清晰推荐)

agg_named_cols_namedagg = df.groupby(‘Category’).agg(
total_value1=pd.NamedAgg(column=’Value1′, aggfunc=’sum’),
avg_value2=pd.NamedAgg(column=’Value2′, aggfunc=’mean’)
)
print(“使用 pd.NamedAgg 指定输出列名:”)
print(agg_named_cols_namedagg)
print(“-” * 30)
“`

3.2 Transformation (转换)

转换操作与聚合不同,它在每个组上应用函数后,返回的结果形状与原始组的形状相同,然后这些结果会被组合起来,形成一个与原始 DataFrame 或 Series 索引对齐的 Series 或 DataFrame。这对于进行组内标准化、填充缺失值、计算组内排名等操作非常有用。

主要使用 transform() 方法。transform() 方法接收一个函数,该函数会应用于每个组。这个函数需要满足:

  1. 输入是一个 Series 或 DataFrame(取决于你调用 transform 的对象)。
  2. 输出是一个长度与输入相同的 Series 或 DataFrame。
  3. 输出的索引与输入的索引相同。

示例:

“`python

在每个 Category 组内,用该组的平均值填充 Value1 的缺失值

df_with_nan_transform = df.copy()
df_with_nan_transform.loc[[0, 5, 9], ‘Value1’] = np.nan # 制造一些 NaN
print(“带有 NaN 的 DataFrame:”)
print(df_with_nan_transform)

df_filled = df_with_nan_transform.groupby(‘Category’)[‘Value1’].transform(lambda x: x.fillna(x.mean()))
df_with_nan_transform[‘Value1_filled’] = df_filled
print(“\n使用组内平均值填充 Value1 缺失值:”)
print(df_with_nan_transform)
print(“-” * 30)

在每个 Category 组内,对 Value2 进行标准化 (Z-score)

df[‘Value2_zscore_by_category’] = df.groupby(‘Category’)[‘Value2’].transform(lambda x: (x – x.mean()) / x.std())
print(“在组内对 Value2 进行标准化:”)
print(df)
print(“-” * 30)

在每个 Category 组内,计算 Value1 的排名

df[‘Value1_rank_by_category’] = df.groupby(‘Category’)[‘Value1’].rank() # rank() 本身就是 groupy transform 的快捷方法
print(“在组内计算 Value1 的排名:”)
print(df)
print(“-” * 30)
“`

transform vs apply:
transform 通常比通用的 apply 更快,特别是对于一些常见的转换操作(如标准化、填充)。如果你的操作符合 transform 的输入输出要求(组内形状不变),优先考虑使用 transform

3.3 Filtration (过滤)

过滤操作根据每个组的特征,决定是保留整个组还是丢弃整个组。例如,你可能只想保留总销售额超过某个阈值的地区的数据。

主要使用 filter() 方法。filter() 方法接收一个函数,该函数会应用于每个组。这个函数需要满足:

  1. 输入是一个 DataFrame 或 Series(代表当前的组)。
  2. 输出是一个布尔值 (TrueFalse),True 表示保留该组,False 表示丢弃该组。

示例:

“`python

保留 Value1 总和大于 200 的 Category 组

df_filtered = df.groupby(‘Category’).filter(lambda x: x[‘Value1’].sum() > 200)
print(“保留 Value1 总和大于 200 的组:”)
print(df_filtered)
print(“-” * 30)

保留组内行数大于 3 的 Category 组

df_filtered_size = df.groupby(‘Category’).filter(lambda x: len(x) > 3)
print(“保留行数大于 3 的组:”)
print(df_filtered_size)
print(“-” * 30)
“`

请注意 filter 函数的返回值必须是单个布尔值,而不是一个布尔 Series。

3.4 Application (应用)

apply() 方法是最通用和灵活的。它可以应用于每个组,并可以返回各种不同类型的结果:

  • 一个标量值 (scalar value)
  • 一个 Series
  • 一个 DataFrame

apply() 会尝试根据函数的返回值来智能地组合结果。

  • 如果函数返回标量值,apply 会返回一个 Series,索引是组名。这类似于聚合操作。
  • 如果函数返回一个 Series,apply 会返回一个 DataFrame,索引是 MultiIndex (组名 + 原始组内索引)。
  • 如果函数返回一个 DataFrame,apply 会返回一个 MultiIndex DataFrame (组名 + 原始组内索引)。

示例:

“`python

使用 apply 实现聚合 (返回标量) – 与 agg 类似

avg_value1_apply = df.groupby(‘Category’)[‘Value1’].apply(lambda x: x.mean())
print(“使用 apply 计算 Value1 平均值:”)
print(avg_value1_apply)
print(“-” * 30)

使用 apply 获取每个组内 Value1 最高的两行 (返回 DataFrame/Series)

apply 函数接收的是组的 DataFrame

top_2_per_group = df.groupby(‘Category’).apply(lambda x: x.nlargest(2, ‘Value1’))
print(“使用 apply 获取每组 Value1 最高的两行:”)
print(top_2_per_group)
print(“-” * 30)

使用 apply 进行自定义计算并返回一个新的 DataFrame

计算每组 Value1 的总和以及 Value2 的平均值

custom_agg_apply = df.groupby(‘Category’).apply(lambda x: pd.Series({
‘Value1_Sum’: x[‘Value1’].sum(),
‘Value2_Mean’: x[‘Value2’].mean()
}))
print(“使用 apply 进行自定义聚合计算:”)
print(custom_agg_apply)
print(“-” * 30)
“`

apply()groupby 链条中的万能工具,但相比 agg, transform, filter,它通常效率较低,尤其是在可以向量化操作的情况下。优先使用特定的方法,只有当它们无法满足需求时再考虑 apply()

4. GroupBy 对象的属性和方法

除了上面提到的聚合、转换、过滤和应用方法,GroupBy 对象还有一些有用的属性和方法:

  • groups: 一个字典,键是组名,值是该组在原始 DataFrame 中的索引。
  • ngroups: 组的数量。
  • get_group(name): 获取指定组名对应的数据(DataFrame)。
  • indices: 一个字典,键是组名,值是 NumPy 数组,包含该组在原始 DataFrame 中的整数位置索引。

“`python

示例属性和方法

grouped = df.groupby(‘Category’)

print(“组名和对应的索引:”)
print(grouped.groups)
print(“-” * 30)

print(f”组的数量: {grouped.ngroups}”)
print(“-” * 30)

获取 ‘A’ 组的数据

group_A = grouped.get_group(‘A’)
print(“获取 ‘A’ 组的数据:”)
print(group_A)
print(“-” * 30)
“`

5. 迭代 GroupBy 对象

虽然大多数 groupby 操作都是通过调用聚合、转换等方法进行向量化处理,但有时为了调试或执行一些非常规操作,你可能需要遍历每个组。

GroupBy 对象是可迭代的,每次迭代返回一个元组 (name, group_df),其中 name 是组名,group_df 是该组对应的 DataFrame。

python
print("迭代 GroupBy 对象:")
for name, group_df in df.groupby('Category'):
print(f"--- 组名: {name} ---")
print(group_df)
print("-" * 10)
print("-" * 30)

6. 处理 MultiIndex 结果

当按多列分组时,默认情况下,分组键会成为结果 DataFrame 的 MultiIndex(多级索引)。

“`python

按 Category 和 SubCategory 分组并求和

multiindex_result = df.groupby([‘Category’, ‘SubCategory’])[[‘Value1’, ‘Value2’]].sum()
print(“按多列分组,结果为 MultiIndex:”)
print(multiindex_result)
print(“-” * 30)
“`

如果你不希望结果是 MultiIndex,可以在 groupby() 中设置 as_index=False

“`python

按 Category 和 SubCategory 分组并求和,不使用索引

flat_result = df.groupby([‘Category’, ‘SubCategory’], as_index=False)[[‘Value1’, ‘Value2’]].sum()
print(“按多列分组,结果不使用 MultiIndex (as_index=False):”)
print(flat_result)
print(“-” * 30)
“`

通常,对于最终的分析结果或需要与其他数据合并时,as_index=False 得到的扁平化 DataFrame 更为方便。

7. 处理缺失值 (dropna)

groupby() 方法有一个 dropna 参数,默认为 True。这意味着在确定分组时,如果分组键中包含 NaN 值,对应的行将被排除在任何分组之外。

“`python
df_with_nan_groupkey = df.copy()
df_with_nan_groupkey.loc[[0, 5], ‘Category’] = np.nan # 制造分组键中的 NaN

print(“包含分组键 NaN 的 DataFrame:”)
print(df_with_nan_groupkey)
print(“-” * 30)

默认 dropna=True,包含 NaN 的行会被忽略

grouped_nan_default = df_with_nan_groupkey.groupby(‘Category’)[‘Value1’].sum()
print(“默认 dropna=True 的分组结果:”)
print(grouped_nan_default)
print(“-” * 30)

设置 dropna=False,包含 NaN 的行会被视为一个单独的组

grouped_nan_false = df_with_nan_groupkey.groupby(‘Category’, dropna=False)[‘Value1’].sum()
print(“设置 dropna=False 的分组结果:”)
print(grouped_nan_false)
print(“-” * 30)
“`

在聚合计算中,Pandas 会默认忽略 NaN 值(例如,求和时跳过 NaN,计算平均值时不将 NaN 计入总数和计数)。如果你需要包含 NaN 进行计算,可以考虑在聚合前进行填充,或者使用 .sum(skipna=False) 等方法(如果聚合函数支持 skipna 参数)。

8. 性能考虑与最佳实践

  • 优先使用内置方法和 agg()/transform(): 对于常见的聚合和转换任务,使用 Pandas 内置的聚合方法(.sum(), .mean() 等)或 agg() / transform() 方法通常比使用 apply() 更高效,因为它们底层是高度优化的 C/Cython 代码。
  • 避免在 apply() 中遍历行: 如果在 apply() 的自定义函数中又对组内的 DataFrame 进行了逐行遍历(例如 for index, row in group_df.iterrows():),这将非常慢。尽量使用 Pandas 的向量化操作处理组内数据。
  • as_index=False: 如果你不需要分组键作为结果索引,使用 as_index=False 可以避免创建 MultiIndex,有时能简化后续处理并略微提升性能。
  • 考虑数据类型: Groupby 操作对数据类型敏感。确保你的数据类型是正确的(例如,数值列是 int/float,日期列是 datetime)。
  • 处理大型数据集: 对于非常大的数据集,groupby 可能消耗大量内存。如果遇到内存问题,考虑分块处理数据,或者使用 Dask 等并行计算库。

9. 总结

Pandas 的 groupby() 是数据分析中的瑞士军刀。它优雅地实现了 Split-Apply-Combine 范式,让你能够轻松地按类别对数据进行各种复杂的统计、转换和过滤操作。

  • 通过 groupby() 方法进行分裂,根据指定的键将数据分成组。
  • 在返回的 GroupBy 对象上调用 agg() 进行聚合,将每组数据压缩成一行摘要。
  • 调用 transform() 进行转换,对组内数据进行操作并保持原组形状,结果与原始 DataFrame 对齐。
  • 调用 filter() 进行过滤,根据组的特征保留或丢弃整个组。
  • 调用 apply() 进行通用的应用,处理更复杂的组内操作,返回灵活的结果。

熟练掌握 groupby() 的不同方法及其适用场景,将是你成为高效 Pandas 数据分析师的关键一步。多加练习,尝试将现实世界的数据分析问题拆解为 Split-Apply-Combine 的步骤,你会发现 groupby() 能帮你解决绝大多数分组计算的需求。


希望这篇详细指南对你理解和使用 Pandas 的 groupby() 方法有所帮助!

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部