Applica vs trasforma su un oggetto gruppo


174

Considera il seguente frame di dati:

     A      B         C         D
0  foo    one  0.162003  0.087469
1  bar    one -1.156319 -1.526272
2  foo    two  0.833892 -1.666304
3  bar  three -2.026673 -0.322057
4  foo    two  0.411452 -0.954371
5  bar    two  0.765878 -0.095968
6  foo    one -0.654890  0.678091
7  foo  three -1.789842 -1.130922

I seguenti comandi funzionano:

> df.groupby('A').apply(lambda x: (x['C'] - x['D']))
> df.groupby('A').apply(lambda x: (x['C'] - x['D']).mean())

ma nessuno dei seguenti lavori:

> df.groupby('A').transform(lambda x: (x['C'] - x['D']))
ValueError: could not broadcast input array from shape (5) into shape (5,3)

> df.groupby('A').transform(lambda x: (x['C'] - x['D']).mean())
 TypeError: cannot concatenate a non-NDFrame object

Perché? L'esempio sulla documentazione sembra suggerire che la chiamata transforma un gruppo consente di eseguire l'elaborazione delle operazioni per riga:

# Note that the following suggests row-wise operation (x.mean is the column mean)
zscore = lambda x: (x - x.mean()) / x.std()
transformed = ts.groupby(key).transform(zscore)

In altre parole, ho pensato che la trasformazione sia essenzialmente un tipo specifico di applicazione (quello che non si aggrega). Dove sbaglio?

Per riferimento, di seguito è riportata la costruzione del frame di dati originale sopra:

df = pd.DataFrame({'A' : ['foo', 'bar', 'foo', 'bar',
                          'foo', 'bar', 'foo', 'foo'],
                   'B' : ['one', 'one', 'two', 'three',
                         'two', 'two', 'one', 'three'],
                   'C' : randn(8), 'D' : randn(8)})

1
La funzione passata a transformdeve restituire un numero, una riga o la stessa forma dell'argomento. se è un numero, il numero verrà impostato su tutti gli elementi del gruppo, se è una riga, verrà trasmesso a tutte le righe del gruppo. Nel tuo codice, la funzione lambda restituisce una colonna che non può essere trasmessa al gruppo.
HYRY,

1
Grazie @HYRY, ma sono confuso. Se guardi l'esempio nella documentazione che ho copiato sopra (cioè con zscore), transformriceve una funzione lambda che presuppone che ciascuno di essi xsia un elemento all'interno di groupe restituisca anche un valore per elemento nel gruppo. Cosa mi sto perdendo?
Amelio Vazquez-Reina,

Per coloro che cercano una soluzione estremamente dettagliata, vedere questa qui sotto .
Ted Petrou,

@TedPetrou: il tl; dr di questo è: 1) applypassa nell'intero df, ma transformpassa ciascuna colonna singolarmente come una serie. 2) applypuò restituire qualsiasi output di forma (scalare / Serie / DataFrame / array / elenco ...), mentre transformdeve restituire una sequenza (Serie 1D / array / elenco) della stessa lunghezza del gruppo. Ecco perché apply()non è necessario il PO transform(). Questa è una buona domanda poiché il documento non ha spiegato chiaramente entrambe le differenze. (simile alla distinzione tra apply/map/applymap, o altre cose ...)
smci

Risposte:


146

Due differenze principali tra applyetransform

Esistono due principali differenze tra i metodi transforme applygroupby.

  • Ingresso:
    • applypassa implicitamente tutte le colonne per ciascun gruppo come DataFrame alla funzione personalizzata.
    • while transformpassa ciascuna colonna per ciascun gruppo singolarmente come una serie alla funzione personalizzata.
  • Produzione:
    • La funzione personalizzata passata a applypuò restituire uno scalare, oppure un Series o DataFrame (o un array numpy o un elenco pari) .
    • La funzione personalizzata passata transformdeve restituire una sequenza (una serie, una matrice o un elenco monodimensionali) della stessa lunghezza del gruppo .

