7.pandas数据分组聚合合并


Pandas 处理数据时,常常会对数据分成若干个组,然后对各个组进行数据处理,比如求和、求平均值等,最后再将各个组的数据汇总起来形成一个新的数据集,这个过程通常被描述为“split-apply-combine” 。

所以本篇要讲的就是 Pandas 中涉及 分组、聚合、合并的一些操作。

在本章中依然使用 wine dataset 数据集:

import pandas as pd
import numpy as np

# nrows=6 表示仅仅读取前10行, 并且只使用 usecols 来限定只取部分列数据
df = pd.read_csv('./datasets/wine', nrows=10, usecols=['citric acid', 'chlorides', 'density', 'pH', 'sulphates' , 'alcohol', 'quality'])
# 使用 rename 来修改一下 行索引和列索引, 使其更精简
df.rename(dict([(i, f'第{i}行') for i in df.index]), inplace=True)
df.rename(dict([(col, col[:4]) for col in df.columns]), axis=1, inplace=True)
df

7.1 分组

7.1.1 groupby 方法

官方文档:groupby

groupby 函数是 Pandas 中分组的最核心函数,其基本参数如下:

  • by: 该参数用于确定 groupby 操作的分组依据,以是一个映射(mapping)、函数(function)、标签(label)、pd.Grouper对象或这样的对象的列表。
    • 如果by是一个函数,那么这个函数会被应用到对象索引的每一个值上,依据函数的返回值进行分组;
    • 如果by是 dict 或 Series,则将使用dict 或 Series 的值来确定分组;
  • axis: 该参数用于确定 对数据沿着什么方向分组,默认为 0,即按行分组,为1时按列分组;该参数在未来新版本中可能会舍弃, 改为只对行数据进行分组,如果要对列数据进行分组,可以先将DataFrame进行转置;
  • level: 该参数是DataFrame具备多层索引时才会用到,用于确定对多层索引的哪一层进行分组,默认为 None,即对所有层进行分组;也可以使用整数或索引名,或者由他们组成的元组序列;
  • as_index: 该参数用于确定分组后,是否将分组的字段作为结果中的索引,默认为 True;
    • as_index=True ,groupby 操作后的返回结果会以分组标签作为索引,也就是说结果 DataFrame 的索引会是每个组的标签。这是最常见的使用方式。
    • as_index=False ,groupby 操作后的返回结果会以原来的 DataFrame 的结构返回,也就是说结果 DataFrame 的索引和原 DataFrame 一样,而分组标签则成为新的列(或行)数据。
  • sort: 该参数用于确定分组后,是否对分组结果按照分组的键(依据)进行排序,默认为 True;但是不会影响分组内部数据的顺序。 如果将该参数设置为 False,一般会执行的快一点,特别是对于数据量很大的DataFrame;
  • group_keys: 当调用apply并且by参数产生类似索引(即转换)结果时,向索引添加组键,以使得结果更具有可读性;
  • observed: 该参数决定了对于分类组器(Categorical groupers)的处理方式,用于确定是否显示观察到的值,默认为 False,即显示观察到的值;当该参数设置为 True 时,会显示未观察到的值;不过该参数在2.1.0版本后被弃用
  • dropna: 该参数用于确定是否删除缺失的数据,默认为 False,即不删除缺失的数据;当该参数设置为 True 时,会删除缺失的数据;

7.1.2 GroupBy 对象

groupby() 函数并不是直接返回一个新的 DataFrame对象,而是返回一个 GroupBy 对象,该对象包含关于分组的信息。

DataFrame 应用 groupby() 之后,返回的是 pandas.api.typing.DataFrameGroupBy> 对象;

Series 应用 groupby() 之后,返回的是 pandas.api.typing.SeriesGroupBy 对象;

官方文档 : GroupBy

对于 GroupBy 对象,并不能直接查看其数据,如下所示:

# 依据 qual 列的值,对行数据进行分组
df_groupby = df.groupby(by='qual')
df_groupby

如果要查看 GroupBy 对象的信息,可以使用以下方法:

  1. 当作迭代器使用
# 会依次产出各个分组的 name 和 分组后的 DataFrame 子集
for group_name, data in df_groupby:
print(f"Group Name: {group_name}\n{data}")

  1. groups 方法

会返回一个字典,字典的键为分组的组名,值为分组后聚在一起的标签:{group name : group labels}

# Dict {group name -> group labels}.
df_groupby.groups

  1. indices 方法

会返回一个字典,字典的键为分组的组名,值为分组后聚在一起的数据的序号:{group name : group indices}

# Dict {group name -> group indices}.
df_groupby.indices

  1. get_group 方法
# 取 分组名为 5 的部分
df_groupby.get_group(5)

7.1.3 分组示例

在知道如何查看 GroupBy 对象之后,可以来看一下 groupby() 方法的使用示例。

  1. 按标签分组示例
# 按标签分组示例, 令 by=列名
df.groupby(by='alco').groups

  1. 按函数分组示例

by 为函数时:it’s called on each value of the object’s index. 即将 DataFrame 对象的每个index都作为函数的输入去获取输出

