Applica più funzioni a più colonne di gruppo


221

I documenti mostrano come applicare più funzioni su un oggetto groupby alla volta usando un dict con i nomi delle colonne di output come chiavi:

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

Tuttavia, questo funziona solo su un oggetto groupby Series. E quando un dict viene passato in modo simile a un gruppo da DataFrame, si aspetta che le chiavi siano i nomi delle colonne a cui verrà applicata la funzione.

Quello che voglio fare è applicare più funzioni a più colonne (ma alcune colonne verranno utilizzate più volte). Inoltre, alcune funzioni dipenderanno da altre colonne nell'oggetto groupby (come le funzioni sumif). La mia soluzione attuale è quella di andare colonna per colonna e fare qualcosa come il codice sopra, usando lambdas per funzioni che dipendono da altre righe. Ma ci vuole molto tempo (penso che ci voglia molto tempo per scorrere un oggetto groupby). Dovrò cambiarlo in modo da iterare l'intero oggetto groupby in una sola corsa, ma mi chiedo se nei panda ci sia un modo integrato per farlo in modo abbastanza pulito.

Ad esempio, ho provato qualcosa di simile

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

ma come previsto ottengo un KeyError (poiché le chiavi devono essere una colonna se aggviene chiamato da un DataFrame).

Esiste un modo integrato per fare ciò che mi piacerebbe fare, o la possibilità che questa funzionalità possa essere aggiunta o avrò solo bisogno di scorrere manualmente il groupby?

Grazie


2
Se arrivate a questa domanda nel 2017+, vedere la risposta di seguito per vedere il modo idiomatico di aggregare più colonne insieme. La risposta attualmente selezionata contiene più deprecazioni, vale a dire che non è più possibile utilizzare un dizionario di dizionari per rinominare le colonne nel risultato di un groupby.
Ted Petrou,

Risposte:


283

La seconda metà della risposta attualmente accettata è obsoleta e presenta due ammortamenti. Primo e, soprattutto, non è più possibile passare un dizionario di dizionari al aggmetodo groupby. Secondo, non usare mai .ix.

Se si desidera lavorare contemporaneamente con due colonne separate, suggerirei di utilizzare il applymetodo che passa implicitamente un DataFrame alla funzione applicata. Usiamo un frame di dati simile a quello dall'alto

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

Un dizionario mappato dai nomi delle colonne alle funzioni di aggregazione è ancora un modo perfettamente valido per eseguire un'aggregazione.

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 non ti piace quel brutto nome della colonna lambda, puoi usare una normale funzione e fornire un nome personalizzato __name__all'attributo speciale come questo:

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

Utilizzo applye restituzione di una serie

Ora, se avevi più colonne che dovevano interagire insieme, non puoi usarle agg, il che passa implicitamente una serie alla funzione di aggregazione. Quando si utilizza applyl'intero gruppo come DataFrame viene passato alla funzione.

Consiglio di creare un'unica funzione personalizzata che restituisca una serie di tutte le aggregazioni. Utilizzare l'indice serie come etichette per le nuove colonne:

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 sei innamorato di MultiIndexes, puoi comunque restituire una serie con una come questa:

    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

3
Adoro lo schema di utilizzo di una funzione che restituisce una serie. Molto pulito.
Stephen McAteer,

2
questo è l'unico modo in cui ho trovato di aggregare un frame di dati tramite più input di colonna in modo simultaneo (l'esempio c_d sopra)
Blake

2
Sono confuso dai risultati, prendendo la somma aall'interno del gruppo non 0dovrebbe essere 0.418500 + 0.446069 = 0.864569? Lo stesso vale per altre celle, i numeri non sembrano sommarsi. Potrebbe essere un frame di dati sottostante leggermente diverso utilizzato negli esempi successivi?
slackline,

Uso spesso .size () con un groupby per vedere il numero di record. C'è un modo per farlo usando il metodo agg: dict? Capisco di poter contare un determinato campo, ma la mia preferenza sarebbe che il conteggio fosse indipendente dal campo.
Chris Decker,

1
@slackline sì. l'ho appena testato e funziona benissimo. Ted deve aver appena creato il frame alcune volte diverse e poiché è stato creato tramite la generazione di numeri casuali, i dati df per generare effettivamente i dati erano diversi da quello utilizzato alla fine nei calcoli
Lucas H

166

Per la prima parte è possibile passare un dettato di nomi di colonna per le chiavi e un elenco di funzioni per i valori:

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

AGGIORNAMENTO 1:

Poiché la funzione di aggregazione funziona su Serie, i riferimenti agli altri nomi di colonne vengono persi. Per ovviare a questo, è possibile fare riferimento al frame di dati completo e indicizzarlo utilizzando gli indici di gruppo all'interno della funzione lambda.

Ecco una soluzione caotica:

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

Qui, la colonna 'D' risultante è composta dai valori 'E' sommati.

AGGIORNAMENTO 2:

Ecco un metodo che penso farà tutto ciò che chiedi. Innanzitutto crea una funzione lambda personalizzata. Di seguito, g fa riferimento al gruppo. Quando si aggregano, g sarà una serie. Passando g.indexa df.ix[]seleziona il gruppo corrente da df. Quindi testerò se la colonna C è inferiore a 0,5. La serie booleana restituita viene passata alla g[]quale seleziona solo quelle righe che soddisfano i criteri.

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

