将多个函数应用于多个分组列

414

文档展示了如何使用带有输出列名作为键的字典,在分组对象上同时应用多个函数:

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 groupby对象。当类似地将一个字典传递给groupby DataFrame时,它期望键是将应用函数的列名。

我想做的是对几个列应用多个函数(但某些列将被多次操作)。此外,一些函数将依赖于groupby对象中的其他列(例如sumif函数)。我的当前解决方案是按列进行,并像上面的代码那样使用lambda为依赖于其他行的函数。但这需要很长时间(我认为迭代groupby对象需要很长时间)。我将不得不更改它,以便在单个运行中遍历整个groupby对象,但我想知道是否有一种内置的方式在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,键必须是一列)。

是否有任何内置的方法可以做我想做的事情,或者可能会添加这个功能,还是我只需要手动遍历分组?


6
如果您在2017年或之后看到此问题,请查看下面的答案,了解将多个列聚合在一起的惯用方法。当前选择的答案已经过时,即您不能再使用字典的字典来重命名groupby结果中的列。 - Ted Petrou
8个回答

597
当前接受的答案的后半部分已经过时,并且有两个废弃功能。首先,您不能再将字典嵌套字典传递给agg groupby方法。其次,永远不要使用.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时,整个分组作为DataFrame被传递到函数中。

我建议创建一个单独的自定义函数,返回所有聚合的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

如果你喜欢使用多级索引,你仍然可以返回一个像这样的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

7
这是我找到的唯一一种同时使用多个列输入汇总数据框的方法(如上面的c_d示例)。 - Blake
1
@slackline 是的。我刚刚测试了一下,它运行得很好。Ted可能只是创建了几个不同的框架,由于是通过随机数生成创建的,用于实际生成数据的 df 数据与最终用于计算的数据不同。 - Lucas H
3
在大型数据框上,这会非常缓慢。有哪些更高效的解决方案的想法? - Hauke
@flow2k 抱歉,我不理解你的问题。你确定是在问我吗? - JejeBelfort
1
е‡Ңж•°fзљ„жњЂеђҺдёЂиҰЊеЏҮд»ӨйЂљиү‡return pd.Series(d, index=list(d.keys()))жқӨеұһеәғе…¶еЃӨеӘ®жЂ§гЂ‚ - mike
显示剩余9条评论

187

对于第一部分,你可以传递一个字典,以列名为键,函数列表为值:

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函数内使用组索引进行索引。

下面是一个hacky的解决方法:

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

有趣的是,我还可以传递一个字典{funcname: func}作为值,而不是列表,以保留我的自定义名称。但在任何情况下,我都不能传递使用其他列的lambda表达式(如上面的lambda x:x['D'] [x ['C'] <3] .sum():“KeyError:'D'”)。有没有想法是否可能? - beardc
我一直在尝试做这件事,但是我得到了错误 KeyError: 'D' - Zelazny7
太棒了,我用 df['A'].ix[g.index][df['C'] < 0].sum() 成功了。不过这开始变得有点混乱了——为了可读性,手动循环可能更好,而且我不确定是否有一种方法可以在 agg 参数中给它指定我的首选名称(而不是 <lambda>)。我希望有人知道更简单的方法... - beardc
3
可以通过传递一个字典作为列值{'D': {'my name':lambda function}},这将使内部字典键成为列名。 - Zelazny7
1
我相信现在的pandas支持对分组数据框应用多个函数:http://pandas.pydata.org/pandas-docs/stable/groupby.html#applying-multiple-functions-at-once - IanS
如果其中一个函数是列表,那么怎么办? - keramat

76

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

4
我喜欢这些命名聚合,但我不知道我们应该如何在多列中使用它们? - Simon Woodhead
好问题,我无法解决这个问题,怀疑这还不可能(但)。我为此打开了一个工单。我会保持我的问题和你的更新。感谢@SimonWoodhead指出。 - Erfan
1
有没有在多列中进行这样的进展?即(['a','b'],'sum') - Derek Eden
@DerekEden 你找到了吗? - mihagazvoda
@mihagazvoda 不好意思。 - Derek Eden
谢谢你,亲爱的Erfan,这很清晰、简洁且直截了当。 - Anoushiravan R

