Pandalardaki for-loop'lar gerçekten kötü mü? Ne zaman umursamalıyım?


108

Are fordöngüler gerçekten "kötü"? Değilse, hangi durumlarda daha geleneksel bir "vektörleştirilmiş" yaklaşım kullanmaktan daha iyi olurlar? 1

"Vektörleştirme" kavramına ve pandaların hesaplamayı hızlandırmak için vektörleştirilmiş teknikleri nasıl kullandıklarına aşinayım. Vectorized işlevler, veriler üzerinde geleneksel olarak yinelemekten çok daha fazla hızlanma elde etmek için tüm seri veya DataFrame üzerinde operasyonları yayınlar.

Ancak, fordöngüleri ve liste anlamalarını kullanarak veriler arasında döngü oluşturmayı içeren sorunlara çözümler sunan çok sayıda kod (Stack Overflow'daki yanıtlar dahil) görmek beni oldukça şaşırttı . Belgeler ve API, döngülerin "kötü" olduğunu ve dizilerin, dizilerin veya DataFrame'lerin "asla" yinelenmemesi gerektiğini söylüyor. Öyleyse, nasıl bazen kullanıcıların döngü tabanlı çözümler önerdiğini görüyorum?


1 - Sorunun kulağa biraz geniş geldiği doğru olsa da, gerçek şu ki, fordöngülerin genellikle geleneksel olarak veri üzerinde yinelemekten daha iyi olduğu çok özel durumlar vardır. Bu gönderi, bunu gelecek nesil için yakalamayı amaçlamaktadır.

Yanıtlar:


146

TLDR; Hayır, fordö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:

  1. Verileriniz küçük olduğunda (... ne yaptığınıza bağlı olarak),
  2. object/ Karışık tipler ile uğraşırken
  3. 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.

  1. Dizin / eksen hizalaması
  2. Karışık veri türlerini işleme
  3. 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ı ( fordö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]

seqPandalar 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 seq1ve nerede seq2sü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, perfplotbu 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:

görüntü açıklamasını buraya girin

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.uniqueve 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

görüntü açıklamasını buraya girin

Sonuçlar daha belirgindir, Counterdaha 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). CounterBir 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, fordö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

görüntü açıklamasını buraya girin

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 / objectdtype 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

görüntü açıklamasını buraya girin

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: mapve 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

görüntü açıklamasını buraya girin

Konumsal Liste İndeksleme
Zamanlamaları, 0'ıncı öğeyi bir sütun listesinden (istisnaları işleme) map, str.geteriş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.

görüntü açıklamasını buraya girin

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

görüntü açıklamasını buraya girin

Hem itertools.chain.from_iterableve 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. applyBu çözümlere zaman ayırmadığıma dikkat edin , çünkü grafiği çarpıtır (evet, o kadar yavaş).


Regex İşlemleri ve .strErişimci Yöntemleri

Pandalar gibi regex işlemlerini uygulayabilir str.contains, str.extractve str.extractallyanı sıra (örneğin öteki "vectorized" string işlemleri str.splitstr.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']]

matcherFonksiyonu ç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 groupveya groupsözniteliğini çıkarın .

İçin str.extractall, değiştirmek p.searchiç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

görüntü açıklamasını buraya girin

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 .valuesen 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 .valuesher 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.Seriesve pd.DataFrameartı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.
GZ0

1
@ GZ0 IIRC, yineleyicileri kabul etmek, API'ye daha yeni bir ektir. "Bu, önce bir liste oluşturmaya ihtiyaç duymaktan ziyade yapıcı işlevlere basitçe bir Python üreteci geçirilebileceği anlamına" gelince, buna katılmıyorum. Hafıza belki, performans hayır. Deneyimlerime göre, listeyi oluşturmak ve aktarmak genellikle neredeyse her zaman daha hızlıdır. FTW kompozisyonlarını listeleyin.
cs95

@ cs95 Neden jeneratörlerin performans kazanımı sağlayacağını düşünmüyorsunuz? Yoksa bununla ilgili herhangi bir test yaptın mı?
GZ0

@ GZ0 Jeneratörlerin performans kazanımı sağlamadığını söylemedim, liste anlamaları kullanarak yaptığınız kadar kazanmadığınızı söylüyorum. Genexps'i başlatmak, durumu korumak, vb. İle ilişkili, listelerde bulunmayan ek yükler vardır. Burada, jeneratör anlayışlarına göre karşılaştırmak için zaman ayırabileceğiniz liste anlayışları ile birçok örnek var. İşte şimdi çalıştırabileceğiniz bir örnek ser = pd.Series([['a', 'b', 'c'], ['d', 'e'], ['f', 'g']] * 10000):; %timeit pd.Series(y for x in ser for y in x); %timeit pd.Series([y for x in ser for y in x])
cs95

1
@ cs95 Not bilinen boyutlu Iterables için, bu inşa etmek daha hızlı olacağını pd.Seriesörneğin listelerine dönüştürerek yerine doğrudan onlardan pd.Series(range(10000)), pd.Series("a" * 10000)ve pd.Series(pd.Index(range(10000)))çok daha hızlı bir listeleri muadillerine göre daha olurdu (sonuncusu daha hızlı göre biraz bile oldu pd.Index.to_series.
GZ0

1

Kısacası

  • for loop + iterrowsson derece yavaştır. Ek yük ~ 1k satırda önemli değil, 10k + satırlarda fark edilebilir.
  • for loop + itertuples, iterrowsveya değerinden çok daha hızlıdır apply.
  • vektörleştirme genellikle daha hızlıdır itertuples

Kıyaslama görüntü açıklamasını buraya girin

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.