Quindi, transformfunziona solo su una serie alla volta e applyfunziona sull'intero DataFrame contemporaneamente.

Ispezione della funzione personalizzata

Può aiutare parecchio a controllare l'input della tua funzione personalizzata passato a applyo transform.

Esempi

Creiamo alcuni dati di esempio e ispezioniamo i gruppi in modo da poter vedere di cosa sto parlando:

import pandas as pd
import numpy as np
df = pd.DataFrame({'State':['Texas', 'Texas', 'Florida', 'Florida'], 
                   'a':[4,5,1,3], 'b':[6,10,3,11]})

     State  a   b
0    Texas  4   6
1    Texas  5  10
2  Florida  1   3
3  Florida  3  11

Creiamo una semplice funzione personalizzata che stampa il tipo di oggetto passato in modo implicito e quindi ha generato un errore in modo da poter interrompere l'esecuzione.

def inspect(x):
    print(type(x))
    raise

Ora passiamo questa funzione sia al groupby applyche ai transformmetodi per vedere quale oggetto gli viene passato:

df.groupby('State').apply(inspect)

<class 'pandas.core.frame.DataFrame'>
<class 'pandas.core.frame.DataFrame'>
RuntimeError

Come puoi vedere, un DataFrame viene passato nella inspectfunzione. Potresti chiederti perché il tipo, DataFrame, è stato stampato due volte. Panda esegue il primo gruppo due volte. Lo fa per determinare se esiste un modo rapido per completare il calcolo o meno. Questo è un dettaglio minore di cui non dovresti preoccuparti.

Ora facciamo la stessa cosa con transform

df.groupby('State').transform(inspect)
<class 'pandas.core.series.Series'>
<class 'pandas.core.series.Series'>
RuntimeError

È passata una serie - un oggetto Pandas completamente diverso.

Quindi, transformè consentito lavorare solo con una singola serie alla volta. È non impossibile di agire su due colonne contemporaneamente. Quindi, se proviamo e sottraiamo colonna adal ball'interno della nostra funzione personalizzata otterremmo un errore con transform. Vedi sotto:

def subtract_two(x):
    return x['a'] - x['b']

df.groupby('State').transform(subtract_two)
KeyError: ('a', 'occurred at index a')

Otteniamo un KeyError mentre i panda stanno tentando di trovare l'indice della serie ache non esiste. È possibile completare questa operazione applyin quanto ha l'intero DataFrame:

df.groupby('State').apply(subtract_two)

State     
Florida  2   -2
         3   -8
Texas    0   -2
         1   -5
dtype: int64

L'output è una serie e un po 'confuso poiché l'indice originale viene mantenuto, ma abbiamo accesso a tutte le colonne.


Visualizzazione dell'oggetto Panda passato

Può aiutare ancora di più a visualizzare l'intero oggetto Panda all'interno della funzione personalizzata, in modo da poter vedere esattamente con cosa stai operando. Puoi usare le printdichiarazioni di Mi piace usare la displayfunzione dal IPython.displaymodulo in modo che i DataFrame vengano emessi piacevolmente in HTML in un taccuino jupyter:

from IPython.display import display
def subtract_two(x):
    display(x)
    return x['a'] - x['b']

Immagine dello schermo: inserisci qui la descrizione dell'immagine


La trasformazione deve restituire una sequenza monodimensionale delle stesse dimensioni del gruppo

L'altra differenza è che transformdeve restituire una sequenza monodimensionale delle stesse dimensioni del gruppo. In questo caso particolare, ogni gruppo ha due righe, quindi transformdeve restituire una sequenza di due righe. In caso contrario, viene generato un errore:

def return_three(x):
    return np.array([1, 2, 3])

df.groupby('State').transform(return_three)
ValueError: transform must return a scalar value for each group

