Aplique várias funções a várias colunas de grupo

221

Os documentos mostram como aplicar várias funções em um objeto de grupo por vez, usando um dict com os nomes das colunas de saída como as chaves:

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

No entanto, isso funciona apenas em um objeto de grupo de séries. E quando um ditado é passado de maneira semelhante a um grupo pelo DataFrame, ele espera que as chaves sejam os nomes das colunas às quais a função será aplicada.

O que quero fazer é aplicar várias funções a várias colunas (mas determinadas colunas serão operadas várias vezes). Além disso, algumas funções dependerão de outras colunas no objeto groupby (como funções sumif). Minha solução atual é ir coluna por coluna e fazer algo como o código acima, usando lambdas para funções que dependem de outras linhas. Mas isso está demorando muito (acho que leva muito tempo para percorrer um objeto groupby). Vou precisar alterá-lo para percorrer todo o objeto groupby em uma única execução, mas estou me perguntando se existe um caminho embutido nos pandas para fazer isso de maneira um tanto limpa.

Por exemplo, eu tentei algo como

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(), ...)

mas como esperado, recebo um KeyError (já que as chaves precisam ser uma coluna se aggforem chamadas de um DataFrame).

Existe alguma maneira integrada de fazer o que eu gostaria de fazer, ou a possibilidade de que essa funcionalidade seja adicionada, ou precisarei apenas percorrer o grupo manualmente?

obrigado

barba
fonte
2
Se você está chegando a esta pergunta em 2017 ou mais, consulte a resposta abaixo para ver a maneira idiomática de agregar várias colunas. A resposta atualmente selecionada possui várias preterições, ou seja, você não pode mais usar um dicionário de dicionários para renomear colunas no resultado de um grupo por.
Ted Petrou #

Respostas:

282

A segunda metade da resposta atualmente aceita está desatualizada e tem duas obsoletas. Primeiro e mais importante, você não pode mais passar um dicionário de dicionários para o aggmétodo groupby. Segundo, nunca use.ix .

Se você deseja trabalhar com duas colunas separadas ao mesmo tempo, sugiro usar o applymétodo que passa implicitamente um DataFrame para a função aplicada. Vamos usar um quadro de dados semelhante ao de cima

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

Um dicionário mapeado de nomes de colunas para funções de agregação ainda é uma maneira perfeitamente boa de realizar uma agregação.

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

Se você não gostar desse nome feio da coluna lambda, poderá usar uma função normal e fornecer um nome personalizado para o __name__atributo especial como este:

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

Usando applye retornando uma série

Agora, se você tinha várias colunas que precisavam interagir juntas, não pode usá- agglas, o que implicitamente passa uma Série para a função de agregação. Ao usar applyo grupo inteiro como um DataFrame, é passado para a função.

Eu recomendo criar uma única função personalizada que retorne uma série de todas as agregações. Use o índice de série como rótulos para as novas colunas:

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

Se você gosta de MultiIndexes, ainda pode devolver uma série com uma como esta:

    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
Ted Petrou
fonte
3
Eu amo o padrão de usar uma função que retorna uma série. Muito arrumado.
Stephen McAteer
2
esta é a única maneira Descobri para agregar uma trama de dados através de múltiplas entradas da coluna simulatneosly (o exemplo acima c_d)
Blake
2
Estou confuso com os resultados, considerando o somatório de adentro do grupo 0, não deveria ser 0.418500 + 0.446069 = 0.864569? O mesmo vale para outras células, os números não parecem somar. Poderia ser usado um quadro de dados subjacente ligeiramente diferente nos exemplos subseqüentes?
slackline
Eu freqüentemente uso .size () com um groupby para ver o número de registros. Existe uma maneira de fazer isso usando o método agg: dict? Entendo que poderia contar um campo específico, mas minha preferência seria que a contagem fosse independente do campo.
Chris Decker
1
@slackline yes. Acabei de testar e funciona bem. Ted deve ter acabado de criar o quadro algumas vezes diferentes e, uma vez que foi criado por meio de geração aleatória de números, os dados df para realmente gerar os dados foram diferentes dos usados ​​nos cálculos
Lucas H #
166

Para a primeira parte, você pode passar um ditado de nomes de colunas para chaves e uma lista de funções para os valores:

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

ATUALIZAÇÃO 1:

Como a função agregada funciona na Série, as referências aos outros nomes de coluna são perdidas. Para contornar isso, você pode fazer referência ao quadro de dados completo e indexá-lo usando os índices de grupo na função lambda.

Aqui está uma solução 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

Aqui, a coluna 'D' resultante é composta pelos valores 'E' somados.

ATUALIZAÇÃO 2:

Aqui está um método que eu acho que fará tudo o que você pedir. Primeiro, crie uma função lambda personalizada. Abaixo, g faz referência ao grupo. Ao agregar, g será uma série. Passando g.indexpara df.ix[]seleciona o grupo atual de df. Em seguida, testo se a coluna C é menor que 0,5. A série booleana retornada é passada para a g[]qual seleciona apenas as linhas que atendem aos critérios.

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
Zelazny7
fonte
Interessante, também posso passar um ditado de {funcname: func}como valores em vez de listas para manter meus nomes personalizados. Mas, em ambos os casos, não posso passar um lambdaque use outras colunas (como lambda x: x['D'][x['C'] < 3].sum()acima: "KeyError: 'D'"). Alguma idéia se isso é possível?
beardc
Eu tenho tentado fazer exatamente isso, e eu recebo o erroKeyError: 'D'
Zelazny7
Legal, eu consegui trabalhar com isso df['A'].ix[g.index][df['C'] < 0].sum(). No entanto, isso está começando a ficar bastante confuso - acho que o loop manual de legibilidade pode ser preferível, além de não ter certeza de que haja uma maneira de atribuir meu nome preferido no aggargumento (em vez de <lambda>). Vou manter a esperança de que alguém pode saber uma maneira mais simples ...
beardc
3
Você pode passar um ditado para o valor da coluna {'D': {'my name':lambda function}}e ele fará da chave do ditado interno o nome da coluna.
usar o seguinte código
1
Acredito que pandas agora suporta múltiplas funções aplicada a uma agrupadas-a trama de dados: pandas.pydata.org/pandas-docs/stable/...
IANS
22

