Używam ramek danych Pandas i chcę utworzyć nową kolumnę jako funkcję istniejących kolumn. Nie widziałem dobrej dyskusji na temat różnicy prędkości między df.apply()
i np.vectorize()
, więc pomyślałem, że zapytam tutaj.
Funkcja Pandy apply()
jest powolna. Z tego, co zmierzyłem (pokazane poniżej w niektórych eksperymentach), użycie np.vectorize()
jest 25 razy szybsze (lub więcej) niż korzystanie z funkcji DataFrame apply()
, przynajmniej na moim MacBooku Pro 2016. Czy jest to oczekiwany wynik i dlaczego?
Na przykład załóżmy, że mam następującą ramkę danych z N
wierszami:
N = 10
A_list = np.random.randint(1, 100, N)
B_list = np.random.randint(1, 100, N)
df = pd.DataFrame({'A': A_list, 'B': B_list})
df.head()
# A B
# 0 78 50
# 1 23 91
# 2 55 62
# 3 82 64
# 4 99 80
Załóżmy dalej, że chcę utworzyć nową kolumnę jako funkcję dwóch kolumn A
i B
. W poniższym przykładzie użyję prostej funkcji divide()
. Aby zastosować tę funkcję, mogę użyć albo df.apply()
albo np.vectorize()
:
def divide(a, b):
if b == 0:
return 0.0
return float(a)/b
df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)
df['result2'] = np.vectorize(divide)(df['A'], df['B'])
df.head()
# A B result result2
# 0 78 50 1.560000 1.560000
# 1 23 91 0.252747 0.252747
# 2 55 62 0.887097 0.887097
# 3 82 64 1.281250 1.281250
# 4 99 80 1.237500 1.237500
Jeśli zwiększę się N
do rzeczywistych rozmiarów, takich jak 1 milion lub więcej, zauważę, że np.vectorize()
jest to 25 razy szybsze lub więcej niż df.apply()
.
Poniżej znajduje się kompletny kod do testów porównawczych:
import pandas as pd
import numpy as np
import time
def divide(a, b):
if b == 0:
return 0.0
return float(a)/b
for N in [1000, 10000, 100000, 1000000, 10000000]:
print ''
A_list = np.random.randint(1, 100, N)
B_list = np.random.randint(1, 100, N)
df = pd.DataFrame({'A': A_list, 'B': B_list})
start_epoch_sec = int(time.time())
df['result'] = df.apply(lambda row: divide(row['A'], row['B']), axis=1)
end_epoch_sec = int(time.time())
result_apply = end_epoch_sec - start_epoch_sec
start_epoch_sec = int(time.time())
df['result2'] = np.vectorize(divide)(df['A'], df['B'])
end_epoch_sec = int(time.time())
result_vectorize = end_epoch_sec - start_epoch_sec
print 'N=%d, df.apply: %d sec, np.vectorize: %d sec' % \
(N, result_apply, result_vectorize)
# Make sure results from df.apply and np.vectorize match.
assert(df['result'].equals(df['result2']))
Wyniki przedstawiono poniżej:
N=1000, df.apply: 0 sec, np.vectorize: 0 sec
N=10000, df.apply: 1 sec, np.vectorize: 0 sec
N=100000, df.apply: 2 sec, np.vectorize: 0 sec
N=1000000, df.apply: 24 sec, np.vectorize: 1 sec
N=10000000, df.apply: 262 sec, np.vectorize: 4 sec
Jeśli np.vectorize()
generalnie jest zawsze szybszy niż df.apply()
, to dlaczego np.vectorize()
nie jest wspominany więcej? Widzę tylko posty StackOverflow związane z df.apply()
, takimi jak:
pandy tworzą nową kolumnę na podstawie wartości z innych kolumn
np.vectorize
jest to w zasadziefor
pętla Pythona (to wygodna metoda), aapply
zapply
wiersza po wierszu, chyba że musisz, i oczywiście funkcja wektoryzowana będzie lepsza od funkcji niewektoryzowanej.np.vectorize
nie jest wektoryzowany. To dobrze znana myląca nazwa.str
akcesoriami pandy . W wielu przypadkach są wolniejsze niż rozumienie z listy. Za dużo zakładamy.Odpowiedzi:
Będę zacząć od stwierdzenia, że moc pand i NumPy tablic pochodzi z wysokowydajnych wektoryzowane obliczeń na macierzach liczbowych. 1 Celem obliczeń wektoryzowanych jest uniknięcie pętli na poziomie Pythona poprzez przeniesienie obliczeń do wysoce zoptymalizowanego kodu C i wykorzystanie ciągłych bloków pamięci. 2
Pętle na poziomie Pythona
Teraz możemy spojrzeć na niektóre czasy. Poniżej znajdują się wszystkie pętle na poziomie Pythona, które produkują albo
pd.Series
,np.ndarray
albolist
obiekty zawierające te same wartości. Na potrzeby przypisania do serii w ramce danych wyniki są porównywalne.# Python 3.6.5, NumPy 1.14.3, Pandas 0.23.0 np.random.seed(0) N = 10**5 %timeit list(map(divide, df['A'], df['B'])) # 43.9 ms %timeit np.vectorize(divide)(df['A'], df['B']) # 48.1 ms %timeit [divide(a, b) for a, b in zip(df['A'], df['B'])] # 49.4 ms %timeit [divide(a, b) for a, b in df[['A', 'B']].itertuples(index=False)] # 112 ms %timeit df.apply(lambda row: divide(*row), axis=1, raw=True) # 760 ms %timeit df.apply(lambda row: divide(row['A'], row['B']), axis=1) # 4.83 s %timeit [divide(row['A'], row['B']) for _, row in df[['A', 'B']].iterrows()] # 11.6 s
Niektóre wnioski:
tuple
metod opartych (pierwsze 4) są bardziej efektywne niż współczynnikpd.Series
metod opartych o (ostatnie 3).np.vectorize
, rozumienie listy +zip
imap
metody, tj. pierwsza trójka, mają mniej więcej taką samą wydajność. Dzieje się tak, ponieważ używajątuple
i omijają niektóre pandy nad głowąpd.DataFrame.itertuples
.raw=True
wpd.DataFrame.apply
porównaniu z używaniem z i bez. Ta opcja przekazuje tablice NumPy do funkcji niestandardowej zamiastpd.Series
obiektów.pd.DataFrame.apply
: po prostu kolejna pętlaAby zobaczyć dokładnie obiekty, które mija Pandy, możesz w prosty sposób zmodyfikować swoją funkcję:
def foo(row): print(type(row)) assert False # because you only need to see this once df.apply(lambda row: foo(row), axis=1)
Wyjście:
<class 'pandas.core.series.Series'>
. Tworzenie, przekazywanie i wysyłanie zapytań do obiektu serii Pandas wiąże się ze znacznymi narzutami w stosunku do tablic NumPy. Nie powinno to być zaskoczeniem: seria Pandy zawiera przyzwoitą ilość rusztowań do przechowywania indeksu, wartości, atrybutów itp.Wykonaj ponownie to samo ćwiczenie,
raw=True
a zobaczysz<class 'numpy.ndarray'>
. Wszystko to jest opisane w dokumentach, ale zobaczenie tego jest bardziej przekonujące.np.vectorize
: fałszywa wektoryzacjaDokumenty dla
np.vectorize
mają następującą uwagę:„Reguły nadawania” nie mają tutaj znaczenia, ponieważ tablice wejściowe mają takie same wymiary. Podobieństwo do
map
jest pouczające, ponieważmap
powyższa wersja ma prawie identyczną wydajność. Do kodu źródłowego pokazuje, co się dzieje:np.vectorize
przekształca swoją funkcję wejścia do funkcji uniwersalnego ( „ufunc”) poprzeznp.frompyfunc
. Istnieje pewna optymalizacja, np. Buforowanie, co może prowadzić do pewnej poprawy wydajności.Krótko mówiąc,
np.vectorize
robi to , co powinna robić pętla na poziomie Pythona , alepd.DataFrame.apply
dodaje masywny narzut. Nie ma żadnej kompilacji JIT, którą widzisznumba
(patrz poniżej). To tylko wygoda .Prawdziwa wektoryzacja: czego powinieneś użyć
Dlaczego nigdzie nie wspomniano o powyższych różnicach? Ponieważ wykonanie prawdziwie zwektoryzowanych obliczeń sprawia, że są one nieistotne:
%timeit np.where(df['B'] == 0, 0, df['A'] / df['B']) # 1.17 ms %timeit (df['A'] / df['B']).replace([np.inf, -np.inf], 0) # 1.96 ms
Tak, to ~ 40x szybciej niż najszybsze z powyższych rozwiązań pętli. Oba są dopuszczalne. Moim zdaniem pierwszy jest zwięzły, czytelny i skuteczny. Spójrz tylko na inne metody, np.
numba
Poniżej, jeśli wydajność jest krytyczna i jest to część twojego wąskiego gardła.numba.njit
: większa wydajnośćKiedy pętle są uważane za wykonalne, są zwykle optymalizowane za
numba
pomocą bazowych tablic NumPy, aby przenieść jak najwięcej do C.Rzeczywiście,
numba
poprawia wydajność do mikrosekund . Bez uciążliwej pracy trudno będzie osiągnąć znacznie większą wydajność.from numba import njit @njit def divide(a, b): res = np.empty(a.shape) for i in range(len(a)): if b[i] != 0: res[i] = a[i] / b[i] else: res[i] = 0 return res %timeit divide(df['A'].values, df['B'].values) # 717 µs
Użycie
@njit(parallel=True)
może zapewnić dalsze przyspieszenie w przypadku większych macierzy.1 Typy numeryczne to:
int
,float
,datetime
,bool
,category
. Oni wykluczyćobject
dtype i mogą być przechowywane w sąsiadujących ze sobą bloków pamięci.2 Istnieją co najmniej 2 powody, dla których operacje NumPy są wydajne w porównaniu z Pythonem:
źródło
parallel
argumentami@njit(parallel=True)
daje mi dalszą poprawę w porównaniu z just@njit
. Być może ty też możesz to dodać.Im bardziej złożone stają się twoje funkcje (tj. Im mniej
numpy
można przenieść do swoich własnych elementów wewnętrznych), tym bardziej zobaczysz, że wydajność nie będzie tak różna. Na przykład:name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=100000)) def parse_name(name): if name.lower().startswith('a'): return 'A' elif name.lower().startswith('e'): return 'E' elif name.lower().startswith('i'): return 'I' elif name.lower().startswith('o'): return 'O' elif name.lower().startswith('u'): return 'U' return name parse_name_vec = np.vectorize(parse_name)
Robienie czasu:
Korzystanie z Zastosuj
Wyniki:
76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Za pomocą
np.vectorize
Wyniki:
77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
Numpy próbuje zamienić funkcje Pythona w
ufunc
obiekty numpy podczas wywołanianp.vectorize
. Jak to robi, właściwie nie wiem - musiałbyś bardziej zagłębić się w wnętrzności numpy niż ja jestem skłonny do bankomatu. To powiedziawszy, wydaje się, że lepiej radzi sobie z prostymi funkcjami numerycznymi niż ta funkcja oparta na łańcuchach.Rozruch do 1000000:
name_series = pd.Series(np.random.choice(['adam', 'chang', 'eliza', 'odom'], replace=True, size=1000000))
apply
Wyniki:
769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
np.vectorize
Wyniki:
794 ms ± 4.85 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Lepszy ( wektoryzowany ) sposób z
np.select
:cases = [ name_series.str.lower().str.startswith('a'), name_series.str.lower().str.startswith('e'), name_series.str.lower().str.startswith('i'), name_series.str.lower().str.startswith('o'), name_series.str.lower().str.startswith('u') ] replacements = 'A E I O U'.split()
Czasy:
Wyniki:
67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
źródło
size=1000000
(1 miliona)?Jestem nowy w Pythonie. Ale w poniższym przykładzie „zastosuj” wydaje się działać szybciej niż „wektoryzacja” lub czegoś mi brakuje.
import numpy as np import pandas as pd B = np.random.rand(1000,1000) fn = np.vectorize(lambda l: 1/(1-np.exp(-l))) print(fn(B)) B = pd.DataFrame(np.random.rand(1000,1000)) fn = lambda l: 1/(1-np.exp(-l)) print(B.apply(fn))
źródło