Terapkan vs transformasi pada objek grup

174

Pertimbangkan kerangka data berikut:

     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

Perintah-perintah berikut ini berfungsi:

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

tetapi tidak satupun dari pekerjaan berikut:

> 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

Mengapa? Contoh pada dokumentasi tampaknya menyarankan bahwa memanggil transformgrup memungkinkan seseorang untuk melakukan pemrosesan operasi baris-bijaksana:

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

Dengan kata lain, saya berpikir bahwa transformasi pada dasarnya adalah tipe penerapan tertentu (yang tidak agregat). Dimana saya salah

Untuk referensi, di bawah ini adalah konstruksi kerangka data asli di atas:

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
sumber
1
Fungsi yang diteruskan ke transformharus mengembalikan angka, baris, atau bentuk yang sama dengan argumen. jika itu angka maka nomor akan diatur ke semua elemen dalam grup, jika itu adalah baris, itu akan disiarkan ke semua baris dalam grup. Dalam kode Anda, fungsi lambda mengembalikan kolom yang tidak dapat disiarkan ke grup.
HYRY
1
Terima kasih @HYRY, tapi saya bingung. Jika Anda melihat contoh dalam dokumentasi yang saya salin di atas (yaitu dengan zscore), transformmenerima fungsi lambda yang mengasumsikan masing x- masing adalah item dalam group, dan juga mengembalikan nilai per item dalam grup. Apa yang saya lewatkan?
Amelio Vazquez-Reina
Bagi mereka yang mencari solusi yang sangat rinci, lihat yang di bawah ini .
Ted Petrou
@TedPetrou: tl; dr dari itu adalah: 1) applymelewati seluruh df, tetapi transformmelewati setiap kolom satu per satu sebagai Seri. 2) applydapat mengembalikan output bentuk apa pun (skalar / Seri / DataFrame / array / list ...), sedangkan transformharus mengembalikan urutan (Seri 1D / array / daftar) dengan panjang yang sama dengan grup. Itu sebabnya OP apply()tidak perlu transform(). Ini adalah pertanyaan yang bagus karena dokter tidak menjelaskan kedua perbedaan dengan jelas. (mirip dengan perbedaan antara apply/map/applymap, atau hal-hal lain ...)
smci

Jawaban:

146

Dua perbedaan utama antara applydantransform

Ada dua perbedaan utama antara metode transformdan applygroupby.

  • Memasukkan:
    • applysecara implisit meneruskan semua kolom untuk setiap grup sebagai DataFrame ke fungsi kustom.
    • sementara transformmelewati setiap kolom untuk setiap grup secara individual sebagai Seri ke fungsi kustom.
  • Keluaran:
    • Fungsi kustom yang diteruskan ke applydapat mengembalikan skalar, atau Seri atau DataFrame (atau array numpy atau daftar genap) .
    • Fungsi kustom yang diteruskan ke transformharus mengembalikan urutan (Seri, array, atau daftar satu dimensi) dengan panjang yang sama dengan grup .

Jadi, transformbekerja hanya pada satu seri pada satu waktu dan applybekerja pada seluruh DataFrame sekaligus.

Memeriksa fungsi kustom

Ini dapat membantu sedikit untuk memeriksa input ke fungsi kustom Anda diteruskan ke applyatau transform.

Contohnya

Mari kita membuat beberapa sampel data dan memeriksa grup sehingga Anda dapat melihat apa yang saya bicarakan:

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

Mari kita buat fungsi kustom sederhana yang mencetak jenis objek yang dilewatkan secara implisit dan kemudian menimbulkan kesalahan sehingga eksekusi dapat dihentikan.

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

Sekarang mari kita lewati fungsi ini ke groupby applydan transformmetode untuk melihat objek apa yang diteruskan ke sana:

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

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

Seperti yang Anda lihat, DataFrame dilewatkan ke inspectfungsi. Anda mungkin bertanya-tanya mengapa jenisnya, DataFrame, dicetak dua kali. Panda menjalankan grup pertama dua kali. Ini dilakukan untuk menentukan apakah ada cara cepat untuk menyelesaikan perhitungan atau tidak. Ini adalah detail kecil yang tidak perlu Anda khawatirkan.

Sekarang, mari kita lakukan hal yang sama dengannya transform

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

Itu dilewatkan Seri - objek Pandas yang sama sekali berbeda.

Jadi, transformhanya diperbolehkan bekerja dengan satu Seri pada satu waktu. Bukan tidak mungkin untuk bertindak pada dua kolom secara bersamaan. Jadi, jika kita mencoba dan mengurangi kolom adari bdalam fungsi kustom kita, kita akan mendapatkan kesalahan transform. Lihat di bawah:

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

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

Kami mendapatkan KeyError karena panda berusaha menemukan indeks Seri ayang tidak ada. Anda dapat menyelesaikan operasi ini applykarena memiliki seluruh DataFrame:

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

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

Outputnya adalah Seri dan sedikit membingungkan karena indeks asli disimpan, tetapi kami memiliki akses ke semua kolom.


Menampilkan objek panda yang dilewati

Ini dapat membantu lebih banyak lagi untuk menampilkan seluruh objek panda dalam fungsi kustom, sehingga Anda dapat melihat dengan tepat apa yang Anda operasikan. Anda dapat menggunakan printpernyataan dengan saya ingin menggunakan displayfungsi dari IPython.displaymodul sehingga DataFrames dapat dihasilkan dengan baik dalam HTML di notebook jupyter:

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

Tangkapan layar: masukkan deskripsi gambar di sini


Transform harus mengembalikan urutan dimensi tunggal dengan ukuran yang sama dengan grup

