Pandas groupby
:数据分组与聚合的利器
在数据分析领域,我们经常需要对数据进行分组统计。例如,我们可能想知道不同产品的总销售额、不同地区的平均气温,或者不同班级的学生数量。Pandas 库中的 groupby()
方法就是用来解决这类问题的强大工具。它允许你根据一个或多个键(keys)将 DataFrame 拆分成若干组,然后对每个组独立地执行一些操作(如计算总和、平均值、计数等),最后将结果合并起来。
理解 groupby
的核心在于其背后的“拆分-应用-合并”(Split-Apply-Combine)范式。掌握了这个范式,你就能游刃有余地使用 groupby
来处理各种复杂的数据分析任务。
本文将详细介绍 groupby
的基础概念、核心原理以及常见用法。
1. 理解“拆分-应用-合并”(Split-Apply-Combine)范式
Pandas groupby
操作的核心思想可以分解为以下三个步骤:
- 拆分 (Split):根据某个或多个键,将原始 DataFrame 拆分成若干个子 DataFrame。每个子 DataFrame 包含原始数据中具有相同键值的所有行。
- 应用 (Apply):对每个拆分出来的子 DataFrame,独立地应用一个函数。这个函数可以是聚合函数(如求和、平均值)、转换函数(如标准化、填充缺失值)或过滤函数(如选取满足条件的组)。
- 合并 (Combine):将所有子 DataFrame 应用函数后得到的结果合并成一个统一的输出结构(通常是一个 Series 或 DataFrame)。
举个例子:
假设你有一个包含学生姓名、班级和分数的数据表。你想计算每个班级的平均分数。
- 拆分: 根据“班级”这一列,将数据拆分成“一班”、“二班”、“三班”等多个小组的数据。
- 应用: 对“一班”的数据计算平均分数,对“二班”的数据计算平均分数,以此类推。
- 合并: 将所有班级计算出的平均分数合并成一个结果表,显示每个班级及其对应的平均分数。
Pandas 的 groupby()
方法就是负责执行第一步“拆分”,它返回一个 GroupBy
对象,这个对象知道如何对各个组执行后续的“应用”和“合并”操作。
2. groupby()
的基本用法
groupby()
方法通常作用于一个 DataFrame,其最基本的语法是:
python
df.groupby(key)
这里的 key
指定了用于分组的依据。key
可以是以下几种类型:
- 单个列名 (字符串):根据某一列的唯一值进行分组。
- 多个列名组成的列表 ([str1, str2, …]):根据多个列的组合唯一值进行分组。
- 一个 Series 或数组:其长度必须与 DataFrame 的行数相同,用于为每行指定其所属的组。
- 一个字典或 Series:将索引值映射到组名。
- 一个函数:作用于 DataFrame 的索引,根据函数返回值进行分组。
df.groupby(key)
的结果并不是一个 DataFrame,而是一个 GroupBy
对象。这个对象包含了分组信息,可以对其进一步调用各种方法来执行“应用”和“合并”操作。
“`python
import pandas as pd
import numpy as np
创建一个示例 DataFrame
data = {
‘Category’: [‘A’, ‘B’, ‘A’, ‘C’, ‘B’, ‘C’, ‘A’, ‘B’, ‘C’, ‘A’],
‘Value1’: [10, 15, 12, 18, 20, 22, 11, 16, 25, 14],
‘Value2’: [100, 150, 120, 180, 200, 220, 110, 160, 250, 140]
}
df = pd.DataFrame(data)
print(“原始 DataFrame:”)
print(df)
按 ‘Category’ 列进行分组
grouped = df.groupby(‘Category’)
print(“\n分组后的 GroupBy 对象:”)
print(grouped) # 输出
可以遍历 GroupBy 对象,查看每个组的数据(虽然不常用)
print(“\n遍历分组:”)
for name, group in grouped:
print(f”组名: {name}”)
print(group)
print(“-” * 20)
“`
从上面的例子可以看出,直接 groupby()
返回的是一个 GroupBy
对象,你需要对这个对象调用后续的方法来获取具体的结果。
3. 应用函数:聚合(Aggregation)
聚合是 groupby
最常见的应用场景。聚合操作会将每个组的多行数据聚合成一个单一的值。常用的聚合函数包括:
sum()
:计算总和mean()
:计算平均值count()
:计算非 NaN 值的数量size()
:计算组的大小(行数,包括 NaN)min()
:计算最小值max()
:计算最大值std()
:计算标准差var()
:计算方差first()
:选取组的第一个值last()
:选取组的最后一个值
你可以直接在 GroupBy
对象上调用这些函数:
“`python
计算每个类别的 Value1 总和
category_sum = grouped[‘Value1’].sum()
print(“\n每个类别的 Value1 总和:”)
print(category_sum)
输出:
Category
A 47
B 51
C 65
Name: Value1, dtype: int64
计算每个类别的 Value2 平均值
category_mean = grouped[‘Value2’].mean()
print(“\n每个类别的 Value2 平均值:”)
print(category_mean)
输出:
Category
A 117.5
B 170.0
C 210.0
Name: Value2, dtype: float64
计算每个类别的行数 (使用 size() 或 count())
size() 包括 NaN,count() 不包括 NaN
category_size = grouped.size()
category_count_v1 = grouped[‘Value1’].count() # Count non-NaNs in Value1
print(“\n每个类别的行数 (size):”)
print(category_size)
输出:
Category
A 4
B 3
C 3
dtype: int64
print(“\n每个类别的 Value1 非NaN计数 (count):”)
print(category_count_v1)
输出:
Category
A 4
B 3
C 3
Name: Value1, dtype: int64
对所有数值列应用相同的聚合函数
all_agg = grouped.sum()
print(“\n每个类别所有数值列的总和:”)
print(all_agg)
输出:
Value1 Value2
Category
A 47 470
B 51 510
C 65 650
“`
注意,当对整个 GroupBy
对象应用聚合函数时(如 grouped.sum()
),Pandas 会自动对所有非分组键的数值列执行聚合操作。如果想对特定列进行聚合,可以通过先选择列再聚合的方式(如 grouped['Value1'].sum()
)。
4. 应用函数:使用 agg()
或 aggregate()
进行多种聚合
当你想对同一个分组执行多种不同的聚合操作,或者对不同的列应用不同的聚合函数时,agg()
(或 aggregate()
, 它们是等价的) 方法就非常有用了。
agg()
方法非常灵活,可以接受多种形式的参数:
- 单个函数名 (字符串或函数对象):对所有适用的列应用同一个聚合函数。
- 函数名列表 ([func1, func2, …]):对所有适用的列应用多个聚合函数,结果是带有 MultiIndex 列的 DataFrame。
- 字典 ({column: func} 或 {column: [func1, func2]}):对指定的列应用指定的聚合函数或函数列表。
示例:
“`python
使用 agg() 对同一列进行多种聚合
agg_multi_func = grouped[‘Value1’].agg([‘sum’, ‘mean’, ‘count’])
print(“\n每个类别的 Value1 的总和、平均值和计数:”)
print(agg_multi_func)
输出:
sum mean count
Category
A 47 11.75 4
B 51 17.00 3
C 65 21.67 3
使用 agg() 对不同列应用不同聚合函数
agg_diff_cols = grouped.agg({‘Value1’: ‘sum’, ‘Value2’: ‘mean’})
print(“\n每个类别的 Value1 总和和 Value2 平均值:”)
print(agg_diff_cols)
输出:
Value1 Value2
Category
A 47 117.5
B 51 170.0
C 65 210.0
使用 agg() 对不同列应用函数列表
agg_complex = grouped.agg({
‘Value1’: [‘sum’, ‘mean’],
‘Value2’: [‘min’, ‘max’, ‘count’]
})
print(“\n每个类别的 Value1 (sum, mean) 和 Value2 (min, max, count):”)
print(agg_complex)
输出结果列会带有 MultiIndex:
Value1 Value2
sum mean min max count
Category
A 47 11.75 100.0 140.0 4
B 51 17.00 150.0 200.0 3
C 65 21.67 180.0 250.0 3
“`
使用字典方式的 agg()
非常强大和灵活,你可以精确控制对哪些列应用哪些聚合函数。
5. 应用函数:转换(Transformation)
转换操作不同于聚合,它不会将每个组的数据聚合成一个单一的值。相反,转换操作会返回一个与原始组具有相同索引和大小的对象(Series 或 DataFrame),其结果的形状与原始 DataFrame 在分组维度上是对应的。转换通常用于在组内进行标准化、填充缺失值或计算排名等操作。
转换操作主要使用 transform()
方法。传递给 transform()
的函数必须返回一个 Series 或 DataFrame,其索引与输入组的索引相同。
示例:
“`python
计算每个类别中 Value1 相对于该类别平均值的偏差
grouped[‘Value1’] 是一个 GroupBy Series 对象
.transform(‘mean’) 会计算每个组的平均值,并将结果广播回原始 DataFrame 的形状
df[‘Value1_Deviation’] = df[‘Value1’] – grouped[‘Value1’].transform(‘mean’)
print(“\n添加了 Value1 相对于类别平均值的偏差列:”)
print(df)
输出 (部分):
Category Value1 Value2 Value1_Deviation
0 A 10 100 -1.75
1 B 15 150 -2.00
2 A 12 120 0.25
…
填充每个类别中 Value2 的缺失值,使用该类别的平均值
假设我们有一些缺失值
df_with_nan = df.copy()
df_with_nan.loc[[0, 4, 8], ‘Value2’] = np.nan
print(“\n带有缺失值的 DataFrame:”)
print(df_with_nan)
df_filled = df_with_nan.copy()
df_filled[‘Value2’] = df_with_nan.groupby(‘Category’)[‘Value2’].transform(lambda x: x.fillna(x.mean())) # 使用 lambda 函数或内置的 fillna
或者更简洁地:
df_filled[‘Value2’] = df_with_nan.groupby(‘Category’)[‘Value2’].transform(‘mean’) # 注意这里是计算平均值再用它来填充
fillna 的 transform 应用方式通常是 lambda
df_filled[‘Value2’] = df_with_nan.groupby(‘Category’)[‘Value2’].transform(lambda x: x.fillna(x.mean()))
print(“\n使用类别平均值填充缺失值后的 DataFrame:”)
print(df_filled)
比较原始 DataFrame 和填充后的 DataFrame,观察 NaN 是否被对应类别的平均值替换
“`
transform()
方法非常强大,因为它允许你在进行组内计算后,将结果“无缝”地添加到原始 DataFrame 中作为新列,这在特征工程中非常常见。
6. 应用函数:过滤(Filtering)
过滤操作用于根据组的属性来丢弃或保留整个组。例如,你可能只想分析那些数据量大于一定阈值的组,或者那些某个指标(如平均值)超过特定标准的组。
过滤操作使用 filter()
方法。传递给 filter()
的函数必须返回一个布尔值(True 表示保留该组,False 表示丢弃该组)。这个函数会接收整个组的数据(一个 DataFrame)作为输入。
示例:
“`python
保留那些 Value1 总和大于 50 的类别
传递给 filter 的函数接收一个组的 DataFrame 作为参数
filtered_df = grouped.filter(lambda x: x[‘Value1’].sum() > 50)
print(“\n保留 Value1 总和大于 50 的类别:”)
print(filtered_df)
只有 Category B 和 C 的 Value1 总和大于 50
输出:
Category Value1 Value2 Value1_Deviation
1 B 15 150 -2.00
4 B 20 200 3.00
5 C 22 220 0.33
7 B 16 160 -1.00
8 C 25 250 3.33
保留那些行数少于 4 的类别
filtered_by_size = grouped.filter(lambda x: len(x) < 4)
print(“\n保留行数少于 4 的类别:”)
print(filtered_by_size)
Category B 和 C 各有 3 行,Category A 有 4 行
输出:
Category Value1 Value2 Value1_Deviation
1 B 15 150 -2.00
3 C 18 180 -3.67
4 B 20 200 3.00
5 C 22 220 0.33
7 B 16 160 -1.00
8 C 25 250 3.33
“`
使用 filter()
时,需要确保你的过滤条件函数能够返回一个布尔值。
7. 按多列进行分组
你可以通过传递一个列名列表给 groupby()
方法来按多个列进行分组。这将根据这些列的组合唯一值来创建组。
“`python
data_multi = {
‘City’: [‘New York’, ‘Paris’, ‘New York’, ‘London’, ‘Paris’, ‘London’, ‘New York’],
‘Year’: [2020, 2020, 2021, 2020, 2021, 2021, 2021],
‘Sales’: [100, 150, 120, 200, 180, 220, 130]
}
df_multi = pd.DataFrame(data_multi)
print(“\n原始多列分组 DataFrame:”)
print(df_multi)
按 ‘City’ 和 ‘Year’ 进行分组
grouped_multi = df_multi.groupby([‘City’, ‘Year’])
计算每个城市每年的总销售额
city_year_sales = grouped_multi[‘Sales’].sum()
print(“\n每个城市每年的总销售额:”)
print(city_year_sales)
输出结果的索引将是 MultiIndex:
City Year
London 2020 200
2021 220
New York 2020 100
2021 250
Paris 2020 150
2021 180
Name: Sales, dtype: int64
你也可以对多列分组结果应用多种聚合
multi_agg = grouped_multi.agg({‘Sales’: [‘sum’, ‘mean’]})
print(“\n每个城市每年的销售总和和平均值:”)
print(multi_agg)
输出:
Sales
sum mean
City Year
London 2020 200.0 200.0
2021 220.0 220.0
New York 2020 100.0 100.0
2021 250.0 125.0
Paris 2020 150.0 150.0
2021 180.0 180.0
“`
按多列分组的结果的索引通常是一个 MultiIndex (分层索引),这在使用 agg()
等方法时尤其明显。你可以使用 .reset_index()
方法将 MultiIndex 转换为普通列。
“`python
将 MultiIndex 转换为普通列
multi_agg_flat = multi_agg.reset_index()
print(“\n将 MultiIndex 转换为普通列:”)
print(multi_agg_flat)
输出:
City Year Sales sum mean
0 London 2020 200.0 200.0
1 London 2021 220.0 220.0
2 New York 2020 100.0 100.0
3 New York 2021 250.0 125.0
4 Paris 2020 150.0 150.0
5 Paris 2021 180.0 180.0
“`
另一种避免 MultiIndex 的方法是在 groupby()
中设置 as_index=False
:
“`python
使用 as_index=False 避免 MultiIndex
grouped_no_index = df_multi.groupby([‘City’, ‘Year’], as_index=False)
city_year_sales_no_index = grouped_no_index[‘Sales’].sum()
print(“\n使用 as_index=False 的分组总销售额:”)
print(city_year_sales_no_index)
输出 (结果是一个 DataFrame):
City Year Sales
0 London 2020 200
1 London 2021 220
2 New York 2020 100
3 New York 2021 250
4 Paris 2020 150
5 Paris 2021 180
“`
使用 as_index=False
可以让分组键变成结果 DataFrame 的普通列,这在某些情况下更方便后续的数据处理。
8. 其他分组键类型
除了列名,groupby()
还可以使用其他类型作为分组键:
-
Series 或数组:与 DataFrame 长度相同的 Series 或数组,其值用于分组。
“`python
使用 Series 作为分组键
group_keys = pd.Series([‘Group1’, ‘Group2’, ‘Group1’, ‘Group1’, ‘Group2’, ‘Group2’, ‘Group1’, ‘Group2’, ‘Group1’, ‘Group1’])
grouped_by_series = df.groupby(group_keys)
print(“\n使用 Series 作为分组键的总和:”)
print(grouped_by_series[‘Value1’].sum())输出:
Group1 97
Group2 71
Name: Value1, dtype: int64
“`
-
函数:函数会作用于 DataFrame 的索引,其返回值用于分组。
“`python
假设 DataFrame 索引是日期,按星期几分组
dates = pd.date_range(‘2023-01-01’, periods=10)
df_date = pd.DataFrame({‘Value’: np.random.rand(10) * 100}, index=dates)
print(“\n日期索引 DataFrame:”)
print(df_date)按索引的星期几分组 (0=周一, 6=周日)
grouped_by_weekday = df_date.groupby(lambda x: x.weekday())
print(“\n按星期几分组的平均值:”)
print(grouped_by_weekday[‘Value’].mean())输出类似:
0 某个平均值 (周一)
1 某个平均值 (周二)
…
“`
-
字典:将索引值映射到组名。
“`python
使用字典将索引映射到组
index_groups = {0: ‘X’, 1: ‘Y’, 2: ‘X’, 3: ‘Y’, 4: ‘X’, 5: ‘Y’, 6: ‘X’, 7: ‘Y’, 8: ‘X’, 9: ‘Y’}
grouped_by_dict = df.groupby(index_groups)
print(“\n使用字典将索引映射到分组的总和:”)
print(grouped_by_dict[‘Value1’].sum())输出:
X 57
Y 106
Name: Value1, dtype: int64
“`
9. 总结
Pandas 的 groupby()
方法是进行分组数据分析的核心工具,其“拆分-应用-合并”范式简洁而强大。通过对 GroupBy
对象应用聚合 (agg
/aggregate
)、转换 (transform
) 和过滤 (filter
) 等操作,你可以轻松地完成各种复杂的统计和数据处理任务。
掌握 groupby
的关键在于:
- 理解 Split-Apply-Combine 原理。
- 知道
groupby()
返回的是一个GroupBy
对象。 - 熟练使用
GroupBy
对象上的聚合、转换和过滤方法。 - 掌握按单列、多列以及其他灵活方式(如函数、Series)进行分组的方法。
- 了解
agg()
在执行多种聚合时的强大灵活性。
通过多加练习,你会发现 groupby
能够极大地提高你处理结构化数据的效率。从基础的求和计数到复杂的分组特征工程,groupby
都是你的得力助手。现在就开始动手尝试吧!