对多个分组列应用多个函数

419 投票
9 回答
494478 浏览
提问于 2025-04-17 13:41

文档中展示了如何在一个分组对象上同时应用多个函数,方法是使用一个字典,字典的键是输出列的名称:

In [563]: grouped['D'].agg({'result1' : np.sum,
   .....:                   'result2' : np.mean})
   .....:
Out[563]: 
      result2   result1
A                      
bar -0.579846 -1.739537
foo -0.280588 -1.402938

不过,这种方法只适用于Series类型的分组对象。当把字典传给DataFrame类型的分组时,它要求字典的键是要应用函数的列名。

我想做的是对多个列应用多个函数(但某些列会被多次操作)。而且,有些函数会依赖于分组对象中的其他列(就像sumif函数那样)。我现在的解决方案是逐列处理,像上面的代码那样,使用lambda表达式来处理依赖于其他行的函数。但这样做花费的时间很长(我觉得遍历一个分组对象的时间比较久)。我需要改变这个方法,让我能在一次运行中遍历整个分组对象,但我在想pandas中是否有内置的方法可以更干净地做到这一点。

例如,我试过类似这样的代码:

grouped.agg({'C_sum' : lambda x: x['C'].sum(),
             'C_std': lambda x: x['C'].std(),
             'D_sum' : lambda x: x['D'].sum()},
             'D_sumifC3': lambda x: x['D'][x['C'] == 3].sum(), ...)

但正如预期的那样,我得到了一个KeyError(因为如果从DataFrame调用agg,键必须是列名)。

有没有什么内置的方法可以实现我想做的,或者这个功能是否有可能被添加,还是说我只能手动遍历分组呢?

9 个回答

81

Pandas >= 0.25.0,命名聚合

从 pandas 版本 0.25.0 开始,我们不再使用基于字典的聚合和重命名方式,而是转向使用 命名聚合,这种方式接受一个 tuple(元组)。现在我们可以同时进行聚合和重命名,让列名更有信息量:

示例

df = pd.DataFrame(np.random.rand(4,4), columns=list('abcd'))
df['group'] = [0, 0, 1, 1]

          a         b         c         d  group
0  0.521279  0.914988  0.054057  0.125668      0
1  0.426058  0.828890  0.784093  0.446211      0
2  0.363136  0.843751  0.184967  0.467351      1
3  0.241012  0.470053  0.358018  0.525032      1

使用命名聚合来应用 GroupBy.agg

df.groupby('group').agg(
             a_sum=('a', 'sum'),
             a_mean=('a', 'mean'),
             b_mean=('b', 'mean'),
             c_sum=('c', 'sum'),
             d_range=('d', lambda x: x.max() - x.min())
)

          a_sum    a_mean    b_mean     c_sum   d_range
group                                                  
0      0.947337  0.473668  0.871939  0.838150  0.320543
1      0.604149  0.302074  0.656902  0.542985  0.057681
188

在第一部分,你可以传递一个字典,字典的键是列名,值是一个函数的列表:

In [28]: df
Out[28]:
          A         B         C         D         E  GRP
0  0.395670  0.219560  0.600644  0.613445  0.242893    0
1  0.323911  0.464584  0.107215  0.204072  0.927325    0
2  0.321358  0.076037  0.166946  0.439661  0.914612    1
3  0.133466  0.447946  0.014815  0.130781  0.268290    1

In [26]: f = {'A':['sum','mean'], 'B':['prod']}

In [27]: df.groupby('GRP').agg(f)
Out[27]:
            A                   B
          sum      mean      prod
GRP
0    0.719580  0.359790  0.102004
1    0.454824  0.227412  0.034060

更新 1:

因为聚合函数是作用在 Series 上的,所以对其他列名的引用会丢失。为了绕过这个问题,你可以引用整个数据框,并在 lambda 函数中使用分组索引来进行索引。

这里有一个简单的解决方法:

In [67]: f = {'A':['sum','mean'], 'B':['prod'], 'D': lambda g: df.loc[g.index].E.sum()}

In [69]: df.groupby('GRP').agg(f)
Out[69]:
            A                   B         D
          sum      mean      prod  <lambda>
GRP
0    0.719580  0.359790  0.102004  1.170219
1    0.454824  0.227412  0.034060  1.182901

在这里,结果的 'D' 列是由 'E' 列的值相加而成的。

