TLDR; Hayır, for
döngüler her zaman değil, en azından "kötü" değildir. Bazı vektörleştirilmiş işlemlerin yinelemeden daha yavaş olduğunu söylemek, yinelemenin bazı vektörleştirilmiş işlemlerden daha hızlı olduğunu söylemek muhtemelen daha doğrudur . Ne zaman ve neden kodunuzdan en yüksek performansı almanın anahtarıdır. Özetle, vektörize edilmiş pandalar işlevlerine bir alternatif düşünmeye değer durumlar şunlardır:
- Verileriniz küçük olduğunda (... ne yaptığınıza bağlı olarak),
object
/ Karışık tipler ile uğraşırken
- Kullanırken
str
/ regex erişimci işlevlerini
Bu durumları ayrı ayrı inceleyelim.
Küçük Verilerde Yineleme V / s Vektörleştirme
Pandas , API tasarımında "Konfigürasyon Üzerinden Kuralı" yaklaşımı izler . Bu, aynı API'nin geniş bir veri yelpazesine ve kullanım durumlarına hitap edecek şekilde yerleştirildiği anlamına gelir.
Bir panda işlevi çağrıldığında, çalışmasını sağlamak için aşağıdaki şeyler (diğerleri arasında) işlev tarafından dahili olarak ele alınmalıdır.
- Dizin / eksen hizalaması
- Karışık veri türlerini işleme
- Eksik verilerin ele alınması
Hemen hemen her işlev, bunlarla değişen düzeylerde başa çıkmak zorunda kalacaktır ve bu bir ek yük sunar . Ek yük, sayısal işlevler için daha azdır (örneğin, Series.add
), oysa dizi işlevleri için daha belirgindir (örneğin, Series.str.replace
).
for
Öte yandan döngüler düşündüğünüzden daha hızlıdır. Daha da iyisi, liste kavramaları ( for
döngüler aracılığıyla listeler oluşturan ) liste oluşturma için optimize edilmiş yinelemeli mekanizmalar olduklarından daha da hızlıdır.
Liste anlayışları kalıbı takip eder
[f(x) for x in seq]
seq
Pandalar serisi veya DataFrame sütunu nerede . Veya birden çok sütun üzerinde çalışırken,
[f(x, y) for x, y in zip(seq1, seq2)]
Nerede seq1
ve nerede seq2
sütunlar.
Sayısal Karşılaştırma
Basit bir mantıksal indeksleme işlemini düşünün. Liste anlama yöntemi, Series.ne
( !=
) ve query
. İşte fonksiyonlar:
# 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
Basit olması için, perfplot
bu gönderideki tüm zaman testlerini çalıştırmak için paketi kullandım . Yukarıdaki işlemler için zamanlamalar aşağıdadır:
Liste anlama query
, orta büyüklükteki N için daha iyi performans gösterir ve hatta küçük N için karşılaştırmaya eşit değil, vektörleştirilenlerden daha iyi performans gösterir.
Not
Listeyi anlamanın yararlarının çoğunun, dizin hizalaması hakkında endişelenmenize gerek kalmamasından kaynaklandığını belirtmek gerekir, ancak bu, kodunuz dizin hizalamasına bağlıysa, bunun bozulacağı anlamına gelir. Bazı durumlarda, temeldeki NumPy dizileri üzerindeki vektörleştirilmiş işlemler , pandas işlevlerinin tüm gereksiz ek yükü olmadan vektörleştirmeye izin vererek "her iki dünyanın en iyilerini" getirdiği düşünülebilir . Bu, yukarıdaki işlemi şu şekilde yeniden yazabileceğiniz anlamına gelir:
df[df.A.values != df.B.values]
Hem pandaları hem de liste anlama eşdeğerlerinden daha iyi performans gösteren:
NumPy vektörleştirme bu yazının kapsamı dışındadır, ancak performans önemliyse kesinlikle dikkate alınmaya değer.
Değer Önemlidir
Başka bir örnekle - bu sefer for döngüsünden daha hızlı olan başka bir vanilya python yapısıyla - collections.Counter
. Ortak bir gereksinim, değer sayılarını hesaplamak ve sonucu bir sözlük olarak döndürmektir. Bu yapılır value_counts
, np.unique
ve Counter
:
# 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
Sonuçlar daha belirgindir, Counter
daha geniş bir küçük N (~ 3500) aralığı için her iki vektörleştirilmiş yöntemi de kazanır.
Not
Daha fazla bilgi (nezaket @ user2357112). Counter
Bir ile uygulanmaktadır C hızlandırıcı daha hızlı bir daha hala, piton yerine altta yatan C veritipinin nesneleri ile hala işe sahiptir böylece iken, for
döngü. Python gücü!
Elbette buradan çıkarılacak şey, performansın verilerinize ve kullanım durumunuza bağlı olmasıdır. Bu örneklerin amacı, sizi bu çözümleri meşru seçenekler olarak göz ardı etmemeye ikna etmektir. Bunlar hala ihtiyacınız olan performansı vermiyorsa, her zaman cython ve numba vardır . Bu testi karışıma ekleyelim.
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, çok güçlü vektörleştirilmiş kod için döngüsel python kodunun JIT derlemesini sunar. Numba'nın nasıl çalıştığını anlamak bir öğrenme eğrisi içerir.
Karışık / object
dtype ile işlemler
Dizeye Dayalı Karşılaştırma İlk bölümdeki filtreleme örneğini tekrar gözden geçirirsek, karşılaştırılan sütunlar dizeler ise ne olur? Yukarıdaki aynı 3 işlevi düşünün, ancak DataFrame girdisi dizeye dönüştürülmüştür.
# 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
Peki ne değişti? Burada dikkat edilmesi gereken nokta, dize işlemlerinin doğal olarak vektörleştirilmesinin zor olduğudur. Pandalar dizeleri nesne olarak görür ve nesneler üzerindeki tüm işlemler yavaş, döngüsel bir uygulamaya geri döner.
Şimdi, bu döngüsel uygulama yukarıda bahsedilen tüm ek yüklerle çevrili olduğundan, aynı ölçeklendirilse de bu çözümler arasında sabit bir büyüklük farkı vardır.
Değişken / karmaşık nesneler üzerindeki işlemler söz konusu olduğunda, hiçbir karşılaştırma yoktur. Liste anlama, dikteler ve listeler içeren tüm işlemlerden daha iyi performans gösterir.
Anahtar ile Sözlük Değerlerine Erişim
Burada, bir sözlükler sütunundan bir değer çıkaran iki işlem için zamanlamalar verilmiştir: map
ve liste kavrama. Kurulum, Ek'te "Kod Parçacıkları" başlığı altında yer almaktadır.
# Dictionary value extraction.
ser.map(operator.itemgetter('value')) # map
pd.Series([x.get('value') for x in ser]) # list comprehension
Konumsal Liste İndeksleme
Zamanlamaları, 0'ıncı öğeyi bir sütun listesinden (istisnaları işleme) map
, str.get
erişimci yönteminden ve liste kavrayışından çıkaran 3 işlem için :
# 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
Not
Dizin önemliyse, şunları yapmak istersiniz:
pd.Series([...], index=ser.index)
Seriyi yeniden oluştururken.
Liste Düzleştirme
Son bir örnek, listeleri düzleştirmedir. Bu başka bir yaygın sorundur ve burada ne kadar güçlü saf piton olduğunu gösterir.
# 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
Hem itertools.chain.from_iterable
ve hem de iç içe geçmiş liste anlayışı saf python yapılardır ve stack
çözümden çok daha iyi ölçeklenir .
Bu zamanlamalar, pandaların karışık tiplerle çalışmak için donanımlı olmadığının ve bunu yapmak için muhtemelen kullanmaktan kaçınmanız gerektiğinin güçlü bir göstergesidir. Mümkün olan her yerde, veriler ayrı sütunlarda skaler değerler (ints / floats / string) olarak bulunmalıdır.
Son olarak, bu çözümlerin uygulanabilirliği büyük ölçüde verilerinize bağlıdır. Bu nedenle, yapılacak en iyi şey, neyle gideceğinize karar vermeden önce bu işlemleri verileriniz üzerinde test etmek olacaktır. apply
Bu çözümlere zaman ayırmadığıma dikkat edin , çünkü grafiği çarpıtır (evet, o kadar yavaş).
Regex İşlemleri ve .str
Erişimci Yöntemleri
Pandalar gibi regex işlemlerini uygulayabilir str.contains
, str.extract
ve str.extractall
yanı sıra (örneğin öteki "vectorized" string işlemleri str.split
str.find, ,
dize sütunlarda, vb str.translate`). Bu işlevler, liste anlamalarından daha yavaştır ve her şeyden daha kullanışlı işlevler olmaları amaçlanmıştır.
Bir normal ifade kalıbını önceden derlemek ve verilerinizi yinelemek genellikle çok daha hızlıdır re.compile
(ayrıca Python'un yeniden yığınını kullanmaya değer mi? Bölümüne bakın ). Şuna eşdeğer liste kompozisyonu str.contains
şuna benzer:
p = re.compile(...)
ser2 = pd.Series([x for x in ser if p.search(x)])
Veya,
ser2 = ser[[bool(p.search(x)) for x in ser]]
NaN'leri işlemeniz gerekiyorsa, aşağıdaki gibi bir şey yapabilirsiniz:
ser[[bool(p.search(x)) if pd.notnull(x) else False for x in ser]]
str.extract
(Gruplar olmadan) ile eşdeğer liste kompozisyonu şöyle görünecektir:
df['col2'] = [p.search(x).group(0) for x in df['col']]
Eşleşme olmayanları ve NaN'leri işlemeniz gerekiyorsa, özel bir işlev kullanabilirsiniz (daha da hızlı!):
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']]
matcher
Fonksiyonu çok genişletilebilir. Gerektiğinde her bir yakalama grubu için bir liste döndürmek üzere yerleştirilebilir. Sadece eşleştirici nesnenin sorgusunu group
veya groups
özniteliğini çıkarın .
İçin str.extractall
, değiştirmek p.search
için p.findall
.
Dize Çıkarma
Basit bir filtreleme işlemini düşünün. Buradaki fikir, önünde bir büyük harf varsa 4 rakam çıkarmaktır.
# 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
Diğer Örnekler
Tam açıklama - Aşağıda listelenen bu yazıların (kısmen veya tamamen) yazarıyım.
Sonuç
Yukarıdaki örneklerde gösterildiği gibi yineleme, küçük DataFrame satırları, karma veri türleri ve normal ifadelerle çalışırken parlar.
Aldığınız hızlanma verilerinize ve sorununuza bağlıdır, bu nedenle kilometreniz değişebilir. Yapılacak en iyi şey, testleri dikkatlice çalıştırmak ve ödemenin çabaya değip değmeyeceğini görmektir.
"Vektörize edilmiş" fonksiyonlar basitlikleri ve okunabilirlikleri ile parlıyor, bu nedenle performans kritik değilse, kesinlikle onları tercih etmelisiniz.
Başka bir not, belirli dizgi işlemleri NumPy'nin kullanımını destekleyen kısıtlamalarla ilgilenir. Dikkatli NumPy vektörleştirmenin python'dan daha iyi performans gösterdiği iki örnek:
Ayrıca, bazen sadece aracılığıyla yatan diziler üzerinde çalışan .values
en olağan senaryolar için sağlıklı yeterince hıza sunabilir Series veya DataFrames konumdan farklı olarak (bkz Not içinde Sayısal Karşılaştırma Yukarıdaki bölümde). Yani, örneğin df[df.A.values != df.B.values]
anlık performans artışlarının bittiğini gösterir df[df.A != df.B]
. Kullanmak .values
her durumda uygun olmayabilir, ancak bilinmesi faydalı bir hack'tir.
Yukarıda belirtildiği gibi, bu çözümlerin uygulama zahmetine değip değmeyeceğine karar vermek size kalmıştır.
Ek: Kod Parçacıkları
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
vepd.DataFrame
artık yinelemelerden oluşturmayı destekliyor. Bu, ilk önce bir liste oluşturmaya ihtiyaç duymak yerine (liste anlayışlarını kullanarak) yapıcı işlevlerine basitçe bir Python üreteci geçirilebileceği anlamına gelir, ki bu çoğu durumda daha yavaş olabilir. Ancak jeneratör çıktısının boyutu önceden belirlenemez. Bunun ne kadar zaman / bellek yüküne neden olacağından emin değilim.