Kapan saya harus menggunakan pandas apply () dalam kode saya?

111

Saya telah melihat banyak jawaban yang diposting untuk pertanyaan tentang Stack Overflow yang melibatkan penggunaan metode Pandas apply. Saya juga melihat pengguna berkomentar di bawah mereka mengatakan bahwa " applylambat, dan harus dihindari".

Saya telah membaca banyak artikel tentang topik kinerja yang menjelaskan applylambat. Saya juga telah melihat penafian di dokumen tentang bagaimana applyfungsi kemudahan untuk meneruskan UDF (sepertinya tidak dapat menemukannya sekarang). Jadi, konsensus umum adalah yang applyharus dihindari jika memungkinkan. Namun, hal ini menimbulkan pertanyaan berikut:

  1. Jika applysangat buruk, lalu mengapa di API?
  2. Bagaimana dan kapan saya harus membuat kode saya applybebas?
  3. Adakah situasi di mana applyyang baik (lebih baik dari solusi lain yang mungkin)?
cs95
sumber
1
returns.add(1).apply(np.log)vs. np.log(returns.add(1)adalah kasus di mana applyumumnya akan sedikit lebih cepat, yang merupakan kotak hijau kanan bawah dalam diagram jpp di bawah ini.
Alexander
@Alexander terima kasih. Tidak secara mendalam menunjukkan situasi ini, tetapi mereka berguna untuk diketahui!
cs95

Jawaban:

108

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.applydan Series.applyadalah fungsi kenyamanan yang ditentukan masing-masing pada objek DataFrame dan Series. applymenerima setiap fungsi yang ditentukan pengguna yang menerapkan transformasi / agregasi pada DataFrame. applysecara efektif adalah peluru perak yang melakukan apa pun yang tidak dapat dilakukan oleh fungsi panda yang ada.

Beberapa hal yang applydapat 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 aggatau transformdalam kasus ini)
  • Lakukan transformasi berdasarkan elemen
  • Menyiarkan hasil agregat ke baris asli (lihat result_typeargumen).
  • 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 applyburuk? Hal ini karena applyini 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 applymenimbulkan beberapa overhead besar pada setiap iterasi. Lebih lanjut, applymengkonsumsi lebih banyak memori, yang merupakan tantangan untuk aplikasi yang dibatasi memori.

Ada sangat sedikit situasi applyyang 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 applyuntuk 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 rawargumen, 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: astypeversusapply

Ini sepertinya merupakan keistimewaan API. Menggunakan applyuntuk mengonversi bilangan bulat dalam Seri menjadi string sebanding (dan terkadang lebih cepat) daripada menggunakan astype.

masukkan deskripsi gambar di sini Grafik diplot menggunakan perfplotperpustakaan.

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 astypesecara 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.applybelum dibahas hingga saat ini, tetapi GroupBy.applyjuga merupakan fungsi kemudahan berulang untuk menangani apa pun yang tidak dimiliki GroupByfungsi 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, applymerupakan solusi yang dapat diterima jika tujuannya adalah untuk mengurangi groupbypanggilan (karena groupbyjuga cukup mahal).


Peringatan Lainnya

Selain dari peringatan yang disebutkan di atas, perlu juga disebutkan bahwa applyberoperasi pada baris pertama (atau kolom) dua kali. Ini dilakukan untuk menentukan apakah fungsi tersebut memiliki efek samping. Jika tidak, applymungkin 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.applypada pandas versi <0,25 (telah diperbaiki untuk 0,25, lihat di sini untuk informasi selengkapnya .)

