杰瑞科技汇

Python groupby 函数如何实现分组聚合?

groupby 是 Python 标准库 itertools 模块中的一个非常有用的函数,它的主要作用是将一个可迭代对象中的连续、相同的元素分组,并返回一个迭代器,每次迭代产生一个键和对应的分组迭代器。

Python groupby 函数如何实现分组聚合?-图1
(图片来源网络,侵删)

核心概念:itertools.groupby

最重要的一点是:groupby 只对相邻的相同元素进行分组,如果你的数据是乱序的,必须先进行排序,否则分组结果会不正确。

函数签名

itertools.groupby(iterable, key=None)

参数

  • iterable: 一个可迭代对象,比如列表、元组等。
  • key: 一个可选的函数,用于从每个元素中提取一个键,这个键将用于分组,如果未提供 key 函数,groupby 默认直接使用元素本身作为键。

返回值

  • 返回一个迭代器,这个迭代器产生的元素是 (key, group_iterator) 对。
    • key: 分组的键。
    • group_iterator: 一个迭代器,可以遍历当前分组内的所有元素。

工作原理与使用步骤

groupby 的工作方式是“懒加载”的,它不会一次性处理所有数据,而是在迭代时逐个处理,要正确使用它,通常遵循以下步骤:

步骤 1:排序 这是最关键的一步!必须先对要分组的字段进行排序groupby 只会“看到”连续的元素。

步骤 2:遍历 使用 for 循环来遍历 groupby 返回的迭代器。

Python groupby 函数如何实现分组聚合?-图2
(图片来源网络,侵删)

步骤 3:处理每个分组 对于 (key, group_iterator) 对,你可以再次使用 for 循环来遍历 group_iterator,从而获取分组内的每一个元素。


示例讲解

示例 1:最基本的使用(无 key 函数)

假设我们有一个列表,想统计连续出现的数字。

from itertools import groupby
data = [1, 1, 2, 2, 2, 3, 3, 1, 1, 4]
# 1. 数据已经是有序的了,可以直接使用 groupby
# 2. 遍历 groupby 的结果
for key, group in groupby(data):
    # key 是分组的元素 (1, 2, 3, 1, 4)
    # group 是一个迭代器,包含了该组内的所有元素
    print(f"键: {key}, 分组元素: {list(group)}")

输出:

键: 1, 分组元素: [1, 1]
键: 2, 分组元素: [2, 2, 2]
键: 3, 分组元素: [3, 3]
键: 1, 分组元素: [1, 1]
键: 4, 分组元素: [4]

注意: 为什么会有两个 1 的分组?因为它们不是连续的,这再次证明了排序的重要性

Python groupby 函数如何实现分组聚合?-图3
(图片来源网络,侵删)

示例 2:使用 key 函数(最常见用法)

假设我们有一个学生列表,想按班级进行分组。

from itertools import groupby
students = [
    {'name': 'Alice', 'class': 'A'},
    {'name': 'Bob', 'class': 'B'},
    {'name': 'Charlie', 'class': 'A'},
    {'name': 'David', 'class': 'B'},
    {'name': 'Eve', 'class': 'A'},
    {'name': 'Frank', 'class': 'C'}
]
# 1. 必须先按 'class' 字段排序!
# 我们使用 lambda 函数指定排序的键
sorted_students = sorted(students, key=lambda x: x['class'])
print("排序后的学生列表:")
print(sorted_students)
print("-" * 20)
# 2. 使用 groupby,并指定 key 函数
for class_name, group_iter in groupby(sorted_students, key=lambda x: x['class']):
    print(f"班级: {class_name}")
    # 3. 遍历当前班级的迭代器,获取所有学生
    for student in group_iter:
        print(f"  - {student['name']}")
    print() # 打印一个空行,使输出更清晰

输出:

排序后的学生列表:
[{'name': 'Alice', 'class': 'A'}, {'name': 'Charlie', 'class': 'A'}, {'name': 'Eve', 'class': 'A'}, {'name': 'Bob', 'class': 'B'}, {'name': 'David', 'class': 'B'}, {'name': 'Frank', 'class': 'C'}]
--------------------
班级: A
  - Alice
  - Charlie
  - Eve
班级: B
  - Bob
  - David
班级: C
  - Frank

示例 3:统计分组数量

一个常见的用例是计算每个分组的元素个数,我们可以使用 sum() 和生成器表达式来高效地完成。

from itertools import groupby
data = [('a', 1), ('a', 2), ('b', 3), ('b', 4), ('b', 5), ('c', 6)]
# 1. 按元组的第一个元素排序
sorted_data = sorted(data, key=lambda x: x[0])
# 2. 分组并统计
for key, group in groupby(sorted_data, key=lambda x: x[0]):
    # group 是一个迭代器,('a', 1), ('a', 2)
    # len(list(group)) 会消耗迭代器,所以我们需要一种不消耗它的方法
    # 一个更 Pythonic 的方法是使用 sum(1 for _ in group)
    count = sum(1 for _ in group)
    print(f"键: {key}, 数量: {count}")

