Zastosuj vs przekształć w obiekcie grupy

174

Rozważmy następującą ramkę danych:

     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

Działają następujące polecenia:

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

ale żadna z następujących prac:

> 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

Czemu? Przykład w dokumentacji wydaje się sugerować, że wywołanie transformgrupy pozwala na wykonanie operacji wierszowej:

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

Innymi słowy, myślałem, że transformacja jest w istocie specyficznym typem zastosowania (takim, które nie agreguje). Gdzie się mylę?

Dla porównania poniżej przedstawiono konstrukcję oryginalnej ramki danych powyżej:

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
źródło
1
Funkcja przekazana do transformmusi zwracać liczbę, wiersz lub ten sam kształt co argument. jeśli jest to liczba, liczba zostanie ustawiona na wszystkie elementy w grupie, jeśli jest to wiersz, zostanie nadana do wszystkich wierszy w grupie. W Twoim kodzie funkcja lambda zwraca kolumnę, której nie można rozgłaszać do grupy.
HYRY
1
Dzięki @HYRY, ale jestem zdezorientowany. Jeśli spojrzysz na przykład w dokumentacji, którą skopiowałem powyżej (tj. Z zscore), transformotrzymujesz funkcję lambda, która zakłada, że ​​każdy xjest elementem w group, a także zwraca wartość na element w grupie. czego mi brakuje?
Amelio Vazquez-Reina
Dla tych, którzy szukają niezwykle szczegółowego rozwiązania, zobacz to poniżej .
Ted Petrou,
@TedPetrou: tl; dr, czyli: 1) applyprzechodzi w całym df, ale transformprzekazuje każdą kolumnę indywidualnie jako serię. 2) applymoże zwracać dowolne dane wyjściowe kształtu (skalarne / serie / DataFrame / tablica / lista ...), podczas gdy transformmusi zwracać sekwencję (seria 1D / tablica / lista) o tej samej długości co grupa. Dlatego OP apply()nie potrzebuje transform(). To dobre pytanie, ponieważ lekarz nie wyjaśnił jasno obu różnic. (podobne do rozróżnienia między apply/map/applymaplub innymi rzeczami ...)
smci

Odpowiedzi:

146

Dwie główne różnice między applyitransform

Istnieją dwie główne różnice między metodami transformi applygrupowaniem.

  • Wejście:
    • applyniejawnie przekazuje wszystkie kolumny dla każdej grupy jako DataFrame do funkcji niestandardowej.
    • podczas gdy transformprzekazuje każdą kolumnę dla każdej grupy indywidualnie jako serię do funkcji niestandardowej.
  • Wynik:
    • Funkcja niestandardowa przekazana do applymoże zwrócić wartość skalarną, Series lub DataFrame (lub tablicę numpy lub nawet listę) .
    • Funkcja niestandardowa przekazana do transformmusi zwracać sekwencję (jednowymiarową serię, tablicę lub listę) o tej samej długości co grupa .

Tak więc transformdziała tylko na jednej serii naraz i applydziała na całej ramce DataFrame na raz.

Sprawdzanie funkcji niestandardowej

Bardzo pomocne może być sprawdzenie danych wejściowych do funkcji niestandardowej przekazanej do applylub transform.

Przykłady

Stwórzmy przykładowe dane i sprawdźmy grupy, abyś mógł zobaczyć, o czym mówię:

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

Stwórzmy prostą funkcję niestandardową, która wypisuje typ niejawnie przekazanego obiektu, a następnie zgłosi błąd, aby można było zatrzymać wykonywanie.

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

Teraz przekażmy tę funkcję zarówno do metody groupby, jak applyi transformmetod, aby zobaczyć, jaki obiekt jest do niej przekazywany:

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

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

Jak widać, DataFrame jest przekazywana do inspectfunkcji. Możesz się zastanawiać, dlaczego typ DataFrame został dwukrotnie wydrukowany. Pandy dwukrotnie prowadzi pierwszą grupę. Robi to w celu określenia, czy istnieje szybki sposób zakończenia obliczeń, czy nie. To drobny szczegół, o który nie powinieneś się martwić.

Teraz zróbmy to samo z transform

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

Przekazano serię - zupełnie inny obiekt Pandy.

W związku z tym transformmożna jednocześnie pracować tylko z jedną serią. To nie da na to, aby działać na dwóch kolumnach w tym samym czasie. Tak więc, jeśli spróbujemy odjąć kolumnę az bwnętrza naszej funkcji niestandardowej, otrzymamy błąd z transform. Zobacz poniżej:

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

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

Otrzymujemy KeyError, gdy pandy próbują znaleźć indeks Serii, aktóry nie istnieje. Możesz wykonać tę operację, applyponieważ ma ona całą ramkę DataFrame:

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

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

Wynik jest serią i jest nieco zagmatwany, ponieważ zachowany jest oryginalny indeks, ale mamy dostęp do wszystkich kolumn.


Wyświetlanie przekazanego obiektu pandy

Jeszcze bardziej może pomóc wyświetlenie całego obiektu pandy w funkcji niestandardowej, dzięki czemu możesz dokładnie zobaczyć, z czym pracujesz. Możesz użyć printoświadczeń, które lubię używać displayfunkcji z IPython.displaymodułu, aby ramki DataFrames były ładnie wyświetlane w HTML w notatniku jupyter:

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

Zrzut ekranu: wprowadź opis obrazu tutaj


Transformacja musi zwracać jednowymiarową sekwencję o takim samym rozmiarze jak grupa

