web-dev-qa-db-fra.com

Moyen le plus rapide pour comparer la ligne et la ligne précédente dans pandas dataframe avec des millions de lignes

Je recherche des solutions pour accélérer une fonction que j'ai écrite pour parcourir un pandas dataframe et comparer les valeurs des colonnes entre la ligne actuelle et la ligne précédente.

À titre d'exemple, il s'agit d'une version simplifiée de mon problème:

   User  Time                 Col1  newcol1  newcol2  newcol3  newcol4
0     1     6     [cat, dog, goat]        0        0        0        0
1     1     6         [cat, sheep]        0        0        0        0
2     1    12        [sheep, goat]        0        0        0        0
3     2     3          [cat, lion]        0        0        0        0
4     2     5  [fish, goat, lemur]        0        0        0        0
5     3     9           [cat, dog]        0        0        0        0
6     4     4          [dog, goat]        0        0        0        0
7     4    11                [cat]        0        0        0        0

Pour le moment, j'ai une fonction qui parcourt et calcule les valeurs de 'newcol1' et 'newcol2 'selon que' User 'a changé depuis la ligne précédente et également si la différence dans les valeurs' Time 'est supérieure à 1. Il examine également la première valeur de les tableaux stockés dans 'Col1' et 'Col2 'et mises à jour' newcol3' et 'newcol4 'si ces valeurs ont changé depuis la ligne précédente.

Voici le pseudo-code de ce que je fais actuellement (puisque j'ai simplifié le problème, je n'ai pas testé cela, mais il est assez similaire à ce que je fais réellement dans le cahier ipython):

 def myJFunc(df):
...     #initialize jnum counter
...     jnum = 0;
...     #loop through each row of dataframe (not including the first/zeroeth)
...     for i in range(1,len(df)):
...             #has user changed?
...             if df.User.loc[i] == df.User.loc[i-1]:
...                     #has time increased by more than 1 (hour)?
...                     if abs(df.Time.loc[i]-df.Time.loc[i-1])>1:
...                             #update new columns
...                             df['newcol2'].loc[i-1] = 1;
...                             df['newcol1'].loc[i] = 1;
...                             #increase jnum
...                             jnum += 1;
...                     #has content changed?
...                     if df.Col1.loc[i][0] != df.Col1.loc[i-1][0]:
...                             #record this change
...                             df['newcol4'].loc[i-1] = [df.Col1.loc[i-1][0], df.Col2.loc[i][0]];
...             #different user?
...             Elif df.User.loc[i] != df.User.loc[i-1]:
...                     #update new columns
...                     df['newcol1'].loc[i] = 1; 
...                     df['newcol2'].loc[i-1] = 1;
...                     #store jnum elsewhere (code not included here) and reset jnum
...                     jnum = 1;

Je dois maintenant appliquer cette fonction à plusieurs millions de lignes et c'est incroyablement lent, alors j'essaie de trouver la meilleure façon d'accélérer. J'ai entendu dire que Cython peut augmenter la vitesse des fonctions mais je n'ai aucune expérience avec lui (et je suis nouveau dans les deux pandas et python). Est-il possible de passer deux rangées d'un dataframe en tant qu'arguments de la fonction, puis utiliser Cython pour l'accélérer ou serait-il nécessaire de créer de nouvelles colonnes avec des valeurs "diff" afin que la fonction ne lise et n'écrive que sur une seule ligne du dataframe à la fois, afin de bénéficier de l'utilisation de Cython? Toute autre astuce de vitesse serait grandement appréciée!

(En ce qui concerne l'utilisation de .loc, j'ai comparé .loc, .iloc et .ix et celui-ci était légèrement plus rapide, c'est la seule raison pour laquelle j'utilise cela actuellement)

(De plus, ma colonne User en réalité est unicode et non int, ce qui pourrait être problématique pour des comparaisons rapides)

16
AdO

