如何使用 Pandas Groupby 进行数据分组与聚合 – wiki基地


深入浅出 Pandas groupby:数据分组与聚合的利器

数据分析是现代科学研究、商业决策乃至日常生活中的重要环节。在处理海量数据时,我们经常需要对数据进行分组,然后对每个组进行汇总统计、转换或过滤。Pandas 库作为 Python 中强大的数据处理工具,提供了 groupby() 函数,它是实现这一目标的核心利器。

本文将详细介绍 Pandas groupby() 的原理、用法以及各种高级应用,帮助你掌握这一强大的数据处理技巧。

一、 理解 groupby 的核心思想:Split-Apply-Combine

Pandas groupby() 操作背后的核心思想可以概括为三个步骤:Split-Apply-Combine

  1. Split(拆分): 根据某个或多个键将数据拆分成若干个组。你可以想象成把一堆水果按照种类(苹果、香蕉、橘子)分开放到不同的篮子里。
  2. Apply(应用): 对每个组独立地应用一个函数。这个函数可以是聚合函数(如求和、求平均)、转换函数(如填充缺失值、标准化)或过滤函数(如筛选出满足条件的组)。对应到水果的例子,你可能对每个篮子里的水果称重、计数或者检查是否有腐烂的。
  3. Combine(合并): 将每个组应用函数后的结果组合成一个新的数据结构(通常是 Series 或 DataFrame)。最后,你将每个篮子的总重量或总数量记录下来,形成一个汇总报告。

groupby() 函数本身执行的是第一步:拆分。它返回一个 GroupBy 对象,这个对象里包含了分组信息以及用于后续“应用”步骤的方法(如 sum(), mean(), agg(), transform(), filter() 等)。

理解 Split-Apply-Combine 模型至关重要,因为它决定了 groupby 的行为以及你如何构建后续的操作。

二、 groupby() 方法的基本用法

groupby() 方法通常作用于 DataFrame 或 Series 上。其基本语法是:

python
df.groupby(by=None, axis=0, level=None, as_index=True, sort=True, group_keys=True, squeeze=False, observed=False, dropna=True)

其中最常用的参数是 by,它指定了用于分组的键。by 参数可以接受以下类型:

  • 单个列名(字符串)
  • 一个列名列表(字符串列表)
  • 一个 Series 或数组(与 DataFrame 长度相同)
  • 一个字典或 Series,用于将索引值映射到组名
  • 一个函数,应用于索引或列索引以计算组名
  • 以上任意类型的组合

让我们通过一个简单的例子来演示 groupby() 的基本用法。首先创建一个示例 DataFrame:

“`python
import pandas as pd
import numpy as np

创建示例 DataFrame

data = {
‘城市’: [‘北京’, ‘上海’, ‘广州’, ‘深圳’, ‘北京’, ‘上海’, ‘广州’, ‘深圳’, ‘北京’, ‘上海’],
‘产品类别’: [‘电子’, ‘服装’, ‘电子’, ‘服装’, ‘电子’, ‘服装’, ‘电子’, ‘服装’, ‘电子’, ‘服装’],
‘销售额’: [12000, 15000, 8000, 9000, 11000, 16000, 8500, 9500, 12500, 15500],
‘数量’: [100, 200, 50, 80, 90, 210, 55, 85, 105, 205],
‘日期’: pd.to_datetime([‘2023-01-15’, ‘2023-01-16’, ‘2023-01-15’, ‘2023-01-16’, ‘2023-01-17’, ‘2023-01-18’, ‘2023-01-17’, ‘2023-01-18’, ‘2023-01-19’, ‘2023-01-20’])
}
df = pd.DataFrame(data)

print(“原始 DataFrame:”)
print(df)
“`

输出:

原始 DataFrame:
城市 产品类别 销售额 数量 日期
0 北京 电子 12000 100 2023-01-15
1 上海 服装 15000 200 2023-01-16
2 广州 电子 8000 50 2023-01-15
3 深圳 服装 9000 80 2023-01-16
4 北京 电子 11000 90 2023-01-17
5 上海 服装 16000 210 2023-01-18
6 广州 电子 8500 55 2023-01-17
7 深圳 服装 9500 85 2023-01-18
8 北京 电子 12500 105 2023-01-19
9 上海 服装 15500 205 2023-01-20

