带有冗余 nan 类别的 Pandas groupby

Posted

技术标签:

【中文标题】带有冗余 nan 类别的 Pandas groupby【英文标题】:Pandas groupby with categories with redundant nan 【发布时间】:2018-07-06 09:36:53 【问题描述】:

我在使用带有分类数据的 pandas groupby 时遇到问题。从理论上讲,它应该非常高效:您通过整数而不是字符串进行分组和索引。但它坚持认为,当按多个类别进行分组时,每个类别组合都必须考虑在内。

即使常见字符串的密度较低,我有时也会使用类别,这仅仅是因为这些字符串很长并且可以节省内存/提高性能。有时每列中有数千个类别。当按 3 列分组时,pandas 强制我们保存 1000^3 组的结果。

我的问题:有没有一种方便的方法可以将groupby 与类别一起使用,同时避免这种不良行为?我不是在寻找这些解决方案中的任何一个:

通过numpy重新创建所有功能。 在groupby 之前不断转换为字符串/代码,稍后恢复为类别。 从组列创建元组列,然后按元组列分组。

我希望有一种方法可以修改这个特定的pandas 特质。下面是一个简单的例子。我最终得到了 12 个,而不是我想要的 4 个输出类别。

import pandas as pd

group_cols = ['Group1', 'Group2', 'Group3']

df = pd.DataFrame([['A', 'B', 'C', 54.34],
                   ['A', 'B', 'D', 61.34],
                   ['B', 'A', 'C', 514.5],
                   ['B', 'A', 'A', 765.4],
                   ['A', 'B', 'D', 765.4]],
                  columns=(group_cols+['Value']))

for col in group_cols:
    df[col] = df[col].astype('category')

df.groupby(group_cols, as_index=False).sum()

Group1  Group2  Group3  Value
#   A   A   A   NaN
#   A   A   C   NaN
#   A   A   D   NaN
#   A   B   A   NaN
#   A   B   C   54.34
#   A   B   D   826.74
#   B   A   A   765.40
#   B   A   C   514.50
#   B   A   D   NaN
#   B   B   A   NaN
#   B   B   C   NaN
#   B   B   D   NaN

赏金更新

pandas 开发团队未能很好地解决这个问题(参见 github.com/pandas-dev/pandas/issues/17594)。因此,我正在寻找解决以下任何问题的回复:

    为什么参考 pandas 源代码,分类数据在 groupby 操作中的处理方式不同? 为什么首选当前实现?我很欣赏这是主观的,但我正在努力寻找这个问题的任何答案。当前的行为在许多情况下是令人望而却步的,没有繁琐且可能代价高昂的变通办法。 是否有一个干净的解决方案来覆盖 pandas 在 groupby 操作中对分类数据的处理?请注意 3 条禁止路线(下拉到 numpy;与代码之间的转换;按元组列创建和分组)。我更喜欢“符合熊猫标准”的解决方案,以尽量减少/避免丢失其他熊猫分类功能。 pandas 开发团队支持和阐明现有治疗方法的回应。另外,为什么要考虑所有类别组合都不能配置为布尔参数?

赏金更新 #2

需要明确的是,我并不期望上述 4 个问题都能得到答案。我要问的主要问题是是否可以或可取地覆盖pandas 库方法,以便以促进groupby / set_index 操作的方式处理类别。

【问题讨论】:

Here 是 Jeff 回答的一个问题,但我无法遵循他的逻辑。 @ayhan,我也不遵循他的逻辑。我认为在groupby 中有一个参数来指定我们是否要计算所有类别组合并不是一项困难的编程任务。 甚至 df.set_index(group_cols).sum(level=[0,1,2]) 返回相同的.. @jp_data_analysis 我认为我们需要一个像 ignore_missing=Boolean 这样的参数来用于 groupby 中缺少的类别组合。将来有人可能会添加它。让我们尝试在 github 中请求功能请求 @Dark,有人(有效地)问过,但很快就被驳回了:github.com/pandas-dev/pandas/issues/17594 【参考方案1】:

