Czy iterrows pandy mają problemy z wydajnością?


96

Zauważyłem bardzo słabą wydajność podczas używania iterrows od pand.

Czy jest to coś, czego doświadczają inni? Czy jest to specyficzne dla iterrows i czy należy unikać tej funkcji dla danych o określonym rozmiarze (pracuję z 2-3 milionami wierszy)?

Ta dyskusja na GitHub doprowadziła mnie do przekonania, że ​​jest to spowodowane mieszaniem dtypów w ramce danych, jednak prosty przykład poniżej pokazuje, że istnieje nawet przy użyciu jednego dtype (float64). Na moim komputerze trwa to 36 sekund:

import pandas as pd
import numpy as np
import time

s1 = np.random.randn(2000000)
s2 = np.random.randn(2000000)
dfa = pd.DataFrame({'s1': s1, 's2': s2})

start = time.time()
i=0
for rowindex, row in dfa.iterrows():
    i+=1
end = time.time()
print end - start

Dlaczego wektoryzowane operacje, takie jak stosowane, są znacznie szybsze? Wyobrażam sobie, że musi tam zachodzić pewna iteracja wiersz po wierszu.

Nie mogę wymyślić, jak nie używać iterows w moim przypadku (to zachowam na przyszłe pytanie). Dlatego byłbym wdzięczny za wysłuchanie, gdybyś konsekwentnie był w stanie uniknąć tej iteracji. Obliczenia wykonuję na podstawie danych w oddzielnych ramkach danych. Dziękuję Ci!

--- Edycja: uproszczona wersja tego, co chcę uruchomić, została dodana poniżej ---

import pandas as pd
import numpy as np

#%% Create the original tables
t1 = {'letter':['a','b'],
      'number1':[50,-10]}

t2 = {'letter':['a','a','b','b'],
      'number2':[0.2,0.5,0.1,0.4]}

table1 = pd.DataFrame(t1)
table2 = pd.DataFrame(t2)

#%% Create the body of the new table
table3 = pd.DataFrame(np.nan, columns=['letter','number2'], index=[0])

#%% Iterate through filtering relevant data, optimizing, returning info
for row_index, row in table1.iterrows():   
    t2info = table2[table2.letter == row['letter']].reset_index()
    table3.ix[row_index,] = optimize(t2info,row['number1'])

#%% Define optimization
def optimize(t2info, t1info):
    calculation = []
    for index, r in t2info.iterrows():
        calculation.append(r['number2']*t1info)
    maxrow = calculation.index(max(calculation))
    return t2info.ix[maxrow]

7
applyNIE jest wektoryzowany. iterrowsjest jeszcze gorszy, ponieważ wszystko pakuje (z tym „różnicą perf” apply). Powinieneś używać tylko iterrowsw bardzo niewielu sytuacjach. IMHO nigdy. Pokaż, z czym tak naprawdę robisz iterrows.
Jeff,

2
Problem, z którym łączysz się zamiast tego, dotyczy umieszczania w polu a DatetimeIndexdo Timestamps(został zaimplementowany w przestrzeni Pythona) i został znacznie ulepszony w pliku master.
Jeff,

1
Zobacz ten numer, aby uzyskać bardziej pełną dyskusję: github.com/pydata/pandas/issues/7194 .
Jeff,

Link do konkretnego pytania (to pozostanie ogólne): stackoverflow.com/questions/24875096/…
KieranPC

Nie polecaj używania iterrows (). Jest to rażący czynnik umożliwiający stworzenie najgorszego anty-wzoru w historii pand.
cs95

Odpowiedzi:


188

Ogólnie iterrowspowinno być używane tylko w bardzo, bardzo szczególnych przypadkach. Oto ogólna kolejność wykonywania różnych operacji:

1) vectorization
2) using a custom cython routine
3) apply
    a) reductions that can be performed in cython
    b) iteration in python space
4) itertuples
5) iterrows
6) updating an empty frame (e.g. using loc one-row-at-a-time)

Korzystanie z niestandardowej procedury Cythona jest zwykle zbyt skomplikowane, więc na razie pomińmy to.

1) Wektoryzacja jest ZAWSZE, ZAWSZE pierwszym i najlepszym wyborem. Istnieje jednak niewielki zbiór przypadków (zwykle obejmujących nawrót), których nie można wektoryzować w oczywisty sposób. Co więcej, w niewielkim stopniu DataFrameszybsze może być użycie innych metod.

3) apply zwykle może być obsługiwane przez iterator w przestrzeni Cython. Jest to obsługiwane wewnętrznie przez pandy, choć zależy to od tego, co dzieje się wewnątrz applywyrażenia. Na przykład, df.apply(lambda x: np.sum(x))zostanie wykonany dość szybko, choć oczywiście df.sum(1)jest jeszcze lepszy. Jednak coś podobnego df.apply(lambda x: x['b'] + 1)zostanie wykonane w przestrzeni Pythona, a co za tym idzie, będzie znacznie wolniejsze.

4) itertuplesnie opakowuje danych w plik Series. Po prostu zwraca dane w postaci krotek.

