Pandas apply () 'ı kodumda ne zaman kullanmak isteyebilirim?


115

Pandas yönteminin kullanımını içeren Stack Overflow'daki sorulara birçok yanıt gönderildi apply. Ayrıca, " applyyavaş ve kaçınılmalıdır" diyen kullanıcıların da altlarına yorum yaptığını gördüm .

Performans konusunda applyyavaş olduğunu açıklayan birçok makale okudum . Belgelerde apply, UDF'leri geçmek için nasıl basitçe bir kolaylık işlevi olduğuna dair bir feragatname de gördüm (şimdi bulamıyorum). Dolayısıyla, genel fikir birliği applymümkünse bundan kaçınılması gerektiğidir. Ancak bu, aşağıdaki soruları gündeme getirir:

  1. Eğer applyçok kötü, niye API nedir?
  2. Kodumu nasıl ve ne zaman applyücretsiz yapmalıyım ?
  3. Herhangi durumlar nereye kadar var applyolan iyi (daha iyi diğer olası çözümlere göre)?

1
returns.add(1).apply(np.log)vs np.log(returns.add(1)bir durumdur applyaltında JPP en şemada sağ alt yeşil kutu genellikle marjinal hızlı olacaktır.
Alexander

@Alexander teşekkürler. Bu durumlara ayrıntılı bir şekilde işaret etmedi, ancak bilmekte fayda var!
cs95

Yanıtlar:


112

apply, Hiç İhtiyaç Duymadığınız Rahatlık Fonksiyonu

OP'deki soruları tek tek ele alarak başlıyoruz.

" Eğer uygulamak o zaman neden API onu, bu yüzden kötü? "

DataFrame.applyve Series.applyolan kolaylık fonksiyonları sırasıyla nesne DataFrame ve Series üzerinde tanımlı. applyDataFrame üzerinde bir dönüştürme / toplama uygulayan herhangi bir kullanıcı tanımlı işlevi kabul eder. applyetkin bir şekilde, mevcut herhangi bir pandanın yapamadığı şeyi yapan sihirli bir değnekdir.

Bazı şeyler applyyapabilir:

  • DataFrame veya Series üzerinde herhangi bir kullanıcı tanımlı işlevi çalıştırın
  • DataFrame'de satır bazında ( axis=1) veya sütun bazında ( axis=0) bir işlev uygulayın
  • İşlevi uygularken dizin hizalaması gerçekleştirin
  • Kullanıcı tanımlı işlevlerle toplama gerçekleştirin (ancak, genellikle tercih ederiz aggveya transformbu durumlarda)
  • Öğe bazlı dönüşümler gerçekleştirin
  • Birleştirilmiş sonuçları orijinal satırlara yayınlayın ( result_typeargümana bakın ).
  • Kullanıcı tanımlı işlevlere iletmek için konumsal / anahtar sözcük bağımsız değişkenlerini kabul edin.

... diğerleri arasında. Daha fazla bilgi için, belgelerdeki Satır veya Sütun Bazında İşlev Uygulaması'na bakın.

Peki, tüm bu özelliklerle neden applykötü? Öyle çünkü applyolduğunu yavaş . Pandalar, işlevinizin doğası hakkında hiçbir varsayımda bulunmaz ve bu nedenle işlevinizi gerektiğinde her satıra / sütuna yinelemeli olarak uygular . Ek olarak, yukarıdaki tüm durumların ele alınması apply, her yinelemede bazı büyük ek yüklere neden olur. Dahası, applyçok daha fazla bellek tüketir, bu da belleğe bağlı uygulamalar için bir zorluktur.

Kullanmanın uygun olduğu çok az durum vardır apply(daha fazlası aşağıdadır). Kullanmanız gerekip gerekmediğinden emin değilseniz apply, muhtemelen kullanmamalısınız .


Bir sonraki soruyu ele alalım.

" Kodumu nasıl ve ne zaman - ücretsiz uygulamalıyım ? "

Yeniden ifade etmek gerekirse, burada herhangi bir çağrıdan kurtulmak isteyeceğiniz bazı genel durumlar verilmiştir apply.

Sayısal Veriler

Sayısal verilerle çalışıyorsanız, muhtemelen tam olarak yapmaya çalıştığınız şeyi yapan vektörleştirilmiş bir cython işlevi vardır (değilse, lütfen Stack Overflow'da bir soru sorun veya GitHub'da bir özellik isteği açın).

applyBasit bir ekleme işlemi için performansını karşılaştırın .

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

Performans açısından karşılaştırma yok, cythonized eşdeğeri çok daha hızlı. Bir grafiğe gerek yoktur, çünkü oyuncak verileri için bile fark açıktır.

%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)

