Auf ein Gruppenobjekt anwenden oder transformieren

173

Betrachten Sie den folgenden Datenrahmen:

     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

Die folgenden Befehle funktionieren:

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

aber keine der folgenden Arbeiten:

> 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

Warum? Das Beispiel in der Dokumentation scheint darauf hinzudeuten, dass das Aufrufen transformeiner Gruppe die zeilenweise Operationsverarbeitung ermöglicht:

# 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)

Mit anderen Worten, ich dachte, dass die Transformation im Wesentlichen eine bestimmte Art der Anwendung ist (die nicht aggregiert). Wo irre ich mich

Als Referenz ist unten die Konstruktion des ursprünglichen Datenrahmens oben aufgeführt:

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)})
Amelio Vazquez-Reina
quelle
1
Die an übergebene Funktion transformmuss eine Zahl, eine Zeile oder dieselbe Form wie das Argument zurückgeben. Wenn es sich um eine Zahl handelt, wird die Zahl auf alle Elemente in der Gruppe festgelegt. Wenn es sich um eine Zeile handelt, wird sie an alle Zeilen in der Gruppe gesendet. In Ihrem Code gibt die Lambda-Funktion eine Spalte zurück, die nicht an die Gruppe gesendet werden kann.
HYRY
1
Danke @HYRY, aber ich bin verwirrt. Wenn Sie sich das Beispiel in der Dokumentation ansehen, die ich oben kopiert habe (dh mit zscore), transformerhalten Sie eine Lambda-Funktion, die davon ausgeht, dass es sich bei jedem xElement um ein Element innerhalb der handelt group, und geben außerdem einen Wert pro Element in der Gruppe zurück. Was vermisse ich?
Amelio Vazquez-Reina
Für diejenigen, die nach einer äußerst detaillierten Lösung suchen, siehe diese unten .
Ted Petrou
@TedPetrou: Das tl; dr davon ist: 1) applyübergibt den gesamten df, transformübergibt aber jede Spalte einzeln als Serie. 2) applykann jede Formausgabe zurückgeben (Skalar / Serie / Datenrahmen / Array / Liste ...), während transformeine Sequenz (1D-Serie / Array / Liste) mit derselben Länge wie die Gruppe zurückgegeben werden muss. Deshalb braucht das OP apply()nicht transform(). Dies ist eine gute Frage, da der Arzt beide Unterschiede nicht klar erklärt hat. (ähnlich der Unterscheidung zwischen apply/map/applymapoder anderen Dingen ...)
smci

Antworten:

144

Zwei Hauptunterschiede zwischen applyundtransform

Es gibt zwei Hauptunterschiede zwischen der transformund der applyGroupby-Methode.

  • Eingang:
    • applyÜbergibt implizit alle Spalten für jede Gruppe als DataFrame an die benutzerdefinierte Funktion.
    • Dabei wird transformjede Spalte für jede Gruppe einzeln als Serie an die benutzerdefinierte Funktion übergeben.
  • Ausgabe:
    • Die an übergebene benutzerdefinierte Funktion applykann einen Skalar oder eine Serie oder einen Datenrahmen (oder ein Numpy-Array oder sogar eine Liste) zurückgeben .
    • Die an übergebene benutzerdefinierte Funktion transformmuss eine Sequenz (eine eindimensionale Serie, ein Array oder eine Liste) mit derselben Länge wie die Gruppe zurückgeben .

Funktioniert also jeweils transformnur für eine Serie und gleichzeitig applyfür den gesamten DataFrame.

Überprüfen der benutzerdefinierten Funktion

Es kann sehr hilfreich sein, die Eingabe in Ihre benutzerdefinierte Funktion zu überprüfen, die an applyoder übergeben wurde transform.

Beispiele

Lassen Sie uns einige Beispieldaten erstellen und die Gruppen untersuchen, damit Sie sehen können, wovon ich spreche:

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

Erstellen wir eine einfache benutzerdefinierte Funktion, die den Typ des implizit übergebenen Objekts ausgibt und dann einen Fehler auslöst, damit die Ausführung gestoppt werden kann.

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