cs95
sumber
Saya pikir kita perlu berhati-hati .. dengan %timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')pasti setelah iterasi pertama itu akan jauh lebih cepat karena Anda beralih datetimeke ... datetime?
jpp
@jpp Saya memiliki perhatian yang sama. Tapi Anda masih perlu melakukan pemindaian linier, memanggil to_datetime pada string secepat memanggilnya pada objek datetime, jika tidak lebih cepat. Timing kasarnya sama. Alternatifnya adalah dengan menerapkan beberapa langkah pra-salin untuk setiap solusi berjangka waktu yang menghilangkan poin utama. Tapi itu adalah perhatian yang valid.
cs95
"Memanggil to_datetimestring secepat pada ... datetimeobjek" .. benarkah? Saya menyertakan pembuatan kerangka data (biaya tetap) dalam pengaturan waktu applyvs forloop dan perbedaannya jauh lebih kecil.
jpp
@jpp Nah, itulah yang saya dapatkan dari pengujian saya (yang memang terbatas). Saya yakin itu tergantung pada datanya, tetapi gagasan umumnya adalah untuk tujuan ilustrasi, perbedaannya adalah "serius, jangan khawatir tentang itu".
cs95
1
@ cs95, Selamat tahun baru!
jpp
49

Tidak semua applysama

Bagan di bawah ini menyarankan kapan harus mempertimbangkan apply1 . Hijau berarti mungkin efisien; merah menghindari.

masukkan deskripsi gambar di sini

Beberapa di antaranya intuitif: pd.Series.applyadalah loop baris-bijaksana tingkat Python, ditto pd.DataFrame.apply-baris-bijaksana ( axis=1). Penyalahgunaan ini banyak dan luas. Posting lainnya membahasnya secara lebih mendalam. Solusi populer adalah menggunakan metode vektor, pemahaman daftar (mengasumsikan data bersih), atau alat yang efisien seperti pd.DataFramekonstruktor (misalnya untuk menghindari apply(pd.Series)).

Jika Anda menggunakan pd.DataFrame.applybijak-baris, menentukan raw=True(jika mungkin) sering kali bermanfaat. Pada tahap ini, numbabiasanya merupakan pilihan yang lebih baik.

GroupBy.apply: umumnya disukai

groupbyOperasi berulang yang harus dihindari applyakan merusak kinerja. GroupBy.applybiasanya baik-baik saja di sini, asalkan metode yang Anda gunakan dalam fungsi kustom Anda sendiri vektorisasi. Terkadang tidak ada metode Pandas asli untuk agregasi berkelompok yang ingin Anda terapkan. Dalam kasus ini, untuk sejumlah kecil grup applydengan fungsi kustom mungkin masih menawarkan kinerja yang wajar.

pd.DataFrame.apply kolom-bijaksana: tas campuran

pd.DataFrame.applycolumn-Wise ( axis=0) adalah kasus yang menarik. Untuk sejumlah kecil baris versus sejumlah besar kolom, biayanya hampir selalu mahal. Untuk jumlah baris yang besar relatif terhadap kolom, kasus yang lebih umum, terkadang Anda mungkin melihat peningkatan kinerja yang signifikan menggunakan apply:

# Python 3.7, Pandas 0.23.4
np.random.seed(0)
df = pd.DataFrame(np.random.random((10**7, 3)))     # Scenario_1, many rows
df = pd.DataFrame(np.random.random((10**4, 10**3))) # Scenario_2, many columns

                                               # Scenario_1  | Scenario_2
%timeit df.sum()                               # 800 ms      | 109 ms
%timeit df.apply(pd.Series.sum)                # 568 ms      | 325 ms

%timeit df.max() - df.min()                    # 1.63 s      | 314 ms
%timeit df.apply(lambda x: x.max() - x.min())  # 838 ms      | 473 ms

%timeit df.mean()                              # 108 ms      | 94.4 ms
%timeit df.apply(pd.Series.mean)               # 276 ms      | 233 ms

1 Ada pengecualian, tetapi biasanya marginal atau tidak umum. Beberapa contoh:

  1. df['col'].apply(str)mungkin sedikit mengungguli df['col'].astype(str).
  2. df.apply(pd.to_datetime)bekerja pada string tidak diskalakan dengan baik dengan baris versus forloop biasa .
