Pandas groupby 教程 – wiki基地


Pandas groupby 深度教程:掌握数据分组聚合的瑞士军刀

数据分析中,我们经常需要对数据进行分组,然后对每个组执行计算或转换。想象一下,您有一份销售订单数据,想知道每个产品的总销售额,或者每个地区的平均销售价格,或者找出每个客户的首次购买日期。这些任务的核心都是“分组”操作。

在 Pandas 库中,groupby() 方法就是执行这类操作的强大工具。它实现了数据处理中一个非常核心的“分裂-应用-合并”(Split-Apply-Combine)范式。

本文将深入探讨 Pandas groupby() 的方方面面,从基本用法到高级技巧,帮助您彻底掌握这个数据分析利器。

1. 理解 Split-Apply-Combine(分裂-应用-合并)范式

groupby() 方法的核心思想可以概括为以下三个步骤:

  1. 分裂 (Split): 根据某个(或多个)键将数据分割成不同的组。这些键可以是 DataFrame 的列、行索引,甚至是一个函数或列表。
  2. 应用 (Apply): 独立地对每个组应用一个函数。这个函数可以是聚合函数(如求和、平均值)、转换函数(如标准化、填充缺失值)、过滤函数(如筛选出满足条件的组),或任何自定义函数。
  3. 合并 (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() vs count(): 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() 的关键。尝试在自己的数据集上应用不同的分组键和操作,理解它们的结果和用途。

希望这篇详细教程对您有所帮助!


发表评论

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

滚动至顶部