关于Pandas版本: 本文基于 pandas2.1.2 编写。
关于本文内容更新: 随着pandas的stable版本更迭,本文持续更新,不断完善补充。
Pandas稳定版更新及变动内容整合专题: Pandas稳定版更新及变动迭持续更新。
本节目录
Pandas.DataFrame.groupby()
DataFrame.groupby()
方法用于使用映射器或指定的列,对 DataFrame
进行数据分组,可以实现类似Excel的数据透视、分类汇总的效果。
-
DataFrame.groupby()
的底层逻辑是:- 1、根据指定的规则(由
by
参数指定)分割DataFrame
为groupby
对象;- 此时只是完成了
DataFrame
分割,仅仅是一个groupby
对象,还没有完成汇总。这意味着无法像观察DataFrame
一样观察它: - 可以使用
for
循环观察groupby
对象。 - 用于分组的分类内容,默认会作为这个新
DataFrame
的索引(行索引,或列名,具体视汇总方向而定)。
- 此时只是完成了
- 2、应用指定的方法,汇总、聚合被分割的数据。
- 如果应用一种汇总计算方法,所有列都是用一种汇总方法进行聚合。
- 也可以通过
DataFrame.agg
指定不同的列使用不同的计算方法作为汇总方式。
- 3、应用聚合方法之后,
DataFrame.groupby()
会自动的将聚合后的数据合并为新的DataFrame
。
- 1、根据指定的规则(由
语法:
DataFrame.groupby (by=None, axis=_NoDefault.no_default, level=None, as_index=True, sort=True, group_keys=True, observed=_NoDefault.no_default, dropna=True)
返回值:
- pandas.api.typing.DataFrameGroupBy
- 返回包含分组信息的
groupby
对象。
- 返回包含分组信息的
参数说明:
by 指定分组依据
-
**by:**mapping, function, label, pd.Grouper or list of such
by
参数用于指定分组的依据(即分割DataFrame
的依据):
- label(列名):用于把某列指定为分组依据
- 当某列的数据具有分类特性,指定这个列的列名,作为分组依据
DataFrame
。
- 当某列的数据具有分类特性,指定这个列的列名,作为分组依据
- mapping(映射):用于直接把行索引的值指定为分组依据
- dict(字典):适用于行索引的值可以拿来做分组(常用于分组名称的重命名)
- 传递一个字典,字典的键是行索引里的可以作为分组的值,字典的值你自定义的分组名;
- 注意!如果只传递字典,你需要提前准备好行索引。并且行索引里的值,应该是可以有效分组的。
- Series(序列):适用于你有一个和
DataFrame
行索引等长的Series
时- 这个
Series
里的值,应该是可以有效分组的; - 这个
Series
建议和DataFrame
行索引等长; - 如果这个
Series
必须和DataFrame
行索引不等长,会自动进行对齐(.align()
),二者数据量如果差距太大,会产生很多缺失值,造成分组后计算不精准的结果。
- 这个
- dict(字典):适用于行索引的值可以拿来做分组(常用于分组名称的重命名)
- function(函数): 函数将作用于行索引的每个值,并使用处理后的值,作为分组依据。
- 行索引中被函数处理后的值,并不会影响计算前的
groupby
对象。 - 行索引中被函数处理后的值,将展示在完成分组计算,合并后的
DataFrame
。
- 行索引中被函数处理后的值,并不会影响计算前的
- pd.Grouper:通常用于按照时间间隔分组,直接作用于行索引
- list of such:多个列构成多维度分组汇总
- 列名列表: 常用于多维度分组汇总,列表里的第1个列名,默认作为顶层行索引,和其他列名构成多层索引。
axis 指定分割方向
-
axis: {0 or ‘index’, 1 or ‘columns’}, default 0
axis
参数用于指定分割方向(可以参照此图,了解什么是分割 数据分组流程示意图):- 0 or ‘index’: 默认为按行索引分割。
- 1 or ‘columns’: 按列分割。
level 指定多层索引的层级编号或层级名称
-
level: int, level name, or sequence of such, default None
如果
DataFrame
具有多层索引,可以用level
参数指定级别的编号或名称,不能和by
参数同时使用。- int:整数层级编号 可以用
整数层级编号
指定分组依据。 - level name:层级名称 可以用层级名称, 指定分组依据。
- sequence of such:层级编号列表,或层级名称列表 可以用层级编号列表,或层级名称列表指定多个分组依据,类似于
by
参数传递列名列表。
- int:整数层级编号 可以用
as_index 排序方法(升序或降序)
-
as_index: bool, default True
as_index
参数控制是否将组标签作为索引返回。- 当
as_index=True
时,组标签将成为输出DataFrame
的索引。 - 当
as_index=False
时,组标签不会成为索引,而是返回一个类似SQL
风格的输出。
- 当
sort 是否对组名排序
-
sort: bool, default True
sort
参数用于控制是否对分组名进行排序,默认sort=True
会对组名进行排序。此参数不会影响每个组内观察值的顺序:- True: 对分组名进行排序。
- False: 关闭分组名排序,如果关闭,则组将按其在原始 DataFrame 中的顺序显示,可以获得更高的性能。
group_keys 是否返回组键
-
group_keys: bool, default True
分组的键指的是
groupby
对象 各分组的行索引。当使用
groupby
调用apply
与by
参数生成分组结果时, 并且 结果行索引数量 和groupby
对象分组数量 不匹配(不匹配则意味着无法汇总),则默认会将groupby
对象各分组的行索引 和 结果行索引 组合为多层行索引,以便观察。- 当
group_keys=True
时(默认值),分组的键会作为结果的索引。这意味着返回的对象会是一个带有分组键的多层次索引的DataFrame
(或者Series
,具体取决于你应用groupby
的对象是DataFrame
还是 Series)。 - 当
group_keys=False
时,分组的键不会作为索引,而是返回一个不带有分组键的普通DataFrame
(或者Series
)。
- 当
observed 只显示观测值或显示所有值
-
observed: bool, default False
观察值是指在实际数据中存在的唯一分类值。当应用
groupby
操作时,有时可能会遇到分类分组器中存在的分类值,但在实际数据中并未出现的情况。observed
参数允许你控制在分组操作中如何处理这些未观察到的分类值:- True: 只显示分类分组器(groupers)的观察值(observed values),而不显示未观察到的值。
- False: 则显示所有分类分组器的可能值,包括未在实际数据中观察到的值。
dropna 其他排序置
-
dropna: bool, default True
dropna
用于控制groupby
对象的行数索引是否可以包含缺失值:- 如果为
True
,并且组键包含缺失值,则将 缺失值与行/列一起删除。 - 如果为
False
,则保留缺失值。
- 如果为
相关方法:
示例:
测试文件下载:
本文所涉及的测试文件,如有需要,可在文章顶部的绑定资源处下载。
若发现文件无法下载,应该是资源包有内容更新,正在审核,请稍后再试。或站内私信作者索要。
例1:如果没有指定聚合计算方法,分组结果将是一个 groupby
对象,只能通过 for
循环观察数据内容
- 例1-1、准备演示数据
import pandas as pd
# 读取一个演示文件
df = pd.read_excel("../../../../数据集/团队成员季度销售额.xlsx")
# 观察数据内容
df.sample(5)
- 例1-2、用 片区列 分组,但是不传递聚合计算方法
grouped = df.sample(5).groupby(by="片区")
grouped
<pandas.core.groupby.generic.DataFrameGroupBy object at 0x000001F9031CB800>
由上面结果可以发现,无法直接观察 GroupBy
对象
- 例1-3、使用
for
循环观察分组内容
for group_name, group_data in grouped:
print(f"Group: {group_name}")
print(group_data)
print("\n")
Group: 华中
姓名 片区 1季度 2季度 3季度 4季度 year
73 庄海彬 华中 2534 968 4128 5454 2023
59 卓小珍 华中 3274 5837 3025 7993 2023
Group: 华北
姓名 片区 1季度 2季度 3季度 4季度 year
95 张华丽 华北 4584 1072 3029 8976 2023
48 紫湉 华北 3046 3918 6908 6444 2023
Group: 华南
姓名 片区 1季度 2季度 3季度 4季度 year
97 王娟 华南 661 6784 3660 8621 2023
例2:分组后,指定汇总计算方式,即可自动完成最终的合并过程,并生成新的 DataFrame
- 例2-1、构建演示数据并观察数据内容
import pandas as pd
# 读取一个演示文件
df = pd.read_excel("../../../../数据集/团队成员季度销售额.xlsx")
# 观察数据内容
df.sample(5)
- 例2-2、以片区列 作为分组依据,只传递一种求和的计算方法。
grouped = df.groupby(by="片区").sum()
grouped
以片区为分组依据,并传递了求和方法后,姓名列因为是字符串,所以相当于拼接。1季度、2季度、3季度、4季度、year等列,完成了求和计算。
- 例2-3、指定计算方法,作为汇总方式,即可观察数据分组后的数据了。
grouped = df.groupby(by="片区").sum()
grouped
- 例2-4、不同的列指定不同的汇总方式,没有指定汇总方式的列,不会出现在汇总结果。例如 姓名列。
grouped = df.groupby(by="片区").agg(
{"1季度": "max", "2季度": "mean", "3季度": "sum", "4季度": "min"} # 最大值 # 平均值 # 总和
) # 最小值
grouped
- 例2-5、用于分组的分类数据,在完成数据分组后,会作为索引使用(行索引或列名,具体视分组方向而定)
grouped.axes
[Index(['华东', '华中', '华北', '华南'], dtype='object', name='片区'),
Index(['1季度', '2季度', '3季度', '4季度'], dtype='object')]
例3:使用字典数据分组(直接把行索引里的值用字典的方式,指定为分组依据)
- 例3-1、构建演示数据并观察数据内容
import pandas as pd
# 读取一个演示文件
df = pd.read_excel("../../../../数据集/团队成员季度销售额.xlsx")
# 将 片区列,设置为索引
df.set_index("片区", inplace=True)
# 观察数据内容
df.sample(5)
- 例3-2、
by
参数传入字典,字典的键是DataFrame
行索引里的值,字典的值是分组名;
grouped = df.groupby(by={"华东": "东部战区", "华南": "南部战区", "华北": "北部战区", "华中": "中部战区"}).agg(
{"1季度": "max", "2季度": "mean", "3季度": "sum", "4季度": "min"} # 最大值 # 平均值 # 总和
) # 最小值
grouped
例4:使用Series数据分组(用Series替换当前行索引,并使用里面的值作为分组依据)
- 例4-1、构建演示数据并观察数据内容
import pandas as pd
# 读取一个演示文件
df = pd.read_excel("../../../../数据集/团队成员季度销售额.xlsx")
# 观察数据内容
df.sample(5)
- 例4-2、使用Series,构建数据分组。(为了方便,我们把片区列拿过来作为Series做演示)
# 提取片区列作为Series
s = df["片区"].copy(deep=True)
# 使用Series,构建数据分组
grouped = df.groupby(by=s).max()
grouped
例5:使用函数数据分组(函数将作用于行索引的每个值)
- 例5-1、构建演示数据并观察数据内容
import pandas as pd
# 读取一个演示文件
df = pd.read_excel("../../../../数据集/团队成员季度销售额.xlsx")
# 将 片区列,设置为索引
# df.set_index('片区',inplace=True)
# 观察数据内容
df.sample(5)
- 例5-1、当前行索引是自然索引,我想按照行索引的单数、双数进行分组,可以这样做:
# 定义区分单数双数的函数
def rename_index(index):
if index % 2 == 0:
return "双数"
else:
return "单数"
# 应用这个函数,处理行索引进行分组
grouped = df.sample(12).groupby(by=rename_index)
# 查看group对象里的内容
for group_name, group_data in grouped:
print(f"Group: {group_name}")
print(group_data)
print("\n")
Group: 单数
姓名 片区 1季度 2季度 3季度 4季度 year
37 邹广坤 华北 3015 5912 2120 3750 2023
77 祝艳斌 华东 5161 1639 1291 7528 2023
97 王娟 华南 661 6784 3660 8621 2023
89 诸子燕 华东 2454 1824 3306 4198 2023
79 祝小娟 华东 4419 6753 2838 1066 2023
11 走向幸福 华南 4312 2189 4431 7493 2023
63 追梦 华中 2689 6790 4247 7637 2023
Group: 双数
姓名 片区 1季度 2季度 3季度 4季度 year
34 邹积杰 华北 2175 4902 4874 3110 2023
92 邹世军 华中 4343 4866 2743 7617 2023
8 左成娟 华南 1747 5823 1480 7025 2023
76 筑梦 华东 1856 3905 5808 6265 2023
98 刘贤 华东 3960 6437 3148 1517 2023
从上面这个结果可以发现,函数处理并没有影响到 groupby
对象
- 例5-2、函数处理行索引的结果,会展现在汇总计算后,合并的新
DataFrame
里
# 给分组对象一个计算方式,完成最终数据合并,并观察
grouped.max()
从上面可以发现,如果 by
参数传递了函数,被修改的 行索引
只会作为分组依据、和分组名称,出现在汇总计算后,合并的新 DataFrame
里。
例6:使用pd.Grouper分组
- 例6-1、构建演示数据并观察数据内容
from datetime import datetime
import numpy as np
import pandas as pd
# 创建一个包含时间序列的DataFrame
date_rng = pd.date_range(start="2022-01-01", end="2022-01-19", freq="D")
df = pd.DataFrame(date_rng, columns=["date"])
# 添加一列随机数值
df["value"] = np.random.randn(len(date_rng))
# 观察数据内容
df
- 例6-2、构建以‘周’为周期的grouper对象,并观察其数据内容
# 创建grouper对象
grouper = pd.Grouper(key="date", freq="W")
grouper
TimeGrouper(key='date', freq=<Week: weekday=6>, axis=0, sort=True, dropna=True, closed='right', label='right', how='mean', convention='e', origin='start_day')
- 例6-3、按周进行分组,求每周的均值
# 按轴分组,并计算每组的均值
result = df.groupby(grouper).mean()
result
例7:by参数传递列名列表,构成多层索引,作为多维度的数据汇总
- 例7-1、构建演示数据并观察数据内容
import pandas as pd
# 读取一个演示文件
df = pd.read_excel("../../../../数据集/团队成员日销售额.xlsx")
# 只保留需要的列
df = df[["职级", "片区", "业绩"]]
# 观察数据内容
df.sample(5)
- 例7-2、传递列名列表,多维度数据分组汇总。观察各职级销售人员,在不同地区的销售表现
df.groupby(by=["职级", "片区"]).sum()
例8:先转置再分割,实现类似纵向分割 axis=1
的效果
- 例8-1、读取演示数据并观察内容
import pandas as pd
# 读取一个演示文件
df = pd.read_excel("../../../../数据集/团队成员日销售额_用于转置.xlsx")
# 观察数据内容
df
3 rows × 101 columns
可以发现,在这个演示数据中,如果需要分组,则需要 axis=1
, 但是这不符合Pandas新版本特性。
- 例8-2、先转置,再用片区分组
df.T.groupby(by=1).max()
分组完毕,by=1
是因为片区的哪一列,此时列名就是1
例9:多层索引需要使用 level
参数传递层级信息指定分组依据
- 例9-1、读取演示数据,构建多层索引,观察数据内容
import pandas as pd
# 读取一个演示文件
df = pd.read_excel("../../../../数据集/团队成员日销售额.xlsx")
# 只保留需要的列
df = df[["职级", "片区", "业绩"]]
# 构建多层索引
df.set_index(["片区", "职级"], inplace=True)
# 观察数据内容
df.sample(5)
- 例9-2、 只使用片区作为分组依据,则只需要传递层级编号,或层级名称即可。
df.groupby(level="片区").sum()
df.groupby(level=0).sum()
- 例9-3、 如果需要使用多列内容,使用列表传递层级编号或层级名称即可(可以混用)
df.groupby(level=[0, "职级"]).sum()
例10:分组名称不再作为索引,使用SQL风格展示分组后的数据
import pandas as pd
# 读取一个演示文件
df = pd.read_excel("../../../../数据集/团队成员日销售额.xlsx")
# 只保留需要的列
df = df[["职级", "片区", "业绩"]]
df
# 用片区进行分组,并关闭索引返回
df.groupby(by="片区", as_index=False).max()
由上面结果可以发现,片区列,没有再作为行索引。
例11:sort参数对分组结果的影响
- 例11-1、默认情况下,数据分组后输出的
DataFrame
会开启组名排序
import pandas as pd
# 构建演示数据
df = pd.DataFrame({"cat": ["b", "b", "a", "a"], "value": [1, 3, 2, 4]})
# 用cat列构建分组,保持分组名排序开启,
grouped = df.groupby(by="cat").mean()
grouped
- 例11-2、 当
sort=False
数据分组后输出的DataFrame
不再对组名排序
# 用cat列构建分组,关闭分组名排序
grouped2 = df.groupby(by="cat", sort=False).mean()
grouped2
例12:应用apply,如果结果行数 > 分组数量,则无法完成汇总,各分组的行索引(组键)会和分组名组成多层索引
- 例12-1、首先来观察以下,各个分组的行索引
import pandas as pd
# 构建演示数据
df = pd.DataFrame({"cat": ["b", "b", "a", "a"], "value": [1, 3, 2, 4]})
# 用cat列构建分组,保持分组名排序开启,
grouped = df.groupby(by="cat")
# 打印每个组的内容
for name, group in grouped:
print(f"Group {name}:")
print(group)
print("\n")
Group a:
cat value
2 a 2
3 a 4
Group b:
cat value
0 b 1
1 b 3
留意上面结果,a和b两个分组的行索引2、3、0、1。
- 例12-2、当 调用
apply
,但是结果行数 > 分组数量时,会产生由分组名、各分组行索引构成的多层索引,
import pandas as pd
# 构建演示数据
df = pd.DataFrame({"cat": ["b", "b", "a", "a"], "value": [1, 3, 2, 4]})
# df['cat'] = df['cat'].astype('category')
# 用cat列构建分组,保持分组名排序开启,
grouped = df.groupby(by="cat").apply(lambda x: x)
grouped
- 例12-3、 当
group_keys=False
时,分组的键不会作为索引,而是返回一个不带有分组键的普通DataFrame
(或者Series
)。
import pandas as pd
# 构建演示数据
df = pd.DataFrame({"cat": ["b", "b", "a", "a"], "value": [1, 3, 2, 4]})
# df['cat'] = df['cat'].astype('category')
# 用cat列构建分组,保持分组名排序开启,
grouped = df.groupby(by="cat", group_keys=False).apply(lambda x: x)
grouped
- 例12-4、再来看一下,正常应该是什么样的
import pandas as pd
# 构建演示数据
df = pd.DataFrame({"cat": ["b", "b", "a", "a"], "value": [1, 3, 2, 4]})
# df['cat'] = df['cat'].astype('category')
# 用cat列构建分组,保持分组名排序开启,
grouped = df.groupby(by="cat").apply(lambda x: x.mean())
grouped
例13:组键(分组名、或可理解为结果的行索引、也可以理解为各分组的行索引)缺失值处理
- 例13-1 构建演示数据并观察
import pandas as pd
# 构建演示数据
l = [["a", 12, 12], [None, 12.3, 33.0], ["b", 12.3, 123], ["a", 1, 1]]
df = pd.DataFrame(l, columns=["a", "b", "c"])
df
- 例13-2 在分组完成时,默认就已经舍弃了缺失值
grouped = df.groupby(by="a")
# 打印每个组的内容
for name, group in grouped:
print(f"Group {name}:")
print(group)
print("\n")
Group a:
a b c
0 a 12.0 12.0
3 a 1.0 1.0
Group b:
a b c
2 b 12.3 123.0
由上面结果可以发现,当完成分组的时候,就已经没有缺失值了,这一步发生在合并每个分组产生结果之前。
- 例13-3
dropna=True
可以保留缺失值
grouped = df.groupby(by="a", dropna=False).mean()
grouped