关于Pandas版本: 本文基于 pandas2.1.2 编写。

关于本文内容更新: 随着pandas的stable版本更迭,本文持续更新,不断完善补充。

Pandas稳定版更新及变动内容整合专题: Pandas稳定版更新及变动迭持续更新。

Pandas API参考所有内容目录

Pandas.DataFrame.groupby()

DataFrame.groupby() 方法用于使用映射器或指定的列,对 DataFrame 进行数据分组,可以实现类似Excel的数据透视、分类汇总的效果。

  • DataFrame.groupby() 的底层逻辑是:

    • 1、根据指定的规则(由 by 参数指定)分割 DataFramegroupby 对象;
      • 此时只是完成了 DataFrame 分割,仅仅是一个 groupby 对象,还没有完成汇总。这意味着无法像观察 DataFrame 一样观察它:
      • 可以使用 for 循环观察groupby 对象。
      • 用于分组的分类内容,默认会作为这个新 DataFrame 的索引(行索引,或列名,具体视汇总方向而定)。
    • 2、应用指定的方法,汇总、聚合被分割的数据。
      • 如果应用一种汇总计算方法,所有列都是用一种汇总方法进行聚合。
      • 也可以通过 DataFrame.agg 指定不同的列使用不同的计算方法作为汇总方式。
    • 3、应用聚合方法之后,DataFrame.groupby() 会自动的将聚合后的数据合并为新的 DataFrame

Pandas.DataFrame.groupby() 数据分组(数据透视、分类汇总) 详解 含代码 含测试数据集 随Pandas版本持续更新-LMLPHP

语法:

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()),二者数据量如果差距太大,会产生很多缺失值,造成分组后计算不精准的结果。
  • 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 参数传递列名列表。

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 调用 applyby 参数生成分组结果时, 并且 结果行索引数量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,则保留缺失值。

相关方法:

示例:

测试文件下载:

本文所涉及的测试文件,如有需要,可在文章顶部的绑定资源处下载。

若发现文件无法下载,应该是资源包有内容更新,正在审核,请稍后再试。或站内私信作者索要。

Pandas.DataFrame.groupby() 数据分组(数据透视、分类汇总) 详解 含代码 含测试数据集 随Pandas版本持续更新-LMLPHP


例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
01-17 08:32