Skip to content

python pandas数据分组对象

0 背景

在标准的数据探索过程中,我们通常按照特定的维度或标准来分割数据集。这种分割帮助我们理解数据在这些维度上的集中趋势。通过对数据按照这些粒度进行聚合分析,我们可以更好地洞察数据模式,从而协助我们解决特定场景下的问题。上一篇我们已经完成了分组对象的创建和一些操作,这个分组对象包含了数据的分组详情。那, 接下来的步骤我们是对这个分组对象进行各种操作,并获取相关信息。这是为了为最终的数据聚合和统计分析打下坚实的基础。本文内容将涉及python pandas数据分组对象的一些操作。

1 使用

创建数据并获取分组对象

import numpy as np
import pandas as pd
data = {
    'A': ['aa', 'ab', 'bc', 'bc', 'ab', 'bc', ],
    'B': [2, 2, 3, 5, 8, 5, ],
    'C': np.random.randn(6),
    'D': np.random.randn(6),
    'E': np.random.randint(1, 10, 6)
}
df = pd.DataFrame(data)
df
    A   B   C   D   E
0   aa  2   0.209521    -0.347615   2
1   ab  2   -1.077420   -0.740337   5
2   bc  3   -0.009468   0.723923    7
3   bc  5   0.682054    1.204262    2
4   ab  8   0.782076    1.100714    4
5   bc  5   0.275896    0.780765    8

获取分组对象g

g = df.groupby('A')

选择分组

分组对象的groups对象会返回一个字典, key是分组的名称, value是每个分组的内容的索引的列表

# 分组对象的groups方法会生成一个字典, key是分组的名称, value是每个分组的内容的索引的列表
g.groups

# {'aa': [0], 'ab': [1, 4], 'bc': [2, 3, 5]}

多层索引的分组的使用:

# 多层索引
g2 = df.groupby([df.A.str[0], 'B'])

# 获取分组的字典,key的多层索引的分组的元组,value是每个分组的内容的索引的列表
g2.groups
# {('a', 2): [0, 1], ('a', 8): [4], ('b', 3): [2], ('b', 5): [3, 5]}

g2.get_group(('b', 5))

#     A  B         C         D  E
# 3  bc  5  0.682054  1.204262  2
# 5  bc  5  0.275896  0.780765  8

g.indices返回一个字典,Key为分组的名称,value为本组索引的array格式,可以实现对单分组数据的选取

# grouped.indices返回一个字典,Key为分组的名称,value为本组索引的array格式,可以实现对单分组数据的选取
g.indices

# {'aa': array([0], dtype=int64),
#  'ab': array([1, 4], dtype=int64),
#  'bc': array([2, 3, 5], dtype=int64)}

g.indices['ab']

# array([1, 4], dtype=int64)

迭代分组

我们对分组对象g进行迭代,可以看到分组的名称和每个组的数据表. 类似于字典的遍历方法for key, value in dict.items(). 因此我们可以通过这种方式迭代分组对象。

# 迭代分组数据,key是分组的名称,value是分组的数据表,
for key, dt_ in g:
    print(key)
    print(dt_)
# aa
#     A  B         C         D  E
# 0  aa  2  0.209521 -0.347615  2
# ab
#     A  B         C         D  E
# 1  ab  2 -1.077420 -0.740337  5
# 4  ab  8  0.782076  1.100714  4
# bc
#     A  B         C         D  E
# 2  bc  3 -0.009468  0.723923  7
# 3  bc  5  0.682054  1.204262  2
# 5  bc  5  0.275896  0.780765  8

选择列

在数据分组之后,如果我们需要选择各组中的某一列,我们也可以像DataFrame选择列一样操作:

# 选择单列或多列, 和DataFrame的选择列的方法一样
g.B

g['B']

g[['B', 'C']]

g[['B', 'C']].sum()

应用函数apply()

分组对象使用apply()调用一个函数,传入的是每组的DataFrame,返回一个经过函数计算后的DataFrame、Series或标量,然后再把数据组合。与DataFrame的方式有些类似。

将所有元素乘2:

# 应用函数apply, 将所有元素乘2
g.apply(lambda x: x * 2)

#          A   B         C         D   E
# A                                     
# aa 0  aaaa   4  0.419042 -0.695229   4
# ab 1  abab   4 -2.154840 -1.480674  10
#    4  abab  16  1.564152  2.201427   8
# bc 2  bcbc   6 -0.018935  1.447845  14
#    3  bcbc  10  1.364108  2.408524   4
#    5  bcbc  10  0.551792  1.561530  16