Interessante, posso anche passare un dettato {funcname: func}come valori anziché come elenchi per mantenere i miei nomi personalizzati. Ma in entrambi i casi non riesco a passare un oggetto lambdache utilizza altre colonne (come lambda x: x['D'][x['C'] < 3].sum()sopra: "KeyError: 'D'"). Qualche idea se è possibile?
Barba

Ho cercato di fare esattamente questo, e ho ricevuto l'erroreKeyError: 'D'
Zelazny7,

Bene, ho capito che funziona df['A'].ix[g.index][df['C'] < 0].sum(). Questo sta iniziando a diventare piuttosto confuso, penso che per la leggibilità il ciclo manuale possa essere preferibile, inoltre non sono sicuro che ci sia un modo per dargli il mio nome preferito aggnell'argomento (invece di <lambda>). Spero che qualcuno possa conoscere un modo più semplice ...
Barba

3
È possibile passare un dict per il valore della colonna {'D': {'my name':lambda function}}e questo renderà la chiave dict interna il nome della colonna.
Zelazny7,

1
Credo che i panda ora supportino più funzioni applicate a un dataframe raggruppato: pandas.pydata.org/pandas-docs/stable/…
IanS

22

In alternativa (soprattutto sull'estetica) alla risposta di Ted Petrou, ho scoperto di preferire un elenco leggermente più compatto. Per favore, non considerare di accettarlo, è solo un commento molto più dettagliato sulla risposta di Ted, oltre a codice / dati. Python / panda non è il mio primo / migliore, ma ho trovato questo per leggere bene:

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

Lo trovo più simile a dplyrpipe e data.tablecomandi concatenati. Per non dire che sono migliori, solo più familiari per me. (Riconosco certamente il potere e, per molti, la preferenza di utilizzare deffunzioni più formalizzate per questo tipo di operazioni. Questa è solo un'alternativa, non necessariamente migliore.)


Ho generato i dati allo stesso modo di Ted, aggiungerò un seme per la riproducibilità.

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

2
Mi piace di più questa risposta. Questo è simile alle pipe dplyr in R.
Renhuai il

18

Pandas >= 0.25.0, denominate aggregazioni

Dalla versione panda 0.25.0o successiva, ci stiamo allontanando dall'aggregazione e dalla ridenominazione basate sul dizionario e ci stiamo spostando verso aggregazioni denominate che accettano untuple . Ora possiamo aggregare + rinominare contemporaneamente un nome di colonna più informativo:

Esempio :

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

Applica GroupBy.aggcon aggregazione denominata:

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

Mi piacciono queste aggregazioni denominate ma non riesco a vedere come dovremmo usarle con più colonne?
Simon Woodhead,

Bella domanda, non riuscivo a capirlo, dubito che sia possibile (ancora). Ho aperto un biglietto per questo. Manterrò la mia domanda e tu sarai aggiornato. Grazie per aver sottolineato @SimonWoodhead
Erfan,

4

Novità nella versione 0.25.0.

Per supportare l'aggregazione specifica della colonna con il controllo sui nomi delle colonne di output, Panda accetta la sintassi speciale in GroupBy.agg () , nota come "aggregazione denominata" , dove

  • Le parole chiave sono i nomi delle colonne di output
  • I valori sono tuple il cui primo elemento è la colonna da selezionare e il secondo elemento è l'aggregazione da applicare a quella colonna. Pandas fornisce a panda.NamedAgg namedtuple con i campi ['colonna', 'aggfunc'] per rendere più chiari gli argomenti. Come al solito, l'aggregazione può essere un alias callable o stringa.
    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 è solo una coppia di nomi. Sono ammesse anche tuple semplici.

    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

Argomenti di parole chiave aggiuntivi non vengono passati alle funzioni di aggregazione. Solo le coppie di (colonna, aggfunc) devono essere passate come ** kwargs. Se le funzioni di aggregazione richiedono argomenti aggiuntivi, applicarli parzialmente con functools.partial ().

L'aggregazione denominata è valida anche per le aggregazioni di gruppo di serie. In questo caso non esiste una selezione di colonne, quindi i valori sono solo le funzioni.

    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

3

La risposta di Ted è sorprendente. Ho finito per usare una versione più piccola di quella nel caso qualcuno fosse interessato. Utile quando cerchi un'aggregazione che dipende dai valori di più colonne:

creare un frame di dati

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

raggruppamento e aggregazione con applicare (utilizzando più colonne)

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

raggruppamento e aggregazione con aggregato (utilizzando più colonne)

Mi piace questo approccio poiché posso ancora usare l'aggregato. Forse le persone mi faranno sapere perché è necessario applicare per accedere a più colonne quando si fanno aggregazioni su gruppi.

Sembra ovvio ora, ma finché non selezioni la colonna di interesse direttamente dopo il groupby , avrai accesso a tutte le colonne del frame di dati all'interno della tua funzione di aggregazione.

accesso solo alla colonna selezionata

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

l'accesso a tutte le colonne poiché la selezione è dopo tutto la magia

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

o similmente

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

Spero che aiuti.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.