从 Pandas 0.23.0 开始,groupby method 现在可以采用参数 observed,如果将其设置为 True(默认为 False),则可以解决此问题。 以下是与问题完全相同的代码,仅添加了observed=True

import pandas as pd

group_cols = ['Group1', 'Group2', 'Group3']

df = pd.DataFrame([['A', 'B', 'C', 54.34],
                   ['A', 'B', 'D', 61.34],
                   ['B', 'A', 'C', 514.5],
                   ['B', 'A', 'A', 765.4],
                   ['A', 'B', 'D', 765.4]],
                  columns=(group_cols+['Value']))

for col in group_cols:
    df[col] = df[col].astype('category')

df.groupby(group_cols, as_index=False, observed=True).sum()

【讨论】:

太棒了...我的 9 列 groupby 的 1 个分类字段将我的 groupby 乘以 21 TB...哎呀。这完全解决了问题。 太好了,谢谢,我遇到了同样的问题,这个参数解决了!【参考方案2】:

我能够得到一个应该工作得很好的解决方案。我会用更好的解释来编辑我的帖子。但与此同时,这对你有用吗?

import pandas as pd

group_cols = ['Group1', 'Group2', 'Group3']

df = pd.DataFrame([['A', 'B', 'C', 54.34],
                   ['A', 'B', 'D', 61.34],
                   ['B', 'A', 'C', 514.5],
                   ['B', 'A', 'A', 765.4],
                   ['A', 'B', 'D', 765.4]],
                  columns=(group_cols+['Value']))
for col in group_cols:
    df[col] = df[col].astype('category')


result = df.groupby([df[col].values.codes for col in group_cols]).sum()
result = result.reset_index()
level_to_column_name = f"level_i":col for i,col in enumerate(group_cols)
result = result.rename(columns=level_to_column_name)
for col in group_cols:
    result[col] = pd.Categorical.from_codes(result[col].values, categories=df[col].values.categories)
result

所以这个问题的答案感觉更像是一个正确的编程,而不是一个普通的 Pandas 问题。在引擎盖下,所有分类系列只是一堆数字,它们索引到类别名称。我对这些基础数字进行了分组,因为它们没有与分类列相同的问题。完成此操作后,我不得不重命名列。然后,我使用 from_codes 构造函数有效地将整数列表转换回分类列。

Group1  Group2  Group3  Value
A       B       C       54.34
A       B       D       826.74
B       A       A       765.40
B       A       C       514.50

所以我知道这不完全是您的答案,但我已将我的解决方案变成了一个小功能,供将来遇到此问题的人使用。

def categorical_groupby(df,group_cols,agg_fuction="sum"):
    "Does a groupby on a number of categorical columns"
    result = df.groupby([df[col].values.codes for col in group_cols]).agg(agg_fuction)
    result = result.reset_index()
    level_to_column_name = f"level_i":col for i,col in enumerate(group_cols)
    result = result.rename(columns=level_to_column_name)
    for col in group_cols:
        result[col] = pd.Categorical.from_codes(result[col].values, categories=df[col].values.categories)
    return result

这样称呼它:

df.pipe(categorical_groupby,group_cols)

【讨论】:

它有效,但我很清楚这个选项。每次要groupby 时都转换为代码(比转换为字符串更好),然后将代码转换回类别。我已经改写了 3 条“我不想走的路线”中的第二条,以使其更清晰。 我更多地考虑的是:“我们能否轻松地为分类重新连接 pandas 组索引器”,以便不需要所有这些映射。 啊。很抱歉没有注意到。好吧,如果你必须经常做这种计算,你可以用所有这些步骤来做一个函数。它并不完美,但我当然有一些这样的功能。快速但使代码混乱的解决方法。 非常感谢,赞成您的回答,以便其他人可以看到如何访问类别代码映射。对于大型数据帧和大量 groupby 操作,来回映射可能会变得很昂贵。 从类别获取代码很便宜。从代码到类别相对昂贵。【参考方案3】:

我发现这种行为类似于Categorical Data 的操作部分中记录的行为。

特别是类似于

In [121]: cats2 = pd.Categorical(["a","a","b","b"], categories=["a","b","c"])

In [122]: df2 = pd.DataFrame("cats":cats2,"B":["c","d","c","d"], "values":[1,2,3,4])

In [123]: df2.groupby(["cats","B"]).mean()
Out[123]: 
        values
cats B        
a    c     1.0
     d     2.0
b    c     3.0
     d     4.0
c    c     NaN
     d     NaN

Seriesgroupby 中描述相关行为的一些其他词。本节末尾还有一个数据透视表示例。

除了 Series.min()、Series.max() 和 Series.mode(),以下 可以对分类数据进行操作:

Series.value_counts() 等系列方法将使用所有类别, 即使数据中不存在某些类别:

Groupby 还会显示“未使用”的类别:

词和例子引用自Categorical Data。

【讨论】:

我很欣赏这是目前的治疗方法。是的,它有据可查。有一两个人要求更灵活的治疗,例如见here,但这些论点似乎被暂时搁置一旁。似乎没有人提议覆盖 pandas 方法,例如 set_indexgroupby 来“正确”修复这个问题 - 可能是有充分理由的。 @jp_data_analysis 我想也许你可以尝试在 pandas repo 中打开一个问题。最好从他们那里获得解释或再次提出该功能。虽然有足够多的人要求该功能,但他们可能会考虑它。【参考方案4】:

这里有很多问题需要回答。 让我们从了解什么是“类别”开始...

分类数据类型的定义

引用pandas docs 的“分类数据”:

Categoricals 是 pandas 的一种数据类型,对应于统计中的分类变量:一个变量,只能取有限且通常固定的可能数量值(类别;R 中的级别)。例如性别、社会阶层、血型、所属国家/地区、观察时间或通过李克特量表评分。

这里有两点我想重点说明:

    分类变量作为统计变量的定义: 基本上,这意味着我们必须从统计的角度来看待它们,而不是“常规”编程的角度。即它们不是“枚举”。统计分类变量有特定的操作和用例,你可以在wikipedia阅读更多关于它们的信息。 我会在第二点之后再谈这个。

    类别是 R 中的级别: 如果我们阅读有关R 级别和因素的信息,我们可以了解更多关于分类的信息。 我对 R 了解不多,但我发现 this source 简单而足够。引用一个有趣的例子:

    When a factor is first created, all of its levels are stored along with the factor, and if subsets of the factor are extracted, they will retain all of the original levels. This can create problems when constructing model matrices and may or may not be useful when displaying the data using, say, the table function. As an example, consider a random sample from the letters vector, which is part of the base R distribution.
    
    > lets = sample(letters,size=100,replace=TRUE)
    > lets = factor(lets)
    > table(lets[1:5])
    
    a b c d e f g h i j k l m n o p q r s t u v w x y z
    1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0 1 0 0 0 0 0 0 1
    
    Even though only five of the levels were actually represented, the table function shows the frequencies for all of the levels of the original factors. To change this, we can simply use another call to factor
    
    > table(factor(lets[1:5]))
    
    a k q s z
    1 1 1 1 1
    

基本上,这告诉我们即使不需要显示/使用所有类别也并不罕见。实际上,这是默认行为! 这是由于统计中分类变量的常见用例。几乎在所有情况下,您确实关心所有类别,即使它们没有被使用。以 pandas 函数cut 为例。

我希望到此为止,您已经理解了为什么 pandas 会出现这种行为。

分类变量上的 GroupBy