rawArgümanla ham dizileri geçirmeyi etkinleştirseniz bile , hala iki kat daha yavaştır.

%timeit df.apply(np.sum, raw=True)
840 µs ± 691 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Başka bir örnek:

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)

Genel olarak, mümkünse vektörleştirilmiş alternatifler arayın.

Dize / Normal İfade

Pandalar çoğu durumda "vektörleştirilmiş" dizgi işlevleri sağlar, ancak bu işlevlerin deyim yerindeyse "uygulanmadığı" ender durumlar vardır.

Yaygın bir sorun, bir sütundaki bir değerin aynı satırın başka bir sütununda bulunup bulunmadığını kontrol etmektir.

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

"Donald" ve "minnie", ilgili "Başlık" sütunlarında mevcut olduğundan, bu ikinci ve üçüncü satırı döndürmelidir.

Apply kullanarak, bu,

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

Ancak, liste anlamaları kullanılarak daha iyi bir çözüm vardır.

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)

Burada dikkat edilmesi gereken nokta, yinelemeli rutinlerin daha applydüşük ek yük nedeniyle olduğundan daha hızlı olmasıdır. NaN'leri ve geçersiz dtype'ları işlemeniz gerekiyorsa, bunu özel bir işlev kullanarak inşa edebilirsiniz, daha sonra liste kavrayışı içindeki argümanlarla çağırabilirsiniz.

Liste anlamalarının ne zaman iyi bir seçenek olarak görülmesi gerektiği hakkında daha fazla bilgi için, yazıma bakın: Pandalarla olan döngüler için - Ne zaman önemsemeliyim? .

Not
Tarih ve tarih saat işlemlerinin vektörleştirilmiş sürümleri de vardır. Yani, örneğin pd.to_datetime(df['date']), diyelim ki, tercih etmelisiniz df['date'].apply(pd.to_datetime).

Dokümanlarda daha fazlasını okuyun .

Sık Karşılaşılan Bir Tuzak: Patlayan Liste Sütunları

s = pd.Series([[1, 2]] * 3)
s

0    [1, 2]
1    [1, 2]
2    [1, 2]
dtype: object

İnsanlar kullanmaya isteklidir apply(pd.Series). Bu performans açısından korkunç .

s.apply(pd.Series)

   0  1
0  1  2
1  1  2
2  1  2

Daha iyi bir seçenek, sütunu listelemek ve pd.DataFrame'e iletmektir.

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)

Son olarak,

" İyi olan herhangi bir durum var apply mı? "

Uygula bir kolaylık işlevidir, bu yüzden orada olan havai affetmek önemsiz yeterlidir durumlar. Bu gerçekten işlevin kaç kez çağrıldığına bağlıdır.

Seri için Vectorized olan ancak DataFrames olmayan fonksiyonlar
Bir dizgi işlemini birden çok sütuna uygulamak isterseniz ne olur? Birden çok sütunu tarih saatine dönüştürmek istiyorsanız ne olur? Bu işlevler yalnızca Seriler için vektörleştirilmiştir, bu nedenle dönüştürmek / üzerinde işlem yapmak istediğiniz her sütuna uygulanmaları gerekir .

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

Bu, aşağıdakiler için kabul edilebilir bir durumdur apply:

df.apply(pd.to_datetime, errors='coerce').dtypes

date1    datetime64[ns]
date2    datetime64[ns]
dtype: object

Bunun da mantıklı olacağını stackveya sadece açık bir döngü kullanacağını unutmayın. Tüm bu seçenekler kullanmaktan biraz daha hızlıdır apply, ancak fark affetmek için yeterince küçüktür.

%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)

Dize işlemleri veya kategoriye dönüştürme gibi diğer işlemler için benzer bir durum oluşturabilirsiniz.

u = df.apply(lambda x: x.str.contains(...))
v = df.apply(lambda x: x.astype(category))

vs

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)

Ve bunun gibi...

İçin Serisi dönüştürme str: astypeversusapply

Bu, API'nin kendine özgü bir özelliği gibi görünüyor. Bir applySerideki tam sayıları dizeye dönüştürmek için kullanmak , kullanmaktan karşılaştırılabilir (ve bazen daha hızlıdır) astype.

görüntü açıklamasını buraya girin Grafik, perfplotkütüphane kullanılarak çizildi .

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())

Floats ile, astypetutarlı bir şekilde kadar hızlı veya biraz daha hızlı olduğunu görüyorum apply. Yani bu, testteki verilerin tamsayı türü olmasıyla ilgilidir.