现在,我们按“城市”列进行分组:

“`python
grouped_by_city = df.groupby(‘城市’)

print(“\ngroupby 返回的对象:”)
print(grouped_by_city)

输出类似:

“`

可以看到,groupby() 返回的是一个 DataFrameGroupBy 对象。这个对象本身并没有显示分组后的数据,它只是一个“分组器”。要查看分组后的数据或进行计算,需要在这个对象上调用方法。

你可以通过迭代的方式查看每个组的内容(这在实际分析中不常用,但有助于理解):

python
print("\n迭代查看分组:")
for name, group in grouped_by_city:
print(f"\n组名: {name}")
print("组内容:")
print(group)

输出:

“`
迭代查看分组:

组名: 上海
组内容:
城市 产品类别 销售额 数量 日期
1 上海 服装 15000 200 2023-01-16
5 上海 服装 16000 210 2023-01-18
9 上海 服装 15500 205 2023-01-20

组名: 北京
组内容:
城市 产品类别 销售额 数量 日期
0 北京 电子 12000 100 2023-01-15
4 北京 电子 11000 90 2023-01-17
8 北京 电子 12500 105 2023-01-19

组名: 广州
组内容:
城市 产品类别 销售额 数量 日期
2 广州 电子 8000 50 2023-01-15
6 广州 电子 8500 55 2023-01-17

组名: 深圳
组内容:
城市 产品类别 销售额 数量 日期
3 深圳 服装 9000 80 2023-01-16
7 深圳 服装 9500 85 2023-01-18
“`

这样我们就看到了按照“城市”划分的四个组:上海、北京、广州、深圳。

三、 应用函数:聚合 (Aggregation)

聚合是将每个组的数据汇总成一个单一值的过程。这是 groupby 最常见的用途之一。GroupBy 对象提供了许多内置的聚合函数,可以直接调用。

3.1 常用的聚合函数

一些常用的聚合函数包括:

  • count(): 计算每个组的非空值的数量。
  • size(): 计算每个组的行数(包括 NaN)。
  • sum(): 计算每个组的总和。
  • mean(): 计算每个组的平均值。
  • median(): 计算每个组的中位数。
  • min(): 计算每个组的最小值。
  • max(): 计算每个组的最大值。
  • std(): 计算每个组的标准差。
  • var(): 计算每个组的方差。
  • first(): 返回每个组的第一个值。
  • last(): 返回每个组的最后一个值。
  • nth(n): 返回每个组的第 n 个值(从 0 开始索引)。
  • nunique(): 计算每个组的唯一值的数量。

示例:计算每个城市的总销售额和总数量

“`python

计算每个城市的总销售额

total_sales_by_city = grouped_by_city[‘销售额’].sum()
print(“\n每个城市的总销售额:”)
print(total_sales_by_city)

计算每个城市的总数量

total_quantity_by_city = grouped_by_city[‘数量’].sum()
print(“\n每个城市的总数量:”)
print(total_quantity_by_city)

计算每个城市的平均销售额和平均数量

average_by_city = grouped_by_city[[‘销售额’, ‘数量’]].mean()
print(“\n每个城市的平均销售额和数量:”)
print(average_by_city)
“`

输出:

“`
每个城市的总销售额:
城市
上海 46500
北京 35500
广州 16500
深圳 18500
Name: 销售额, dtype: int64

每个城市的总数量:
城市
上海 615
北京 295
广州 105
深圳 165
Name: 数量, dtype: int64

每个城市的平均销售额和数量:
销售额 数量
城市
上海 15500 205
北京 11833 98
广州 8250 52
深圳 9250 82
“`

注意:
* 当我们对整个 GroupBy 对象直接调用聚合函数时,它会尝试对所有数值列进行聚合。
* 我们可以先选择需要聚合的列(例如 grouped_by_city['销售额']grouped_by_city[['销售额', '数量']]),然后再调用聚合函数。这通常是更推荐的做法,因为它避免了对不相关的列进行聚合(例如日期列的求和没有意义)。
* 默认情况下,用于分组的列会成为结果 DataFrame 的索引。这由 as_index=True 控制。如果 as_index=False,分组列会作为普通列保留。