# 按函数分组示例, 会将 DataFrame 的每个 index 作为函数的输入
# 该示例的函数是以每一行数据的最小值作为分组依据
df.groupby(lambda x: min(df.loc[x])).groups

# 依据每一行 的 pH 的不同范围,映射不同的分组组名
def gen_key(S_, col):
if S_[col] <3.3:
return 'G1'
elif S_[col] >3.4:
return 'G2'
else:
return 'G3'

df.groupby(by=lambda index: gen_key(df.loc[index], 'pH') ).apply(lambda x:x)

  1. 表达式分组示例
# by 为一个表达式,此例中的表达式是判断表达式, 所以会分为 False 或者 True 两种分组
df.groupby(by=df.pH>3.3).groups

  1. as_index 示例

as_index 为 False 时,分组的组名不会作为index

df.groupby(by=df.pH>3.3, as_index=True).sum()

df.groupby(by=df.pH>3.3, as_index=False).sum()

  1. 分组器 Grouper

分组器是 pandas 提供的一个可以执行较为复杂操作的分组工具,具体用法可以参考官方文档:Grouper
它在处理一些复杂结构的 DataFrame时比较高效,比如有时间序列的 DataFrame,这里不过多描述。

7.1.4 分组对象的操作

使用 groupby 方法对数据进行分组之后,可以对分组对象进行一些操作,来对各个分组的数据做进一步的处理。在 7.1.2 小节中介绍了查看分组对象的操作,这里主要是讲一下如何对分组数据进行处理的操作。

官方文档:function-application

  1. apply

    官方文档:DataFrameGroupBy.apply

    它和 DataFrame 的 apply 方法类似,只不过 DataFrameGroupBy 的 apply 方法是对分组后每个子集上的数据进行处理。从下面的code可以清晰的分辨出二者的区别:

    # 使用 DataFrame 的 apply 方法, 默认情况下是对整个DataFrame的每列数据进行处理
    # 所以下方的 code 是求每一列的均值
    df.apply(lambda x:x.mean())

    # 使用 groupby 对象的 apply 方法, 是对各个分组内的 DataFrame 进行处理
    # 所以以下code是对 分组 5,6,7 的三个 子 DataFrame 进行处理,分别得到了 3个不同分组内 各列的数据的均值
    df_groupby.apply(lambda x: x.mean() )

    由于 apply 是一个非常灵活的方法,所以如果有一些特定的方法支持特定的操作,建议使用特定的操作,这样效率更高,比如上面两个code可以改写为:

    df.mean()
    df_groupby.mean()

    效果不变,但是运行效率更高。

    # 定义一个 根据 col 的值,取最大的两行数据的函数
    def first_2(sub_df_, col):
    return sub_df_.nlargest(2, col)

    # 对各个分组应用这个函数, 第一个 sub_df_ 已经由各个分组的子 DataFrame 自动传入了
    df_groupby.apply(first_2, 'chlo')



  2. transform

    它对每个分组调用一个函数,并返回一个与 原始 DataFrame 具有相同索引的 DataFrame,并使用经过转换的值替换原来位置的值。

    官方文档:transform

    # 依据 qual 分组, 然后对每组的数据应用 Transform
    df.groupby(by='qual').transform('mean')

    由示例结果可知,分组的组名并未被展示出来,且数据也并未按照组别进行分布,还是保持了与原来 DataFrame 的 index 相同的 index。 但是仔细观察的话,会发现, transform 确实是对各个组内的数据来进行应用的。

    比如

    • [‘第0行’, ‘第1行’, ‘第2行’, ‘第4行’, ‘第5行’, ‘第6行’, ‘第9行’] 的数据是一样的,这是因为这些行在分组时,被分到了同一个组(qual=5),
    • [‘第7行’, ‘第8行’] 的数据是一样的,这是因为这两行在分组时被分到了(qual=7),
    • 还剩一个[ ‘第3行’ ] 的数据独自成组(qual=6);

      因此,各个组分别被应用了transform(‘mean’), 求出了各个组内的行均值,并且原始的行数据被该结果替换掉。

  3. filter

    它对每个分组调用判断标准,并返回一个布尔值,用于过滤分组,只保留通过过滤的组。

    官方文档:DataFrameGroupBy.filter

    # 条件为: 各个分组内的 chlo列均值 小于 0.07
    df.groupby(by='qual').filter(lambda x: x['chlo'].mean() < 0.07)
    # 只会返回满足条件的组

    # 其实可以应用 apply 函数来查看过滤条件返回的bool值
    df.groupby(by='qual').apply(lambda x: x['chlo'].mean() < 0.07)

    如果所示,filter 当中的过滤条件就是这样的,每个组都对应了一个 bool 值。

  4. pipe

    管道函数,可以将函数作为参数,将函数作用于分组后的数据。pipe 方法主要是为了增加可读性和代码简洁性。

    # 增加新列的示例函数
    def assign_new_col(df_, col):
    df_['testa'] = df_[col]*10
    return df_

    # 用pipe管道对分组对象连续进行函数操作
    df.groupby('qual').pipe(lambda x: x.mean()) \
    .pipe(assign_new,'alco')

7.2 聚合