GroupBy zincirleme dönüşümlü işlemler

GroupBy.applyşimdiye kadar tartışılmadı, ancak GroupBy.applyaynı zamanda mevcut GroupByişlevlerin yapmadığı her şeyi ele almak için yinelemeli bir kolaylık işlevidir .

Yaygın bir gereksinim, bir GroupBy ve ardından "gecikmeli cumsum" gibi iki ana işlem gerçekleştirmektir:

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

Burada art arda iki grup görüşmesine ihtiyacınız olacak:

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

Kullanarak apply, bunu tek bir aramaya kısaltabilirsiniz.

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

Verilere bağlı olduğu için performansı ölçmek çok zordur. Ancak genel olarak, applyamaç bir groupbyaramayı azaltmaksa (çünkü groupbyaynı zamanda oldukça pahalıdır) kabul edilebilir bir çözümdür .


Diğer Uyarılar

Yukarıda belirtilen uyarıların yanı sıra apply, ilk satırda (veya sütunda) iki kez çalıştığını belirtmekte fayda var . Bu, fonksiyonun herhangi bir yan etkisinin olup olmadığını belirlemek için yapılır. Değilse apply, sonucu değerlendirmek için hızlı bir yol kullanabilir, aksi takdirde yavaş bir uygulamaya geri döner.

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

Bu davranış aynı zamanda GroupBy.apply<0.25 pandalar sürümlerinde de görülür (0.25 için düzeltilmiştir, daha fazla bilgi için buraya bakın .)


Bence dikkatli olmalıyız .. %timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')ilk iterasyondan sonra kesinlikle çok daha hızlı olacak çünkü datetime... datetime?
jpp

@jpp Ben de aynı endişeyi yaşadım. Ancak yine de her iki şekilde de doğrusal bir tarama yapmanız gerekir, dizelerde to_datetime çağrısı, daha hızlı değilse, datetime nesnelerinde çağırmak kadar hızlıdır. Basketbol sahası zamanlamaları aynıdır. Alternatif, ana noktadan uzaklaşan her zamanlanmış çözüm için bir ön kopyalama adımı uygulamak olabilir. Ancak bu geçerli bir endişedir.
cs95

" to_datetimeDizeleri aramak ... datetimenesneler kadar hızlıdır " .. gerçekten? I dataframe oluşturma (sabit maliyet dahil) applykarşı forhalka zamanlamaları ve fark çok daha küçüktür.
jpp

@jpp (Kuşkusuz sınırlı) testimden elde ettiğim şey buydu. Eminim verilere bağlıdır, ancak genel fikir, örnekleme amacıyla, farkın "cidden, bunun için endişelenmeyin" olduğudur.
cs95

1
@ cs95, Mutlu yıllar!
jpp

52

Hepsi applyaynı değil

Aşağıdaki tablo ne zaman dikkate alınması gerektiğini gösteriyor apply1 . Yeşil, muhtemelen verimli anlamına gelir; kırmızı kaçının.

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

Bunlardan bazıları sezgiseldir: pd.Series.applyPython düzeyinde satır bazında döngü, aynen pd.DataFrame.applysatır bazında ( axis=1). Bunların kötüye kullanımı çok ve geniş kapsamlıdır. Diğer gönderi onlarla daha derinlemesine ilgileniyor. Popüler çözümler, vektörleştirilmiş yöntemler, liste anlamaları (temiz verileri varsayar) veya pd.DataFramekurucu gibi verimli araçlar (örneğin kaçınmak için apply(pd.Series)) kullanmaktır.

pd.DataFrame.applySatır bazında kullanıyorsanız , raw=True(mümkünse) belirtmek genellikle yararlıdır. Bu aşamada numbagenellikle daha iyi bir seçimdir.

GroupBy.apply: genellikle tercih edilir

groupbyKaçınılması gereken işlemleri tekrarlamak applyperformansa zarar verecektir. GroupBy.applyÖzel işlevinizde kullandığınız yöntemlerin kendilerinin vektörleştirilmiş olması koşuluyla, burada genellikle iyidir. Bazen, uygulamak istediğiniz grup bazlı bir toplama için yerel Pandalar yöntemi yoktur. Bu durumda, applyözel bir işleve sahip az sayıda grup için yine de makul bir performans sunabilir.

pd.DataFrame.apply sütun açısından: karışık bir çanta