jpp
sumber
2
Terima kasih telah
bergabung
1
@coldspeed, Terima kasih, tidak ada yang salah dengan kiriman Anda (selain dari beberapa pembandingan yang kontradiktif vs milik saya, tetapi dapat berupa masukan atau penyiapan). Cuma merasa ada cara berbeda untuk melihat masalah.
jpp
@jpp Saya selalu digunakan Anda flowchart baik sedang sebagai pedoman sampai ketika aku melihat hari ini bahwa baris-bijaksanaapply secara signifikan lebih cepat daripada solusi saya dengan any. Ada pemikiran tentang ini?
Stef
1
@jpp: Anda benar: untuk 1 juta baris x 100 cols anysekitar 100 kali lebih cepat dari apply. Itu melakukan tes pertama saya dengan 2000 baris x 1000 cols dan di sini applydua kali lebih cepatany
Stef
1
@jpp Saya ingin menggunakan gambar Anda dalam presentasi / artikel. Apakah kamu baik-baik saja dengan itu? Saya jelas akan menyebutkan sumbernya. Terima kasih
Erfan
3

Untuk axis=1(yaitu fungsi baris-bijaksana) maka Anda bisa menggunakan fungsi berikut sebagai pengganti apply. Saya bertanya-tanya mengapa ini bukan pandasperilakunya. (Belum teruji dengan indeks gabungan, tetapi tampaknya jauh lebih cepat daripada apply)

def faster_df_apply(df, func):
    cols = list(df.columns)
    data, index = [], []
    for row in df.itertuples(index=True):
        row_dict = {f:v for f,v in zip(cols, row[1:])}
        data.append(func(row_dict))
        index.append(row[0])
    return pd.Series(data, index=index)
Pete Cacioppi
sumber
Saya sangat terkejut menemukan ini memberi saya kinerja yang lebih baik dalam beberapa kasus. Ini sangat berguna ketika saya perlu melakukan banyak hal, masing-masing dengan subset nilai kolom yang berbeda. Jawaban "Semua penerapan tidak sama" mungkin membantu mencari tahu kapan kemungkinan akan membantu, tetapi tidak terlalu sulit untuk menguji sampel data Anda.
denson
Beberapa petunjuk: untuk kinerja pemahaman daftar akan mengungguli perulangan for; zip(df, row[1:])cukup di sini; Sungguh, pada tahap ini, pertimbangkan numbaapakah func adalah perhitungan numerik. Lihat jawaban ini untuk penjelasannya.
jpp
@jpp - jika Anda memiliki fungsi yang lebih baik, silakan bagikan. Saya pikir ini cukup mendekati optimal dari analisis saya. Ya numbalebih cepat, faster_df_applydimaksudkan untuk orang yang hanya menginginkan sesuatu yang setara, tetapi lebih cepat daripada, DataFrame.apply(yang anehnya lambat).
Pete Cacioppi
2

Apakah pernah ada situasi applyyang baik? Ya kadang kadang.

Tugas: memecahkan kode string Unicode.

import numpy as np
import pandas as pd
import unidecode

s = pd.Series(['mañana','Ceñía'])
s.head()
0    mañana
1     Ceñía


s.apply(unidecode.unidecode)
0    manana
1     Cenia

Pembaruan
Saya sama sekali tidak menganjurkan penggunaan apply, hanya berpikir karena NumPytidak dapat menangani situasi di atas, itu bisa menjadi kandidat yang baik pandas apply. Tapi saya lupa pemahaman daftar biasa berkat pengingat oleh @jpp.

astro123
sumber
Tidak. Bagaimana ini lebih baik dari [unidecode.unidecode(x) for x in s]atau list(map(unidecode.unidecode, s))?
jpp
1
Karena sudah seri pandas, saya tergoda untuk menggunakan apply, Ya Anda benar, lebih baik menggunakan list-comp daripada apply, Tapi downvote sedikit keras, saya tidak menganjurkan apply, hanya berpikir ini bisa menjadi bagus kasus penggunaan.
astro123