55

作为(主要是在审美上的)Ted Petrou答案的替代方案,我发现我更喜欢略微更紧凑的列表。请不要考虑接受它,它只是对Ted答案的更详细评论,以及代码/数据。Python/pandas不是我的第一选择,但我发现这个读起来很好:

df.groupby('group') \
  .apply(lambda x: pd.Series({
      'a_sum'       : x['a'].sum(),
      'a_max'       : x['a'].max(),
      'b_mean'      : x['b'].mean(),
      'c_d_prodsum' : (x['c'] * x['d']).sum()
  })
)

          a_sum     a_max    b_mean  c_d_prodsum
group                                           
0      0.530559  0.374540  0.553354     0.488525
1      1.433558  0.832443  0.460206     0.053313

我觉得这更像是dplyr管道和data.table链式命令。并不是说它们更好,只是对我来说更熟悉。(我当然认识到使用更正式的def函数进行这些操作的能力和偏好,对许多人而言可能更好。但这只是一种替代方案,不一定更好。)


我以与Ted相同的方式生成数据,为了可重现性,我会添加一个种子。

import numpy as np
np.random.seed(42)
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.374540  0.950714  0.731994  0.598658      0
1  0.156019  0.155995  0.058084  0.866176      0
2  0.601115  0.708073  0.020584  0.969910      1
3  0.832443  0.212339  0.181825  0.183405      1

6
我最喜欢这个答案。这与R中的dplyr管道类似。 - Renhuai
1
为了使这个完整,就像Ted Petrou的回答一样:如果你想要多重索引,你可以指定元组作为字典的键,然后将其传递给pd.Series。例如,('a', 'sum') : x['a'].sum() 而不是 'a_sum' : x['a'].sum() - Michele Piccolini

19

版本 0.25.0 中新增功能。

为了支持对特定列进行聚合并控制输出列名,pandas 接受在 GroupBy.agg() 中使用的特殊语法,称为“命名聚合”,其中:

  • 关键字是输出列名
  • 值是元组,其第一个元素是要选择的列,第二个元素是要应用于该列的聚合。Pandas 提供了带有字段 ['column', 'aggfunc'] 的 pandas.NamedAgg 命名元组,以使参数更加清晰明了。与往常一样,聚合可以是可调用对象或字符串别名。
>>> animals = pd.DataFrame({
...     'kind': ['cat', 'dog', 'cat', 'dog'],
...     'height': [9.1, 6.0, 9.5, 34.0],
...     'weight': [7.9, 7.5, 9.9, 198.0]
... })

>>> print(animals)
  kind  height  weight
0  cat     9.1     7.9
1  dog     6.0     7.5
2  cat     9.5     9.9
3  dog    34.0   198.0

>>> print(
...     animals
...     .groupby('kind')
...     .agg(
...         min_height=pd.NamedAgg(column='height', aggfunc='min'),
...         max_height=pd.NamedAgg(column='height', aggfunc='max'),
...         average_weight=pd.NamedAgg(column='weight', aggfunc=np.mean),
...     )
... )
      min_height  max_height  average_weight
kind                                        
cat          9.1         9.5            8.90
dog          6.0        34.0          102.75

pandas.NamedAgg只是一个命名元组,普通元组也是可以使用的。

>>> print(
...     animals
...     .groupby('kind')
...     .agg(
...         min_height=('height', 'min'),
...         max_height=('height', 'max'),
...         average_weight=('weight', np.mean),
...     )
... )
      min_height  max_height  average_weight
kind                                        
cat          9.1         9.5            8.90
dog          6.0        34.0          102.75

额外的关键字参数不会传递给聚合函数。只有(column, aggfunc)的一对对应该作为**kwargs传递。如果您的聚合函数需要额外的参数,请使用functools.partial()进行部分应用。

对于Series groupby聚合,命名聚合也是有效的。在这种情况下,没有列选择,因此值只是函数。