g.apply(lambda x: x * 2).loc[('aa', 0)]

# A        aaaa
# B           4
# C    0.419042
# D   -0.695229
# E           4
# Name: (aa, 0), dtype: object

将分组后的数据按某一列输出为列表, 实现Hive SQL类似collect_list函数功能:

# 将分组后的数据按某一列输出为列表, 实现Hive SQL类似collect_list函数功能
c = g.apply(lambda x: x.B.tolist())
c
# A
# aa          [2]
# ab       [2, 8]
# bc    [3, 5, 5]
# dtype: object

获取每组中C列数值最大的两个(类似于:获取每科目中成绩最高的两个分数)

# 获取每组中C列数值最大的两个(类似于:获取每科目中成绩最高的两个分数)
def get_two_max(df_, col):
    return df_[col].sort_values(ascending=False).head(2)


g.apply(lambda x: get_two_max(x, 'C'))

设置group_keys的参数,可以使分组字段不作为索引

# 设置group_keys参数,可以使分组字段不作为索引
df.groupby('A', group_keys=False).apply(get_two_max, "C")

# 0    0.209521
# 4    0.782076
# 1   -1.077420
# 3    0.682054
# 5    0.275896
# Name: C, dtype: float64

管道函数pipe()

分组对象的管道方法与 DataFrame 的管道方法类似。它首先接收一个分组对象,然后将这个对象中同一组的所有数据应用于指定的方法。经过这一过程,最终返回的是经过函数处理并符合特定数据格式的结果。这种方法允许我们对分组数据进行有效的函数操作和转换。

# 最大值与最小值相加
g.pipe(lambda x: x.max() + x.min())

#      B         C         D   E
# A                             
# aa   4  0.419042 -0.695229   4
# ab  10 -0.295344  0.360377   9
# bc   8  0.672586  1.928185  10
def mean_diff(x):
    return x.get_group('ab').drop('A', axis=1).mean() - x.get_group('bc').drop('A', axis=1).mean()


# 使用函数
g.pipe(mean_diff)

转换函数transform()

transform() 函数在具有与 agg() 函数相似的功能。然而,不同于 agg()的是,transform() 的独特之处在于它返回一个与原始数据结构相同的 DataFrame。这个过程涉及将每个数据点的原始值替换为经过统计处理的值。例如,如果我们按组计算学生成绩的平均值,那么 transform() 会生成一个新的 DataFrame,其中每个学生的成绩被替换为其所在组的平均成绩。

g.transform('mean')

#           B         C         D         E
# 0  2.000000  0.209521 -0.347615  2.000000
# 1  5.000000 -0.147672  0.180188  4.500000
# 2  4.333333  0.316161  0.902983  5.666667
# 3  4.333333  0.316161  0.902983  5.666667
# 4  5.000000 -0.147672  0.180188  4.500000
# 5  4.333333  0.316161  0.902983  5.666667
# 将分组的数据的位置往上移动一维,通常用于当你需要基于组内的相对位置进行数据操作时,比如在时间序列数据中计算每个组的后一个值。
g.transform(lambda x: x.shift(-1))

#      B         C         D    E
# 0  NaN       NaN       NaN  NaN
# 1  8.0  0.782076  1.100714  4.0
# 2  5.0  0.682054  1.204262  2.0
# 3  5.0  0.275896  0.780765  8.0
# 4  NaN       NaN       NaN  NaN
# 5  NaN       NaN       NaN  NaN
# 按照每组标准化
g.transform(lambda x: x / (x.max() - x.min()))

#   B   C   D   E
# 0 inf inf -inf    inf
# 1 0.333333    -0.579415   -0.402127   5.000000
# 2 1.500000    -0.013691   1.507106    1.166667
# 3 2.500000    0.986309    2.507106    0.333333
# 4 1.333333    0.420585    0.597873    4.000000
# 5 2.500000    0.398969    1.625444    1.333333
# 按组进行筛选, 选出B列中大于3的数据
df[g.transform('mean').B > 3]

#     A  B         C         D  E
# 1  ab  2 -1.077420 -0.740337  5
# 2  bc  3 -0.009468  0.723923  7
# 3  bc  5  0.682054  1.204262  2
# 4  ab  8  0.782076  1.100714  4
# 5  bc  5  0.275896  0.780765  8

