如何在Pandas groupby之后获得多个条件操作?

ℕʘʘ*_*ḆḽḘ 6 python pandas

考虑以下示例:

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
Run Code Online (Sandbox Code Playgroud)

在这里,我需要计算,对于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'
Run Code Online (Sandbox Code Playgroud)

所以它似乎apply是首选(因为应用看到我认为的所有数据帧),但不幸的是我不能使用字典apply.所以我被困住了.有任何想法吗?

一个不那么漂亮的不那么有效的解决方案是在运行之前创建这些条件变量groupby,但我确信这个解决方案不会使用它的潜力Pandas.

因此,例如,该组的预期产出barcolumn B

+10 (indeed C equals 5 and is >=0)
-4 (indeed C equals 0 and is >=0)
+5 = 11
Run Code Online (Sandbox Code Playgroud)

另一个例子:组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
Run Code Online (Sandbox Code Playgroud)

注意我使用NaNs而不是零,因为如果我们要放零,则除了sum之外的其他函数会给出错误的结果(中位数).

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

unu*_*tbu 9

另一种方法是在使用之前预先计算您需要的值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'})
Run Code Online (Sandbox Code Playgroud)

让我们比较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)
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

使用上面的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
Run Code Online (Sandbox Code Playgroud)

为什么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'})
Run Code Online (Sandbox Code Playgroud)

而且,你必须对每个小组做的工作越少,你的表现就越好.必须为每个组执行双重索引会损害性能.

另外一个杀手的性能是通过groupby/apply(func)func 回报Series.这为结果的每一行形成一个Series,然后使Pandas对齐并连接所有Seri​​es.由于通常系列往往很短并且系列的数量往往很大,所以这些小系列的连接往往很慢.同样,在大型阵列上执行矢量化操作时,您倾向于从Pandas/NumPy中获得最佳性能.循环通过许多微小的结果会导致性能下降.


jez*_*ael 3

我认为你可以使用:

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  
Run Code Online (Sandbox Code Playgroud)

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

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 
Run Code Online (Sandbox Code Playgroud)

编辑:

root 的解决方案非常好,但还可以更好:

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
Run Code Online (Sandbox Code Playgroud)