Il messaggio di errore non è realmente descrittivo del problema. È necessario restituire una sequenza della stessa lunghezza del gruppo. Quindi, una funzione come questa avrebbe funzionato:

def rand_group_len(x):
    return np.random.rand(len(x))

df.groupby('State').transform(rand_group_len)

          a         b
0  0.962070  0.151440
1  0.440956  0.782176
2  0.642218  0.483257
3  0.056047  0.238208

Restituire un singolo oggetto scalare funziona anche per transform

Se si restituisce solo un singolo scalare dalla funzione personalizzata, transformverrà utilizzato per ciascuna delle righe nel gruppo:

def group_sum(x):
    return x.sum()

df.groupby('State').transform(group_sum)

   a   b
0  9  16
1  9  16
2  4  14
3  4  14

3
npnon è definito. Presumo che i principianti apprezzerebbero se includessi import numpy as npnella tua risposta.
Riassunto il

187

Mentre mi sentivo allo stesso modo confuso con l' .transformoperazione vs. .applyho trovato alcune risposte che chiarivano la questione. Questa risposta, ad esempio, è stata molto utile.

Finora il mio takeout è che .transformfunzionerà (o gestirà) con Series(colonne) separatamente l'uno dall'altro . Ciò significa che nelle ultime due chiamate:

df.groupby('A').transform(lambda x: (x['C'] - x['D']))
df.groupby('A').transform(lambda x: (x['C'] - x['D']).mean())

Hai chiesto .transformdi prendere valori da due colonne e "esso" in realtà non "vede" entrambi allo stesso tempo (per così dire). transformesaminerà le colonne dei frame di dati una alla volta e tornerà indietro di una serie (o gruppo di serie) "fatta" di scalari che sono ripetuti len(input_column)tempi.

Quindi questo scalare, che dovrebbe essere usato .transformper fare il, Seriesè il risultato di alcune funzioni di riduzione applicate su un input Series(e solo su UNA serie / colonna alla volta).

Considera questo esempio (sul tuo frame di dati):

zscore = lambda x: (x - x.mean()) / x.std() # Note that it does not reference anything outside of 'x' and for transform 'x' is one column.
df.groupby('A').transform(zscore)

produrrà:

       C      D
0  0.989  0.128
1 -0.478  0.489
2  0.889 -0.589
3 -0.671 -1.150
4  0.034 -0.285
5  1.149  0.662
6 -1.404 -0.907
7 -0.509  1.653

Che è esattamente lo stesso che se lo utilizzassi solo su una colonna alla volta:

df.groupby('A')['C'].transform(zscore)

cedendo:

0    0.989
1   -0.478
2    0.889
3   -0.671
4    0.034
5    1.149
6   -1.404
7   -0.509

Nota che .applynell'ultimo esempio ( df.groupby('A')['C'].apply(zscore)) funzionerebbe esattamente allo stesso modo, ma fallirebbe se provassi a usarlo su un frame di dati:

df.groupby('A').apply(zscore)

dà errore:

ValueError: operands could not be broadcast together with shapes (6,) (2,)

Quindi, dove altro è .transformutile? Il caso più semplice sta tentando di riassegnare i risultati della funzione di riduzione al frame di dati originale.

df['sum_C'] = df.groupby('A')['C'].transform(sum)
df.sort('A') # to clearly see the scalar ('sum') applies to the whole column of the group

cedendo:

     A      B      C      D  sum_C
1  bar    one  1.998  0.593  3.973
3  bar  three  1.287 -0.639  3.973
5  bar    two  0.687 -1.027  3.973
4  foo    two  0.205  1.274  4.373
2  foo    two  0.128  0.924  4.373
6  foo    one  2.113 -0.516  4.373
7  foo  three  0.657 -1.179  4.373
0  foo    one  1.270  0.201  4.373

Provando lo stesso con la .applydarebbe NaNsin sum_C. Perché .applyrestituirebbe un ridotto Series, che non sa come ritrasmettere:

df.groupby('A')['C'].apply(sum)

dando:

A
bar    3.973
foo    4.373

Ci sono anche casi in cui .transformviene utilizzato per filtrare i dati:

df[df.groupby(['B'])['D'].transform(sum) < -1]

     A      B      C      D
3  bar  three  1.287 -0.639
7  foo  three  0.657 -1.179

Spero che questo aggiunga un po 'più di chiarezza.


4
OH MIO DIO. La differenza è così sottile.
Dawei,

3
.transform()potrebbe essere utilizzato anche per riempire i valori mancanti. Soprattutto se si desidera trasmettere la media del gruppo o la statistica del gruppo ai NaNvalori in quel gruppo. Sfortunatamente, anche la documentazione di Panda non è stata utile per me.
cyber-matematica,

Penso che nell'ultimo caso .groupby().filter()faccia la stessa cosa. Grazie per la tua spiegazione .apply()e .transform()mi rende molto confuso.
Jiaxiang,

questo spiega perché df.groupby().transform()non può funzionare per un sottogruppo df, ricevo sempre l'errore ValueError: transform must return a scalar value for each groupperché transformvede le colonne una alla volta
jerrytim

Mi è piaciuto molto l'ultimo esempio .transform utilizzato per filtrare i dati. super gentile!
Rishi jain,

13

Userò uno snippet molto semplice per illustrare la differenza:

test = pd.DataFrame({'id':[1,2,3,1,2,3,1,2,3], 'price':[1,2,3,2,3,1,3,1,2]})
grouping = test.groupby('id')['price']

DataFrame è simile al seguente:

    id  price   
0   1   1   
1   2   2   
2   3   3   
3   1   2   
4   2   3   
5   3   1   
6   1   3   
7   2   1   
8   3   2   

In questa tabella sono presenti 3 ID cliente, ogni cliente ha effettuato tre transazioni e ha pagato 1,2,3 dollari ogni volta.

Ora, voglio trovare il pagamento minimo effettuato da ciascun cliente. Esistono due modi per farlo:

  1. Utilizzando apply:

    grouping.min ()

Il ritorno è simile al seguente:

id
1    1
2    1
3    1
Name: price, dtype: int64

pandas.core.series.Series # return type
Int64Index([1, 2, 3], dtype='int64', name='id') #The returned Series' index
# lenght is 3
  1. Utilizzando transform:

    grouping.transform (min)

Il ritorno è simile al seguente:

0    1
1    1
2    1
3    1
4    1
5    1
6    1
7    1
8    1
Name: price, dtype: int64

pandas.core.series.Series # return type
RangeIndex(start=0, stop=9, step=1) # The returned Series' index
# length is 9    

Entrambi i metodi restituiscono un Seriesoggetto, ma il lengthprimo è 3 e lengthil secondo è 9.

Se vuoi rispondere What is the minimum price paid by each customer, allora il applymetodo è quello più adatto da scegliere.

Se vuoi rispondere What is the difference between the amount paid for each transaction vs the minimum payment, allora vuoi usare transform, perché:

test['minimum'] = grouping.transform(min) # ceates an extra column filled with minimum payment
test.price - test.minimum # returns the difference for each row

Apply non funziona qui semplicemente perché restituisce una serie di dimensioni 3, ma la lunghezza del df originale è 9. Non è possibile integrarla facilmente nel df originale.


3
Penso che questa sia un'ottima risposta! Grazie per il tempo dedicato a dare una risposta più di quattro anni dopo la domanda!
Benjamin Dubreu,

4
tmp = df.groupby(['A'])['c'].transform('mean')

è come

tmp1 = df.groupby(['A']).agg({'c':'mean'})
tmp = df['A'].map(tmp1['c'])

o

tmp1 = df.groupby(['A'])['c'].mean()
tmp = df['A'].map(tmp1)
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.