“`python

使用 as_index=False

average_by_city_as_column = df.groupby(‘城市’, as_index=False)[[‘销售额’, ‘数量’]].mean()
print(“\n每个城市的平均销售额和数量 (城市作为列):”)
print(average_by_city_as_column)
“`

输出:

每个城市的平均销售额和数量 (城市作为列):
城市 销售额 数量
0 上海 15500 205
1 北京 11833 98
2 广州 8250 52
3 深圳 9250 82

3.2 使用 agg() / aggregate() 进行多种聚合

agg()(或 aggregate(),两者是同义词)方法提供了更灵活的聚合方式。你可以对同一个列应用多种聚合函数,或者对不同的列应用不同的聚合函数。

示例:对同一个列应用多种聚合

计算每个城市的总销售额、平均销售额、销售额最大值和最小值:

python
sales_summary_by_city = grouped_by_city['销售额'].agg(['sum', 'mean', 'max', 'min'])
print("\n每个城市的销售额汇总:")
print(sales_summary_by_city)

输出:

每个城市的销售额汇总:
sum mean max min
城市
上海 46500 15500.0 16000 15000
北京 35500 11833.3 12500 11000
广州 16500 8250.0 8500 8000
深圳 18500 9250.0 9500 9000

agg() 接受一个列表,列表中包含要应用的聚合函数名(字符串形式)或函数对象。

示例:对不同列应用不同的聚合

计算每个城市的销售额总和、平均销售额,以及数量的总和、平均数量:

python
multi_col_agg = grouped_by_city.agg({
'销售额': ['sum', 'mean'],
'数量': ['sum', 'mean', 'max'] # 数量列额外加一个最大值
})
print("\n每个城市销售额和数量的多种聚合:")
print(multi_col_agg)

输出:

每个城市销售额和数量的多种聚合:
销售额 数量
sum mean sum mean max
城市
上海 46500 15500.0 615 205 210
北京 35500 11833.3 295 98 105
广州 16500 8250.0 105 52 55
深圳 18500 9250.0 165 82 85

agg() 接受一个字典,字典的键是要聚合的列名,值是要应用于该列的一个或多个聚合函数(字符串列表或函数列表)。注意结果 DataFrame 会有一个多级列索引。

示例:使用元组重命名聚合结果列

当你使用 agg() 进行多种聚合时,可以使用元组 (新列名, 聚合函数) 来指定结果列的名称。

python
renamed_agg = grouped_by_city.agg(
总销售额=('销售额', 'sum'),
平均销售额=('销售额', 'mean'),
总数量=('数量', 'sum'),
平均数量=('数量', 'mean')
)
print("\n聚合结果列重命名:")
print(renamed_agg)

输出:

聚合结果列重命名:
总销售额 平均销售额 总数量 平均数量
城市
上海 46500 15500.000 615 205.0
北京 35500 11833.333 295 98.3
广州 16500 8250.000 105 52.5
深圳 18500 9250.000 165 82.5

这种方式非常清晰,推荐在需要自定义结果列名时使用。

3.3 分组依据多种列

我们可以使用一个列名列表作为 by 参数,按多个列进行分组。例如,按城市和产品类别分组:

“`python
grouped_by_city_product = df.groupby([‘城市’, ‘产品类别’])

计算每个城市-产品类别的总销售额和平均数量

city_product_summary = grouped_by_city_product.agg({
‘销售额’: ‘sum’,
‘数量’: ‘mean’
})
print(“\n按城市和产品类别分组聚合:”)
print(city_product_summary)
“`

输出:

按城市和产品类别分组聚合:
销售额 数量
城市 产品类别
上海 服装 46500 205
北京 电子 35500 98
广州 电子 16500 52
深圳 服装 18500 82

结果 DataFrame 的索引是一个 MultiIndex(多级索引),由分组列的值组成。

3.4 分组依据函数、字典或 Series

