数据分析必备:Pandas Groupby 使用教程详解
在数据分析的浩瀚世界里,我们经常需要对数据进行分组处理,例如计算不同产品的总销售额、统计各区域的用户数量、分析不同时间段的平均气温等等。面对这些需求,Pandas 库提供了一个强大而灵活的工具——groupby
。掌握 groupby
的使用,可以说是成为一名高效数据分析师的必备技能。
本文将深入浅出地介绍 Pandas groupby
的各个方面,从基本概念到高级用法,并通过丰富的代码示例帮助你彻底理解和掌握这一核心功能。
目录
- 引言:为什么需要 Groupby?
- 现实世界中的分组需求
- Pandas Groupby 的核心思想:Split-Apply-Combine
- Groupby 的基本概念和用法
- 创建 GroupBy 对象
- GroupBy 对象的组成
- 最常用的聚合操作(Aggregation)
- 详解 Split-Apply-Combine 范式
- Split(分割)
- Apply(应用)
- Combine(合并)
- 常用的 Groupby 聚合函数 (Aggregation)
- 内置聚合函数:
sum()
,mean()
,count()
,size()
,min()
,max()
,std()
,var()
,first()
,last()
count()
vssize()
的区别- 对多个列进行不同的聚合操作
- 自定义聚合函数的应用
- 内置聚合函数:
- Groupby 的强大武器:Apply 方法
- Apply 的灵活性:对每个组应用任意函数
- 返回不同类型的结果 (Series, DataFrame)
- 应用复杂的组内逻辑 (例如:找出每组的前 N 行)
- Apply vs Aggregation:何时使用 Apply
- Groupby 的另一个利器:Transform 方法
- Transform 的核心:返回与原 DataFrame 相同索引的结果
- 常见应用场景:组内标准化、填充缺失值
- Transform vs Aggregation vs Apply:理清三者区别
- 使用 Filter 方法过滤分组
- 根据组的属性过滤数据
filter()
函数的使用
- 高级 Groupby 用法
- 按多个列进行分组
- 按 Index Levels 进行分组
- 按函数或映射进行分组
- 结合
pd.cut()
或pd.qcut()
按区间分组 - 时间序列数据的分组 (
resample
的关联)
- Groupby 的性能考虑
- 大型数据集的优化
- 选择合适的 Apply/Transform/Aggregate 方法
- 常见问题与技巧
- 处理
NaN
值 as_index=False
的作用- 链式操作
- 处理
- 总结
1. 引言:为什么需要 Groupby?
想象一下,你手头有一份包含全国各地、各种商品销售记录的大型数据集。你想要知道:
- 每个省份的总销售额是多少?
- 每种商品的平均价格是多少?
- 哪个季度某个区域的销售额增长最快?
这些问题都有一个共同点:它们都需要你将数据按照某个或某几个维度(省份、商品、区域、季度等)进行“分组”,然后在每个分组内进行计算(求和、求平均、查找最大值等)。
如果没有 groupby
,你可能需要编写复杂的循环、条件判断甚至手动分割数据集,这不仅代码量大、易出错,而且效率低下。
Pandas groupby
解决了这个问题。它的核心思想源自数据库中的 GROUP BY
语句,但提供了更加灵活和强大的数据处理能力。它遵循一个经典的数据处理范式:Split-Apply-Combine(分割-应用-合并)。
- Split (分割):根据某些条件将数据分割成若干个组。
- Apply (应用):对每个组独立地应用一个函数(聚合、转换、过滤等)。
- Combine (合并):将每个组的处理结果合并成一个新的数据结构。
理解这个范式是掌握 groupby
的关键。
2. Groupby 的基本概念和用法
在 Pandas 中,使用 groupby
函数(通常是 DataFrame 或 Series 对象的方法)来启动分组过程。
首先,我们需要导入 Pandas 并创建一个示例 DataFrame:
“`python
import pandas as pd
import numpy as np
创建示例数据
data = {
‘Region’: [‘North’, ‘North’, ‘South’, ‘South’, ‘East’, ‘East’, ‘North’, ‘South’, ‘East’],
‘Product’: [‘A’, ‘B’, ‘A’, ‘C’, ‘A’, ‘B’, ‘A’, ‘C’, ‘C’],
‘Sales’: [100, 150, 200, 50, 300, 250, 120, 60, 180],
‘Quantity’: [10, 15, 20, 5, 30, 25, 12, 6, 18],
‘Date’: pd.to_datetime([‘2023-01-15’, ‘2023-01-20’, ‘2023-01-18’, ‘2023-01-25’, ‘2023-02-01’, ‘2023-02-05’, ‘2023-02-10’, ‘2023-02-12’, ‘2023-02-15’])
}
df = pd.DataFrame(data)
print(“原始 DataFrame:”)
print(df)
print(“-” * 30)
“`
原始 DataFrame:
Region Product Sales Quantity Date
0 North A 100 10 2023-01-15
1 North B 150 15 2023-01-20
2 South A 200 20 2023-01-18
3 South C 50 5 2023-01-25
4 East A 300 30 2023-02-01
5 East B 250 25 2023-02-05
6 North A 120 12 2023-02-10
7 South C 60 6 2023-02-12
8 East C 180 18 2023-02-15
现在,我们尝试按 Region
列进行分组:
python
grouped_by_region = df.groupby('Region')
print("GroupBy 对象类型:", type(grouped_by_region))
print("-" * 30)
输出:
GroupBy 对象类型: <class 'pandas.core.groupby.generic.DataFrameGroupBy'>
当我们调用 df.groupby('Region')
时,Pandas 并不会立即执行计算,而是返回一个 DataFrameGroupBy
对象。这个对象本质上是一个特殊的视图,它存储了原始 DataFrame 的信息以及如何将行分割成组的信息。你可以把它理解为一个“分组器”。
GroupBy 对象的组成
这个 GroupBy
对象包含关于分组的信息,例如:
groups
属性: 返回一个字典,键是组名(这里的地区名),值是对应组的行索引列表。
python
print("分组信息 (groups):")
print(grouped_by_region.groups)
print("-" * 30)
输出:
分组信息 (groups):
{'East': [4, 5, 8], 'North': [0, 1, 6], 'South': [2, 3, 7]}get_group()
方法: 可以用来获取特定组的数据。
python
print("获取 'North' 组的数据:")
print(grouped_by_region.get_group('North'))
print("-" * 30)
输出:
获取 'North' 组的数据:
Region Product Sales Quantity Date
0 North A 100 10 2023-01-15
1 North B 150 15 2023-01-20
6 North A 120 12 2023-02-10- 迭代 GroupBy 对象: 可以直接迭代 GroupBy 对象,每次迭代返回一个 (组名, 该组对应的 DataFrame) 的元组。
python
print("迭代 GroupBy 对象:")
for name, group in grouped_by_region:
print(f"组名: {name}")
print(group)
print("-" * 20)
print("-" * 30)
输出 (部分):
迭代 GroupBy 对象:
组名: East
Region Product Sales Quantity Date
4 East A 300 30 2023-02-01
5 East B 250 25 2023-02-05
8 East C 180 18 2023-02-15
--------------------
组名: North
Region Product Sales Quantity Date
0 North A 100 10 2023-01-15
1 North B 150 15 2023-01-20
6 North A 120 12 2023-02-10
--------------------
...
最常用的聚合操作 (Aggregation)
GroupBy 对象本身并没有数据,它只是描述了如何分组。要得到有意义的结果,我们需要对每个组应用一个操作。最常见的操作就是聚合 (Aggregation),即将每个组的多行数据汇总成一行或一个值。
例如,计算每个地区的总销售额:
“`python
计算每个地区的总销售额
region_sales_sum = grouped_by_region[‘Sales’].sum()
print(“按地区分组的总销售额:”)
print(region_sales_sum)
print(“-” * 30)
“`
输出:
按地区分组的总销售额:
Region
East 730
North 370
South 310
Name: Sales, dtype: int64
这里,我们首先通过 grouped_by_region['Sales']
选择了 Sales
列,得到了一个 SeriesGroupBy
对象,然后调用了 sum()
方法。Pandas 对每个组的 Sales
列求和,并将结果合并成一个新的 Series,索引是组名 (Region
)。
3. 详解 Split-Apply-Combine 范式
让我们回过头来更详细地看看 Split-Apply-Combine 范式是如何体现在 groupby
中的:
-
Split (分割): 当你调用
df.groupby('Region')
时,Pandas 在幕后根据Region
列的值将 DataFrame 的行分割成三个逻辑组:’East’, ‘North’, ‘South’。原始 DataFrame 并没有被修改,只是内部记录了哪些行属于哪个组。 -
Apply (应用): 当你调用
grouped_by_region['Sales'].sum()
时,sum()
函数被独立地应用到每一个组的Sales
列上。- 对 ‘East’ 组的
Sales
(300, 250, 180) 执行sum()
,得到 730。 - 对 ‘North’ 组的
Sales
(100, 150, 120) 执行sum()
,得到 370。 - 对 ‘South’ 组的
Sales
(200, 50, 60) 执行sum()
,得到 310。
- 对 ‘East’ 组的
-
Combine (合并): Pandas 将每个组的计算结果 (730, 370, 310) 合并成一个新的 Series。默认情况下,组名 (‘East’, ‘North’, ‘South’) 会成为新 Series 的索引。原始 DataFrame 的列名 (
Sales
) 成为新 Series 的名称。
这个范式不仅适用于简单的聚合,也适用于后面我们将要介绍的 apply()
和 transform()
方法。理解这个过程对于预测 groupby
操作的结果至关重要。
4. 常用的 Groupby 聚合函数 (Aggregation)
Pandas GroupBy 对象有很多内置的聚合函数可以直接调用:
sum()
: 计算总和mean()
: 计算平均值count()
: 计算非NaN
值的数量size()
: 计算组的大小(包含NaN
值)min()
: 计算最小值max()
: 计算最大值std()
: 计算标准差var()
: 计算方差first()
: 获取组中的第一个值last()
: 获取组中的最后一个值
示例:计算不同聚合指标
“`python
按地区计算各种统计量
region_stats = grouped_by_region[‘Sales’].agg([‘sum’, ‘mean’, ‘min’, ‘max’])
print(“按地区分组的销售统计量:”)
print(region_stats)
print(“-” * 30)
对整个 GroupBy 对象应用聚合函数(适用于数值列)
非数值列会被忽略
df_agg = df.groupby(‘Region’).sum()
print(“按地区分组并对所有数值列求和:”)
print(df_agg)
print(“-” * 30)
“`
输出:
“`
按地区分组的销售统计量:
sum mean min max
Region
East 730 243.333333 180 300
North 370 123.333333 100 150
South 310 103.333333 50 200
按地区分组并对所有数值列求和:
Sales Quantity
Region
East 730 73
North 370 37
South 310 31
“`
注意,当对整个 GroupBy 对象应用聚合函数时,Pandas 会自动选择合适的列进行计算(通常是数值列),并忽略不适用的列。
count()
vs size()
的区别
这是新手常遇到的一个点。count()
计算的是组中非 NaN
的行数,而 size()
计算的是组的总行数(包括 NaN
行)。
“`python
创建一个带 NaN 的示例
df_nan = pd.DataFrame({‘Group’: [‘A’, ‘A’, ‘B’, ‘B’, ‘B’],
‘Value’: [1, 2, 3, np.nan, 5]})
print(“带 NaN 的 DataFrame:”)
print(df_nan)
print(“-” * 30)
grouped_nan = df_nan.groupby(‘Group’)
print(“使用 count():”)
print(grouped_nan[‘Value’].count()) # B组 Value 有一个 NaN,所以 count 是 2
print(“-” * 30)
print(“使用 size():”)
print(grouped_nan[‘Value’].size()) # B组总行数是 3
print(“-” * 30)
“`
输出:
“`
带 NaN 的 DataFrame:
Group Value
0 A 1.0
1 A 2.0
2 B 3.0
3 B NaN
4 B 5.0
使用 count():
Group
A 2
B 2
Name: Value, dtype: int64
使用 size():
Group
A 2
B 3
Name: Value, dtype: int64
“`
size()
通常用于计算每个组有多少个原始数据点,而 count()
用于计算每个组有多少个有效(非 NaN
)的测量值。
对多个列进行不同的聚合操作 (.agg()
)
agg()
(或 aggregate()
) 方法非常灵活,可以让你对不同的列应用不同的聚合函数,甚至对同一个列应用多个聚合函数。
语法:groupby_object.agg({ 'column1': func1, 'column2': [func2, func3], ... })
“`python
对 Sales 列求和和平均,对 Quantity 列求和
region_agg_specific = df.groupby(‘Region’).agg({
‘Sales’: [‘sum’, ‘mean’],
‘Quantity’: ‘sum’
})
print(“对不同列应用不同聚合函数:”)
print(region_agg_specific)
print(“-” * 30)
也可以使用元组为聚合结果命名
region_agg_named = df.groupby(‘Region’).agg(
TotalSales=(‘Sales’, ‘sum’),
AverageSales=(‘Sales’, ‘mean’),
TotalQuantity=(‘Quantity’, ‘sum’)
)
print(“对聚合结果命名:”)
print(region_agg_named)
print(“-” * 30)
“`
输出:
“`
对不同列应用不同聚合函数:
Sales Quantity
sum mean sum
Region
East 730 243.333333 73
North 370 123.333333 37
South 310 103.333333 31
对聚合结果命名:
TotalSales AverageSales TotalQuantity
Region
East 730 243.333333 73
North 370 123.333333 37
South 310 103.333333 31
“`
这种 .agg()
方法非常强大,可以清晰地定义你想要从每个组中提取的各种汇总信息。
自定义聚合函数的应用
除了使用内置函数,你还可以将自定义函数传递给 agg()
。这个自定义函数会接收每个组的 Series 或 DataFrame 作为输入,并返回一个单个值。
“`python
定义一个自定义函数,计算销售额的变异系数 (Coefficient of Variation = std / mean)
def cv(x):
if x.mean() == 0: # 避免除以零
return 0
return x.std() / x.mean()
region_sales_cv = df.groupby(‘Region’)[‘Sales’].agg(cv)
print(“按地区分组的销售额变异系数:”)
print(region_sales_cv)
print(“-” * 30)
在 agg() 中使用 lambda 函数
region_sales_range = df.groupby(‘Region’)[‘Sales’].agg(lambda x: x.max() – x.min())
print(“按地区分组的销售额范围:”)
print(region_sales_range)
print(“-” * 30)
“`
输出:
“`
按地区分组的销售额变异系数:
Region
East 0.245645
North 0.216933
South 0.765462
Name: Sales, dtype: float64
按地区分组的销售额范围:
Region
East 120
North 50
South 150
Name: Sales, dtype: int64
“`
自定义函数极大地扩展了 agg()
的能力,你可以执行任何你需要的组内计算,只要最终返回一个单一的值即可。
5. Groupby 的强大武器:Apply 方法
apply()
方法是 groupby
中最灵活的一个。它允许你对每个组应用一个可以返回 Pandas Series、DataFrame 或标量的函数。传入 apply()
的函数会接收当前组的 DataFrame 或 Series 作为第一个参数。
与聚合不同,apply()
不局限于返回一个汇总值,它可以对每个组执行更复杂的逻辑,并返回一个结构可能与原始组不同的结果。
示例:找出每个地区销售额最高的记录
“`python
定义一个函数,返回 DataFrame 中 Sales 最高的行
def get_top_sale(group):
return group.loc[group[‘Sales’].idxmax()] # idxmax() 返回最大值对应的索引
top_sales_per_region = df.groupby(‘Region’).apply(get_top_sale)
print(“每个地区销售额最高的记录:”)
print(top_sales_per_region)
print(“-” * 30)
“`
输出:
“`
每个地区销售额最高的记录:
Region Product Sales Quantity Date
Region
East East A 300 30 2023-02-01
North North B 150 15 2023-01-20
South South A 200 20 2023-01-18
“`
在这个例子中,get_top_sale
函数接收每个地区的子 DataFrame,然后找出销售额最大的那一行的索引,并返回该行数据。apply
方法将每个组返回的 Series(对应一行数据)合并成一个新的 DataFrame。
示例:计算每个地区的销售额占总销售额的比例
“`python
首先计算总销售额
total_sales = df[‘Sales’].sum()
定义函数计算比例
def sales_proportion(group):
group[‘Sales_Proportion’] = group[‘Sales’] / total_sales
return group
应用函数
df_with_proportion = df.groupby(‘Region’).apply(sales_proportion)
print(“带有销售额占总销售额比例的新列:”)
print(df_with_proportion)
print(“-” * 30)
“`
输出 (部分):
带有销售额占总销售额比例的新列:
Region Product Sales Quantity Date Sales_Proportion
0 North A 100 10 2023-01-15 0.083333
1 North B 150 15 2023-01-20 0.125000
2 South A 200 20 2023-01-18 0.166667
3 South C 50 5 2023-01-25 0.041667
...
这里,apply
函数在每个组内增加了一列 Sales_Proportion
。由于函数返回的是一个 DataFrame(修改后的组),apply
会将这些修改后的组重新拼接起来,生成一个新的 DataFrame。
Apply vs Aggregation:何时使用 Apply
- 如果你只是想对每个组计算一个或几个汇总值(如总和、平均值、计数等),优先使用
.agg()
或直接调用聚合函数 (.sum()
,.mean()
)。这些方法通常经过高度优化,性能更好。 - 如果你需要对每个组应用更复杂的逻辑,例如:
- 返回组内的一个子集(如前 N 行/列)。
- 在组内执行行间的计算。
- 对组内数据进行排序、排名。
- 创建依赖于组内数据的新列。
- 执行任何不能用简单聚合表达的操作。
这时,apply()
是更合适的选择。
注意: apply()
的一个潜在缺点是,对于非常大的数据集,它的速度可能比优化的 agg()
和 transform()
慢,因为它可能需要将每个组的数据复制出来进行处理。在追求极致性能时,应考虑是否有 agg
或 transform
的替代方案。
6. Groupby 的另一个利器:Transform 方法
transform()
方法是 groupby
中另一个非常有用的操作。它的核心特点是:它返回一个与原始 DataFrame (或 Series) 具有相同索引的新 Series 或 DataFrame。
这使得 transform()
非常适合在组内计算一个值,然后将这个值“广播”回原始组的每一行,用于创建新的列或者进行数据转换。传入 transform()
的函数必须返回:
- 一个标量(这个标量值会广播到组的每一行)。
- 一个长度与组的大小相同的 Series 或 NumPy 数组。
示例:在原始 DataFrame 中添加每个地区的总销售额作为新列
“`python
计算每个地区的总销售额,并将其广播到原始 DataFrame
df[‘Region_Total_Sales’] = df.groupby(‘Region’)[‘Sales’].transform(‘sum’)
print(“原始 DataFrame 添加了地区总销售额列:”)
print(df)
print(“-” * 30)
“`
输出 (部分):
原始 DataFrame 添加了地区总销售额列:
Region Product Sales Quantity Date Region_Total_Sales
0 North A 100 10 2023-01-15 370
1 North B 150 15 2023-01-20 370
2 South A 200 20 2023-01-18 310
3 South C 50 5 2023-01-25 310
4 East A 300 30 2023-02-01 730
...
这里,df.groupby('Region')['Sales'].transform('sum')
首先计算出每个地区 Sales
的总和(’East’: 730, ‘North’: 370, ‘South’: 310),然后将这些总和值广播回原始 DataFrame 中对应地区的每一行。结果是一个 Series,其索引与原始 DataFrame 相同,可以直接赋值给新列。
示例:在组内进行数据标准化 (Z-score)
“`python
计算每个组的平均值和标准差,并计算 Z-score
df[‘Sales_ZScore’] = df.groupby(‘Region’)[‘Sales’].transform(lambda x: (x – x.mean()) / x.std())
print(“原始 DataFrame 添加了组内销售额 Z-score 列:”)
print(df)
print(“-” * 30)
“`
输出 (部分):
原始 DataFrame 添加了组内销售额 Z-score 列:
Region Product Sales Quantity Date Region_Total_Sales Sales_ZScore
0 North A 100 10 2023-01-15 370 -1.057163
1 North B 150 15 2023-01-20 370 1.057163
2 South A 200 20 2023-01-18 310 1.020621
3 South C 50 5 2023-01-25 310 -0.714435
4 East A 300 30 2023-02-01 730 0.637844
...
这里,lambda 函数接收每个组的 Sales
Series x
。它计算 x
的平均值和标准差,然后计算组内每个元素的 Z-score。结果是一个长度与原始组相同的 Series,transform
将这些 Series 拼接起来,索引对齐原始 DataFrame。
示例:用组内的平均值填充缺失值
“`python
创建一个带 NaN 的新 DataFrame
df_fill = df.copy()
df_fill.loc[[1, 5], ‘Sales’] = np.nan # 在 North 和 East 各制造一个 NaN
print(“带有缺失值的新 DataFrame:”)
print(df_fill)
print(“-” * 30)
使用组内的平均值填充缺失值
df_filled = df_fill.groupby(‘Region’)[‘Sales’].transform(lambda x: x.fillna(x.mean()))
df_fill[‘Sales_Filled’] = df_filled
print(“使用组内平均值填充缺失值后的 DataFrame:”)
print(df_fill)
print(“-” * 30)
“`
输出 (部分):
“`
带有缺失值的新 DataFrame:
Region Product Sales Quantity Date Region_Total_Sales Sales_ZScore
0 North A 100.0 10 2023-01-15 370 -1.057163
1 North B NaN 15 2023-01-20 370 1.057163 <– NaN
2 South A 200.0 20 2023-01-18 310 1.020621
3 South C 50.0 5 2023-01-25 310 -0.714435
4 East A 300.0 30 2023-02-01 730 0.637844
5 East B NaN 25 2023-02-05 730 -0.281063 <– NaN
…
使用组内平均值填充缺失值后的 DataFrame:
Region Product Sales Quantity Date Region_Total_Sales Sales_ZScore Sales_Filled
0 North A 100.0 10 2023-01-15 370 -1.057163 100.0
1 North B NaN 15 2023-01-20 370 1.057163 110.0 <– 填充为 North 组的平均值 (100+120)/2 = 110
2 South A 200.0 20 2023-01-18 310 1.020621 200.0
3 South C 50.0 5 2023-01-25 310 -0.714435 50.0
4 East A 300.0 30 2023-02-01 730 0.637844 300.0
5 East B NaN 25 2023-02-05 730 -0.281063 240.0 <– 填充为 East 组的平均值 (300+180)/2 = 240
…
“`
transform
在这里接收一个 lambda 函数,该函数对组内的 Series 使用 fillna(x.mean())
。transform
要求函数返回的结果形状与输入组相同,fillna
正好满足这个条件。
Transform vs Aggregation vs Apply:理清三者区别
这是理解 groupby
的关键之一:
- Aggregation (
.agg()
或直接调用聚合函数): 将每个组的多行数据聚合成一个或几个单值。结果的行数等于组的数量,索引是组名。 - Transform (
.transform()
): 对每个组进行计算,并将结果广播回原始组的每一行。结果的形状与原始 DataFrame/Series 相同,索引与原始 DataFrame/Series 相同。常用于在原始数据框中添加基于组的特征。 - Apply (
.apply()
): 最灵活。对每个组应用任意函数。函数可以返回标量、Series 或 DataFrame。apply
会尝试智能地合并结果:如果函数返回标量或 Series,结果会是一个 Series 或 DataFrame (索引为组名);如果函数返回 DataFrame,结果会是一个 MultiIndex DataFrame 或拼接在一起的 DataFrame (取决于group_keys
参数)。
简单总结:
- 想要每组一个汇总值? ->
agg
- 想要基于组计算一个值并应用到原始数据框的每一行? ->
transform
- 想要对每个组执行任意复杂操作并返回自定义结果? ->
apply
7. 使用 Filter 方法过滤分组
filter()
方法允许你根据每个组的属性来决定是保留还是丢弃整个组。传入 filter()
的函数必须接收一个组的 DataFrame 作为输入,并返回一个布尔值(True 表示保留该组,False 表示丢弃该组)。
示例:只保留总销售额大于 400 的地区
“`python
过滤掉总销售额小于等于 400 的地区
filtered_regions = df.groupby(‘Region’).filter(lambda x: x[‘Sales’].sum() > 400)
print(“只保留总销售额大于 400 的地区的数据:”)
print(filtered_regions)
print(“-” * 30)
“`
输出:
只保留总销售额大于 400 的地区的数据:
Region Product Sales Quantity Date Region_Total_Sales Sales_ZScore
4 East A 300 30 2023-02-01 730 0.637844
5 East B 250 25 2023-02-05 730 -0.281063
8 East C 180 18 2023-02-15 730 -0.702688
原始数据中有 North (370) 和 South (310) 两个地区的总销售额小于等于 400,filter
方法将它们对应的所有行都过滤掉了,只保留了总销售额为 730 的 East 地区的所有行。
filter()
函数非常有用,比如:
- 只分析包含样本数量大于 N 的组。
- 只保留满足特定条件的组(如平均值超过阈值)。
8. 高级 Groupby 用法
按多个列进行分组
可以传递一个列名列表给 groupby
,实现多级分组。
“`python
按地区和产品分组,计算总销售额
region_product_sales = df.groupby([‘Region’, ‘Product’])[‘Sales’].sum()
print(“按地区和产品分组的总销售额:”)
print(region_product_sales)
print(“-” * 30)
“`
输出:
按地区和产品分组的总销售额:
Region Product
East A 300
B 250
C 180
North A 220
B 150
South A 200
C 110
Name: Sales, dtype: int64
结果是一个 MultiIndex Series(或 DataFrame,如果对多列聚合或使用 as_index=False
)。
按 Index Levels 进行分组
如果你的 DataFrame 有 MultiIndex 作为索引,你可以根据索引的级别进行分组。
“`python
创建一个 MultiIndex DataFrame
index = pd.MultiIndex.from_tuples([(‘North’, ‘A’), (‘North’, ‘B’), (‘South’, ‘A’), (‘South’, ‘C’)],
names=[‘Region’, ‘Product’])
data_indexed = {‘Sales’: [100, 150, 200, 50],
‘Quantity’: [10, 15, 20, 5]}
df_multi = pd.DataFrame(data_indexed, index=index)
print(“带有 MultiIndex 的 DataFrame:”)
print(df_multi)
print(“-” * 30)
按第一个索引级别 (Region) 分组
multi_grouped_level0 = df_multi.groupby(level=’Region’).sum()
print(“按索引级别 ‘Region’ 分组求和:”)
print(multi_grouped_level0)
print(“-” * 30)
按第二个索引级别 (Product) 分组
multi_grouped_level1 = df_multi.groupby(level=’Product’).sum()
print(“按索引级别 ‘Product’ 分组求和:”)
print(multi_grouped_level1)
print(“-” * 30)
“`
输出:
“`
带有 MultiIndex 的 DataFrame:
Sales Quantity
Region Product
North A 100 10
B 150 15
South A 200 20
C 50 5
按索引级别 ‘Region’ 分组求和:
Sales Quantity
Region
North 250 25
South 250 25
按索引级别 ‘Product’ 分组求和:
Sales Quantity
Product
A 300 30
B 150 15
C 50 5
“`
按函数或映射进行分组
除了列名或索引级别,你还可以传递一个函数或字典给 groupby
。
- 如果传入函数,它会作用于 DataFrame 的索引,返回值用于分组。
- 如果传入字典或 Series,它被用作一个映射,将索引或列的值映射到组名。
更常见的是使用函数作用于某一列,通常用 lambda 函数:
“`python
按产品的首字母分组
grouped_by_product_initial = df.groupby(df[‘Product’].str[0])[‘Sales’].sum()
print(“按产品首字母分组的总销售额:”)
print(grouped_by_product_initial)
print(“-” * 30)
按日期分组 (例如按月份) – 需要 Date 列是 datetime 类型
df[‘Month’] = df[‘Date’].dt.month # 提取月份
grouped_by_month = df.groupby(‘Month’)[‘Sales’].sum()
print(“按月份分组的总销售额:”)
print(grouped_by_month)
print(“-” * 30)
“`
输出:
“`
按产品首字母分组的总销售额:
Product
A 620
B 400
C 290
Name: Sales, dtype: int64
按月份分组的总销售额:
Month
1 500
2 760
Name: Sales, dtype: int64
“`
这里我们利用了 Pandas Series 的 .str
访问器来获取字符串的首字母,以及 .dt
访问器来提取日期时间属性。
结合 pd.cut()
或 pd.qcut()
按区间分组
pd.cut()
可以将数据划分到等宽或指定边界的箱体中,pd.qcut()
可以将数据划分到等频率的箱体中。将这些函数的输出作为 groupby
的分组键,可以方便地按数值区间进行分组。
“`python
按销售额大小将数据分成4个等频率的区间,并统计每个区间的数量
df[‘Sales_Bucket’] = pd.qcut(df[‘Sales’], q=4, labels=False, duplicates=’drop’) # duplicates=’drop’ 处理重复边界
sales_bucket_counts = df.groupby(‘Sales_Bucket’).size()
print(“按销售额等频率分组的数量:”)
print(sales_bucket_counts)
print(“-” * 30)
查看每个 bucket 的实际销售额范围
print(“按销售额等频率分组的销售额范围:”)
print(df.groupby(‘Sales_Bucket’)[‘Sales’].agg([‘min’, ‘max’]))
print(“-” * 30)
按指定销售额区间分组
bins = [0, 100, 200, 300, 500] # 指定区间边界
pd.cut 返回 IntervalIndex,直接用于分组
sales_intervals = pd.cut(df[‘Sales’], bins=bins)
interval_sales_sum = df.groupby(sales_intervals)[‘Sales’].sum()
print(“按指定销售额区间分组的总销售额:”)
print(interval_sales_sum)
print(“-” * 30)
“`
输出 (示例):
“`
按销售额等频率分组的数量:
Sales_Bucket
0 3
1 2
2 2
3 2
dtype: int64
按销售额等频率分组的销售额范围:
min max
Sales_Bucket
0 50 100
1 120 150
2 180 200
3 250 300
按指定销售额区间分组的总销售额:
Sales
(0.0, 100.0] 150
(100.0, 200.0] 470
(200.0, 300.0] 550
(300.0, 500.0] NaN # 没有数据落在这个区间
Name: Sales, dtype: int64
“`
9. Groupby 的性能考虑
对于小型数据集,性能差异不大。但处理大型数据集时,选择合适的 Groupby 操作至关重要:
- Aggregate (
.agg()
,.sum()
, etc.): 通常是最快的,特别是使用内置函数时,因为它们很多在底层是用优化的 C 代码实现的。 - Transform (
.transform()
): 通常也比较高效,特别是当传递一个广播标量或简单的内置函数时。 - Apply (
.apply()
): 最灵活,但也可能最慢,因为它涉及 Python 函数的调用和可能的组数据复制。如果可以用agg
或transform
实现的功能,尽量避免使用apply
。
避免在 apply
中执行非常低效的操作(例如,在每个组内重新进行一次 groupby
)。在可能的情况下,将复杂计算分解,或者考虑其他并行化处理库(如 Dask)。
对于简单的计数,df['column'].value_counts()
通常比 df.groupby('column').size()
更快。
10. 常见问题与技巧
- 处理
NaN
值: 默认情况下,大多数聚合函数(如sum
,mean
,count
,min
,max
)会忽略NaN
值。size
包括NaN
值。如果需要包含NaN
进行计算(例如first
/last
遇到NaN
),或者使用其他填充策略,需要在操作前处理NaN
。 as_index=False
: 默认情况下,groupby
聚合后的结果会以组名作为索引。如果你希望结果是一个普通的 DataFrame,并且将组名作为一列,可以在groupby
时设置as_index=False
。
python
region_sales_sum_df = df.groupby('Region', as_index=False)['Sales'].sum()
print("使用 as_index=False:")
print(region_sales_sum_df)
输出:
使用 as_index=False:
Region Sales
0 East 730
1 North 370
2 South 310
这在后续需要将结果与其他 DataFrame 合并时非常方便。- 链式操作:
groupby
操作常常与其他 Pandas 操作链式调用,使代码更简洁和高效。例如:df.groupby('Region')['Sales'].sum().sort_values(ascending=False)
。
11. 总结
Pandas groupby
是数据分析中进行分组计算和转换的核心工具。通过理解 Split-Apply-Combine 范式,并熟练掌握 agg
、apply
、transform
和 filter
这四大方法,你可以高效地解决绝大多数基于分类数据的数据分析问题。
- 使用
agg
进行高效的组内聚合计算。 - 使用
transform
进行组内计算并将结果广播回原始 DataFrame,常用于创建组级特征或填充缺失值。 - 使用
apply
处理更复杂的、无法用agg
或transform
完成的组内逻辑。 - 使用
filter
根据组的属性过滤数据。
记住,实践是掌握 groupby
的最好方法。尝试用不同的分组键、不同的方法和函数来处理你的数据集,体会它们各自的作用和适用场景。一旦你熟练掌握了 groupby
,你会发现数据分析的效率和能力将迈上一个新的台阶。
希望这篇详细教程能帮助你全面理解并自信地使用 Pandas groupby
进行数据分析!