【问题标题】:how to get multiple conditional operations after a Pandas groupby?如何在 Pandas groupby 之后获得多个条件操作?
【发布时间】:2016-07-20 02:52:49
【问题描述】:

考虑以下示例:

import pandas as pd
import numpy as np

df = pd.DataFrame({'A' : ['foo', 'bar', 'foo', 'bar',
                         'foo', 'bar', 'foo', 'foo'],
                   'B' : [12,10,-2,-4,-2,5,8,7],
                   'C' : [-5,5,-20,0,1,5,4,-4]})

df
Out[12]: 
     A   B   C
0  foo  12  -5
1  bar  10   5
2  foo  -2 -20
3  bar  -4   0
4  foo  -2   1
5  bar   5   5
6  foo   8   4
7  foo   7  -4

这里我需要为A中的每个组计算B中的元素的总和 以C为非负为条件(即> = 0,基于另一列的条件)。 C 反之亦然。

但是,我下面的代码失败了。

df.groupby('A').agg({'B': lambda x: x[x.C>0].sum(),
                     'C': lambda x: x[x.B>0].sum()})      

AttributeError: 'Series' object has no attribute 'B'

所以看来apply 是首选(因为应用看到我认为的所有数据框),但不幸的是我不能使用带有apply 的字典。所以我被困住了。有什么想法吗?

一个不太漂亮的不太高效的解决方案是在运行groupby 之前创建这些条件变量,但我确信这个解决方案不会利用Pandas.

因此,例如,barcolumn B 组的预期输出将是

+10 (indeed C equals 5 and is >=0)
-4 (indeed C equals 0 and is >=0)
+5 = 11

另一个例子: 群组foocolumn B

NaN (indeed C equals -5 so I dont want to consider the 12 value in B)
+ NaN   (indeed C= -20)
-2    (indeed C=1 so its positive)
+ 8
+NaN = 6

请注意,我使用 NaNs 而不是零,因为如果我们将零放在另一个函数而不是 sum 会给出错误的结果(中位数)。

换句话说,这是一个简单的条件求和,其中条件基于另一列。 谢谢!