Inna różnica polega na tym, że transformmusi zwracać jednowymiarową sekwencję o takim samym rozmiarze jak grupa. W tym konkretnym przypadku każda grupa ma dwa wiersze, więc transformmusi zwrócić sekwencję dwóch wierszy. Jeśli tak nie jest, zgłaszany jest błąd:

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

Komunikat o błędzie tak naprawdę nie opisuje problemu. Musisz zwrócić sekwencję o tej samej długości co grupa. Tak więc działałaby taka funkcja:

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

Zwracanie pojedynczego obiektu skalarnego działa również w przypadku transform

Jeśli zwrócisz tylko jeden skalar z funkcji niestandardowej, transformużyje go dla każdego z wierszy w grupie:

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
źródło
3
npnie jest zdefiniowany. Zakładam, że początkujący byliby wdzięczni za uwzględnienie import numpy as npw odpowiedzi.
Qaswed
187

Ponieważ czułem się podobnie zdezorientowany .transformoperacją vs. .apply, znalazłem kilka odpowiedzi, które rzucają trochę światła na ten problem. Na przykład ta odpowiedź była bardzo pomocna.

Jak dotąd moje wyniki są takie, że .transformbędzie działać (lub poradzić sobie) z Series(kolumnami) w oderwaniu od siebie . Oznacza to, że w Twoich dwóch ostatnich rozmowach:

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

Poprosiłeś .transformo pobranie wartości z dwóch kolumn i „to” w rzeczywistości nie „widzi” obu w tym samym czasie (że tak powiem). transformprzyjrzy się kolumnom ramki danych jedna po drugiej i zwróci serię (lub grupę serii) „utworzonych” skalarami, które są powtarzane len(input_column)razy.

Więc ten skalar, który powinien być użyty .transformdo uzyskania wyniku, Seriesjest wynikiem jakiejś funkcji redukcji zastosowanej na wejściu Series(i tylko na JEDNEJ serii / kolumnie na raz).

Rozważ ten przykład (w swojej ramce danych):

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)

przyniesie:

       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

To dokładnie to samo, co gdybyś używał go tylko na jednej kolumnie naraz:

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

wydajność:

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

Zwróć uwagę, że .applyw ostatnim przykładzie ( df.groupby('A')['C'].apply(zscore)) działałoby dokładnie w ten sam sposób, ale nie powiedzie się, jeśli spróbujesz użyć jej na ramce danych:

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

daje błąd:

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

Więc gdzie jeszcze jest .transformprzydatne? Najprostszym przypadkiem jest próba przypisania wyników funkcji redukcji z powrotem do oryginalnej ramki danych.

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

wydajność:

     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

Próbuje to samo z .applydałoby NaNssię sum_C. Bo .applyzwróciłby zredukowany Series, którego nie wie jak odesłać z powrotem:

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

dający:

A
bar    3.973
foo    4.373

Istnieją również przypadki, w których .transformjest używany do filtrowania danych:

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

Mam nadzieję, że to doda nieco większej przejrzystości.

Elementarz
źródło
4
O MÓJ BOŻE. Różnica jest tak subtelna.
Dawei,
3
.transform()może również służyć do uzupełniania brakujących wartości. Zwłaszcza jeśli chcesz rozgłaszać średnią grupy lub statystykę grupową do NaNwartości w tej grupie. Niestety, dokumentacja pand również nie była dla mnie pomocna.
cyber-matematyka
Myślę, że w ostatnim przypadku .groupby().filter()robi to samo. Dziękuję za wyjaśnienie .apply()i .transform()bardzo mnie wprawia w zakłopotanie.
Jiaxiang
to wyjaśnia, dlaczego df.groupby().transform()nie mogę pracować dla podgrupy df, zawsze otrzymuję błąd, ValueError: transform must return a scalar value for each groupponieważ transform
widzę
Bardzo podobał mi się ostatni przykład .transform użyty do filtrowania danych. Super miły!
rishi jain
13

Mam zamiar użyć bardzo prostego fragmentu, aby zilustrować różnicę:

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 wygląda następująco:

    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   

W tej tabeli znajdują się 3 identyfikatory klientów, każdy klient dokonał trzech transakcji i za każdym razem zapłacił 1,2,3 dolara.

Teraz chcę znaleźć minimalną kwotę płatności dokonaną przez każdego klienta. Można to zrobić na dwa sposoby:

  1. Używając apply:

    grouping.min ()

Zwrot wygląda następująco:

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. Używając transform:

    grouping.transform (min)

Zwrot wygląda następująco:

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    

Obie metody zwracają Seriesobiekt, ale lengthpierwsza to 3, a lengthdruga to 9.

Jeśli chcesz odpowiedzieć What is the minimum price paid by each customer, applymetoda jest bardziej odpowiednia do wyboru.

Jeśli chcesz odpowiedzieć What is the difference between the amount paid for each transaction vs the minimum payment, to chcesz skorzystać transform, ponieważ:

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

Apply nie działa tutaj tylko dlatego, że zwraca serię o rozmiarze 3, ale oryginalna długość pliku df wynosi 9. Nie można go łatwo zintegrować z powrotem z oryginalnym df.

Cheng
źródło
3
Myślę, że to świetna odpowiedź! Dziękujemy za poświęcenie czasu na udzielenie odpowiedzi ponad cztery lata po zadaniu pytania!
Benjamin Dubreu
4
tmp = df.groupby(['A'])['c'].transform('mean')

jest jak

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

lub

tmp1 = df.groupby(['A'])['c'].mean()
tmp = df['A'].map(tmp1)
shui
źródło