Übergeben wir diese Funktion nun sowohl an groupby applyals auch an die transformMethoden, um zu sehen, welches Objekt an sie übergeben wird:

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

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

Wie Sie sehen können, wird ein DataFrame an die inspectFunktion übergeben. Sie fragen sich vielleicht, warum der Typ DataFrame zweimal ausgedruckt wurde. Pandas führt die erste Gruppe zweimal aus. Auf diese Weise wird festgestellt, ob es einen schnellen Weg gibt, die Berechnung abzuschließen, oder nicht. Dies ist ein kleines Detail, über das Sie sich keine Sorgen machen sollten.

Jetzt machen wir dasselbe mit transform

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

Es wird eine Serie übergeben - ein völlig anderes Pandas-Objekt.

Es transformdarf also immer nur mit einer Serie gleichzeitig gearbeitet werden. Es ist nicht unmöglich, auf zwei Spalten gleichzeitig zu wirken. Also, wenn wir versuchen , Spalte und subtrahieren avon binnen unserer benutzerdefinierten Funktion würden wir einen Fehler bekommen transform. Siehe unten:

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

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

Wir erhalten einen KeyError, da Pandas versucht, den anicht vorhandenen Serienindex zu finden . Sie können diesen Vorgang mit ausführen, applyda der gesamte DataFrame vorhanden ist:

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

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

Die Ausgabe ist eine Serie und etwas verwirrend, da der ursprüngliche Index beibehalten wird, aber wir haben Zugriff auf alle Spalten.


Anzeige des übergebenen Pandas-Objekts

Es kann noch hilfreicher sein, das gesamte Pandas-Objekt in der benutzerdefinierten Funktion anzuzeigen, sodass Sie genau sehen können, mit was Sie arbeiten. Sie können printAnweisungen verwenden, indem ich die displayFunktion aus dem IPython.displayModul verwenden möchte, damit die DataFrames in einem Jupyter-Notizbuch gut in HTML ausgegeben werden:

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

Bildschirmfoto: Geben Sie hier die Bildbeschreibung ein


Die Transformation muss eine eindimensionale Sequenz mit der gleichen Größe wie die Gruppe zurückgeben

Der andere Unterschied besteht darin, dass transformeine eindimensionale Sequenz mit der gleichen Größe wie die Gruppe zurückgegeben werden muss. In diesem speziellen Fall hat jede Gruppe zwei Zeilen und transformmuss daher eine Folge von zwei Zeilen zurückgeben. Ist dies nicht der Fall, wird ein Fehler ausgelöst:

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

Die Fehlermeldung beschreibt das Problem nicht wirklich. Sie müssen eine Sequenz mit der gleichen Länge wie die Gruppe zurückgeben. Eine solche Funktion würde also funktionieren:

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

Das Zurückgeben eines einzelnen skalaren Objekts funktioniert auch für transform

Wenn Sie nur einen einzelnen Skalar von Ihrer benutzerdefinierten Funktion zurückgeben, transformwird er für jede der Zeilen in der Gruppe verwendet:

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
Ted Petrou
quelle
3
npist nicht definiert. Ich gehe davon aus, dass Anfänger es begrüßen würden, wenn Sie import numpy as npin Ihre Antwort aufnehmen.
Qaswed
187

Da ich mich mit .transformOperation im Vergleich zu Operation ähnlich verwirrt fühlte, .applyfand ich einige Antworten, die etwas Licht in das Problem brachten. Diese Antwort war zum Beispiel sehr hilfreich.

Mein Takeout ist so weit , dass .transformWille Arbeit (oder viel) mit Series(Spalten) isoliert voneinander . Dies bedeutet, dass in Ihren letzten beiden Anrufen:

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

Sie haben darum gebeten .transform, Werte aus zwei Spalten zu übernehmen, und "es" sieht tatsächlich nicht beide gleichzeitig (sozusagen). transformSchauen Sie sich die Datenrahmenspalten nacheinander an und geben Sie eine Reihe (oder Gruppe von Reihen) zurück, die aus Skalaren besteht, die sich mehrmals wiederholen len(input_column).