除了列名,groupby 还可以接受其他类型的 by 参数:

  • 函数: 函数应用于索引或列索引。例如,按日期的月份分组:

    “`python

    需要先确保日期列是 datetime 类型

    df[‘日期’] = pd.to_datetime(df[‘日期’])
    grouped_by_month = df.groupby(df[‘日期’].dt.month)

    计算每个月的总销售额

    monthly_sales = grouped_by_month[‘销售额’].sum()
    print(“\n按月份分组聚合:”)
    print(monthly_sales)
    “`

    输出 (如果数据跨越多个月):

    按月份分组聚合:
    日期
    1 98000
    Name: 销售额, dtype: int64

    因为我们的示例数据都在1月份,所以只显示1月的结果。

  • 字典或 Series: 用于将索引值映射到组名。这在你需要根据现有索引进行灵活分组时很有用。

    “`python

    示例:根据索引位置分组(0-4为A组,5-9为B组)

    index_map = {i: ‘A组’ if i < 5 else ‘B组’ for i in df.index}
    grouped_by_index_map = df.groupby(index_map)

    计算每组的总销售额

    index_group_sales = grouped_by_index_map[‘销售额’].sum()
    print(“\n按索引映射分组聚合:”)
    print(index_group_sales)
    “`

    输出:

    按索引映射分组聚合:
    A组 55000
    B组 43500
    Name: 销售额, dtype: int64

3.5 分组依据索引级别 (MultiIndex)

如果你的 DataFrame 有 MultiIndex,你可以根据某个或多个索引级别进行分组,使用 level 参数。

“`python

创建一个带 MultiIndex 的 DataFrame

data_mi = {
(‘销售’, ‘销售额’): [12000, 15000, 8000, 9000, 11000],
(‘销售’, ‘数量’): [100, 200, 50, 80, 90],
(‘地区’, ‘城市’): [‘北京’, ‘上海’, ‘广州’, ‘深圳’, ‘北京’],
(‘地区’, ‘省份’): [‘河北’, ‘上海’, ‘广东’, ‘广东’, ‘河北’]
}
index_mi = [(‘A’, 1), (‘A’, 2), (‘B’, 1), (‘B’, 2), (‘A’, 3)]
df_mi = pd.DataFrame(data_mi, index=index_mi)

print(“\n带有 MultiIndex 的 DataFrame:”)
print(df_mi)

按第一级索引 (‘A’, ‘B’) 分组

grouped_by_level0 = df_mi.groupby(level=0)
print(“\n按第一级索引分组聚合:”)
print(grouped_by_level0[(‘销售’, ‘销售额’)].sum())

按第二级索引 (1, 2, 3) 分组

grouped_by_level1 = df_mi.groupby(level=1)
print(“\n按第二级索引分组聚合:”)
print(grouped_by_level1[(‘销售’, ‘数量’)].mean())
“`

输出:

“`
带有 MultiIndex 的 DataFrame:
销售 地区
销售额 数量 城市 省份
(A, 1) 12000 100 北京 河北
(A, 2) 15000 200 上海 上海
(B, 1) 8000 50 广州 广东
(B, 2) 9000 80 深圳 广东
(A, 3) 11000 90 北京 河北

按第一级索引分组聚合:
A 38000
B 17000
Name: (销售, 销售额), dtype: int64

按第二级索引分组聚合:
1 75.0
2 140.0
3 90.0
Name: (销售, 数量), dtype: float64
“`

四、 应用函数:转换 (Transformation)

转换操作与聚合不同,它的结果形状(索引和列)与原始 DataFrame 相同。转换通常用于执行组内标准化、填充组内缺失值等操作。

GroupBy 对象提供了 transform() 方法用于执行转换。传递给 transform() 的函数必须返回一个与输入组具有相同索引的 Series 或 DataFrame。

示例:计算每个城市的销售额在其城市总销售额中的占比

“`python

计算每个城市的总销售额(作为 Series)

city_total_sales = df.groupby(‘城市’)[‘销售额’].transform(‘sum’)

计算占比

df[‘城市销售占比’] = df[‘销售额’] / city_total_sales

print(“\n转换示例:计算城市销售占比”)
print(df[[‘城市’, ‘销售额’, ‘城市销售占比’]])
“`

输出:

转换示例:计算城市销售占比
城市 销售额 城市销售占比
0 北京 12000 0.338
1 上海 15000 0.323
2 广州 8000 0.485
3 深圳 9000 0.486
4 北京 11000 0.310
5 上海 16000 0.344
6 广州 8500 0.515
7 深圳 9500 0.514
8 北京 12500 0.352
9 上海 15500 0.333