输出:

键: a, 数量: 2
键: b, 数量: 3
键: c, 数量: 1

常见误区与注意事项

误区 1:忘记排序

这是 groupby 最常见的错误。

# 错误示例
data = [2, 2, 1, 1, 3, 3]
for key, group in groupby(data):
    print(key, list(group))
# 输出:
# 2 [2, 2]
# 1 [1, 1]
# 3 [3, 3]
# 这个例子看起来“碰巧”对了,但如果数据是 [2, 1, 2, 1] 呢?
data = [2, 1, 2, 1]
for key, group in groupby(data):
    print(key, list(group))
# 输出:
# 2 [2]
# 1 [1]
# 2 [2]
# 1 [1]
# 分组结果完全错误!

误区 2:迭代器只能消耗一次

groupby 返回的 group_iter 是一个迭代器,它只能被遍历一次,如果你尝试遍历两次,第二次会得到空的结果。

from itertools import groupby
data = [1, 1, 2, 2, 3, 3]
sorted_data = sorted(data) # [1, 1, 2, 2, 3, 3]
for key, group_iter in groupby(sorted_data):
    print(f"键: {key}")
    # 第一次遍历
    print("第一次遍历:", list(group_iter))
    # 第二次遍历
    print("第二次遍历:", list(group_iter)) # 会得到空列表 []

输出:

键: 1
第一次遍历: [1, 1]
第二次遍历: []
键: 2
第一次遍历: [2, 2]
第二次遍历: []
键: 3
第一次遍历: [3, 3]
第二次遍历: []

误区 3:key 函数的误用

key 函数是用来提取分组依据的,而不是用来过滤数据的,如果你想在 key 函数里做 if 判断来过滤掉某些元素,groupby 的行为可能会让你感到困惑。

需求: 只统计班级为 'A' 和 'B' 的学生,并按班级分组。

from itertools import groupby
students = [
    {'name': 'Alice', 'class': 'A'},
    {'name': 'Bob', 'class': 'B'},
    {'name': 'Charlie', 'class': 'A'},
    {'name': 'David', 'class': 'B'},
    {'name': 'Eve', 'class': 'A'},
    {'name': 'Frank', 'class': 'C'} # 我们想排除掉 Frank
]
# 错误的做法:在 key 函数中过滤
# sorted_students = sorted(students, key=lambda x: x['class'] if x['class'] in ('A', 'B') else None)
# 这种排序方式会把 'C' 班的学生排到前面或后面,打乱原有的 'A' 和 'B' 的顺序。
# 正确的做法:
# 1. 先过滤数据
filtered_students = [s for s in students if s['class'] in ('A', 'B')]
# 2. 再排序
sorted_students = sorted(filtered_students, key=lambda x: x['class'])
# 3. 最后分组
for class_name, group_iter in groupby(sorted_students, key=lambda x: x['class']):
    print(f"班级: {class_name}")
    for student in group_iter:
        print(f"  - {student['name']}")

groupby vs. pandas.groupby

对于数据分析任务,你可能会遇到 pandas 库的 groupby,它们名字相似,但功能和用法有很大区别。

特性 itertools.groupby pandas.groupby
来源 Python 标准库 itertools 第三方库 pandas
数据类型 任何可迭代对象 (列表, 元组等) pandas.DataFramepandas.Series
核心功能 按连续元素分组,基于迭代 按列值分组,基于索引和列,功能极其强大
排序要求 必须预先排序 不需要预先排序,它会自动处理
主要用途 简单的、基于内存的、逐个元素的分组和聚合 数据分析、统计、数据清洗、复杂的聚合操作
性能 内存效率高,适合流式处理 针对大型数据集进行了优化,使用 C 语言后端
聚合操作 手动实现 (如 sum(), len()) 内置丰富的聚合函数 (.sum(), .mean(), .count(), .agg() 等)

  • 如果你只是处理一个列表或元组,需要对连续的、已排序的元素进行简单分组,用 itertools.groupby
  • 如果你正在处理表格数据(像 Excel),需要进行复杂的分组统计、聚合计算,用 pandas.groupby

itertools.groupby 是一个非常强大且高效的工具,尤其适用于处理已排序的序列数据,记住它的三大黄金法则:

  1. 先排序groupby 只对相邻元素分组。
  2. 迭代器group 对象只能遍历一次。
  3. 按需处理:它是一个惰性求值的迭代器,非常适合处理大数据流。

希望这个详细的解释能帮助你完全理解并掌握 groupby 函数!

分享:
扫描分享到社交APP
上一篇
下一篇