TLDR; Tidak, for
loop bukanlah selimut yang "buruk", setidaknya tidak selalu. Mungkin lebih akurat untuk mengatakan bahwa beberapa operasi vektorisasi lebih lambat daripada iterasi , dibandingkan dengan mengatakan bahwa iterasi lebih cepat daripada beberapa operasi vektor. Mengetahui kapan dan mengapa adalah kunci untuk mendapatkan performa maksimal dari kode Anda. Singkatnya, ini adalah situasi di mana perlu mempertimbangkan alternatif untuk fungsi panda vektor:
- Ketika data Anda kecil (... tergantung pada apa yang Anda lakukan),
- Saat berhadapan dengan
object
/ tipe campuran
- Saat menggunakan
str
fungsi pengakses / regex
Mari kita periksa situasi ini satu per satu.
Iterasi v / s Vektorisasi pada Data Kecil
Pandas mengikuti pendekatan "Konvensi Selama Konfigurasi" dalam desain API-nya. Artinya, API yang sama telah dipasang untuk melayani berbagai data dan kasus penggunaan.
Ketika fungsi pandas dipanggil, hal-hal berikut (antara lain) harus ditangani secara internal oleh fungsi tersebut, untuk memastikan berfungsi
- Perataan indeks / sumbu
- Menangani tipe data campuran
- Menangani data yang hilang
Hampir setiap fungsi harus menangani hal ini hingga tingkat yang berbeda-beda, dan ini menimbulkan biaya tambahan . Overhead lebih sedikit untuk fungsi numerik (misalnya, Series.add
), sementara itu lebih diucapkan untuk fungsi string (misalnya, Series.str.replace
).
for
loop, di sisi lain, lebih cepat dari yang Anda pikirkan. Apa yang lebih baik adalah pemahaman daftar (yang membuat daftar melalui for
loop) bahkan lebih cepat karena mekanisme iteratif dioptimalkan untuk pembuatan daftar.
Pemahaman daftar mengikuti polanya
[f(x) for x in seq]
Di mana seq
deretan panda atau kolom DataFrame. Atau, saat mengoperasikan beberapa kolom,
[f(x, y) for x, y in zip(seq1, seq2)]
Dimana seq1
dan seq2
merupakan kolom.
Perbandingan Numerik
Pertimbangkan operasi pengindeksan boolean sederhana. Metode pemahaman daftar telah disesuaikan waktunya dengan Series.ne
( !=
) dan query
. Berikut fungsinya:
# Boolean indexing with Numeric value comparison.
df[df.A != df.B] # vectorized !=
df.query('A != B') # query (numexpr)
df[[x != y for x, y in zip(df.A, df.B)]] # list comp
Untuk kesederhanaan, saya telah menggunakan perfplot
paket untuk menjalankan semua tes timeit di posting ini. Pengaturan waktu untuk operasi di atas adalah di bawah ini:
Pemahaman daftar berkinerja lebih baik query
untuk N berukuran sedang, dan bahkan mengungguli vektorized tidak sama dengan perbandingan untuk N kecil Sayangnya, pemahaman daftar berskala linier, jadi tidak menawarkan banyak peningkatan kinerja untuk N. yang lebih besar.
Catatan
Perlu disebutkan bahwa sebagian besar manfaat pemahaman daftar berasal dari tidak perlu khawatir tentang penyelarasan indeks, tetapi ini berarti bahwa jika kode Anda bergantung pada penyelarasan indeks, ini akan rusak. Dalam beberapa kasus, operasi vektorisasi pada array NumPy yang mendasari dapat dianggap membawa "yang terbaik dari kedua dunia", memungkinkan untuk vektorisasi tanpa semua overhead yang tidak diperlukan dari fungsi panda. Ini berarti Anda dapat menulis ulang operasi di atas sebagai
df[df.A.values != df.B.values]
Yang mengungguli kedua pandas dan pemahaman daftar yang setara:
Vektorisasi NumPy berada di luar cakupan posting ini, tetapi pasti layak dipertimbangkan, jika kinerja penting.
Nilai Hitungan
Mengambil contoh lain - kali ini, dengan konstruksi python vanilla lain yang lebih cepat daripada for loop - collections.Counter
. Persyaratan umum adalah menghitung jumlah nilai dan mengembalikan hasilnya sebagai kamus. Hal ini dilakukan dengan value_counts
, np.unique
, danCounter
:
# Value Counts comparison.
ser.value_counts(sort=False).to_dict() # value_counts
dict(zip(*np.unique(ser, return_counts=True))) # np.unique
Counter(ser) # Counter
Hasilnya lebih jelas, Counter
menang atas kedua metode vektorisasi untuk rentang yang lebih besar dari N kecil (~ 3500).
Catatan
Lebih banyak hal sepele (courtesy @ user2357112). Ini Counter
diimplementasikan dengan a akselerator C , jadi meskipun masih harus bekerja dengan objek python alih-alih tipe data C yang mendasarinya, ini masih lebih cepat daripada for
loop. Kekuatan Python!
Tentu saja, yang dapat diambil dari sini adalah bahwa kinerjanya bergantung pada data dan kasus penggunaan Anda. Inti dari contoh-contoh ini adalah untuk meyakinkan Anda agar tidak mengesampingkan solusi ini sebagai opsi yang sah. Jika ini masih tidak memberikan kinerja yang Anda butuhkan, selalu ada cython dan numba . Mari tambahkan tes ini ke dalam campuran.
from numba import njit, prange
@njit(parallel=True)
def get_mask(x, y):
result = [False] * len(x)
for i in prange(len(x)):
result[i] = x[i] != y[i]
return np.array(result)
df[get_mask(df.A.values, df.B.values)] # numba
Numba menawarkan kompilasi JIT kode python loopy menjadi kode vektorisasi yang sangat kuat. Memahami cara membuat numba berhasil melibatkan kurva pembelajaran.
Operasi dengan Campuran / object
tipe
Perbandingan Berbasis String
Melihat kembali contoh pemfilteran dari bagian pertama, bagaimana jika kolom yang dibandingkan adalah string? Pertimbangkan 3 fungsi yang sama di atas, tetapi dengan input DataFrame dilemparkan ke string.
# Boolean indexing with string value comparison.
df[df.A != df.B] # vectorized !=
df.query('A != B') # query (numexpr)
df[[x != y for x, y in zip(df.A, df.B)]] # list comp
Jadi, apa yang berubah? Hal yang perlu diperhatikan di sini adalah bahwa operasi string pada dasarnya sulit untuk melakukan vektorisasi. Pandas memperlakukan string sebagai objek, dan semua operasi pada objek kembali ke implementasi yang lambat dan gila-gilaan.
Sekarang, karena implementasi gila ini dikelilingi oleh semua overhead yang disebutkan di atas, ada perbedaan besaran yang konstan di antara solusi ini, meskipun skalanya sama.
Ketika datang ke operasi pada objek yang bisa berubah / kompleks, tidak ada perbandingan. Pemahaman daftar mengungguli semua operasi yang melibatkan dicts dan daftar.
Mengakses Nilai Kamus dengan Kunci
Berikut adalah pengaturan waktu untuk dua operasi yang mengekstrak nilai dari kolom kamus: map
dan pemahaman daftar. Setup ada di Appendix, di bawah judul "Code Snippets".
# Dictionary value extraction.
ser.map(operator.itemgetter('value')) # map
pd.Series([x.get('value') for x in ser]) # list comprehension
Posisi Daftar Indexing
Timing untuk 3 operasi yang mengekstrak elemen 0 dari daftar kolom (penanganan pengecualian), map
, str.get
accessor metode , dan daftar pemahaman:
# List positional indexing.
def get_0th(lst):
try:
return lst[0]
# Handle empty lists and NaNs gracefully.
except (IndexError, TypeError):
return np.nan
ser.map(get_0th) # map
ser.str[0] # str accessor
pd.Series([x[0] if len(x) > 0 else np.nan for x in ser]) # list comp
pd.Series([get_0th(x) for x in ser]) # list comp safe
Catatan
Jika indeks penting, Anda ingin melakukan:
pd.Series([...], index=ser.index)
Saat merekonstruksi seri.
List Flattening
Contoh terakhir adalah meratakan daftar. Ini adalah masalah umum lainnya, dan menunjukkan betapa kuatnya python murni di sini.
# Nested list flattening.
pd.DataFrame(ser.tolist()).stack().reset_index(drop=True) # stack
pd.Series(list(chain.from_iterable(ser.tolist()))) # itertools.chain
pd.Series([y for x in ser for y in x]) # nested list comp
Keduanya itertools.chain.from_iterable
dan pemahaman daftar bersarang adalah konstruksi python murni, dan skalanya jauh lebih baik daripada stack
solusi.
Pengaturan waktu ini merupakan indikasi kuat dari fakta bahwa panda tidak dilengkapi peralatan untuk bekerja dengan tipe-tipe campuran, dan Anda mungkin sebaiknya menahan diri untuk tidak menggunakannya. Jika memungkinkan, data harus disajikan sebagai nilai skalar (ints / floats / string) di kolom terpisah.
Terakhir, penerapan solusi ini sangat bergantung pada data Anda. Jadi, hal terbaik yang harus dilakukan adalah menguji operasi ini pada data Anda sebelum memutuskan apa yang akan digunakan. Perhatikan bagaimana saya tidak apply
menentukan waktu pada solusi ini, karena itu akan membuat grafik miring (ya, selambat itu).
Operasi Regex, dan .str
Metode Accessor
Panda dapat menerapkan operasi regex seperti str.contains
, str.extract
, dan str.extractall
, serta "Vectorized" operasi string lainnya (seperti str.split
, str.find,
str.translate`, dan sebagainya) pada kolom string. Fungsi-fungsi ini lebih lambat daripada pemahaman daftar, dan dimaksudkan sebagai fungsi yang lebih memudahkan daripada yang lainnya.
Biasanya jauh lebih cepat untuk melakukan pra-kompilasi pola regex dan melakukan iterasi pada data Anda dengan re.compile
(juga lihat Apakah layak menggunakan kompilasi ulang Python? ). Daftar comp setara dengan str.contains
terlihat seperti ini:
p = re.compile(...)
ser2 = pd.Series([x for x in ser if p.search(x)])
Atau,
ser2 = ser[[bool(p.search(x)) for x in ser]]
Jika Anda perlu menangani NaN, Anda dapat melakukan sesuatu seperti
ser[[bool(p.search(x)) if pd.notnull(x) else False for x in ser]]
Daftar comp yang setara dengan str.extract
(tanpa grup) akan terlihat seperti ini:
df['col2'] = [p.search(x).group(0) for x in df['col']]
Jika Anda perlu menangani no-match dan NaN, Anda bisa menggunakan fungsi custom (masih lebih cepat!):
def matcher(x):
m = p.search(str(x))
if m:
return m.group(0)
return np.nan
df['col2'] = [matcher(x) for x in df['col']]
The matcher
Fungsi ini sangat extensible. Ini dapat dipasang untuk mengembalikan daftar untuk setiap grup tangkapan, sesuai kebutuhan. Cukup ekstrak kueri group
ataugroups
atribut dari objek matcher.
Untuk str.extractall
, ganti p.search
ke p.findall
.
Ekstraksi String
Pertimbangkan operasi pemfilteran sederhana. Idenya adalah untuk mengekstrak 4 digit jika diawali dengan huruf besar.
# Extracting strings.
p = re.compile(r'(?<=[A-Z])(\d{4})')
def matcher(x):
m = p.search(x)
if m:
return m.group(0)
return np.nan
ser.str.extract(r'(?<=[A-Z])(\d{4})', expand=False) # str.extract
pd.Series([matcher(x) for x in ser]) # list comprehension
Lebih Banyak Contoh
Pengungkapan penuh - Saya adalah penulis (sebagian atau keseluruhan) posting yang tercantum di bawah ini.
Kesimpulan
Seperti yang ditunjukkan dari contoh di atas, iterasi bersinar saat bekerja dengan baris kecil DataFrame, tipe data campuran, dan ekspresi reguler.
Percepatan yang Anda dapatkan bergantung pada data dan masalah Anda, jadi jarak tempuh Anda mungkin berbeda. Hal terbaik yang harus dilakukan adalah menjalankan pengujian dengan hati-hati dan melihat apakah pembayarannya sepadan.
Fungsi "vektorisasi" bersinar dalam kesederhanaan dan keterbacaannya, jadi jika kinerja tidak kritis, Anda harus lebih memilihnya.
Catatan samping lain, operasi string tertentu menangani kendala yang mendukung penggunaan NumPy. Berikut adalah dua contoh di mana vektorisasi NumPy yang cermat mengungguli python:
Selain itu, kadang-kadang hanya beroperasi pada array yang mendasari melalui .values
sebagai lawan pada Seri atau DataFrames dapat menawarkan percepatan yang cukup sehat untuk sebagian besar skenario (lihat Catatan di bagian Perbandingan Numerik di atas). Jadi, misalnya df[df.A.values != df.B.values]
akan menunjukkan peningkatan kinerja instan df[df.A != df.B]
. Menggunakan.values
mungkin tidak selalu sesuai di setiap situasi, tetapi ini adalah peretasan yang berguna untuk diketahui.
Seperti disebutkan di atas, terserah Anda untuk memutuskan apakah solusi ini sepadan dengan penerapannya.
Lampiran: Cuplikan Kode
import perfplot
import operator
import pandas as pd
import numpy as np
import re
from collections import Counter
from itertools import chain
# Boolean indexing with Numeric value comparison.
perfplot.show(
setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B']),
kernels=[
lambda df: df[df.A != df.B],
lambda df: df.query('A != B'),
lambda df: df[[x != y for x, y in zip(df.A, df.B)]],
lambda df: df[get_mask(df.A.values, df.B.values)]
],
labels=['vectorized !=', 'query (numexpr)', 'list comp', 'numba'],
n_range=[2**k for k in range(0, 15)],
xlabel='N'
)
# Value Counts comparison.
perfplot.show(
setup=lambda n: pd.Series(np.random.choice(1000, n)),
kernels=[
lambda ser: ser.value_counts(sort=False).to_dict(),
lambda ser: dict(zip(*np.unique(ser, return_counts=True))),
lambda ser: Counter(ser),
],
labels=['value_counts', 'np.unique', 'Counter'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=lambda x, y: dict(x) == dict(y)
)
# Boolean indexing with string value comparison.
perfplot.show(
setup=lambda n: pd.DataFrame(np.random.choice(1000, (n, 2)), columns=['A','B'], dtype=str),
kernels=[
lambda df: df[df.A != df.B],
lambda df: df.query('A != B'),
lambda df: df[[x != y for x, y in zip(df.A, df.B)]],
],
labels=['vectorized !=', 'query (numexpr)', 'list comp'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# Dictionary value extraction.
ser1 = pd.Series([{'key': 'abc', 'value': 123}, {'key': 'xyz', 'value': 456}])
perfplot.show(
setup=lambda n: pd.concat([ser1] * n, ignore_index=True),
kernels=[
lambda ser: ser.map(operator.itemgetter('value')),
lambda ser: pd.Series([x.get('value') for x in ser]),
],
labels=['map', 'list comprehension'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# List positional indexing.
ser2 = pd.Series([['a', 'b', 'c'], [1, 2], []])
perfplot.show(
setup=lambda n: pd.concat([ser2] * n, ignore_index=True),
kernels=[
lambda ser: ser.map(get_0th),
lambda ser: ser.str[0],
lambda ser: pd.Series([x[0] if len(x) > 0 else np.nan for x in ser]),
lambda ser: pd.Series([get_0th(x) for x in ser]),
],
labels=['map', 'str accessor', 'list comprehension', 'list comp safe'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# Nested list flattening.
ser3 = pd.Series([['a', 'b', 'c'], ['d', 'e'], ['f', 'g']])
perfplot.show(
setup=lambda n: pd.concat([ser2] * n, ignore_index=True),
kernels=[
lambda ser: pd.DataFrame(ser.tolist()).stack().reset_index(drop=True),
lambda ser: pd.Series(list(chain.from_iterable(ser.tolist()))),
lambda ser: pd.Series([y for x in ser for y in x]),
],
labels=['stack', 'itertools.chain', 'nested list comp'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
# Extracting strings.
ser4 = pd.Series(['foo xyz', 'test A1234', 'D3345 xtz'])
perfplot.show(
setup=lambda n: pd.concat([ser4] * n, ignore_index=True),
kernels=[
lambda ser: ser.str.extract(r'(?<=[A-Z])(\d{4})', expand=False),
lambda ser: pd.Series([matcher(x) for x in ser])
],
labels=['str.extract', 'list comprehension'],
n_range=[2**k for k in range(0, 15)],
xlabel='N',
equality_check=None
)
pd.Series
danpd.DataFrame
sekarang mendukung konstruksi dari iterable. Itu berarti seseorang dapat dengan mudah meneruskan generator Python ke fungsi konstruktor daripada perlu membuat daftar terlebih dahulu (menggunakan pemahaman daftar), yang bisa lebih lambat dalam banyak kasus. Namun besarnya keluaran genset tidak dapat ditentukan sebelumnya. Saya tidak yakin berapa banyak waktu / memori overhead yang akan terjadi.