至于为什么groupby 会考虑所有类别的组合:我不能肯定地说,但基于对源代码(以及您提到的 github 问题)的快速回顾,我的最佳猜测是,它们考虑分类变量上的groupby 和它们之间的interaction。因此,它应该考虑所有对/元组(如笛卡尔积)。 AFAIK,当您尝试执行 ANOVA 之类的操作时,这很有帮助。 这也意味着,在这种情况下,您无法用通常的类似 SQL 的术语来考虑它。

解决方案?

好的,但是如果您不想要这种行为怎么办? 据我所知,考虑到我昨晚在 pandas 源代码中跟踪它,你不能“禁用”它。它在每个关键步骤中都经过硬编码。 然而,由于groupby 的工作方式,实际的“扩展”在需要时才会发生。例如,当通过组调用sum 或尝试打印它们时。 因此,您可以执行以下任何操作来仅获取所需的组:

df.groupby(group_cols).indices
#('A', 'B', 'C'): array([0]),
# ('A', 'B', 'D'): array([1, 4]),
# ('B', 'A', 'A'): array([3]),
# ('B', 'A', 'C'): array([2])

df.groupby(group_cols).groups
#('A', 'B', 'C'): Int64Index([0], dtype='int64'),
# ('A', 'B', 'D'): Int64Index([1, 4], dtype='int64'),
# ('B', 'A', 'A'): Int64Index([3], dtype='int64'),
# ('B', 'A', 'C'): Int64Index([2], dtype='int64')

# an example
for g in df.groupby(group_cols).groups:
    print(g, grt.get_group(g).sum()[0])
#('A', 'B', 'C') 54.34
#('A', 'B', 'D') 826.74
#('B', 'A', 'A') 765.4
#('B', 'A', 'C') 514.5

我知道这对你来说是不行的,但我 99% 确信没有直接的方法可以做到这一点。 我同意应该有一个布尔变量来禁用此行为并使用“常规”类似 SQL 的变量。

【讨论】:

【参考方案5】:

我在调试类似的东西时发现了这篇文章。非常好的帖子,我真的很喜欢包含边界条件!

这是实现初始目标的代码:

r = df.groupby(group_cols, as_index=False).agg('Value': 'sum')

r.columns = ['_'.join(col).strip('_') for col in r.columns]

此解决方案的缺点是它会导致您可能希望展平的分层列索引(尤其是在您有多个统计信息时)。我在上面的代码中包含了列索引的展平。

不知道为什么是实例方法:

df.groupby(group_cols).sum() 
df.groupby(group_cols).mean()
df.groupby(group_cols).stdev()

使用分类变量的所有唯一组合,而 .agg() 方法:

df.groupby(group_cols).agg(['count', 'sum', 'mean', 'std']) 

忽略组中未使用的级别组合。这似乎不一致。很高兴我们可以使用 .agg() 方法而不必担心笛卡尔组合爆炸。

另外,我认为与笛卡尔积相比,唯一基数计数要低得多是很常见的。想一想数据包含“州”、“县”、“邮编”等列的所有情况……这些都是嵌套变量,并且许多数据集都有高度嵌套的变量。

在我们的例子中,分组变量的笛卡尔积与自然发生的组合之间的差异超过 1000 倍(起始数据集超过 1,000,000 行)。

因此,我会投票支持将 observed=True 作为默认行为。

【讨论】:

以上是关于带有冗余 nan 类别的 Pandas groupby的主要内容,如果未能解决你的问题,请参考以下文章

将带有分组数据的 CSV 导入 Pandas 数据框 [重复]

来自带有 NaN 的 pandas 数据框的 seaborn 热图

如何将带有 NaN 的合并 Excel 单元格读入 Pandas DataFrame

带有衰减的 Pandas 数据帧前向填充

Pandas - 列中 groupby 之后的 Concat 字符串,忽略 NaN,忽略重复项

带有递增值的 Pandas fillna