【问题讨论】:

    标签: python pandas


    【解决方案1】:

    另一种选择是在使用 groupby/agg 之前预先计算您需要的值:

    import numpy as np
    import pandas as pd
    
    N = 1000
    df = pd.DataFrame({'A' : np.random.choice(['foo', 'bar'], replace=True, size=(N,)),
                       'B' : np.random.randint(-10, 10, size=(N,)),
                       'C' : np.random.randint(-10, 10, size=(N,))})
    
    def using_precomputation(df):
        df['B2'] = df['B'] * (df['C'] >= 0).astype(int)
        df['C2'] = df['C'] * (df['B'] >= 0).astype(int)
        result = df.groupby('A').agg({'B2': 'sum', 'C2': 'sum'})   
        return result.rename(columns={'B2':'B', 'C2':'C'})
    

    让我们比较using_precomputationusing_indexusing_apply

    def using_index(df):
        result = df.groupby('A').agg({'B': lambda x: df.loc[x.index, 'C'][x >= 0].sum(), 
                                      'C': lambda x: df.loc[x.index, 'B'][x >= 0].sum()}) 
        return result.rename(columns={'B':'C', 'C':'B'})
    
    def my_func(row):
        b = row[row.C >= 0].B.sum()
        c = row[row.B >= 0].C.sum()
        return pd.Series({'B':b, 'C':c})
    
    def using_apply(df):
        return df.groupby('A').apply(my_func)
    

    首先,让我们检查它们是否都返回相同的结果:

    def is_equal(df, func1, func2):
        result1 = func1(df).sort_index(axis=1)
        result2 = func2(df).sort_index(axis=1)
        assert result1.equals(result2)
    is_equal(df, using_precomputation, using_index)
    is_equal(df, using_precomputation, using_apply)
    

    使用上面的 1000 行 DataFrame:

    In [83]: %timeit using_precomputation(df)
    100 loops, best of 3: 2.45 ms per loop
    
    In [84]: %timeit using_index(df)
    100 loops, best of 3: 4.2 ms per loop
    
    In [85]: %timeit using_apply(df)
    100 loops, best of 3: 6.84 ms per loop
    

    为什么using_precomputation 更快?

    预计算允许我们利用快速矢量化算法 整个列 并允许聚合函数是简单的内置函数 sum。内置聚合器往往比自定义聚合函数更快 比如这里使用的(基于jezrael的解决方案):

    def using_index(df):
        result = df.groupby('A').agg({'B': lambda x: df.loc[x.index, 'C'][x >= 0].sum(), 
                                      'C': lambda x: df.loc[x.index, 'B'][x >= 0].sum()}) 
        return result.rename(columns={'B':'C', 'C':'B'})
    

    此外,您对每个小组做的工作越少,您的生活就越好 是性能方面的。必须对每个组进行双索引会损害性能。

    另外一个性能杀手是使用groupby/apply(func) 其中func 返回Series。这为结果的每一行形成一个系列,然后 导致 Pandas 对齐并连接所有系列。由于通常 系列往往很短,系列的数量往往很大,连接 所有这些小系列往往很慢。再一次,你往往会得到最好的 在执行矢量化操作时,Pandas/NumPy 的性能 大数组。循环遍历许多微小的结果会破坏性能。

    【讨论】:

    • 谢谢你,很有启发性。您的意思不是说结果的每一列都形成一个系列吗?
    • 回复:这为结果的每一行形成一个系列groupby/agg(func) 为每个组调用一次 funcfunc 返回一个系列。粗略地说,系列是水平布局然后连接起来的。所以每个Series对应groupby/agg(func)返回的结果的一行。
    【解决方案2】:

    我认为你可以使用:

    print df.groupby('A').agg({'B': lambda x: df.loc[x.index, 'C'][x >= 0].sum(), 
                               'C': lambda x: df.loc[x.index, 'B'][x >= 0].sum()})  
          C   B
    A          
    bar  11  10
    foo   6  -5  
    

    更好理解的是自定义函数,与上面相同:

    def f(x):
        s = df.loc[x.index, 'C']
        return s[x>=0].sum()
    def f1(x):
        s = df.loc[x.index, 'B']
        return s[x>=0].sum()
    
    
    print df.groupby('A').agg({'B': f, 'C': f1})
          C   B
    A          
    bar  11  10
    foo   6  -5 
    

    编辑:

    root的solution很不错,但还可以更好:

    def my_func(row):
        b = row[row.C >= 0].B.sum()
        c = row[row.B >= 0].C.sum()
        return pd.Series({'C':b, 'B':c})
    
    result = df.groupby('A').apply(my_func)
          C   B
    A          
    bar  11  10
    foo   6  -5
    

    【讨论】:

    • 感谢jezrael,但我认为这行不通。我正在调节总和中的 其他变量
    • 非常感谢。你能在这里解释一下机制吗? df.loc[x.index, 'C'][df.loc[x.index,'C'] > 0] 到底是做什么的?
    • 输出是否正确?例如, (foo, 'B') 不应该是 6 吗?还是我误解了这个问题?
    • 是的,我认为输出实际上是不正确的。让我把问题说得更清楚。但我相信解决方案会非常接近
    • 谢谢jezrael,如果我没记错的话,你不是在第二个例子中错误地切换了B和C吗?
    【解决方案3】:

    您可以使用apply 返回一个包含所需字段的元组,然后使用zip 将它们解包。

    def my_func(row):
        b = row[row.C >= 0].B.sum()
        c = row[row.B >= 0].C.sum()
        return b, c
    
    # Perform the groupby aggregation.
    result = df.groupby('A').apply(my_func).to_frame()
    
    # Unpack the resulting tuple and drop the extra column.
    result['B'], result['C'] = zip(*result[0])
    result.drop(0, axis=1, inplace=True)
    

    这会产生以下输出:

          B   C
    A          
    bar  11  10
    foo   6  -5
    

    【讨论】:

      猜你喜欢
      • 2017-01-02
      • 1970-01-01
      • 2021-10-28
      • 2020-04-26
      • 2020-08-24
      • 1970-01-01
      • 2022-01-23
      • 2021-04-06
      • 2018-03-07
      相关资源
      最近更新 更多