Perbedaan lainnya adalah bahwa transformharus mengembalikan urutan dimensi tunggal dengan ukuran yang sama dengan grup. Dalam contoh khusus ini, setiap grup memiliki dua baris, jadi transformharus mengembalikan urutan dua baris. Jika tidak maka kesalahan muncul:

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

Pesan kesalahan sebenarnya tidak deskriptif tentang masalahnya. Anda harus mengembalikan urutan dengan panjang yang sama dengan grup. Jadi, fungsi seperti ini akan berfungsi:

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

Mengembalikan objek skalar tunggal juga berfungsi untuk transform

Jika Anda mengembalikan hanya satu skalar dari fungsi khusus Anda, maka transformakan menggunakannya untuk setiap baris dalam grup:

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
sumber
3
nptak terdefinisi. Saya menganggap pemula akan menghargai jika Anda memasukkan import numpy as npke dalam jawaban Anda.
Qaswed
187

Karena saya juga merasa bingung dengan .transformoperasi vs. .applySaya menemukan beberapa jawaban yang menjelaskan masalah ini. Misalnya jawaban ini sangat membantu.

Takeout saya sejauh ini adalah yang .transformakan bekerja (atau menangani) dengan Series(kolom) terpisah satu sama lain . Apa artinya ini dalam dua panggilan terakhir Anda:

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

Anda diminta .transformuntuk mengambil nilai dari dua kolom dan 'itu' sebenarnya tidak 'melihat' keduanya pada saat yang sama (bisa dikatakan). transformakan melihat kolom dataframe satu per satu dan mengembalikan seri (atau grup seri) yang 'terbuat' dari skalar yang berulang len(input_column)kali.

Jadi skalar ini, yang harus digunakan .transformuntuk membuat Seriesadalah hasil dari beberapa fungsi reduksi yang diterapkan pada input Series(dan hanya pada SATU seri / kolom pada suatu waktu).

Pertimbangkan contoh ini (pada kerangka data Anda):

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)

akan menghasilkan:

       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

Yang persis sama seperti jika Anda menggunakannya hanya pada satu kolom pada satu waktu:

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

menghasilkan:

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

Perhatikan bahwa .applydalam contoh terakhir ( df.groupby('A')['C'].apply(zscore)) akan bekerja dengan cara yang persis sama, tetapi akan gagal jika Anda mencoba menggunakannya pada dataframe:

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

memberikan kesalahan:

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

Jadi di mana lagi yang .transformberguna? Kasus paling sederhana adalah mencoba untuk menetapkan hasil fungsi reduksi kembali ke kerangka data asli.

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

menghasilkan:

     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

Mencoba sama dengan .applyakan memberikan NaNsdi sum_C. Karena .applyakan mengembalikan dikurangi Series, yang tidak tahu bagaimana menyiarkan kembali:

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

memberi:

A
bar    3.973
foo    4.373

Ada juga kasus ketika .transformdigunakan untuk memfilter data:

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

Saya harap ini menambah sedikit kejelasan.

Primer
sumber
4
OH TUHAN. Perbedaannya sangat halus.
Dawei
3
.transform()dapat juga digunakan untuk mengisi nilai yang hilang. Terutama jika Anda ingin menyiarkan rata-rata grup atau statistik grup ke NaNnilai di grup itu. Sayangnya, dokumentasi panda juga tidak membantu saya.
cyber-math
Saya pikir dalam kasus terakhir, .groupby().filter()melakukan hal yang sama. Terima kasih atas penjelasan Anda, .apply()dan .transform()membuat saya sangat bingung.
Jiaxiang
yang menjelaskan mengapa df.groupby().transform()tidak dapat bekerja untuk sub grup df, saya selalu mendapatkan kesalahan ValueError: transform must return a scalar value for each groupkarena transformmelihat kolom satu per satu
jerrytim
Saya sangat menyukai contoh terakhir. Mentransformasi yang digunakan untuk memfilter data. sangat baik!
rishi jain
13

Saya akan menggunakan cuplikan yang sangat sederhana untuk menggambarkan perbedaan:

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 terlihat seperti ini:

    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   

Ada 3 ID pelanggan dalam tabel ini, masing-masing pelanggan melakukan tiga transaksi dan membayar 1,2,3 dolar setiap kali.

Sekarang, saya ingin menemukan pembayaran minimum yang dilakukan oleh setiap pelanggan. Ada dua cara untuk melakukannya:

  1. Menggunakan apply:

    grouping.min ()

Pengembalian terlihat seperti ini:

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. Menggunakan transform:

    pengelompokan.transformasi (mnt)

Pengembalian terlihat seperti ini:

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    

Kedua metode mengembalikan Seriesobjek, tetapi lengthyang pertama adalah 3 danlength yang kedua adalah 9.

Jika Anda ingin menjawab What is the minimum price paid by each customer, maka applymetode yang paling cocok untuk dipilih.

Jika Anda ingin menjawab What is the difference between the amount paid for each transaction vs the minimum payment, maka Anda ingin menggunakan transform, karena:

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

Apply tidak bekerja di sini hanya karena mengembalikan Seri ukuran 3, tetapi panjang df asli adalah 9. Anda tidak dapat mengintegrasikannya kembali ke df asli dengan mudah.

Cheng
sumber
3
Saya pikir ini adalah jawaban yang bagus! Terima kasih telah meluangkan waktu untuk membuat jawaban lebih dari empat tahun setelah pertanyaan diajukan!
Benjamin Dubreu
4
tmp = df.groupby(['A'])['c'].transform('mean')

seperti

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

atau

tmp1 = df.groupby(['A'])['c'].mean()
tmp = df['A'].map(tmp1)
shui
sumber