>>> print(
...     animals
...     .groupby('kind')
...     .height
...     .agg(
...         min_height='min',
...         max_height='max',
...     )
... )
      min_height  max_height
kind                        
cat          9.1         9.5
dog          6.0        34.0

我的下一个评论是一个提示,展示如何使用具有命名聚合的字典。然而,我似乎无法在评论中漂亮地格式化代码,因此我还创建了一个下面的答案。 - Mint
1
"min_height": pd.NamedAgg(column='height', aggfunc='min'), "max_height": pd.NamedAgg(column='height', aggfunc='max'), "average_weight": pd.NamedAgg(column='weight', aggfunc=np.mean) } animals.groupby("kind").agg(**agg_dict)``` - Mint

7
这是对“exans”的回答的变化,使用了命名聚合。它基本相同,但使用参数解包,这使您仍然可以将字典传递给agg函数。
命名聚合是一个很好的特性,但乍一看可能难以以编程方式编写,因为它们使用关键字,但实际上使用参数/关键字解包非常简单。
animals = pd.DataFrame({'kind': ['cat', 'dog', 'cat', 'dog'],
                         'height': [9.1, 6.0, 9.5, 34.0],
                         'weight': [7.9, 7.5, 9.9, 198.0]})
 
agg_dict = {
    "min_height": pd.NamedAgg(column='height', aggfunc='min'),
    "max_height": pd.NamedAgg(column='height', aggfunc='max'),
    "average_weight": pd.NamedAgg(column='weight', aggfunc=np.mean)
}

animals.groupby("kind").agg(**agg_dict)

结果

      min_height  max_height  average_weight
kind                                        
cat          9.1         9.5            8.90
dog          6.0        34.0          102.75

4
泰德的回答非常棒。如果有人感兴趣,我最终使用了一个较小版本。当您正在寻找依赖于多个列的值的单个聚合时非常有用:
# 创建一个数据框
df = pd.DataFrame({
    'a': [1, 2, 3, 4, 5, 6], 
    'b': [1, 1, 0, 1, 1, 0], 
    'c': ['x', 'x', 'y', 'y', 'z', 'z']
})

print(df)
   a  b  c
0  1  1  x
1  2  1  x
2  3  0  y
3  4  1  y
4  5  1  z
5  6  0  z

使用 apply 进行分组和聚合(使用多列)

print(
    df
    .groupby('c')
    .apply(lambda x: x['a'][(x['a'] > 1) & (x['b'] == 1)]
    .mean()
)
c
x    2.0
y    4.0
z    5.0

使用aggregate进行分组和聚合(使用多列)

我喜欢这种方法,因为我仍然可以使用aggregate。也许有人会告诉我为什么在对组进行聚合时需要使用apply来获取多个列。

现在看起来很明显,但只要您不直接在groupby之后选择感兴趣的列,您将可以从聚合函数内访问数据框的所有列。

仅访问所选列

df.groupby('c')['a'].aggregate(lambda x: x[x > 1].mean())

由于选择是魔法的一部分,因此可以访问所有列

df.groupby('c').aggregate(lambda x: x[(x['a'] > 1) & (x['b'] == 1)].mean())['a']

或类似的
df.groupby('c').aggregate(lambda x: x['a'][(x['a'] > 1) & (x['b'] == 1)].mean())

我希望这能有所帮助。

0
df.groupby('c').aggregate(lambda x: x[(x['a'] > 1) & (x['b'] == 1)].mean())['a']

并且

df.groupby('c').aggregate(lambda x: x['a'][(x['a'] > 1) & (x['b'] == 1)].mean())

不工作。抛出一个关键错误:“KeyError: 'a'”
这个可以工作(@r2evans的答案):
df.groupby('group') \
  .apply(lambda x: pd.Series({
      'a_sum'       : x['a'].sum(),
      'a_max'       : x['a'].max(),
      'b_mean'      : x['b'].mean(),
      'c_d_prodsum' : (x['c'] * x['d']).sum()
  })
)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接