Pandas groupby与冗余nan的类别

时间:2018-01-27 01:12:48

标签: python pandas numpy group-by pandas-groupby

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

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

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

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

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

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开发团队很难解决这个问题(cf github.com/pandas-dev/pandas/issues/17594)。因此,我正在寻找解决以下任何问题的回复:

  1. 参考pandas源代码,为什么在groupby操作中对分类数据的处理方式不同?
  2. 为什么当前的实施是首选?我很欣赏这是主观的,但我很难找到这个问题的答案。在许多情况下,当前的行为是禁止的,没有繁琐,可能很昂贵的解决方法。
  3. 是否有一个干净的解决方案来覆盖groupby操作中的分类数据的pandas处理?注意3条禁止路线(下降到numpy;转换到代码/从代码转换;通过元组列创建和分组)。我更喜欢的解决方案是" pandas-compliant"尽量减少/避免丢失其他大熊猫的分类功能。
  4. 熊猫开发团队的回应,以支持和澄清现有的治疗方法。另外,为什么要考虑所有类别组合不能配置为布尔参数?
  5. Bounty update#2

    要明确的是,我没有期待上述所有4个问题的答案。我要问的主要问题是,覆盖pandas库方法是否可行或不可取,以便以便于groupby / set_index操作的方式处理类别。

5 个答案:

答案 0 :(得分:7)

由于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()

enter image description here

答案 1 :(得分: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')


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)

答案 2 :(得分:4)

我发现行为类似于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

答案 3 :(得分:3)

这里有一个很多的问题需要解答 让我们首先了解“类别”是什么......

分类dtype的定义

pandas docs引用“分类数据”:

  

分类是一种熊猫数据类型, 对应于统计中的分类变量 :一个变量,它只能采用有限的,通常是固定的数量值( 类别; R 中的级别)。例如性别,社会阶层,血型,国家隶属关系,观察时间或通过李克特量表评级。

我想在此重点关注两点:

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

  2. 类别是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
    
  3. 基本上,这告诉我们即使不需要显示/使用所有类别也不是那么罕见。实际上,这是默认行为!
    这是由于统计中分类变量的常见用例。几乎在所有情况下, 关心所有类别,即使它们未被使用。以pandas函数cut为例。

    我希望在这一点上你明白为什么这种行为存在于熊猫中。

    GroupBy on Categorical Variables

    为什么groupby会考虑所有类别的组合:我不能肯定地说,但基于对源代码(以及您提到的github问题)的快速回顾,我的最佳猜测是他们认为分类变量groupbyinteraction之间存在ANOVA。因此,它应该考虑所有对/元组(如笛卡尔积)。 AFAIK,当您尝试执行{{3}}之类的操作时,这会有很大帮助 这也意味着在这种情况下你不能用通常的类似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。

答案 4 :(得分:0)

我在调试类似内容时找到了这篇文章。很好的帖子,我真的很喜欢包含边界条件!

这是完成最初目标的代码:

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

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

enter image description here

此解决方案的缺点是,它会导致您可能希望将其扁平化(特别是如果您有多个统计信息的话)的分层列索引。我在上面的代码中包括了列索引的展平。

我不知道为什么使用实例方法:

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()方法,而不必担心笛卡尔组合爆炸。

此外,我认为与基数乘积相比,唯一基数少得多的情况很常见。考虑一下所有数据具有诸如“州”,“县”,“邮政编码”之类的列的情况……这些都是嵌套变量,并且那里的许多数据集都具有高度嵌套的变量。

在我们的案例中,分组变量的笛卡尔积与自然出现的组合之间的差超过1000x(并且起始数据集超过1,000,000行)。

因此,我本来会投票赞成将Observed = True设为默认行为。