Pandas Groupby 完整使用指南:数据分组与聚合的艺术
在数据分析的广阔天地里,我们经常需要按不同的类别或维度对数据进行分组统计、计算或转换。设想一下,你有一份包含数百万条销售记录的数据集,你可能想知道:
* 每个地区的总销售额是多少?
* 每种产品的平均价格是多少?
* 每个月销量最好的产品是什么?
* 不同用户群体的购买习惯有何差异?
这些问题的核心都在于一个操作:分组 (Grouping)。将具有相同特征的数据行归为一组,然后对每一组独立地进行某种计算。在 Pandas 中,实现这一强大功能的关键就是 groupby()
方法。
groupby()
是 Pandas 中最强大和灵活的工具之一,它实现了数据处理中一个非常重要的范式:Split-Apply-Combine(分裂-应用-组合)。理解并熟练掌握 groupby()
的使用,能极大地提升你的数据处理效率和分析能力。
本文将带你深入探索 Pandas groupby()
的方方面面,从基本概念到高级用法,力求为你提供一份完整的指南。
1. Split-Apply-Combine 范式
在深入代码之前,理解 groupby()
背后的核心思想——Split-Apply-Combine 范式——至关重要。
- Split (分裂): 根据某个或多个键(key),将原始数据(通常是一个 DataFrame)分裂成多个独立的数据块。每个数据块包含原 DataFrame 中所有属于同一组的数据行。
- Apply (应用): 对每个独立的数据块应用一个函数。这个函数可以是聚合函数(如求和、平均值)、转换函数(如填充缺失值、标准化)或过滤函数(如选择满足条件的组)。
- 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 中的 Apply 和 Combine 步骤。
主要的操作类型包括:
- 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()
方法接收一个函数,该函数会应用于每个组。这个函数需要满足:
- 输入是一个 Series 或 DataFrame(取决于你调用 transform 的对象)。
- 输出是一个长度与输入相同的 Series 或 DataFrame。
- 输出的索引与输入的索引相同。
示例:
“`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()
方法接收一个函数,该函数会应用于每个组。这个函数需要满足:
- 输入是一个 DataFrame 或 Series(代表当前的组)。
- 输出是一个布尔值 (
True
或False
),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()
方法有所帮助!