pd.DataFrame.applycolumn-wise ( axis=0) ilginç bir durumdur. Çok sayıda sütun yerine az sayıda satır için neredeyse her zaman pahalıdır. Sütunlara göre çok sayıda satır için, daha yaygın olan durum, bazen aşağıdakileri kullanarak önemli performans iyileştirmeleri görebilirsiniz 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 İstisnalar vardır, ancak bunlar genellikle marjinaldir veya nadirdir. Birkaç örnek:

  1. df['col'].apply(str)biraz daha iyi performans gösterebilir df['col'].astype(str).
  2. df.apply(pd.to_datetime)dizeler üzerinde çalışmak, normal fordöngü yerine satırlarla iyi ölçeklenmez .


1
@coldspeed, Teşekkürler, gönderinizde yanlış bir şey yok (benimki ile bazı çelişkili karşılaştırmalar dışında, ancak giriş veya kurulum tabanlı olabilir). Soruna bakmanın farklı bir yolu olduğunu hissettim.
jpp

@jpp Hep bir bugün görünce dek size bir yol gösterici olarak konum mükemmel akış kullanılan sıra sıraapply hızlı göre anlamlı olduğunu benim çözüm ile any. Bununla ilgili herhangi bir fikrin var mı?
Stef

1
@jpp: haklısınız: 1 milyon satır için x 100 sütun any, bundan yaklaşık 100 kat daha hızlıdır apply. İlk testlerimi 2000 sıra x 1000 sütunla yaptı ve işte applyiki kat daha hızlıydıany
Stef

1
@jpp Resminizi bir sunumda / makalede kullanmak istiyorum. Bu durumdan memnun musun? Açıkça kaynağından bahsedeceğim. Teşekkürler
Erfan

4

İçin axis=1(yani sıra sıra fonksiyonları) daha sonra sadece yerine aşağıdaki işlevi kullanabilirsiniz apply. Neden bu pandasdavranış olmadığını merak ediyorum . (Bileşik dizinler ile test edilmemiştir, ancak çok daha hızlı görünmektedir 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)

Bunun bazı durumlarda bana daha iyi performans sağladığını görünce çok şaşırdım. Her biri farklı sütun değerleri alt kümesine sahip birden çok şey yapmam gerektiğinde özellikle yararlı oldu. "Her şey aynı değildir" yanıtı, ne zaman yardımcı olabileceğini anlamanıza yardımcı olabilir, ancak verilerinizin bir örneği üzerinde test etmek çok zor değildir.
denson

1
Birkaç işaret: performans için bir liste anlama, for döngüsünden daha iyi performans gösterir; zip(df, row[1:])burada yeterlidir; gerçekten, bu aşamada, numbafunc'un sayısal bir hesaplama olup olmadığını düşünün . Açıklama için bu cevaba bakın .
jpp

@jpp - daha iyi bir işleve sahipseniz lütfen paylaşın. Analizime göre bunun optimal seviyeye oldukça yakın olduğunu düşünüyorum. Evet numbadaha hızlıdır, faster_df_applysadece eşdeğer bir şey isteyen ancak daha hızlı olan DataFrame.apply(tuhaf bir şekilde yavaş olan) insanlar içindir.
Pete Cacioppi

Bu aslında nasıl .applyuygulandığına çok yakın , ancak onu önemli ölçüde yavaşlatan bir şey yapıyor, esasen yapıyor: row = pd.Series({f:v for f,v in zip(cols, row[1:])})bu da çok fazla sürükleme ekliyor. Uygulamayı açıklayan bir cevap yazdım , ancak modası geçmiş olduğunu düşünüyorum, son sürümler Cython'dan yararlanmaya çalıştı .apply, inanıyorum (benden alıntı yapmayın)
juanpa.arrivillaga

2

applyİyi olan herhangi bir durum var mı? Evet bazen.

Görev: Unicode dizelerinin kodunu çöz.

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

Güncelleme
hiçbir şekilde kullanımını savunmuyordum apply, sadece NumPyyukarıdaki durumla başa çıkamayacağına göre, bunun için iyi bir aday olabileceğini düşündüm pandas apply. Ama @jpp'nin hatırlatıcısı sayesinde düz liste anlayışını unutuyordum.


Hayır. Bu nasıl [unidecode.unidecode(x) for x in s]veya daha iyi list(map(unidecode.unidecode, s))?
jpp

1
Zaten bir pandalar dizisi olduğundan, başvurmayı kullanmak istedim, Evet haklısın, list-comp kullanmak başvurmaktan daha iyidir, Ama olumsuz oy biraz sertti, savunmuyordum apply, sadece bunun iyi olabileceğini düşündüm kullanım durumu.
astro123
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.