筛选函数filter()

首先, 我们可以用下面的方法, 求一组的平均值

g.mean().mean(1)

g.get_group('aa').drop('A', axis=1).mean(1).mean()

获取每组平均值大于2的:

# 获取每组平均值大于2的
g.filter(lambda x: x.drop('A', axis=1).mean(1).mean() > 2)

#     A  B         C         D  E
# 1  ab  2 -1.077420 -0.740337  5
# 2  bc  3 -0.009468  0.723923  7
# 3  bc  5  0.682054  1.204262  2
# 4  ab  8  0.782076  1.100714  4
# 5  bc  5  0.275896  0.780765  8

组内B列至少有1个大于5的组:

# 组内B列至少有1个大于5的组
g.filter(lambda x: (x.B > 5).any())

#     A  B         C         D  E
# 1  ab  2 -1.077420 -0.740337  5
# 4  ab  8  0.782076  1.100714  4

组内B列之和大于10的组:

# 组内B列之和大于10的组
g.filter(lambda x: x.B.sum() > 10)

#     A  B         C         D  E
# 2  bc  3 -0.009468  0.723923  7
# 3  bc  5  0.682054  1.204262  2
# 5  bc  5  0.275896  0.780765  8

组内所有B列之和大于10的组:

# 组内所有B列之和大于10的组
g.filter(lambda x: (x.B.mean() >= 3).all())

# A B   C   D   E
# 1 ab  2   -1.077420   -0.740337   5
# 2 bc  3   -0.009468   0.723923    7
# 3 bc  5   0.682054    1.204262    2
# 4 ab  8   0.782076    1.100714    4
# 5 bc  5   0.275896    0.780765    8

其他方法

# 组内第一个
g.first()

# 组内最后一个
g.last()
# 分组数
g.ngroups
# 每条数据所在的组
g.ngroup()

# 0    0
# 1    1
# 2    2
# 3    2
# 4    1
# 5    2
# dtype: int64
# 每组显示前n个, 默认是5个
g.head()

# 每组最后n个, 默认是5个
g.tail(1)
# 排序值
g.rank()
# 分组的第一个值
g.nth(1)

# 分组的最后一个值
g.nth(-1)

# 分组的最后两个值
g.nth([-2, -1])

# 第n个非空项
g.nth(0, dropna='all')
g.nth(0, dropna='any')

# 组内位置偏移
g.shift(1)

# 按时间周期偏移, 在时间序列时
g.tshift(1)

# 在 SeriesGroupBy 时可用
# 每组最大的两个
g.B.nlargest(2)
# 每组最小的两个
g.B.nsmallest(2)

# 每组去重数量
g.B.nunique()

# 每组去重值
g.B.unique()

# 每组去重值及数量
g.B.value_counts()
g.B.value_counts(normalize = True)

# 每组值是否单调递增
g.B.is_monotonic_increasing

# 每组值是否单调递减
g.B.is_monotonic_decreasing
# 仅 DataFrameGroupBy 可用
g.corrwith(df2)

其中, 在g.B.value_counts()时, value_counts()有几个重要的参数:

  1. normalize (布尔值,默认为 False):
  2. 如果设置为 True,结果将显示值的相对频率而非计数,即每个唯一值的比例。
  3. sort (布尔值,默认为 True):
  4. 如果为 True,结果将按计数值降序排列。
  5. 如果为 False,结果将按出现的值的顺序排列。
  6. ascending (布尔值,默认为 False):
  7. sortTrue 时,这个参数控制排序的顺序。
  8. 如果为 True,结果将按计数值升序排列。
  9. bins (整数或者一维数组,默认为 None):
  10. 仅当列是数字类型时适用。
  11. 如果提供了整数,则它定义了值的分箱数量,类似于直方图的箱数。
  12. 如果提供了一维数组,则数组中的数字定义了箱边界。
  13. dropna (布尔值,默认为 True):
  14. 如果为 True,则在计数时忽略 NaN 值。
  15. 如果为 False,则包含 NaN 值在内的计数。

2 总结

groupby 函数可以简单的理解为为拆开数据、应用数据和合并数据。本文主要介绍python Pandas提供的分组对象的一些方法,能够让我们灵活方便地对数据分组对象进行单独的操作,达到更好的数据分析的效果。

欢迎关注我的微信公众号