5) iterrowsCZY pakuje dane do pliku Series. Jeśli naprawdę tego nie potrzebujesz, użyj innej metody.

6) Aktualizacja pustej ramki po jednym wierszu na raz. Widziałem, jak ta metoda jest zbyt często używana. Jest to zdecydowanie najwolniejszy. Prawdopodobnie jest to powszechne miejsce (i dość szybkie w przypadku niektórych struktur Pythona), ale a DataFramewykonuje sporą liczbę kontroli indeksowania, więc aktualizacja wiersza na raz będzie zawsze bardzo powolna. Znacznie lepiej jest tworzyć nowe struktury i concat.


1
Tak, użyłem numeru 6 (i 5). Muszę się trochę nauczyć. Wydaje się, że jest to oczywisty wybór dla stosunkowo początkującego.
KieranPC,

3
Z mojego doświadczenia wynika, że ​​różnica między 3, 4 i 5 jest ograniczona w zależności od przypadku użycia.
IanS

8
Próbowałem sprawdzić środowiska wykonawcze w tym notatniku . Jakoś itertuplesjest szybszy niż apply:(
Dimgold

1
pd.DataFrame.applyjest często wolniejszy niż itertuples. Dodatkowo warto rozważyć zdania listowe, mapsłabo nazwane np.vectorizei numba(w przypadkowej kolejności) dla obliczeń niewektorowalnych , np. Zobacz tę odpowiedź .
jpp

2
@Jeff, z ciekawości, dlaczego nie dodałeś tutaj wyrażeń listowych? Chociaż prawdą jest, że nie obsługują one wyrównania indeksu ani brakujących danych (chyba że używasz funkcji z try-catch), są dobre w wielu przypadkach użycia (elementy string / regex), w których metody pandy nie mają wektoryzacji ( w najprawdziwszym tego słowa znaczeniu) implementacje. Czy uważasz, że warto wspomnieć, że LC są szybszą, niższą alternatywą dla pand i wieloma funkcjami strun pand?
cs95

17

Operacje wektorowe w Numpy i pandas są znacznie szybsze niż operacje skalarne w waniliowym Pythonie z kilku powodów:

  • Wyszukiwanie typów amortyzowanych : Python jest językiem z typami dynamicznymi, więc każdy element tablicy ma narzut w czasie wykonywania. Jednak Numpy (a tym samym pandy) wykonują obliczenia w języku C (często przez Cython). Typ tablicy jest określany tylko na początku iteracji; sama ta oszczędność jest jedną z największych korzyści.

  • Lepsze buforowanie : Iterowanie po tablicy C jest przyjazne dla pamięci podręcznej, a zatem bardzo szybkie. Pandy DataFrame to „tabela zorientowana na kolumny”, co oznacza, że ​​każda kolumna jest w rzeczywistości tylko tablicą. Więc natywne akcje, które możesz wykonać na DataFrame (takie jak sumowanie wszystkich elementów w kolumnie) będą miały kilka chybień w pamięci podręcznej.

  • Więcej możliwości równoległości : Prosta tablica C może być obsługiwana za pomocą instrukcji SIMD. Niektóre części Numpy włączają SIMD, w zależności od procesora i procesu instalacji. Korzyści z równoległości nie będą tak dramatyczne, jak statyczne pisanie i lepsze buforowanie, ale nadal są solidną wygraną.

Morał z tej historii: użyj operacji na wektorach w Numpy i pandach. Są szybsze niż operacje skalarne w Pythonie z tego prostego powodu, że te operacje są dokładnie tym, co programista C i tak napisałby ręcznie. (Tyle że pojęcie tablicy jest znacznie łatwiejsze do odczytania niż jawne pętle z osadzonymi instrukcjami SIMD).


11

Oto sposób rozwiązania problemu. To wszystko jest wektoryzowane.

In [58]: df = table1.merge(table2,on='letter')

In [59]: df['calc'] = df['number1']*df['number2']

In [60]: df
Out[60]: 
  letter  number1  number2  calc
0      a       50      0.2    10
1      a       50      0.5    25
2      b      -10      0.1    -1
3      b      -10      0.4    -4

In [61]: df.groupby('letter')['calc'].max()
Out[61]: 
letter
a         25
b         -1
Name: calc, dtype: float64

In [62]: df.groupby('letter')['calc'].idxmax()
Out[62]: 
letter
a         1
b         2
Name: calc, dtype: int64

In [63]: df.loc[df.groupby('letter')['calc'].idxmax()]
Out[63]: 
  letter  number1  number2  calc
1      a       50      0.5    25
2      b      -10      0.1    -1

Bardzo jasna odpowiedź dzięki. Spróbuję scalić, ale mam wątpliwości, ponieważ wtedy będę miał 5 miliardów wierszy (2,5 miliona * 2000). Aby zachować ten generał Q, stworzyłem specjalne pytanie. Z przyjemnością zobaczę alternatywę, aby uniknąć tego gigantycznego stołu, jeśli znasz jeden: tutaj: stackoverflow.com/questions/24875096/ ...
KieranPC

1
nie tworzy to iloczynu kartezjańskiego - jest to skompresowana przestrzeń i dość wydajna pamięć. to, co robisz, jest bardzo standardowym problemem. Spróbuj. (Twoje połączone pytanie ma bardzo podobne rozwiązanie)
Jeff

7

Inną opcją jest użycie to_records(), które jest szybsze niż oba itertuplesi iterrows.

Ale w twoim przypadku jest dużo miejsca na inne rodzaje ulepszeń.

Oto moja ostateczna zoptymalizowana wersja

def iterthrough():
    ret = []
    grouped = table2.groupby('letter', sort=False)
    t2info = table2.to_records()
    for index, letter, n1 in table1.to_records():
        t2 = t2info[grouped.groups[letter].values]
        # np.multiply is in general faster than "x * y"
        maxrow = np.multiply(t2.number2, n1).argmax()
        # `[1:]`  removes the index column
        ret.append(t2[maxrow].tolist()[1:])
    global table3
    table3 = pd.DataFrame(ret, columns=('letter', 'number2'))

Test porównawczy:

-- iterrows() --
100 loops, best of 3: 12.7 ms per loop
  letter  number2
0      a      0.5
1      b      0.1
2      c      5.0
3      d      4.0

-- itertuple() --
100 loops, best of 3: 12.3 ms per loop

-- to_records() --
100 loops, best of 3: 7.29 ms per loop

-- Use group by --
100 loops, best of 3: 4.07 ms per loop
  letter  number2
1      a      0.5
2      b      0.1
4      c      5.0
5      d      4.0

-- Avoid multiplication --
1000 loops, best of 3: 1.39 ms per loop
  letter  number2
0      a      0.5
1      b      0.1
2      c      5.0
3      d      4.0

Pełny kod:

import pandas as pd
import numpy as np

#%% Create the original tables
t1 = {'letter':['a','b','c','d'],
      'number1':[50,-10,.5,3]}

t2 = {'letter':['a','a','b','b','c','d','c'],
      'number2':[0.2,0.5,0.1,0.4,5,4,1]}

table1 = pd.DataFrame(t1)
table2 = pd.DataFrame(t2)

#%% Create the body of the new table
table3 = pd.DataFrame(np.nan, columns=['letter','number2'], index=table1.index)


print('\n-- iterrows() --')

def optimize(t2info, t1info):
    calculation = []
    for index, r in t2info.iterrows():
        calculation.append(r['number2'] * t1info)
    maxrow_in_t2 = calculation.index(max(calculation))
    return t2info.loc[maxrow_in_t2]

#%% Iterate through filtering relevant data, optimizing, returning info
def iterthrough():
    for row_index, row in table1.iterrows():   
        t2info = table2[table2.letter == row['letter']].reset_index()
        table3.iloc[row_index,:] = optimize(t2info, row['number1'])

%timeit iterthrough()
print(table3)

print('\n-- itertuple() --')
def optimize(t2info, n1):
    calculation = []
    for index, letter, n2 in t2info.itertuples():
        calculation.append(n2 * n1)
    maxrow = calculation.index(max(calculation))
    return t2info.iloc[maxrow]

def iterthrough():
    for row_index, letter, n1 in table1.itertuples():   
        t2info = table2[table2.letter == letter]
        table3.iloc[row_index,:] = optimize(t2info, n1)

%timeit iterthrough()


print('\n-- to_records() --')
def optimize(t2info, n1):
    calculation = []
    for index, letter, n2 in t2info.to_records():
        calculation.append(n2 * n1)
    maxrow = calculation.index(max(calculation))
    return t2info.iloc[maxrow]

def iterthrough():
    for row_index, letter, n1 in table1.to_records():   
        t2info = table2[table2.letter == letter]
        table3.iloc[row_index,:] = optimize(t2info, n1)

%timeit iterthrough()

print('\n-- Use group by --')

def iterthrough():
    ret = []
    grouped = table2.groupby('letter', sort=False)
    for index, letter, n1 in table1.to_records():
        t2 = table2.iloc[grouped.groups[letter]]
        calculation = t2.number2 * n1
        maxrow = calculation.argsort().iloc[-1]
        ret.append(t2.iloc[maxrow])
    global table3
    table3 = pd.DataFrame(ret)

%timeit iterthrough()
print(table3)

print('\n-- Even Faster --')
def iterthrough():
    ret = []
    grouped = table2.groupby('letter', sort=False)
    t2info = table2.to_records()
    for index, letter, n1 in table1.to_records():
        t2 = t2info[grouped.groups[letter].values]
        maxrow = np.multiply(t2.number2, n1).argmax()
        # `[1:]`  removes the index column
        ret.append(t2[maxrow].tolist()[1:])
    global table3
    table3 = pd.DataFrame(ret, columns=('letter', 'number2'))

%timeit iterthrough()
print(table3)

Ostateczna wersja jest prawie 10x szybsza niż oryginalny kod. Strategia jest następująca:

  1. Użyj, groupbyaby uniknąć wielokrotnego porównywania wartości.
  2. Służy to_recordsdo uzyskiwania dostępu do surowych obiektów numpy.records.
  3. Nie wykonuj operacji na DataFrame, dopóki nie skompilujesz wszystkich danych.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.