Widziałem wiele odpowiedzi opublikowanych na pytania dotyczące przepełnienia stosu, w których zastosowano metodę Pandy apply
. Widziałem również użytkowników komentujących pod nimi, mówiąc, że „ apply
jest powolny i należy go unikać”.
Przeczytałem wiele artykułów na temat wydajności, które wyjaśniają, że apply
jest powolny. Widziałem również zastrzeżenie w dokumentach o tym, jak apply
jest to po prostu wygodna funkcja przekazywania UDF (nie wydaje się, aby tego teraz znaleźć). Tak więc ogólny konsensus jest taki, że w apply
miarę możliwości należy unikać. Rodzi to jednak następujące pytania:
- Jeśli
apply
jest tak źle, to dlaczego jest w API? - Jak i kiedy powinienem zwolnić mój kod
apply
? - Czy są kiedykolwiek sytuacje, w których
apply
jest dobrze (lepiej niż inne możliwe rozwiązania)?
python
pandas
performance
apply
cs95
źródło
źródło
returns.add(1).apply(np.log)
vs.np.log(returns.add(1)
to przypadek, w którymapply
generalnie będzie nieznacznie szybszy, co stanowi dolne prawe zielone pole na poniższym diagramie jpp.Odpowiedzi:
apply
, wygodna funkcja, której nigdy nie potrzebujeszRozpoczynamy od odpowiedzi na pytania w PO, jeden po drugim.
DataFrame.apply
iSeries.apply
są wygodnymi funkcjami zdefiniowanymi odpowiednio w obiekcie DataFrame i Series.apply
akceptuje dowolną funkcję zdefiniowaną przez użytkownika, która stosuje transformację / agregację w DataFrame.apply
jest w rzeczywistości srebrną kulą, która robi wszystko, czego żadna istniejąca funkcja pandy nie może zrobić.Oto niektóre z rzeczy
apply
:axis=1
) lub column-wise (axis=0
) w DataFrameagg
lubtransform
w takich przypadkach)result_type
argument)....Pośród innych. Aby uzyskać więcej informacji, zobacz Aplikacja funkcji wierszowej lub kolumnowej w dokumentacji.
Więc przy tych wszystkich funkcjach, dlaczego jest
apply
źle? To dlatego, żeapply
jest powolny . Pandy nie przyjmują żadnych założeń co do natury twojej funkcji, więc stosuj ją iteracyjnie do każdego wiersza / kolumny, jeśli to konieczne. Ponadto obsługa wszystkich powyższych sytuacjiapply
wiąże się z pewnym dużym obciążeniem przy każdej iteracji. Ponadtoapply
zużywa dużo więcej pamięci, co jest wyzwaniem dla aplikacji ograniczonych do pamięci.Istnieje bardzo niewiele sytuacji, w których
apply
jest stosowanie (więcej o tym poniżej). Jeśli nie masz pewności, czy powinieneś używaćapply
, prawdopodobnie nie powinieneś.Odpowiedzmy na następne pytanie.
Mówiąc inaczej, oto kilka typowych sytuacji, w których będziesz chciał pozbyć się wszelkich połączeń do
apply
.Dane liczbowe
Jeśli pracujesz z danymi liczbowymi, prawdopodobnie istnieje już zwektoryzowana funkcja cython, która robi dokładnie to, co próbujesz zrobić (jeśli nie, zadaj pytanie na temat przepełnienia stosu lub otwórz żądanie funkcji w GitHub).
Porównaj działanie programu,
apply
aby uzyskać prostą operację dodawania.Jeśli chodzi o wydajność, nie ma porównania, cytonizowany odpowiednik jest znacznie szybszy. Nie ma potrzeby tworzenia wykresu, ponieważ różnica jest oczywista nawet w przypadku danych dotyczących zabawki.
Nawet jeśli włączysz przekazywanie surowych tablic z
raw
argumentem, nadal jest to dwa razy wolniejsze.Inny przykład:
Ogólnie rzecz biorąc, jeśli to możliwe , szukaj wektoryzowanych alternatyw.
Ciąg / Regex
Pandy w większości sytuacji zapewniają „zwektoryzowane” funkcje łańcuchowe, ale są rzadkie przypadki, w których te funkcje nie ... „stosują się”, że tak powiem.
Częstym problemem jest sprawdzenie, czy wartość w kolumnie znajduje się w innej kolumnie tego samego wiersza.
Powinno to zwrócić wiersz drugi i trzeci wiersz, ponieważ „donald” i „minnie” znajdują się w odpowiednich kolumnach „Tytuł”.
Używając Apply, można to zrobić za pomocą
Istnieje jednak lepsze rozwiązanie przy użyciu list składanych.
Należy tu zauważyć, że procedury iteracyjne są szybsze niż z
apply
powodu niższego narzutu. Jeśli potrzebujesz obsługiwać NaN i nieprawidłowe dtypes, możesz zbudować na tym, używając niestandardowej funkcji, którą możesz następnie wywołać z argumentami wewnątrz listy.Aby uzyskać więcej informacji na temat tego, kiedy listy składane powinny być uważane za dobrą opcję, zobacz mój zapis: W przypadku pętli z pandami - kiedy powinno mnie to obchodzić? .
A Common Pitfall: Exploding Columns of Lists
Ludzie są kuszeni, aby używać
apply(pd.Series)
. To jest okropne pod względem wydajności.Lepszą opcją jest wyświetlenie kolumny i przekazanie jej do pd.DataFrame.
W końcu,
Zastosuj to funkcja wygodna, więc zdarzają się sytuacje, w których koszty ogólne są na tyle znikome, że można je wybaczyć. To naprawdę zależy od tego, ile razy funkcja jest wywoływana.
Funkcje wektoryzowane dla serii, ale nie ramki danych
Co zrobić, jeśli chcesz zastosować operację na łańcuchach na wielu kolumnach? A co jeśli chcesz przekonwertować wiele kolumn na datę i godzinę? Te funkcje są wektoryzowane tylko dla serii, więc należy je zastosować do każdej kolumny, na której chcesz konwertować / operować.
Jest to dopuszczalny przypadek dla
apply
:Zauważ, że sensowne byłoby również
stack
użycie jawnej pętli. Wszystkie te opcje są nieco szybsze niż używanieapply
, ale różnica jest na tyle mała, że można ją wybaczyć.Możesz zrobić podobny przypadek dla innych operacji, takich jak operacje na łańcuchach lub konwersja do kategorii.
vs
I tak dalej...
Konwersja serii na
str
:astype
versusapply
Wydaje się, że jest to cecha charakterystyczna API. Użycie
apply
do konwersji liczb całkowitych w serii na ciąg jest porównywalne (i czasami szybsze) niż użycieastype
.Wykres został wykreślony przy użyciu
perfplot
biblioteki.W przypadku pływaków widzę, że
astype
jest on konsekwentnie tak szybki lub nieco szybszy niżapply
. Jest to więc związane z faktem, że dane w teście są typu całkowitego.GroupBy
operacje z przekształceniami łańcuchowymiGroupBy.apply
nie było omawiane do tej pory, aleGroupBy.apply
jest to również iteracyjna funkcja wygodna do obsługi wszystkiego,GroupBy
czego nie obsługują istniejące funkcje.Jednym z typowych wymagań jest wykonanie operacji GroupBy, a następnie dwóch głównych operacji, takich jak „lagged cumsum”:
Potrzebujesz tutaj dwóch kolejnych połączeń grupowych:
Używając
apply
, możesz skrócić to do pojedynczego połączenia.Bardzo trudno jest oszacować wydajność, ponieważ zależy ona od danych. Ale ogólnie
apply
jest to akceptowalne rozwiązanie, jeśli celem jest zmniejszenie liczbygroupby
połączeń (ponieważgroupby
jest również dość drogie).Inne zastrzeżenia
Oprócz powyższych zastrzeżeń warto również wspomnieć, że
apply
operuje na pierwszym rzędzie (lub kolumnie) dwukrotnie. Ma to na celu określenie, czy funkcja ma jakiekolwiek skutki uboczne. Jeśli nie,apply
może być w stanie użyć szybkiej ścieżki do oceny wyniku, w przeciwnym razie powróci do powolnej implementacji.To zachowanie jest również widoczne
GroupBy.apply
na pandach w wersjach <0.25 (zostało to naprawione dla 0.25, zobacz tutaj, aby uzyskać więcej informacji ).źródło
%timeit for c in df.columns: df[c] = pd.to_datetime(df[c], errors='coerce')
pewnością po pierwszej iteracji będzie to znacznie szybsze, ponieważ przechodziszdatetime
na ...datetime
?to_datetime
łańcuchów jest tak samo szybkie jak…datetime
obiekty”… naprawdę? Uwzględniłem tworzenie ramek danych (koszt stały) w taktowaniu wapply
porównaniu dofor
czasów pętli i różnica jest znacznie mniejsza.Nie wszystkie
apply
są takie samePoniższy wykres sugeruje, kiedy wziąć pod uwagę
apply
1 . Zielony oznacza możliwie efektywny; czerwony unikaj.Niektóre z nich są intuicyjne:
pd.Series.apply
jest to pętla wierszowa na poziomie Pythona, podobnie jakpd.DataFrame.apply
wierszowa (axis=1
). Nadużycia ich są liczne i mają szeroki zakres. Drugi post omawia je bardziej szczegółowo. Popularnymi rozwiązaniami są metody zwektoryzowane, listy składane (zakłada czyste dane) lub wydajne narzędzia, takie jakpd.DataFrame
konstruktor (np. Unikaćapply(pd.Series)
).Jeśli korzystasz z
pd.DataFrame.apply
wierszy, określenieraw=True
(jeśli to możliwe) jest często korzystne. Na tym etapienumba
jest zwykle lepszym wyborem.GroupBy.apply
: ogólnie preferowanyPowtarzanie
groupby
operacji, których należy unikaćapply
, obniża wydajność.GroupBy.apply
zwykle jest w porządku, pod warunkiem, że metody używane w funkcji niestandardowej są same wektoryzowane. Czasami nie ma natywnej metody Pandas dla agregacji grupowej, którą chcesz zastosować. W takim przypadku niewielka liczba grupapply
z funkcją niestandardową może nadal oferować rozsądną wydajność.pd.DataFrame.apply
kolumnowo: mieszana torbapd.DataFrame.apply
kolumnowym (axis=0
) jest interesującym przypadkiem. W przypadku małej liczby wierszy w porównaniu z dużą liczbą kolumn jest to prawie zawsze drogie. W przypadku dużej liczby wierszy względem kolumn, w bardziej typowym przypadku, czasami można zauważyć znaczną poprawę wydajności przy użyciuapply
:1 Są wyjątki, ale są one zwykle marginalne lub rzadkie. Kilka przykładów:
df['col'].apply(str)
może nieznacznie przewyższaćdf['col'].astype(str)
.df.apply(pd.to_datetime)
praca na łańcuchach nie skaluje się dobrze z wierszami w porównaniu ze zwykłąfor
pętlą.źródło
apply
jest znacznie szybsze niż moje rozwiązanie zany
. Jakieś przemyślenia na ten temat?any
jest około 100 razy szybsze niżapply
. Zrobił moje pierwsze testy z 2000 rzędami x 1000 kolumn i tutajapply
był dwa razy szybszy niżany
Dla
axis=1
(tj. Funkcji wierszowych) możesz po prostu użyć następującej funkcji zamiastapply
. Zastanawiam się, dlaczego tak nie jestpandas
. (Nie testowane z indeksami złożonymi, ale wydaje się być znacznie szybsze niżapply
)źródło
zip(df, row[1:])
tutaj wystarczy; naprawdę, na tym etapie rozważ,numba
czy func jest obliczeniem numerycznym. Zobacz tę odpowiedź, aby uzyskać wyjaśnienie.numba
jest szybsze,faster_df_apply
jest przeznaczone dla osób, które chcą czegoś równoważnego, ale szybszego niżDataFrame.apply
(co jest dziwnie wolne).Czy są kiedykolwiek sytuacje, w których
apply
jest dobrze? Tak czasami.Zadanie: dekodowanie ciągów Unicode.
Aktualizacja w
żadnym wypadku nie opowiadałem się za używaniem
apply
, po prostu myślałem od czasuNumPy
nie sobie z powyższą sytuacją, mógł być dobrym kandydatempandas apply
. Ale zapomniałem o zwykłym zrozumieniu listy ol dzięki przypomnieniu @jpp.źródło
[unidecode.unidecode(x) for x in s]
lublist(map(unidecode.unidecode, s))
?apply
, po prostu pomyślałem, że to mogłoby być dobre przypadek użycia.