Pandas groupby
深度教程:掌握数据分组聚合的瑞士军刀
数据分析中,我们经常需要对数据进行分组,然后对每个组执行计算或转换。想象一下,您有一份销售订单数据,想知道每个产品的总销售额,或者每个地区的平均销售价格,或者找出每个客户的首次购买日期。这些任务的核心都是“分组”操作。
在 Pandas 库中,groupby()
方法就是执行这类操作的强大工具。它实现了数据处理中一个非常核心的“分裂-应用-合并”(Split-Apply-Combine)范式。
本文将深入探讨 Pandas groupby()
的方方面面,从基本用法到高级技巧,帮助您彻底掌握这个数据分析利器。
1. 理解 Split-Apply-Combine(分裂-应用-合并)范式
groupby()
方法的核心思想可以概括为以下三个步骤:
- 分裂 (Split): 根据某个(或多个)键将数据分割成不同的组。这些键可以是 DataFrame 的列、行索引,甚至是一个函数或列表。
- 应用 (Apply): 独立地对每个组应用一个函数。这个函数可以是聚合函数(如求和、平均值)、转换函数(如标准化、填充缺失值)、过滤函数(如筛选出满足条件的组),或任何自定义函数。
- 合并 (Combine): 将每个组的应用结果合并成一个单一的 Pandas 对象(Series 或 DataFrame)。
理解了这个范式,就能更好地理解 groupby()
的各种用法。
2. groupby()
的基本用法
groupby()
方法通常是 DataFrame 的一个方法,它的最简单形式是传入一个或多个用于分组的列名。
首先,创建一个示例 DataFrame:
“`python
import pandas as pd
import numpy as np
创建示例数据
data = {
‘类别’: [‘A’, ‘B’, ‘A’, ‘C’, ‘B’, ‘C’, ‘A’, ‘B’, ‘C’, ‘A’],
‘地区’: [‘East’, ‘West’, ‘North’, ‘East’, ‘North’, ‘West’, ‘East’, ‘North’, ‘West’, ‘North’],
‘值1’: [10, 20, 15, 25, 30, 22, 12, 28, 26, 18],
‘值2’: [100, 200, 150, 250, 300, 220, 120, 280, 260, 180],
‘日期’: pd.to_datetime([‘2023-01-15’, ‘2023-01-18’, ‘2023-02-01’, ‘2023-02-10’, ‘2023-02-15’,
‘2023-03-01’, ‘2023-03-05’, ‘2023-03-10’, ‘2023-03-12’, ‘2023-03-20’])
}
df = pd.DataFrame(data)
print(“原始 DataFrame:”)
print(df)
“`
输出:
原始 DataFrame:
类别 地区 值1 值2 日期
0 A East 10 100 2023-01-15
1 B West 20 200 2023-01-18
2 A North 15 150 2023-02-01
3 C East 25 250 2023-02-10
4 B North 30 300 2023-02-15
5 C West 22 220 2023-03-01
6 A East 12 120 2023-03-05
7 B North 28 280 2023-03-10
8 C West 26 260 2023-03-12
9 A North 18 180 2023-03-20
对 ‘类别’ 列进行分组:
python
grouped = df.groupby('类别')
print("\n对 '类别' 列进行分组后的对象:")
print(grouped)
输出:
对 '类别' 列进行分组后的对象:
<pandas.core.groupby.generic.DataFrameGroupBy object at ...>
注意,df.groupby('类别')
返回的不是一个 DataFrame,而是一个 DataFrameGroupBy
对象。这个对象代表了分组结构,但数据还没有被实际计算或操作。你需要对这个对象应用一个操作(聚合、转换、过滤等)才能看到结果。
3. 分组后的操作:聚合 (Aggregation)
聚合是最常见的 groupby()
操作。它对每个组的数据进行汇总计算,例如求和、平均值、计数、最大值、最小值等。结果的行索引通常是分组的键。
你可以对整个分组对象应用聚合函数,也可以先选择特定的列再应用。
3.1 应用单个聚合函数
“`python
计算每个类别的 ‘值1’ 的总和
sum_by_category = grouped[‘值1’].sum()
print(“\n按类别分组,计算 ‘值1’ 的总和:”)
print(sum_by_category)
计算每个类别的所有数值列的平均值
mean_by_category = grouped.mean()
print(“\n按类别分组,计算所有数值列的平均值:”)
print(mean_by_category)
计算每个类别的样本数量 (包括 NaN)
size_by_category = grouped.size()
print(“\n按类别分组,计算每组样本数量 (size):”)
print(size_by_category)
计算每个类别的非 NaN 样本数量 (count)
为了演示 count,我们在数据中加入一个 NaN
df_with_nan = df.copy()
df_with_nan.loc[0, ‘值1’] = np.nan
grouped_with_nan = df_with_nan.groupby(‘类别’)
count_by_category = grouped_with_nan[‘值1’].count()
print(“\n按类别分组,计算 ‘值1’ 的非 NaN 样本数量 (count):”)
print(count_by_category)
注意 size 和 count 的区别:size 统计组中所有行数,count 统计指定列的非 NaN 行数
“`
输出示例:
“`
按类别分组,计算 ‘值1’ 的总和:
类别
A 55
B 78
C 73
Name: 值1, dtype: int64
按类别分组,计算所有数值列的平均值:
值1 值2
类别
A 13.75 137.5
B 26.00 260.0
C 24.33 243.3
按类别分组,计算每组样本数量 (size):
类别
A 4
B 3
C 3
dtype: int64
按类别分组,计算 ‘值1’ 的非 NaN 样本数量 (count):
类别
A 3
B 3
C 3
Name: 值1, dtype: int64
“`
常用的聚合函数方法包括:.sum()
, .mean()
, .median()
, .min()
, .max()
, .count()
, .size()
, .std()
, .var()
, .first()
, .last()
, .nth()
, .ohlc()
.
3.2 应用多个聚合函数 (.agg()
或 .aggregate()
)
如果您想对同一列应用多个聚合函数,或者对不同的列应用不同的聚合函数,可以使用 .agg()
方法。
“`python
对 ‘值1’ 列计算总和、平均值和计数
agg_single_col_multi_func = grouped[‘值1’].agg([‘sum’, ‘mean’, ‘count’])
print(“\n对 ‘值1’ 列应用多个聚合函数:”)
print(agg_single_col_multi_func)
对不同的列应用不同的聚合函数
agg_multi_col_multi_func = grouped.agg({
‘值1’: [‘sum’, ‘mean’],
‘值2’: ‘max’,
‘日期’: ‘min’ # 对日期列求最小值
})
print(“\n对不同列应用不同的聚合函数:”)
print(agg_multi_col_multi_func)
也可以使用元组给聚合结果的列命名
agg_with_custom_names = grouped[‘值1’].agg(
total_值1=(‘sum’),
average_值1=(‘mean’)
)
print(“\n对 ‘值1’ 列应用多个聚合函数并指定结果列名:”)
print(agg_with_custom_names)
“`
输出示例:
“`
对 ‘值1’ 列应用多个聚合函数:
sum mean count
类别
A 55.0 13.75 4
B 78.0 26.00 3
C 73.0 24.33 3
对不同列应用不同的聚合函数:
值1 值2 日期
sum mean max min
类别
A 55.0 13.75 180 2023-01-15
B 78.0 26.00 300 2023-01-18
C 73.0 24.33 260 2023-02-10
对 ‘值1’ 列应用多个聚合函数并指定结果列名:
total_值1 average_值1
类别
A 55.0 13.75
B 78.0 26.00
C 73.0 24.33
“`
.agg()
提供了极大的灵活性来定制聚合结果的结构和命名。
3.3 as_index=False
参数
默认情况下,groupby
的分组键会成为结果 DataFrame 的索引。如果你不想让分组键成为索引,可以使用 as_index=False
参数。
“`python
计算每个类别的 ‘值1’ 的总和,并将类别作为普通列
sum_as_column = df.groupby(‘类别’, as_index=False)[‘值1’].sum()
print(“\n按类别分组求和,类别作为普通列:”)
print(sum_as_column)
对多个列分组时使用 as_index=False
sum_multi_group_as_column = df.groupby([‘类别’, ‘地区’], as_index=False)[‘值1’].sum()
print(“\n按类别和地区分组求和,分组键作为普通列:”)
print(sum_multi_group_as_column)
“`
输出示例:
“`
按类别分组求和,类别作为普通列:
类别 值1
0 A 55
1 B 78
2 C 73
按类别和地区分组求和,分组键作为普通列:
类别 地区 值1
0 A East 22
1 A North 33
2 B North 58
3 B West 20
4 C East 25
5 C West 48
“`
这对于后续的数据处理非常有用,比如直接与原始 DataFrame 合并。
4. 分组后的操作:转换 (Transformation)
转换操作会返回一个与原始 DataFrame 具有相同索引的 Pandas 对象。它不是对每个组进行汇总,而是对每个组的每个元素应用一个操作,并将结果放回与原位置对应的位置。
这通常用于在原始数据中添加一些基于组的计算结果,比如计算每个值在组内的 z-score,或者用组的平均值填充组内的缺失值。
transform()
方法是执行转换操作的主要工具。它要求传递的函数必须返回一个 Series 或 DataFrame,其索引与输入的组 DataFrame/Series 的索引相同,并且形状与输入的组数据兼容(通常是相同的形状或广播后相同形状)。
“`python
计算每个值在组内的标准化 (z-score)
z = (x – mean) / std
zscore_by_category = grouped[‘值1’].transform(lambda x: (x – x.mean()) / x.std())
print(“\n按类别分组,计算 ‘值1’ 的组内标准化 (z-score):”)
print(zscore_by_category)
使用组的平均值填充缺失值 (再次使用带有 NaN 的 df_with_nan)
filled_with_group_mean = grouped_with_nan[‘值1’].transform(lambda x: x.fillna(x.mean()))
print(“\n按类别分组,使用组平均值填充 ‘值1’ 的缺失值:”)
print(filled_with_group_mean)
计算组内的排名
rank_by_category = grouped[‘值1’].transform(lambda x: x.rank()) # 或者 grouped[‘值1’].rank()
print(“\n按类别分组,计算 ‘值1’ 的组内排名:”)
print(rank_by_category)
“`
输出示例:
“`
按类别分组,计算 ‘值1’ 的组内标准化 (z-score):
0 -1.240347
1 -1.732051
2 0.413449
3 0.000000
4 1.732051
5 -0.577350
6 -0.620174
7 0.000000
8 1.154701
9 1.446072
Name: 值1, dtype: float64
按类别分组,使用组平均值填充 ‘值1’ 的缺失值:
0 13.75
1 20.00
2 15.00
3 25.00
4 30.00
5 22.00
6 12.00
7 28.00
8 26.00
9 18.00
Name: 值1, dtype: float64
按类别分组,计算 ‘值1’ 的组内排名:
0 1.0
1 1.0
2 2.0
3 1.0
4 3.0
5 2.0
6 1.0
7 2.0
8 3.0
9 4.0
Name: 值1, dtype: float64
“`
transform()
是一个非常强大的工具,尤其适用于特征工程,比如创建基于分组的新的特征列。
5. 分组后的操作:过滤 (Filtering)
过滤操作会根据对组的属性进行的判断,丢弃或保留整个组。它返回一个原始 DataFrame 的子集。
filter()
方法是执行过滤操作的主要工具。它要求传递的函数必须接受一个组的 DataFrame 作为输入,并返回一个布尔值(True 表示保留该组,False 表示丢弃该组)。
“`python
过滤掉那些 ‘值1’ 总和小于 50 的类别
filtered_groups_sum = grouped.filter(lambda x: x[‘值1’].sum() >= 50)
print(“\n过滤掉 ‘值1’ 总和小于 50 的组:”)
print(filtered_groups_sum) # A, B, C 的总和都 >= 50,所以保留所有组
过滤掉那些组的行数少于 4 的类别
filtered_groups_size = grouped.filter(lambda x: len(x) >= 4)
print(“\n过滤掉行数少于 4 的组:”)
print(filtered_groups_size) # 只有 A 组的行数是 4,所以只保留 A 组
“`
输出示例:
“`
过滤掉 ‘值1’ 总和小于 50 的组:
类别 地区 值1 值2 日期
0 A East 10 100 2023-01-15
1 B West 20 200 2023-01-18
2 A North 15 150 2023-02-01
3 C East 25 250 2023-02-10
4 B North 30 300 2023-02-15
5 C West 22 220 2023-03-01
6 A East 12 120 2023-03-05
7 B North 28 280 2023-03-10
8 C West 26 260 2023-03-12
9 A North 18 180 2023-03-20
过滤掉行数少于 4 的组:
类别 地区 值1 值2 日期
0 A East 10 100 2023-01-15
2 A North 15 150 2023-02-01
6 A East 12 120 2023-03-05
9 A North 18 180 2023-03-20
“`
filter()
对于清洗数据非常有用,比如只保留那些样本数量足够多的组,或者只保留那些某个统计量满足特定条件的组。
6. 分组后的操作:应用 (Apply)
apply()
是 groupby()
操作中最灵活、最通用的方法。它可以执行聚合、转换或过滤以外的任何操作,或者当这些专用方法无法满足需求时使用。
apply()
方法接收一个函数,该函数会接收每个分组作为一个 DataFrame,并返回一个 Pandas 对象(Series 或 DataFrame),或者一个标量。Pandas 会尝试将这些结果合并起来。
虽然灵活,但 apply()
通常比 agg()
, transform()
, filter()
慢,因为它需要在 Python 中迭代处理每个组。优先使用专用方法,除非 apply()
是唯一或最方便的解决方案。
“`python
使用 apply 计算每个组的 ‘值1’ 与 ‘值2’ 的比值的平均值
注意:这里只是演示 apply 的能力,这个特定计算也可以通过先计算比值列再分组求平均实现
ratio_mean_by_category = grouped.apply(lambda x: (x[‘值1’] / x[‘值2’]).mean())
print(“\n按类别分组,计算 (‘值1’ / ‘值2’) 的平均值 (使用 apply):”)
print(ratio_mean_by_category)
使用 apply 查找每个组中 ‘值1’ 最大的那一行
row_max_value1_by_category = grouped.apply(lambda x: x.loc[x[‘值1’].idxmax()])
print(“\n按类别分组,找到 ‘值1’ 最大的行 (使用 apply):”)
print(row_max_value1_by_category)
“`
输出示例:
“`
按类别分组,计算 (‘值1’ / ‘值2’) 的平均值 (使用 apply):
类别
A 0.10
B 0.10
C 0.10
dtype: float64
按类别分组,找到 ‘值1’ 最大的行 (使用 apply):
类别 地区 值1 值2 日期
类别
A 9 A North 18 180 2023-03-20
B 4 B North 30 300 2023-02-15
C 3 C East 25 250 2023-02-10
“`
在第二个 apply
示例中,注意结果的索引变成了 MultiIndex,第一层是分组键 ‘类别’,第二层是原始 DataFrame 中该行对应的索引。这是 apply
尝试合并不同组返回的 DataFrame 时的一种常见行为。
apply
可以做很多事情,比如:
* 对每个组应用一套复杂的逻辑。
* 返回每个组的前 N 行(如上面的例子,虽然用 nlargest
或排序后 head
可能更高效)。
* 对每个组进行回归分析等。
7. 高级 groupby
7.1 按多个列分组
只需将用于分组的列名列表传递给 groupby()
方法即可。结果的索引将是一个 MultiIndex (多级索引)。
“`python
按 ‘类别’ 和 ‘地区’ 进行分组
grouped_multi = df.groupby([‘类别’, ‘地区’])
计算每个类别下每个地区的 ‘值1’ 的总和
sum_by_category_region = grouped_multi[‘值1’].sum()
print(“\n按类别和地区分组,计算 ‘值1’ 的总和:”)
print(sum_by_category_region)
“`
输出示例:
按类别和地区分组,计算 '值1' 的总和:
类别 地区
A East 22
North 33
B North 58
West 20
C East 25
West 48
Name: 值1, dtype: int64
7.2 按非列值分组
groupby()
的键不必是 DataFrame 的列名。它可以是:
- Series 或 Index: 与 DataFrame 具有相同长度的 Series 或 Index,其值用于分组。
- 字典: 将索引值映射到组键的字典。
- 函数: 应用于索引的函数,其返回值作为组键。
- 列表或数组: 与 DataFrame 具有相同长度的列表或数组,其值用于分组。
- 索引级别 (对于 MultiIndex): 如果 DataFrame 有 MultiIndex,可以按索引的某个级别分组。
示例:按日期所在的年份分组
“`python
按日期所在的年份分组
grouped_by_year = df.groupby(df[‘日期’].dt.year)
sum_by_year = grouped_by_year[‘值1’].sum()
print(“\n按年份分组,计算 ‘值1’ 的总和:”)
print(sum_by_year)
按日期所在的月份分组
grouped_by_month = df.groupby(df[‘日期’].dt.month)
sum_by_month = grouped_by_month[‘值1’].sum()
print(“\n按月份分组,计算 ‘值1’ 的总和:”)
print(sum_by_month)
使用函数作为分组键 (例如,按值的奇偶性分组,假设值1是整数)
grouped_by_parity = df.groupby(df[‘值1’].apply(lambda x: ‘Even’ if x % 2 == 0 else ‘Odd’))
sum_by_parity = grouped_by_parity[‘值2’].sum()
print(“\n按 ‘值1’ 的奇偶性分组,计算 ‘值2’ 的总和:”)
print(sum_by_parity)
“`
输出示例:
“`
按年份分组,计算 ‘值1’ 的总和:
日期
2023 206
Name: 值1, dtype: int64
按月份分组,计算 ‘值1’ 的总和:
日期
1 45
2 80
3 81
Name: 值1, dtype: int64
“`
7.3 迭代分组
虽然不常用,但你可以迭代 GroupBy
对象来查看每个分组的数据:
python
print("\n迭代分组:")
for name, group in df.groupby('类别'):
print(f"\n分组名称: {name}")
print(group)
输出示例 (部分):
“`
迭代分组:
分组名称: A
类别 地区 值1 值2 日期
0 A East 10 100 2023-01-15
2 A North 15 150 2023-02-01
6 A East 12 120 2023-03-05
9 A North 18 180 2023-03-20
分组名称: B
类别 地区 值1 值2 日期
1 B West 20 200 2023-01-18
4 B North 30 300 2023-02-15
7 B North 28 280 2023-03-10
…
“`
这种方式主要用于调试或执行一些非常定制化的操作,但对于标准聚合、转换、过滤任务,应优先使用 .agg()
, .transform()
, .filter()
, .apply()
。
8. groupby
的常见技巧和注意事项
- 性能: 尽量使用
.agg()
,.transform()
,.filter()
等专用方法,它们通常比.apply()
更高效,因为它们在底层 C 语言中实现了优化。只在必要时使用.apply()
。 size()
vscount()
:size()
计算每个组的行数,包括缺失值。count()
计算每个组中非缺失值的数量。as_index=False
: 如果不想让分组键成为结果的索引,记住使用这个参数。- 处理 MultiIndex: 当按多个列分组并聚合时,结果是 MultiIndex。可以使用
.reset_index()
将 MultiIndex 转换为普通列。 - 排序: 默认情况下,
groupby
会对分组键进行排序。如果不需要排序且性能敏感,可以使用sort=False
参数。 - 缺失值:
groupby
默认会忽略分组键中的缺失值。在进行聚合计算时,大部分聚合函数(如 sum, mean, count)会跳过被聚合列中的缺失值,但size
会计算所有行(包括缺失值行)。 - 链式操作:
groupby()
的结果可以方便地与其他 Pandas 方法链式调用,例如df.groupby('类别')['值1'].sum().sort_values(ascending=False)
.
9. 实际应用示例
Pandas groupby
在实际数据分析中无处不在。以下是一些常见的应用场景:
- 销售分析: 计算每个地区、产品或时间段的总销售额、平均价格、销量等。
- 用户行为分析: 计算每个用户的总活跃时间、购买次数、平均订单金额等。
- 金融数据: 计算股票在不同时间段(如每月、每年)的收益率、波动性等。
- 实验数据: 对不同实验组的数据进行比较分析,计算各组的平均值、标准差等统计量。
- 数据清洗/预处理: 用组内统计量填充缺失值,识别或过滤异常组。
例如,计算每个地区和类别的总销售额:
“`python
假设 ‘值1′ 是销量,’值2’ 是单价
df[‘销售额’] = df[‘值1’] * df[‘值2’]
sales_summary = df.groupby([‘地区’, ‘类别’])[‘销售额’].sum()
print(“\n按地区和类别计算总销售额:”)
print(sales_summary)
“`
输出示例:
按地区和类别计算总销售额:
地区 类别
East A 1200
C 6250
North A 2700
B 8400
West B 4000
C 5720
Name: 销售额, dtype: int64
总结
Pandas groupby()
是进行分组数据分析的核心工具。通过掌握其 Split-Apply-Combine 范式以及聚合、转换、过滤和应用这四类主要操作,您可以高效地解决各种数据汇总、转换和分析问题。
记住优先使用 agg()
, transform()
, filter()
等专用方法以获得更好的性能,并在需要更复杂逻辑时使用 apply()
。熟练使用 groupby()
将极大地提升您的数据处理能力。
多加实践是掌握 groupby()
的关键。尝试在自己的数据集上应用不同的分组键和操作,理解它们的结果和用途。
希望这篇详细教程对您有所帮助!