Como alternativa (principalmente estética) à resposta de Ted Petrou, achei que preferia uma listagem um pouco mais compacta. Por favor, não considere aceitá-lo, é apenas um comentário muito mais detalhado sobre a resposta de Ted, além de código / dados. Python / pandas não é o meu primeiro / melhor, mas achei isso para ler bem:

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

Acho mais remanescente de dplyrtubos e data.tablecomandos encadeados. Para não dizer que são melhores, apenas mais familiares para mim. (Eu certamente reconheço o poder e, para muitos, a preferência de usar deffunções mais formalizadas para esses tipos de operações. Esta é apenas uma alternativa, não necessariamente melhor.)


Gerei dados da mesma maneira que Ted, vou adicionar uma semente para reprodutibilidade.

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
r2evans
fonte
2
Eu gosto mais desta resposta. Isto é semelhante ao dplyr tubos em R.
Renhuai
18

Pandas >= 0.25.0, agregações nomeadas

Desde a versão pandas 0.25.0ou superior, estamos nos afastando da agregação e renomeação baseadas em dicionário e indo para agregações nomeadas que aceitam a tuple. Agora podemos agregar + renomear simultaneamente para um nome de coluna mais informativo:

Exemplo :

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

Aplique GroupBy.aggcom agregação nomeada:

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
Erfan
fonte
Eu gosto dessas agregações nomeadas, mas não pude ver como devemos usá-las com várias colunas?
Simon Woodhead
Boa pergunta, não foi possível descobrir isso, duvide que isso seja possível (ainda). Abri um ingresso para isso. Manterá minha pergunta e você será atualizado. Obrigado por apontar @SimonWoodhead
Erfan
4

Novo na versão 0.25.0.

Para oferecer suporte à agregação específica de coluna com controle sobre os nomes das colunas de saída, o pandas aceita a sintaxe especial em GroupBy.agg () , conhecida como "agregação nomeada" , em que

  • As palavras-chave são os nomes das colunas de saída
  • Os valores são tuplas cujo primeiro elemento é a coluna a ser selecionada e o segundo elemento é a agregação a ser aplicada a essa coluna. O Pandas fornece os pandas.NamedAgg nomeadopleu com os campos ['column', 'aggfunc'] para deixar mais claro quais são os argumentos. Como de costume, a agregação pode ser um alias que pode ser chamado ou de sequência.
    In [79]: 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]})
       ....: 

    In [80]: animals
    Out[80]: 
      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

    In [81]: 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),
       ....: )
       ....: 
    Out[81]: 
          min_height  max_height  average_weight
    kind                                        
    cat          9.1         9.5            8.90
    dog          6.0        34.0          102.75

pandas.NamedAgg é apenas um duplo nomeado. Tuplas simples também são permitidas.

    In [82]: animals.groupby("kind").agg(
       ....:     min_height=('height', 'min'),
       ....:     max_height=('height', 'max'),
       ....:     average_weight=('weight', np.mean),
       ....: )
       ....: 
    Out[82]: 
          min_height  max_height  average_weight
    kind                                        
    cat          9.1         9.5            8.90
    dog          6.0        34.0          102.75

Argumentos de palavras-chave adicionais não são passados ​​para as funções de agregação. Somente pares de (coluna, aggfunc) devem ser passados ​​como ** kwargs. Se suas funções de agregação exigirem argumentos adicionais, aplique-as parcialmente com functools.partial ().

A agregação nomeada também é válida para agregações de grupos por séries. Nesse caso, não há seleção de coluna; portanto, os valores são apenas as funções.

    In [84]: animals.groupby("kind").height.agg(
       ....:     min_height='min',
       ....:     max_height='max',
       ....: )
       ....: 
    Out[84]: 
          min_height  max_height
    kind                        
    cat          9.1         9.5
    dog          6.0        34.0
exan
fonte
3

A resposta de Ted é incrível. Acabei usando uma versão menor, caso alguém esteja interessado. Útil quando você procura uma agregação que depende dos valores de várias colunas:

criar um quadro de dados

df=pd.DataFrame({'a': [1,2,3,4,5,6], 'b': [1,1,0,1,1,0], 'c': ['x','x','y','y','z','z']})


   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

agrupando e agregando com apply (usando várias colunas)

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

agrupando e agregando com agregação (usando várias colunas)

Eu gosto dessa abordagem, pois ainda posso usar agregados. Talvez as pessoas me digam por que a aplicação é necessária para obter várias colunas ao fazer agregações em grupos.

Parece óbvio agora, mas desde que você não selecione a coluna de interesse diretamente após o grupo por , terá acesso a todas as colunas do quadro de dados na sua função de agregação.

somente acesso à coluna selecionada

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

acesso a todas as colunas, já que a seleção é, afinal, a mágica

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

ou similar

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

Eu espero que isso ajude.

campo
fonte