Dieser Skalar, der verwendet werden sollte .transform, um das zu erstellen , Seriesist das Ergebnis einer Reduktionsfunktion, die auf eine Eingabe angewendet wird Series(und jeweils nur auf EINE Reihe / Spalte).

Betrachten Sie dieses Beispiel (in Ihrem Datenrahmen):

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)

wird ergeben:

       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

Das ist genau das Gleiche, als würden Sie es jeweils nur für eine Spalte verwenden:

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

Nachgeben:

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

Beachten Sie, dass .applyim letzten Beispiel ( df.groupby('A')['C'].apply(zscore)) genau so funktioniert, es jedoch fehlschlägt, wenn Sie versuchen, es in einem Datenrahmen zu verwenden:

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

gibt Fehler:

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

Wo sonst ist es .transformnützlich? Im einfachsten Fall wird versucht, die Ergebnisse der Reduktionsfunktion wieder dem ursprünglichen Datenrahmen zuzuweisen.

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

Nachgeben:

     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

Die gleichen Versuch , mit .applywürde NaNsin sum_C. Denn .applywürde ein reduziertes zurückgeben Series, das es nicht zurücksenden kann:

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

Geben:

A
bar    3.973
foo    4.373

Es gibt auch Fälle, in denen .transformdie Daten gefiltert werden:

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

Ich hoffe, das bringt etwas mehr Klarheit.

Grundierung
quelle
4
OH MEIN GOTT. Der Unterschied ist so subtil.
Dawei
3
.transform()könnte auch zum Füllen fehlender Werte verwendet werden. Insbesondere, wenn Sie den Gruppenmittelwert oder die Gruppenstatistik an NaNWerte in dieser Gruppe senden möchten . Leider war die Pandas-Dokumentation auch für mich nicht hilfreich.
Cyber-Mathe
Ich denke im letzten Fall .groupby().filter()macht das gleiche. Vielen Dank für Ihre Erklärung .apply()und .transform()macht mich auch sehr verwirrt.
Jiaxiang
das erklärt, warum df.groupby().transform()für eine Untergruppe df nicht funktionieren kann, ich erhalte immer den Fehler, ValueError: transform must return a scalar value for each groupweil transformSpalten
einzeln angezeigt werden
Das letzte Beispiel .transform, mit dem die Daten gefiltert wurden, hat mir sehr gut gefallen. Super nett!
Rishi Jain
13

Ich werde ein sehr einfaches Snippet verwenden, um den Unterschied zu veranschaulichen:

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']

Der DataFrame sieht folgendermaßen aus:

    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   

Diese Tabelle enthält 3 Kunden-IDs. Jeder Kunde hat drei Transaktionen durchgeführt und jedes Mal 1,2,3 Dollar bezahlt.

Jetzt möchte ich die Mindestzahlung finden, die jeder Kunde geleistet hat. Es gibt zwei Möglichkeiten:

  1. Verwenden von apply:

    grouping.min ()

Die Rückkehr sieht so aus:

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. Verwenden von transform:

    grouping.transform (min)

Die Rückkehr sieht so aus:

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    

Beide Methoden geben ein SeriesObjekt zurück, aber das lengthdes ersten ist 3 und das lengthdes zweiten ist 9.

Wenn Sie antworten möchten What is the minimum price paid by each customer, ist die applyMethode die geeignetere.

Wenn Sie antworten What is the difference between the amount paid for each transaction vs the minimum paymentmöchten, möchten Sie Folgendes verwenden transform:

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

Apply funktioniert hier nicht einfach, weil es eine Serie der Größe 3 zurückgibt, aber die Länge des ursprünglichen df beträgt 9. Sie können es nicht einfach wieder in den ursprünglichen df integrieren.

Cheng
quelle
3
Ich denke, das ist eine großartige Antwort! Vielen Dank, dass Sie sich mehr als vier Jahre nach der Beantwortung der Frage die Zeit genommen haben, eine Antwort zu geben!
Benjamin Dubreu
4
tmp = df.groupby(['A'])['c'].transform('mean')

ist wie

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

oder

tmp1 = df.groupby(['A'])['c'].mean()
tmp = df['A'].map(tmp1)
Shui
quelle