更新 2:

这里有一个我认为可以满足你所有需求的方法。首先,创建一个自定义的 lambda 函数。下面的 g 代表分组。当进行聚合时,g 将是一个 Series。将 g.index 传递给 df.ix[] 可以从 df 中选择当前的分组。我接着测试列 C 是否小于 0.5。返回的布尔系列会传递给 g[],这样就只选择那些符合条件的行。

In [95]: cust = lambda g: g[df.loc[g.index]['C'] < 0.5].sum()

In [96]: f = {'A':['sum','mean'], 'B':['prod'], 'D': {'my name': cust}}

In [97]: df.groupby('GRP').agg(f)
Out[97]:
            A                   B         D
          sum      mean      prod   my name
GRP
0    0.719580  0.359790  0.102004  0.204072
1    0.454824  0.227412  0.034060  0.570441
607

当前被接受的答案的后半部分已经过时,并且有两个不再推荐使用的地方。首先也是最重要的,你不能再把一个字典的字典传给 agg 的分组方法了。其次,永远不要使用 .ix

如果你想同时处理两个不同的列,我建议使用 apply 方法,它会自动把一个数据框(DataFrame)传给你要用的函数。我们可以用一个和上面类似的数据框来演示:

df = pd.DataFrame(np.random.rand(4,4), columns=list('abcd'))
df['group'] = [0, 0, 1, 1]
df

          a         b         c         d  group
0  0.418500  0.030955  0.874869  0.145641      0
1  0.446069  0.901153  0.095052  0.487040      0
2  0.843026  0.936169  0.926090  0.041722      1
3  0.635846  0.439175  0.828787  0.714123      1

用一个字典把列名映射到聚合函数,仍然是进行聚合的好方法。

df.groupby('group').agg({'a':['sum', 'max'], 
                         'b':'mean', 
                         'c':'sum', 
                         'd': lambda x: x.max() - x.min()})

              a                   b         c         d
            sum       max      mean       sum  <lambda>
group                                                  
0      0.864569  0.446069  0.466054  0.969921  0.341399
1      1.478872  0.843026  0.687672  1.754877  0.672401

如果你不喜欢那个看起来很丑的 lambda 列名,你可以用一个普通的函数,并像这样给特殊的 __name__ 属性提供一个自定义的名字:

def max_min(x):
    return x.max() - x.min()

max_min.__name__ = 'Max minus Min'

df.groupby('group').agg({'a':['sum', 'max'], 
                         'b':'mean', 
                         'c':'sum', 
                         'd': max_min})

              a                   b         c             d
            sum       max      mean       sum Max minus Min
group                                                      
0      0.864569  0.446069  0.466054  0.969921      0.341399
1      1.478872  0.843026  0.687672  1.754877      0.672401

使用 apply 并返回一个 Series

现在,如果你有多个列需要一起互动,那么你就不能使用 agg,因为它会自动把一个 Series 传给聚合函数。而使用 apply 时,整个分组会作为一个数据框传入函数。

我建议你创建一个自定义的函数,返回一个包含所有聚合结果的 Series。用 Series 的索引作为新列的标签:

def f(x):
    d = {}
    d['a_sum'] = x['a'].sum()
    d['a_max'] = x['a'].max()
    d['b_mean'] = x['b'].mean()
    d['c_d_prodsum'] = (x['c'] * x['d']).sum()
    return pd.Series(d, index=['a_sum', 'a_max', 'b_mean', 'c_d_prodsum'])

df.groupby('group').apply(f)

         a_sum     a_max    b_mean  c_d_prodsum
group                                           
0      0.864569  0.446069  0.466054     0.173711
1      1.478872  0.843026  0.687672     0.630494

如果你喜欢多重索引(MultiIndexes),你仍然可以返回一个这样的 Series:

    def f_mi(x):
        d = []
        d.append(x['a'].sum())
        d.append(x['a'].max())
        d.append(x['b'].mean())
        d.append((x['c'] * x['d']).sum())
        return pd.Series(d, index=[['a', 'a', 'b', 'c_d'], 
                                   ['sum', 'max', 'mean', 'prodsum']])

df.groupby('group').apply(f_mi)

              a                   b       c_d
            sum       max      mean   prodsum
group                                        
0      0.864569  0.446069  0.466054  0.173711
1      1.478872  0.843026  0.687672  0.630494

撰写回答