在这个例子中,transform('sum') 计算了每个城市的总销售额,并将这个总和广播(填充)回原始 DataFrame 中属于该城市的每一行。结果 city_total_sales 是一个与原始 DataFrame 形状相同的 Series。然后我们用原始销售额除以这个广播后的总销售额,得到占比。

示例:在组内进行标准化 (Z-score)

计算每个城市销售额的 Z-score((x - mean) / std):

“`python
def zscore(x):
return (x – x.mean()) / x.std()

df[‘销售额_城市Zscore’] = df.groupby(‘城市’)[‘销售额’].transform(zscore)

print(“\n转换示例:计算组内 Z-score”)
print(df[[‘城市’, ‘销售额’, ‘销售额_城市Zscore’]])
“`

输出:

转换示例:计算组内 Z-score
城市 销售额 销售额_城市Zscore
0 北京 12000 0.313
1 上海 15000 -0.945
2 广州 8000 -1.000
3 深圳 9000 -1.000
4 北京 11000 -0.805
5 上海 16000 0.315
6 广州 8500 1.000
7 深圳 9500 1.000
8 北京 12500 0.492
9 上海 15500 0.630

这里我们定义了一个 zscore 函数,它接收一个 Series (每个组的销售额列),计算其均值和标准差,然后返回一个 Series。transform 将这个函数应用于每个组的 ‘销售额’ Series,并将结果拼接起来。

Transform 的重要特性:

  • transform 的输出形状必须与输入组的形状相同(即索引必须对齐)。
  • transform 函数必须返回 Series 或 DataFrame。
  • 结果会自动对齐到原始 DataFrame 的索引。

五、 应用函数:过滤 (Filtration)

过滤操作用于根据每个组的某些属性来决定是保留还是丢弃整个组。例如,你可能只想分析那些包含超过一定数量记录的城市。

GroupBy 对象提供了 filter() 方法用于执行过滤。传递给 filter() 的函数必须返回一个布尔值(True 表示保留该组,False 表示丢弃该组)。传递给函数的输入是每个组的子 DataFrame。

示例:筛选出销售额总计超过 40000 的城市

“`python

过滤出总销售额 > 40000 的组

filtered_df = df.groupby(‘城市’).filter(lambda x: x[‘销售额’].sum() > 40000)

print(“\n过滤示例:筛选总销售额 > 40000 的城市”)
print(filtered_df)
“`

输出:

过滤示例:筛选总销售额 > 40000 的城市
城市 产品类别 销售额 数量 日期 城市销售占比 销售额_城市Zscore
1 上海 服装 15000 200 2023-01-16 0.323 -0.945
5 上海 服装 16000 210 2023-01-18 0.344 0.315
9 上海 服装 15500 205 2023-01-20 0.333 0.630

Lambda 函数 lambda x: x['销售额'].sum() > 40000 接收每个组的子 DataFrame x,计算该组的销售额总和,并检查是否大于 40000。如果为 True,则保留该组的所有行;如果为 False,则丢弃该组的所有行。

示例:筛选出包含至少 3 条记录的城市

“`python

过滤出包含记录数 >= 3 的组

filtered_df_count = df.groupby(‘城市’).filter(lambda x: len(x) >= 3)

print(“\n过滤示例:筛选记录数 >= 3 的城市”)
print(filtered_df_count)
“`

输出:

过滤示例:筛选记录数 >= 3 的城市
城市 产品类别 销售额 数量 日期 城市销售占比 销售额_城市Zscore
0 北京 电子 12000 100 2023-01-15 0.338 0.313
1 上海 服装 15000 200 2023-01-16 0.323 -0.945
4 北京 电子 11000 90 2023-01-17 0.310 -0.805
5 上海 服装 16000 210 2023-01-18 0.344 0.315
8 北京 电子 12500 105 2023-01-19 0.352 0.492
9 上海 服装 15500 205 2023-01-20 0.333 0.630

Filter 的重要特性:

  • filter 函数必须返回单个布尔值。
  • 结果是一个 DataFrame,包含所有被保留组的原始行。
  • 用于过滤的条件通常基于组的属性(聚合结果、组大小等)。

