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.apply
ve Series.apply
olan kolaylık fonksiyonları sırasıyla nesne DataFrame ve Series üzerinde tanımlı. apply
DataFrame üzerinde bir dönüştürme / toplama uygulayan herhangi bir kullanıcı tanımlı işlevi kabul eder. apply
etkin bir şekilde, mevcut herhangi bir pandanın yapamadığı şeyi yapan sihirli bir değnekdir.
Bazı şeyler apply
yapabilir:
- 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
agg
veya transform
bu durumlarda)
- Öğe bazlı dönüşümler gerçekleştirin
- Birleştirilmiş sonuçları orijinal satırlara yayınlayın (
result_type
argü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 apply
kötü? Öyle çünkü apply
olduğ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).
apply
Basit 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)
raw
Argü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 apply
düşü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ı stack
veya 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
: astype
versusapply
Bu, API'nin kendine özgü bir özelliği gibi görünüyor. Bir apply
Serideki 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
.
Grafik, perfplot
kü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, astype
tutarlı 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.apply
aynı zamanda mevcut GroupBy
iş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, apply
amaç bir groupby
aramayı azaltmaksa (çünkü groupby
aynı 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 .)
returns.add(1).apply(np.log)
vsnp.log(returns.add(1)
bir durumdurapply
altında JPP en şemada sağ alt yeşil kutu genellikle marjinal hızlı olacaktır.