Logo
Section 1 / 21

分组

了解不同类型的分组。

导入

分组

了解不同类型的分组。

按层级分组

我们刚刚看到,层级列曾给我们带来不少困扰。但它们有时也非常有用。现在我们将学习如何创建层级索引。假设有人想了解每个国家和编辑的最小年龄和最大年龄。我们希望将国家和编辑都作为索引的一部分。为此,只需传入一个包含我们希望作为索引的列名列表即可:

1print(jb2.pivot_table(index=['country_live', 'ide_main'], 2 values='age', aggfunc=[min, max]))

以下是使用 groupby 的版本:

1print(jb2 2 .groupby(by=['country_live', 'ide_main']) 3 [['age']] 4 .agg([min, max]) 5)
...
使用列名列表进行分组会创建多级索引,即具有层级结构的索引

仔细观察可以发现,groupby 和 pivot_table 执行多个聚合操作的结果并不完全相同。存在一些差异:

  • 层级列的顺序不同(在透视表中,age 在 min 和 max 内部;而在分组时,age 在外部)。
  • 行数不同。

我们尚不清楚 pandas 为何会交换层级顺序。我们可以使用 swaplevel 方法来调整。不过,建议使用带有命名聚合的 groupby 来获得扁平化的列结构:

1print(jb2 2 .groupby(by=['country_live', 'ide_main']) 3 [['age']] 4 .agg([min, max]) 5 .swaplevel(axis='columns') 6)

让我们尝试不使用 swaplevel 方法:

1print(jb2 2 .groupby(by=['country_live', 'ide_main']) 3 .agg(age_min=('age', min), age_max=('age', max)) 4)

行数不同的原因稍显复杂。我们已将 country_live 和 ide_main 列设置为分类类型。当使用分类列进行 groupby 时,pandas 会生成这些列的笛卡尔积,即使没有对应的实际值也会创建。从上面可以看到一些两个值均为 <NA> 的行。而本节开头的透视表版本并未包含缺失值。

注意: 使用多个高基数的分类列进行分组时需格外小心。这可能导致生成非常庞大且稀疏的结果!

我们当然可以在之后调用 dropna,但更推荐使用 observed=True 参数:

1print(jb2 2 .groupby(by=['country_live', 'ide_main'], observed=True) 3 .agg(age_min=('age', min), age_max=('age', max)) 4)

这样看起来就更好了!

使用函数进行分组

到目前为止,我们一直根据列中的具体值进行分组。但有时我们需要根据非现有列的内容进行分组,这时有几种实现方式。

通常我们会创建一个特殊列,其中包含我们想要分组的值。此外,透视表和 groupby 操作都支持传入一个函数而非列名。该函数接收单个索引标签,并返回用于分组的值。在下面的例子中,我们根据索引值是奇数还是偶数来进行分组,然后计算每组的大小。这是分组函数和 pivot_table 的实现:

1def even_grouper(idx): 2 return 'odd' if idx % 2 else 'even' 3 4print(jb2.pivot_table(index=even_grouper, aggfunc='size'))

这是 groupby 的版本:

1print(jb2 2 .groupby(even_grouper) 3 .size() 4)

当我们后续学习时间序列处理时,会发现 pandas 提供了一个便捷的 pd.Grouper 类,可帮助我们轻松按时间属性进行分组。

总结

分组是 pandas 提供的最强大工具之一。它是 pivot_table 方法的基础,而 pivot_table 又实现了 pd.crosstab 函数。这些功能的学习难度较高,原因在于操作本身的复杂性、结果的层级结构以及语法表达。如果使用 groupby,我们必须记得写出完整的链式调用,并逐步推进理解每一步的作用。这有助于我们掌握其内部机制。同时,也需要不断练习。一旦熟悉了语法,持续练习将帮助我们真正掌握这些概念。

数据框

更多聚合操作