六、 应用函数:应用任意函数 (apply)

apply()groupby 中最灵活的方法。它可以执行聚合、转换或过滤操作,甚至可以执行这些操作的组合。传递给 apply() 的函数也接收每个组的子 DataFrame 作为输入。apply() 的返回值决定了最终结果的结构。

  • 如果函数返回一个 Series,并且所有组返回的 Series 索引相似,则结果将是一个带有 MultiIndex 的 DataFrame。
  • 如果函数返回一个标量值,则结果将是一个 Series (类似于聚合)。
  • 如果函数返回一个 DataFrame,则结果将是一个带有 MultiIndex 的 DataFrame。

因为 apply() 接收整个组(子 DataFrame),所以你可以使用 Pandas 几乎所有的功能来处理每个组。

示例:获取每个城市销售额最高的记录

“`python
def get_top_sale(group):
# 找到组内销售额最大值的行
return group.loc[group[‘销售额’].idxmax()]

top_sale_per_city = df.groupby(‘城市’).apply(get_top_sale)

print(“\nApply 示例:获取每个城市销售额最高的记录”)
print(top_sale_per_city)
“`

输出:

Apply 示例:获取每个城市销售额最高的记录
城市 产品类别 销售额 数量 日期 城市销售占比 销售额_城市Zscore
城市
上海 上海 服装 16000 210 2023-01-18 0.344 0.315
北京 北京 电子 12500 105 2023-01-19 0.352 0.492
广州 广州 电子 8500 55 2023-01-17 0.515 1.000
深圳 深圳 服装 9500 85 2023-01-18 0.514 1.000

在这个例子中,get_top_sale 函数接收一个 DataFrame (每个城市的子 DataFrame),然后找到其中销售额最大的那一行,并返回该行(一个 Series)。apply 将这些返回的 Series 合并成一个新的 DataFrame。

示例:对每个组进行更复杂的计算

计算每个城市销售额总和,并将销售额除以总和(类似 transform),但使用 apply。

“`python
def calculate_sales_ratio(group):
group[‘组内销售占比’] = group[‘销售额’] / group[‘销售额’].sum()
return group

df_with_ratio = df.groupby(‘城市’).apply(calculate_sales_ratio)

print(“\nApply 示例:在组内计算并添加新列”)
print(df_with_ratio[[‘城市’, ‘销售额’, ‘组内销售占比’]])
“`

输出:

Apply 示例:在组内计算并添加新列
城市 销售额 组内销售占比
0 北京 12000 0.338
1 上海 15000 0.323
2 广州 8000 0.485
3 深圳 9000 0.486
4 北京 11000 0.310
5 上海 16000 0.344
6 广州 8500 0.515
7 深圳 9500 0.514
8 北京 12500 0.352
9 上海 15500 0.333

这个例子展示了 apply 如何修改组内的 DataFrame 并返回它,最终将修改后的组拼接起来。这与 transform 类似,但 apply 提供了更大的灵活性,比如你可以添加或删除列。

Apply 的权衡:

apply() 非常强大和灵活,因为它允许你对每个组应用几乎任何逻辑。然而,它的性能可能不如内置的 agg(), transform(), filter() 方法,尤其是在处理大型数据集时,因为 apply 可能会在 Python 级别迭代处理每个组。对于简单的聚合、转换或过滤任务,优先使用内置方法通常是更高效的选择。只有当内置方法不足以满足需求时,才考虑使用 apply()

七、 处理 groupby 中的缺失值 (NaN)

默认情况下,groupby() 在进行聚合计算时会忽略缺失值 (NaN)。例如,sum() 会将 NaN 视为 0,mean() 会排除 NaN 计算平均,count() 只计算非 NaN 值。

“`python
df_nan = pd.DataFrame({
‘A’: [‘X’, ‘Y’, ‘X’, ‘Y’, ‘X’],
‘B’: [1, 2, np.nan, 4, 5]
})

print(“\n带有 NaN 的 DataFrame:”)
print(df_nan)

分组求和

grouped_nan_sum = df_nan.groupby(‘A’)[‘B’].sum()
print(“\n分组求和 (忽略 NaN):”)
print(grouped_nan_sum)

分组计数

grouped_nan_count = df_nan.groupby(‘A’)[‘B’].count()
print(“\n分组计数 (只计算非 NaN):”)
print(grouped_nan_count)

分组大小 (计算所有行)

grouped_nan_size = df_nan.groupby(‘A’)[‘B’].size()
print(“\n分组大小 (计算所有行):”)
print(grouped_nan_size)
“`

