数据分析必备:Pandas Groupby 使用教程 – wiki基地


数据分析必备:Pandas Groupby 使用教程详解

在数据分析的浩瀚世界里,我们经常需要对数据进行分组处理,例如计算不同产品的总销售额、统计各区域的用户数量、分析不同时间段的平均气温等等。面对这些需求,Pandas 库提供了一个强大而灵活的工具——groupby。掌握 groupby 的使用,可以说是成为一名高效数据分析师的必备技能。

本文将深入浅出地介绍 Pandas groupby 的各个方面,从基本概念到高级用法,并通过丰富的代码示例帮助你彻底理解和掌握这一核心功能。

目录

  1. 引言:为什么需要 Groupby?
    • 现实世界中的分组需求
    • Pandas Groupby 的核心思想:Split-Apply-Combine
  2. Groupby 的基本概念和用法
    • 创建 GroupBy 对象
    • GroupBy 对象的组成
    • 最常用的聚合操作(Aggregation)
  3. 详解 Split-Apply-Combine 范式
    • Split(分割)
    • Apply(应用)
    • Combine(合并)
  4. 常用的 Groupby 聚合函数 (Aggregation)
    • 内置聚合函数:sum(), mean(), count(), size(), min(), max(), std(), var(), first(), last()
    • count() vs size() 的区别
    • 对多个列进行不同的聚合操作
    • 自定义聚合函数的应用
  5. Groupby 的强大武器:Apply 方法
    • Apply 的灵活性:对每个组应用任意函数
    • 返回不同类型的结果 (Series, DataFrame)
    • 应用复杂的组内逻辑 (例如:找出每组的前 N 行)
    • Apply vs Aggregation:何时使用 Apply
  6. Groupby 的另一个利器:Transform 方法
    • Transform 的核心:返回与原 DataFrame 相同索引的结果
    • 常见应用场景:组内标准化、填充缺失值
    • Transform vs Aggregation vs Apply:理清三者区别
  7. 使用 Filter 方法过滤分组
    • 根据组的属性过滤数据
    • filter() 函数的使用
  8. 高级 Groupby 用法
    • 按多个列进行分组
    • 按 Index Levels 进行分组
    • 按函数或映射进行分组
    • 结合 pd.cut()pd.qcut() 按区间分组
    • 时间序列数据的分组 (resample 的关联)
  9. Groupby 的性能考虑
    • 大型数据集的优化
    • 选择合适的 Apply/Transform/Aggregate 方法
  10. 常见问题与技巧
    • 处理 NaN
    • as_index=False 的作用
    • 链式操作
  11. 总结

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 中的:

  1. Split (分割): 当你调用 df.groupby('Region') 时,Pandas 在幕后根据 Region 列的值将 DataFrame 的行分割成三个逻辑组:’East’, ‘North’, ‘South’。原始 DataFrame 并没有被修改,只是内部记录了哪些行属于哪个组。

  2. 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。
  3. 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() 慢,因为它可能需要将每个组的数据复制出来进行处理。在追求极致性能时,应考虑是否有 aggtransform 的替代方案。

6. Groupby 的另一个利器:Transform 方法

transform() 方法是 groupby 中另一个非常有用的操作。它的核心特点是:它返回一个与原始 DataFrame (或 Series) 具有相同索引的新 Series 或 DataFrame。

这使得 transform() 非常适合在组内计算一个值,然后将这个值“广播”回原始组的每一行,用于创建新的列或者进行数据转换。传入 transform() 的函数必须返回:

  1. 一个标量(这个标量值会广播到组的每一行)。
  2. 一个长度与组的大小相同的 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 函数的调用和可能的组数据复制。如果可以用 aggtransform 实现的功能,尽量避免使用 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 范式,并熟练掌握 aggapplytransformfilter 这四大方法,你可以高效地解决绝大多数基于分类数据的数据分析问题。

  • 使用 agg 进行高效的组内聚合计算。
  • 使用 transform 进行组内计算并将结果广播回原始 DataFrame,常用于创建组级特征或填充缺失值。
  • 使用 apply 处理更复杂的、无法用 aggtransform 完成的组内逻辑。
  • 使用 filter 根据组的属性过滤数据。

记住,实践是掌握 groupby 的最好方法。尝试用不同的分组键、不同的方法和函数来处理你的数据集,体会它们各自的作用和适用场景。一旦你熟练掌握了 groupby,你会发现数据分析的效率和能力将迈上一个新的台阶。

希望这篇详细教程能帮助你全面理解并自信地使用 Pandas groupby 进行数据分析!

发表评论

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

滚动至顶部