Wydajność Pand stosuje się w porównaniu z np.vectorize do tworzenia nowej kolumny z istniejących kolumn

81

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

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 Ai 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ę Ndo 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

Jak używać funkcji „Zastosuj” Pandy do wielu kolumn?

Jak zastosować funkcję do dwóch kolumn ramki danych Pandas

stackoverflowuser2010
źródło
Nie zagłębiłem się w szczegóły twojego pytania, ale np.vectorizejest to w zasadzie forpętla Pythona (to wygodna metoda), a applyz
lambdą
„Jeśli np.vectorize () jest generalnie zawsze szybsze niż df.apply (), to dlaczego np.vectorize () nie jest częściej wspominane?” Ponieważ nie powinieneś używać applywiersza po wierszu, chyba że musisz, i oczywiście funkcja wektoryzowana będzie lepsza od funkcji niewektoryzowanej.
PMende
1
@ PMende, ale np.vectorizenie jest wektoryzowany. To dobrze znana myląca nazwa
roganjosh
1
@ PMende, Jasne, nie sugerowałem inaczej. Nie powinieneś czerpać opinii na temat implementacji na podstawie czasu. Tak, są wnikliwe. Ale mogą sprawić, że będziesz zakładać rzeczy, które nie są prawdą.
jpp
3
@ PMende baw się z .strakcesoriami pandy . W wielu przypadkach są wolniejsze niż rozumienie z listy. Za dużo zakładamy.
roganjosh

Odpowiedzi:

115

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.ndarrayalbo listobiekty 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:

  1. Do tuplemetod opartych (pierwsze 4) są bardziej efektywne niż współczynnik pd.Seriesmetod opartych o (ostatnie 3).
  2. np.vectorize, rozumienie listy + zipi mapmetody, 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.
  3. Istnieje znaczna poprawa szybkości raw=Truew pd.DataFrame.applyporównaniu z używaniem z i bez. Ta opcja przekazuje tablice NumPy do funkcji niestandardowej zamiast pd.Seriesobiektów.

pd.DataFrame.apply: po prostu kolejna pętla

Aby 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=Truea zobaczysz <class 'numpy.ndarray'>. Wszystko to jest opisane w dokumentach, ale zobaczenie tego jest bardziej przekonujące.

np.vectorize: fałszywa wektoryzacja

Dokumenty dla np.vectorizemają następującą uwagę:

Funkcja wektoryzowana oblicza pyfuncna podstawie kolejnych krotek tablic wejściowych, podobnie jak funkcja mapy w języku Python, z wyjątkiem tego, że używa reguł emisji numpy.

„Reguły nadawania” nie mają tutaj znaczenia, ponieważ tablice wejściowe mają takie same wymiary. Podobieństwo do mapjest pouczające, ponieważ mappowyższa wersja ma prawie identyczną wydajność. Do kodu źródłowego pokazuje, co się dzieje: np.vectorizeprzekształca swoją funkcję wejścia do funkcji uniwersalnego ( „ufunc”) poprzez np.frompyfunc. Istnieje pewna optymalizacja, np. Buforowanie, co może prowadzić do pewnej poprawy wydajności.

Krótko mówiąc, np.vectorizerobi to , co powinna robić pętla na poziomie Pythona , ale pd.DataFrame.applydodaje masywny narzut. Nie ma żadnej kompilacji JIT, którą widzisz numba(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. numbaPoniżej, jeśli wydajność jest krytyczna i jest to część twojego wąskiego gardła.

numba.njit: większa wydajność

Kiedy pętle uważane za wykonalne, są zwykle optymalizowane za numbapomocą bazowych tablic NumPy, aby przenieść jak najwięcej do C.

Rzeczywiście, numbapoprawia 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:

  • Wszystko w Pythonie jest obiektem. Obejmuje to, w przeciwieństwie do C., liczby. Dlatego typy Pythona mają narzut, który nie istnieje w przypadku natywnych typów C.
  • Metody NumPy są zwykle oparte na C. Ponadto w miarę możliwości stosowane są zoptymalizowane algorytmy.
jpp
źródło
1
@jpp: Używanie dekoratora z parallelargumentami @njit(parallel=True)daje mi dalszą poprawę w porównaniu z just @njit. Być może ty też możesz to dodać.
Sheldore
1
Masz podwójne sprawdzenie dla b [i]! = 0. Normalne zachowanie Pythona i Numby polega na sprawdzeniu 0 i wyświetleniu błędu. To prawdopodobnie przerywa wektoryzację SIMD i ma zwykle duży wpływ na szybkość wykonywania. Ale możesz to zmienić w Numba na @njit (error_model = 'numpy'), aby uniknąć podwójnego sprawdzania dzielenia przez 0. Zalecane jest również przydzielenie pamięci za pomocą np.empty i ustawienie wyniku na 0 w instrukcji else.
max9111
1
error_model numpy używa tego, co procesor podaje w dzieleniu przez 0 -> NaN. Przynajmniej w Numba 0.41dev obie wersje używają wektoryzacji SIMD. Możesz to sprawdzić w sposób opisany tutaj numba.pydata.org/numba-doc/dev/user/faq.html (1.16.2.3. Dlaczego moja pętla nie jest wektoryzowana?) Po prostu dodam instrukcję else do Twojej funkcji (res [ i] = 0.) i przydziel pamięć przy użyciu np.empty. Powinno to w połączeniu z error_model = 'numpy' poprawić wydajność o około 20%. Na starszych wersjach Numba był większy wpływ na wydajność ...
max9111
2
@ stackoverflowuser2010, Nie ma uniwersalnej odpowiedzi „dla dowolnych funkcji”. Musisz wybrać odpowiednie narzędzie do odpowiedniego zadania, które jest częścią zrozumienia programowania / algorytmów.
jpp
1
Wesołych Świąt!
cs95
5

Im bardziej złożone stają się twoje funkcje (tj. Im mniej numpymoż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

%timeit name_series.apply(parse_name)

Wyniki:

76.2 ms ± 626 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Za pomocą np.vectorize

%timeit parse_name_vec(name_series)

Wyniki:

77.3 ms ± 216 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Numpy próbuje zamienić funkcje Pythona w ufuncobiekty numpy podczas wywołania np.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

%timeit name_series.apply(parse_name)

Wyniki:

769 ms ± 5.88 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

np.vectorize

%timeit parse_name_vec(name_series)

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:

%timeit np.select(cases, replacements, default=name_series)

Wyniki:

67.2 ms ± 683 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
PMende
źródło
A co, jeśli zwiększysz to do size=1000000(1 miliona)?
stackoverflowuser2010
2
Jestem prawie pewien, że twoje twierdzenia są nieprawidłowe. Nie mogę na razie poprzeć tego stwierdzenia kodem, mam nadzieję, że ktoś inny to
zrobi
@ stackoverflowuser2010 Zaktualizowałem go, wraz z rzeczywistym podejściem wektorowym.
PMende
0

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))
fordlab22
źródło