Les pandas iterrows ont-ils des problèmes de performances?


92

J'ai remarqué de très mauvaises performances lors de l'utilisation d'iterrows de pandas.

Est-ce quelque chose que les autres vivent? Est-ce spécifique à iterrows et cette fonction doit-elle être évitée pour les données d'une certaine taille (je travaille avec 2-3 millions de lignes)?

Cette discussion sur GitHub m'a amené à croire que cela est causé lors du mélange de dtypes dans le dataframe, mais l'exemple simple ci-dessous montre qu'il existe même lorsque vous utilisez un dtype (float64). Cela prend 36 secondes sur ma machine:

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

Pourquoi les opérations vectorisées comme s'appliquent-elles tellement plus rapidement? J'imagine qu'il doit y avoir une itération ligne par ligne là aussi.

Je ne peux pas comprendre comment ne pas utiliser iterrows dans mon cas (je vais le garder pour une question future). Par conséquent, j'apprécierais de savoir si vous avez toujours pu éviter cette itération. Je fais des calculs basés sur des données dans des dataframes séparés. Je vous remercie!

--- Edit: une version simplifiée de ce que je veux exécuter a été ajoutée ci-dessous ---

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
applyn'est PAS vectorisé. iterrowsc'est encore pire car il boxe tout (avec qui 'la perf diffère apply). Vous ne devriez l'utiliser que iterrowsdans très très peu de situations. IMHO jamais. Montrez ce que vous faites réellement iterrows.
Jeff

2
Le problème que vous avez lié à la place a à voir avec l'encadrement d'un DatetimeIndexinto Timestamps(a été implémenté dans l'espace python), et cela a été beaucoup amélioré dans master.
Jeff

1
Consultez ce numéro pour une discussion plus complète: github.com/pydata/pandas/issues/7194 .
Jeff

Lien vers la question spécifique (celle-ci restera générale): stackoverflow.com/questions/24875096/…
KieranPC

Veuillez ne pas recommander l'utilisation de iterrows (). C'est un catalyseur flagrant du pire anti-modèle de l'histoire des pandas.
cs95

Réponses:


181

Généralement, iterrowsne doit être utilisé que dans des cas très, très spécifiques. Voici l'ordre de priorité général pour l'exécution de diverses opérations:

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)

L'utilisation d'une routine Cython personnalisée est généralement trop compliquée, alors sautons cela pour l'instant.

1) La vectorisation est TOUJOURS, TOUJOURS le premier et le meilleur choix. Cependant, il existe un petit ensemble de cas (impliquant généralement une récurrence) qui ne peuvent pas être vectorisés de manière évidente. De plus, sur une petite échelle DataFrame, il peut être plus rapide d'utiliser d'autres méthodes.

3) peut apply généralement être géré par un itérateur dans l'espace Cython. Ceci est géré en interne par les pandas, bien que cela dépende de ce qui se passe à l'intérieur duapply expression. Par exemple, df.apply(lambda x: np.sum(x))sera exécuté assez rapidement, mais bien sûr, df.sum(1)c'est encore mieux. Cependant, quelque chose comme df.apply(lambda x: x['b'] + 1)sera exécuté dans l'espace Python, et par conséquent est beaucoup plus lent.

4) itertuplesne place pas les données dans un fichier Series. Il renvoie simplement les données sous forme de tuples.

5) iterrows BOITE les données dans un fichier Series. À moins que vous n'en ayez vraiment besoin, utilisez une autre méthode.

6) Mise à jour d'une image vide une seule ligne à la fois. J'ai vu cette méthode trop utilisée. C'est de loin le plus lent. C'est probablement un endroit courant (et raisonnablement rapide pour certaines structures python), mais a DataFramefait un bon nombre de vérifications sur l'indexation, donc ce sera toujours très lent pour mettre à jour une ligne à la fois. Beaucoup mieux pour créer de nouvelles structures et concat.


1
Oui, j'ai utilisé le numéro 6 (et 5). J'ai un peu d'apprentissage à faire. Cela semble être le choix évident pour un débutant relatif.
KieranPC