Je pensais dans le même sens qu'Andy, juste avec groupby ajouté, et je pense que c'est complémentaire à la réponse d'Andy. L'ajout de groupby va juste avoir pour effet de mettre un NaN dans la première ligne chaque fois que vous faites un diff ou shift. (Notez que ce n'est pas une tentative de réponse exacte, juste pour esquisser quelques techniques de base.)

df['time_diff'] = df.groupby('User')['Time'].diff()

df['Col1_0'] = df['Col1'].apply( lambda x: x[0] )

df['Col1_0_prev'] = df.groupby('User')['Col1_0'].shift()

   User  Time                 Col1  time_diff Col1_0 Col1_0_prev
0     1     6     [cat, dog, goat]        NaN    cat         NaN
1     1     6         [cat, sheep]          0    cat         cat
2     1    12        [sheep, goat]          6  sheep         cat
3     2     3          [cat, lion]        NaN    cat         NaN
4     2     5  [fish, goat, lemur]          2   fish         cat
5     3     9           [cat, dog]        NaN    cat         NaN
6     4     4          [dog, goat]        NaN    dog         NaN
7     4    11                [cat]          7    cat         dog

Dans le prolongement du point d'Andy sur le stockage d'objets, notez que ce que j'ai fait ici a été d'extraire le premier élément de la colonne de liste (et d'ajouter également une version décalée). En procédant ainsi, il vous suffit de faire une extraction coûteuse une fois et ensuite, vous pouvez vous en tenir aux méthodes standard pandas.

13
JohnE

Utilisez pandas (constructions) et vectorisez votre code, c'est-à-dire n'utilisez pas de boucles, utilisez plutôt les fonctions pandas/numpy.

"newcol1" et "newcol2" selon que "l'utilisateur" a changé depuis la ligne précédente et également si la différence dans les valeurs "Time" est supérieure à 1.

Calculez-les séparément:

df['newcol1'] = df['User'].shift() == df['User']
df.ix[0, 'newcol1'] = True # possibly Tweak the first row??

df['newcol1'] = (df['Time'].shift() - df['Time']).abs() > 1

Le but de Col1 n'est pas clair pour moi, mais les python dans les colonnes ne sont pas bien mis à l'échelle (vous ne pouvez pas utiliser le chemin rapide et le contenu est dispersé dans la mémoire). La plupart du temps vous pouvez vous en sortir en utilisant autre chose ...


Cython est la toute dernière option , et non nécessaire dans 99% des cas d'utilisation, mais voir amélioration de la section performances de les docs pour des conseils.

8
Andy Hayden

Dans votre problème, il semble que vous souhaitiez parcourir la ligne par paire. La première chose que vous pourriez faire est quelque chose comme ceci:

from itertools import tee, izip
def pairwise(iterable):
    "s -> (s0,s1), (s1,s2), (s2, s3), ..."
    a, b = tee(iterable)
    next(b, None)
    return izip(a, b)

for (idx1, row1), (idx2, row2) in pairwise(df.iterrows()):
    # you stuff

Cependant, vous ne pouvez pas modifier directement row1 et row2, vous devrez toujours utiliser .loc ou .iloc avec les index.

Si les flèches sont encore trop lentes, je suggère de faire quelque chose comme ceci:

  • Créez une colonne user_id à partir de vos noms unicode en utilisant pd.unique (User) et en mappant le nom avec un dictionnaire aux identifiants entiers.

  • Créer une trame de données delta: dans une trame de données décalée avec la colonne user_id et time, vous soustrayez la trame de données d'origine.

    df[[col1, ..]].shift() - df[[col1, ..]])
    

Si user_id> 0, cela signifie que l'utilisateur a changé sur deux lignes consécutives. La colonne de temps peut être filtrée directement avec delta [delta ['time'> 1]] Avec cette trame de données delta, vous enregistrez les modifications en ligne. Vous pouvez l'utiliser comme un masque pour mettre à jour les colonnes dont vous avez besoin à partir de votre trame de données d'origine.

1
Kikohs