apply
, Fungsi Kenyamanan yang Tidak Pernah Anda Butuhkan
Kami mulai dengan menjawab pertanyaan-pertanyaan di OP, satu per satu.
" Jika penerapan sangat buruk, lalu mengapa di API? "
DataFrame.apply
dan Series.apply
adalah fungsi kenyamanan yang ditentukan masing-masing pada objek DataFrame dan Series. apply
menerima setiap fungsi yang ditentukan pengguna yang menerapkan transformasi / agregasi pada DataFrame. apply
secara efektif adalah peluru perak yang melakukan apa pun yang tidak dapat dilakukan oleh fungsi panda yang ada.
Beberapa hal yang apply
dapat dilakukan:
- Jalankan fungsi apa pun yang ditentukan pengguna pada DataFrame atau Seri
- Menerapkan fungsi baik baris-bijaksana (
axis=1
) atau kolom-bijaksana ( axis=0
) pada DataFrame
- Lakukan perataan indeks saat menerapkan fungsi
- Lakukan agregasi dengan fungsi yang ditentukan pengguna (namun, biasanya kami lebih suka
agg
atau transform
dalam kasus ini)
- Lakukan transformasi berdasarkan elemen
- Menyiarkan hasil agregat ke baris asli (lihat
result_type
argumen).
- Terima argumen posisi / kata kunci untuk diteruskan ke fungsi yang ditentukan pengguna.
...Diantara yang lain. Untuk informasi lebih lanjut, lihat Aplikasi Fungsi Baris atau Kolom dalam dokumentasi.
Jadi, dengan semua fitur ini, mengapa apply
buruk? Hal ini karena apply
ini lambat . Panda tidak membuat asumsi tentang sifat fungsi Anda, dan karenanya menerapkan fungsi Anda secara berulang ke setiap baris / kolom seperlunya. Selain itu, menangani semua situasi di atas berarti apply
menimbulkan beberapa overhead besar pada setiap iterasi. Lebih lanjut, apply
mengkonsumsi lebih banyak memori, yang merupakan tantangan untuk aplikasi yang dibatasi memori.
Ada sangat sedikit situasi apply
yang sesuai untuk digunakan (lebih lanjut tentang itu di bawah). Jika Anda tidak yakin apakah Anda harus menggunakan apply
, Anda mungkin sebaiknya tidak menggunakannya.
Mari kita bahas pertanyaan selanjutnya.
" Bagaimana dan kapan saya harus membuat kode saya berlaku -gratis? "
Untuk mengubah kalimatnya, berikut adalah beberapa situasi umum di mana Anda ingin menghilangkan panggilan ke apply
.
Data Numerik
Jika Anda bekerja dengan data numerik, kemungkinan sudah ada fungsi cython yang di-vectorisasi yang melakukan apa yang Anda coba lakukan (jika tidak, silakan ajukan pertanyaan di Stack Overflow atau buka permintaan fitur di GitHub).
Bandingkan kinerja apply
untuk operasi penjumlahan sederhana.
df = pd.DataFrame({"A": [9, 4, 2, 1], "B": [12, 7, 5, 4]})
df
A B
0 9 12
1 4 7
2 2 5
3 1 4
df.apply(np.sum)
A 16
B 28
dtype: int64
df.sum()
A 16
B 28
dtype: int64
Dari segi kinerja, tidak ada perbandingan, setara dengan cythonized jauh lebih cepat. Tidak perlu grafik, karena perbedaannya jelas bahkan untuk data mainan.
%timeit df.apply(np.sum)
%timeit df.sum()
2.22 ms ± 41.2 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
471 µs ± 8.16 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Bahkan jika Anda mengaktifkan melewatkan larik mentah dengan raw
argumen, itu masih dua kali lebih lambat.
%timeit df.apply(np.sum, raw=True)
840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
Contoh lain:
df.apply(lambda x: x.max() - x.min())
A 8
B 8
dtype: int64
df.max() - df.min()
A 8
B 8
dtype: int64
%timeit df.apply(lambda x: x.max() - x.min())
%timeit df.max() - df.min()
2.43 ms ± 450 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
1.23 ms ± 14.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Secara umum, carilah alternatif vektor jika memungkinkan.
String / Regex
Pandas menyediakan fungsi string "vektor" di sebagian besar situasi, tetapi ada kasus yang jarang terjadi di mana fungsi tersebut tidak ... "berlaku", bisa dikatakan.
Masalah umum adalah memeriksa apakah nilai dalam kolom ada di kolom lain dari baris yang sama.
df = pd.DataFrame({
'Name': ['mickey', 'donald', 'minnie'],
'Title': ['wonderland', "welcome to donald's castle", 'Minnie mouse clubhouse'],
'Value': [20, 10, 86]})
df
Name Value Title
0 mickey 20 wonderland
1 donald 10 welcome to donald's castle
2 minnie 86 Minnie mouse clubhouse
Ini harus mengembalikan baris kedua dan ketiga, karena "donald" dan "minnie" ada di kolom "Judul" masing-masing.
Menggunakan apply, ini akan dilakukan dengan menggunakan
df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)
0 False
1 True
2 True
dtype: bool
df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
Name Title Value
1 donald welcome to donald's castle 10
2 minnie Minnie mouse clubhouse 86
Namun, solusi yang lebih baik ada dengan menggunakan pemahaman daftar.
df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
Name Title Value
1 donald welcome to donald's castle 10
2 minnie Minnie mouse clubhouse 86
%timeit df[df.apply(lambda x: x['Name'].lower() in x['Title'].lower(), axis=1)]
%timeit df[[y.lower() in x.lower() for x, y in zip(df['Title'], df['Name'])]]
2.85 ms ± 38.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
788 µs ± 16.4 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Hal yang perlu diperhatikan di sini adalah bahwa rutinitas berulang terjadi lebih cepat daripada apply
, karena overhead yang lebih rendah. Jika Anda perlu menangani NaN dan dtypes yang tidak valid, Anda dapat membangunnya menggunakan fungsi kustom yang kemudian dapat Anda panggil dengan argumen di dalam pemahaman daftar.
Untuk informasi lebih lanjut tentang kapan pemahaman daftar harus dianggap sebagai pilihan yang baik, lihat artikel saya: Untuk loop dengan pandas - Kapan saya harus peduli? .
Catatan
Operasi tanggal dan waktu juga memiliki versi vektor. Jadi, misalnya, Anda harus memilih pd.to_datetime(df['date'])
, daripada, katakanlah df['date'].apply(pd.to_datetime)
,.
Baca lebih lanjut di
dokumen .
Kesalahan Umum: Kolom Daftar yang Meledak
s = pd.Series([[1, 2]] * 3)
s
0 [1, 2]
1 [1, 2]
2 [1, 2]
dtype: object
Orang-orang tergoda untuk menggunakan apply(pd.Series)
. Ini mengerikan dalam hal performa.
s.apply(pd.Series)
0 1
0 1 2
1 1 2
2 1 2
Pilihan yang lebih baik adalah dengan mendengarkan kolom dan meneruskannya ke pd.DataFrame.
pd.DataFrame(s.tolist())
0 1
0 1 2
1 1 2
2 1 2
%timeit s.apply(pd.Series)
%timeit pd.DataFrame(s.tolist())
2.65 ms ± 294 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
816 µs ± 40.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Akhirnya,
" Apakah ada situasi di mana apply
yang baik? "
Terapkan adalah fungsi kenyamanan, jadi ada yang situasi di mana overhead cukup diabaikan untuk memaafkan. Itu benar-benar tergantung pada berapa kali fungsi tersebut dipanggil.
Fungsi yang Vectorized untuk Seri, tapi bukan DataFrames
Bagaimana jika Anda ingin menerapkan operasi string pada beberapa kolom? Bagaimana jika Anda ingin mengonversi beberapa kolom menjadi datetime? Fungsi ini dibuat vektor untuk Seri saja, sehingga harus diterapkan di setiap kolom yang ingin Anda konversi / operasikan.
df = pd.DataFrame(
pd.date_range('2018-12-31','2019-01-31', freq='2D').date.astype(str).reshape(-1, 2),
columns=['date1', 'date2'])
df
date1 date2
0 2018-12-31 2019-01-02
1 2019-01-04 2019-01-06
2 2019-01-08 2019-01-10
3 2019-01-12 2019-01-14
4 2019-01-16 2019-01-18
5 2019-01-20 2019-01-22
6 2019-01-24 2019-01-26
7 2019-01-28 2019-01-30
df.dtypes
date1 object
date2 object
dtype: object
Ini adalah kasus yang dapat diterima untuk apply
:
df.apply(pd.to_datetime, errors='coerce').dtypes
date1 datetime64[ns]
date2 datetime64[ns]
dtype: object
Perhatikan bahwa ini juga masuk akal stack
, atau hanya menggunakan loop eksplisit. Semua opsi ini sedikit lebih cepat daripada menggunakan apply
, tetapi perbedaannya cukup kecil untuk dimaafkan.
%timeit df.apply(pd.to_datetime, errors='coerce')
%timeit pd.to_datetime(df.stack(), errors='coerce').unstack()
%timeit pd.concat([pd.to_datetime(df[c], errors='coerce') for c in df], axis=1)
%timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')
5.49 ms ± 247 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.94 ms ± 48.1 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
3.16 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
2.41 ms ± 1.71 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Anda dapat membuat kasus serupa untuk operasi lain seperti operasi string, atau konversi ke kategori.
u = df.apply(lambda x: x.str.contains(...))
v = df.apply(lambda x: x.astype(category))
v / s
u = pd.concat([df[c].str.contains(...) for c in df], axis=1)
v = df.copy()
for c in df:
v[c] = df[c].astype(category)
Dan seterusnya...
Mengonversi Seri menjadi str
: astype
versusapply
Ini sepertinya merupakan keistimewaan API. Menggunakan apply
untuk mengonversi bilangan bulat dalam Seri menjadi string sebanding (dan terkadang lebih cepat) daripada menggunakan astype
.
Grafik diplot menggunakan perfplot
perpustakaan.
import perfplot
perfplot.show(
setup=lambda n: pd.Series(np.random.randint(0, n, n)),
kernels=[
lambda s: s.astype(str),
lambda s: s.apply(str)
],
labels=['astype', 'apply'],
n_range=[2**k for k in range(1, 20)],
xlabel='N',
logx=True,
logy=True,
equality_check=lambda x, y: (x == y).all())
Dengan pelampung, saya melihat astype
secara konsisten secepat, atau sedikit lebih cepat dari apply
. Jadi ini ada hubungannya dengan fakta bahwa data dalam pengujian adalah tipe integer.
GroupBy
operasi dengan transformasi berantai
GroupBy.apply
belum dibahas hingga saat ini, tetapi GroupBy.apply
juga merupakan fungsi kemudahan berulang untuk menangani apa pun yang tidak dimiliki GroupBy
fungsi yang ada .
Satu persyaratan umum adalah melakukan GroupBy dan kemudian dua operasi utama seperti "cumsum tertinggal":
df = pd.DataFrame({"A": list('aabcccddee'), "B": [12, 7, 5, 4, 5, 4, 3, 2, 1, 10]})
df
A B
0 a 12
1 a 7
2 b 5
3 c 4
4 c 5
5 c 4
6 d 3
7 d 2
8 e 1
9 e 10
Anda memerlukan dua panggilan grup melalui telepon di sini:
df.groupby('A').B.cumsum().groupby(df.A).shift()
0 NaN
1 12.0
2 NaN
3 NaN
4 4.0
5 9.0
6 NaN
7 3.0
8 NaN
9 1.0
Name: B, dtype: float64
Dengan menggunakan apply
, Anda dapat mempersingkat ini menjadi satu panggilan.
df.groupby('A').B.apply(lambda x: x.cumsum().shift())
0 NaN
1 12.0
2 NaN
3 NaN
4 4.0
5 9.0
6 NaN
7 3.0
8 NaN
9 1.0
Name: B, dtype: float64
Sangat sulit untuk mengukur kinerja karena bergantung pada data. Tetapi secara umum, apply
merupakan solusi yang dapat diterima jika tujuannya adalah untuk mengurangi groupby
panggilan (karena groupby
juga cukup mahal).
Peringatan Lainnya
Selain dari peringatan yang disebutkan di atas, perlu juga disebutkan bahwa apply
beroperasi pada baris pertama (atau kolom) dua kali. Ini dilakukan untuk menentukan apakah fungsi tersebut memiliki efek samping. Jika tidak, apply
mungkin dapat menggunakan jalur cepat untuk mengevaluasi hasil, jika tidak, akan kembali ke implementasi yang lambat.
df = pd.DataFrame({
'A': [1, 2],
'B': ['x', 'y']
})
def func(x):
print(x['A'])
return x
df.apply(func, axis=1)
# 1
# 1
# 2
A B
0 1 x
1 2 y
Perilaku ini juga terlihat GroupBy.apply
pada pandas versi <0,25 (telah diperbaiki untuk 0,25, lihat di sini untuk informasi selengkapnya .)
returns.add(1).apply(np.log)
vs.np.log(returns.add(1)
adalah kasus di manaapply
umumnya akan sedikit lebih cepat, yang merupakan kotak hijau kanan bawah dalam diagram jpp di bawah ini.