3
D'après mon expérience, la différence entre 3, 4 et 5 est limitée en fonction du cas d'utilisation.
IanS

8
J'ai essayé de vérifier les temps d'exécution dans ce cahier . D'une manière ou d'une autre itertuplesest plus rapide que apply:(
Dimgold

1
pd.DataFrame.applyest souvent plus lent que itertuples. De plus, il vaut la peine de considérer les compréhensions de liste map, les mal nommés np.vectorizeet numba(sans ordre particulier) pour les calculs non vectorisables , par exemple voir cette réponse .
jpp

2
@Jeff, par curiosité, pourquoi n'avez-vous pas ajouté des listes de compréhension ici? S'il est vrai qu'ils ne gèrent pas l'alignement d'index ou les données manquantes (à moins que vous n'utilisiez une fonction avec un try-catch), ils conviennent à de nombreux cas d'utilisation (string / regex stuff) où les méthodes pandas n'ont pas vectorisé ( dans le vrai sens du terme) implémentations. Pensez-vous qu'il vaut la peine de mentionner que les LC sont une alternative plus rapide et plus faible aux pandas s'appliquent et à de nombreuses fonctions de chaînes de pandas?
cs95

17

Les opérations vectorielles dans Numpy et pandas sont beaucoup plus rapides que les opérations scalaires dans vanilla Python pour plusieurs raisons:

  • Recherche de type amorti : Python est un langage typé dynamiquement, il y a donc une surcharge d'exécution pour chaque élément d'un tableau. Cependant, Numpy (et donc pandas) effectuent des calculs en C (souvent via Cython). Le type du tableau n'est déterminé qu'au début de l'itération; cette économie à elle seule est l'une des plus grandes victoires.

  • Meilleure mise en cache : l'itération sur un tableau C est compatible avec le cache et donc très rapide. Un pandas DataFrame est une "table orientée colonnes", ce qui signifie que chaque colonne n'est en réalité qu'un tableau. Ainsi, les actions natives que vous pouvez effectuer sur un DataFrame (comme la somme de tous les éléments dans une colonne) vont avoir quelques échecs de cache.

  • Plus de possibilités de parallélisme : un simple tableau C peut être utilisé via des instructions SIMD. Certaines parties de Numpy activent SIMD, en fonction de votre CPU et du processus d'installation. Les avantages du parallélisme ne seront pas aussi spectaculaires que le typage statique et une meilleure mise en cache, mais ils restent une victoire solide.

Morale de l'histoire: utilisez les opérations vectorielles dans Numpy et pandas. Elles sont plus rapides que les opérations scalaires en Python pour la simple raison que ces opérations sont exactement ce qu'un programmeur C aurait écrit à la main de toute façon. (Sauf que la notion de tableau est beaucoup plus facile à lire que les boucles explicites avec des instructions SIMD intégrées.)


11

Voici comment résoudre votre problème. Tout cela est vectorisé.

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

Réponse très claire merci. J'essaierai de fusionner mais j'ai des doutes car j'aurai alors 5 milliards de lignes (2,5 millions * 2000). Afin de garder ce Q général, j'ai créé un Q. spécifique. Je serais heureux de voir une alternative pour éviter cette table géante, si vous en connaissez une: ici: stackoverflow.com/questions/24875096/…
KieranPC

1
cela ne crée pas le produit cartésien - c'est un espace compressé et est assez efficace en mémoire. ce que vous faites est un problème très courant. essayer. (votre question liée a une solution très similaire)
Jeff

7

Une autre option consiste à utiliser to_records(), qui est plus rapide que les deux itertupleset iterrows.

Mais pour votre cas, il y a beaucoup de place pour d'autres types d'améliorations.

Voici ma dernière version optimisée

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 de référence:

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

Code complet:

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)

La version finale est presque 10 fois plus rapide que le code d'origine. La stratégie est:

  1. Utilisez groupbypour éviter la comparaison répétée des valeurs.
  2. Permet to_recordsd'accéder aux objets numpy.records bruts.
  3. N'opérez pas sur DataFrame tant que vous n'avez pas compilé toutes les données.


En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.