输出:

“`
带有 NaN 的 DataFrame:
A B
0 X 1.0
1 Y 2.0
2 X NaN
3 Y 4.0
4 X 5.0

分组求和 (忽略 NaN):
A
X 6.0
Y 6.0
Name: B, dtype: float64

分组计数 (只计算非 NaN):
A
X 2
Y 2
Name: B, dtype: int64

分组大小 (计算所有行):
A
X 3
Y 2
Name: B, dtype: int64
“`

可以看到,sum() 将 X 组的 NaN 忽略,计算结果为 1+5=6。count() 只计算了非 NaN 的值,X 组有两个非 NaN 值。size() 计算了 X 组的总行数,为 3。

如果你需要在分组时包含 NaN 作为单独的组,可以在 groupby 中设置 dropna=False (在 Pandas 1.1.0 之后)。

“`python

添加一个带有 NaN 分组键的行

df_nan_group = pd.DataFrame({
‘A’: [‘X’, ‘Y’, ‘X’, ‘Y’, ‘X’, np.nan],
‘B’: [1, 2, np.nan, 4, 5, 6]
})

print(“\n带有 NaN 分组键的 DataFrame:”)
print(df_nan_group)

默认行为 (dropna=True)

grouped_nan_key_default = df_nan_group.groupby(‘A’)[‘B’].sum()
print(“\n分组键包含 NaN (默认 dropna=True):”)
print(grouped_nan_key_default)

包含 NaN 分组键 (dropna=False)

grouped_nan_key_include = df_nan_group.groupby(‘A’, dropna=False)[‘B’].sum()
print(“\n分组键包含 NaN (dropna=False):”)
print(grouped_nan_key_include)
“`

输出:

“`
带有 NaN 分组键的 DataFrame:
A B
0 X 1.0
1 Y 2.0
2 X NaN
3 Y 4.0
4 X 5.0
5 NaN 6.0

分组键包含 NaN (默认 dropna=True):
A
X 6.0
Y 6.0
Name: B, dtype: float64

分组键包含 NaN (dropna=False):
A
X 6.0
Y 6.0
NaN 6.0
Name: B, dtype: float64
“`

dropna=False 时,分组键中的 NaN 值会被视为一个单独的组。

八、 性能考虑

Pandas groupby 操作通常是高度优化的,特别是在使用内置的聚合函数 (sum, mean, etc.)、transformfilter 时。这是因为 Pandas 内部使用 C/Cython 实现了这些操作,避免了 Python 循环的开销。

然而,使用 apply() 时需要谨慎。如果 apply 中使用的函数是一个简单的聚合或转换,可以考虑是否能用 agg()transform() 替代,以获得更好的性能。如果 apply 中包含复杂的 Python 逻辑或循环,并且数据集非常大,这可能会成为性能瓶颈。在这种情况下,可能需要考虑其他并行计算库(如 Dask)或优化 apply 中函数的实现。

对于大型数据集,确保你的分组键是 Pandas Categorical 类型有时也能带来性能提升,因为 Pandas 可以更有效地处理分类数据。

九、 总结

Pandas groupby() 是进行数据分析和处理的强大工具,它基于 Split-Apply-Combine 模型,能够灵活地对数据进行分组、聚合、转换和过滤。

  • 使用 groupby() 创建 GroupBy 对象。
  • 使用内置聚合函数 (sum, mean, etc.) 或 agg() 进行数据汇总。
  • 使用 transform() 在保持原始形状的同时进行组内计算。
  • 使用 filter() 根据组的属性筛选数据。
  • 使用 apply() 进行更复杂或自定义的组操作。

掌握 groupby 的各种用法和 Split-Apply-Combine 思想,将极大地提升你在数据分析和清洗工作中的效率和能力。多加实践,结合实际数据场景,才能真正熟练运用这一强大功能。

希望本文对你理解和使用 Pandas groupby 有所帮助!


发表评论

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

滚动至顶部