Comparaison efficace des listes dans deux colonnes en ligne

16

Lorsque vous avez un Pandas DataFrame comme celui-ci:

import pandas as pd
import numpy as np
df = pd.DataFrame({'today': [['a', 'b', 'c'], ['a', 'b'], ['b']], 
                   'yesterday': [['a', 'b'], ['a'], ['a']]})
                 today        yesterday
0      ['a', 'b', 'c']       ['a', 'b']
1           ['a', 'b']            ['a']
2                ['b']            ['a']                          
... etc

Mais avec environ 100 000 entrées, je cherche à trouver les ajouts et les suppressions de ces listes dans les deux colonnes par ligne.

Elle est comparable à cette question: Pandas: comment comparer les colonnes de listes en ligne dans un DataFrame avec Pandas (pas pour la boucle)? mais je regarde les différences, et la Pandas.applyméthode ne semble pas être aussi rapide pour autant d'entrées. C'est le code que j'utilise actuellement. Pandas.applyavec numpy's setdiff1dméthode:

additions = df.apply(lambda row: np.setdiff1d(row.today, row.yesterday), axis=1)
removals  = df.apply(lambda row: np.setdiff1d(row.yesterday, row.today), axis=1)

Cela fonctionne bien, mais cela prend environ une minute pour 120 000 entrées. Existe-t-il un moyen plus rapide d'accomplir cela?

MegaCookie
la source
Combien d'éléments au maximum (sur une seule ligne) une de ces colonnes peut contenir?
thushv89
2
avez-vous essayé les méthodes de ce post que vous avez liées? spécifiquement ceux qui utilisent l'intersection d'ensemble, tout ce que vous auriez à faire est d'utiliser la différence d'ensemble à la place, non?
gold_cy
1
@aws_apprentice cette solution est essentiellement ce que OP a ici.
Quang Hoang
Un Pandas DataFrame peut ne pas être la bonne structure de données pour cela. Pouvez-vous partager un peu plus d'informations sur le programme et les données?
AMC

Réponses:

14

Je ne suis pas sûr des performances, mais en l'absence d'une meilleure solution, cela pourrait s'appliquer:

temp = df[['today', 'yesterday']].applymap(set)
removals = temp.diff(periods=1, axis=1).dropna(axis=1)
additions = temp.diff(periods=-1, axis=1).dropna(axis=1) 

Déménagements:

  yesterday
0        {}
1        {}
2       {a}

Ajouts:

  today
0   {c}
1   {b}
2   {b}
tour
la source
2
C'est très rapide.
rpanai
2
C'est en effet très rapide. Cela s'est réduit à environ 2 secondes!
MegaCookie
2
Wow, je suis également surpris par la performance applymap, mais heureux que cela ait fonctionné pour vous!
r.ook
2
Maintenant, comme nous savons que la solution de rook est rapide, quelqu'un peut-il m'expliquer? Pourquoi c'était plus rapide?
Grijesh Chauhan
7
df['today'].apply(set) - df['yesterday'].apply(set)
Andreas K.
la source
Merci! C'est, je pense, la solution la plus lisible, mais la solution de r.ook est légèrement plus rapide.
MegaCookie
5

Je vais vous suggérer de calculer additionsetremovals dans le même cas d'appliquer.

Générez un plus grand exemple

import pandas as pd
import numpy as np
df = pd.DataFrame({'today': [['a', 'b', 'c'], ['a', 'b'], ['b']], 
                   'yesterday': [['a', 'b'], ['a'], ['a']]})
df = pd.concat([df for i in range(10_000)], ignore_index=True)

Votre solution

%%time
additions = df.apply(lambda row: np.setdiff1d(row.today, row.yesterday), axis=1)
removals  = df.apply(lambda row: np.setdiff1d(row.yesterday, row.today), axis=1)
CPU times: user 10.9 s, sys: 29.8 ms, total: 11 s
Wall time: 11 s

Votre solution sur une seule demande

%%time
df["out"] = df.apply(lambda row: [np.setdiff1d(row.today, row.yesterday),
                                  np.setdiff1d(row.yesterday, row.today)], axis=1)
df[['additions','removals']] = pd.DataFrame(df['out'].values.tolist(), columns=['additions','removals'])
df = df.drop("out", axis=1)

CPU times: user 4.97 s, sys: 16 ms, total: 4.99 s
Wall time: 4.99 s

En utilisant set

À moins que vos listes soient très grandes, vous pouvez éviter numpy

def fun(x):
    a = list(set(x["today"]).difference(set(x["yesterday"])))
    b = list((set(x["yesterday"])).difference(set(x["today"])))
    return [a,b]

%%time
df["out"] = df.apply(fun, axis=1)
df[['additions','removals']] = pd.DataFrame(df['out'].values.tolist(), columns=['additions','removals'])
df = df.drop("out", axis=1)

CPU times: user 1.56 s, sys: 0 ns, total: 1.56 s
Wall time: 1.56 s

La solution de @ r.ook

Si vous êtes satisfait d'avoir des ensembles au lieu de listes en sortie, vous pouvez utiliser le code de @ r.ook

%%time
temp = df[['today', 'yesterday']].applymap(set)
removals = temp.diff(periods=1, axis=1).dropna(axis=1)
additions = temp.diff(periods=-1, axis=1).dropna(axis=1) 
CPU times: user 93.1 ms, sys: 12 ms, total: 105 ms
Wall time: 104 ms

La solution de @Andreas K.

%%time
df['additions'] = (df['today'].apply(set) - df['yesterday'].apply(set))
df['removals'] = (df['yesterday'].apply(set) - df['today'].apply(set))

CPU times: user 161 ms, sys: 28.1 ms, total: 189 ms
Wall time: 187 ms

et vous pouvez éventuellement ajouter .apply(list)pour obtenir votre même sortie

rpanai
la source
1
Cool comparaison que vous avez faite!
MegaCookie
1

En voici une avec l'idée de décharger la partie de calcul vers des outils NumPy vectorisés. Nous rassemblerons toutes les données dans des tableaux uniques pour chaque en-tête, effectuerons toutes les correspondances requises sur NumPy et, enfin, nous redirigerons vers les entrées de ligne requises. Sur le NumPy qui effectue la partie de levage de charges lourdes, nous utiliserons le hachage en fonction des ID de groupe et des ID au sein de chaque groupe utilisant np.searchsorted. Nous utilisons également des nombres car ceux-ci sont plus rapides avec NumPy. L'implémentation ressemblerait à ceci -

t = df['today']
y = df['yesterday']
tc = np.concatenate(t)
yc = np.concatenate(y)

tci,tcu = pd.factorize(tc)

tl = np.array(list(map(len,t)))
ty = np.array(list(map(len,y)))

grp_t = np.repeat(np.arange(len(tl)),tl)
grp_y = np.repeat(np.arange(len(ty)),ty)

sidx = tcu.argsort()
idx = sidx[np.searchsorted(tcu,yc,sorter=sidx)]

s = max(tci.max(), idx.max())+1
tID = grp_t*s+tci
yID = grp_y*s+idx

t_mask = np.isin(tID, yID, invert=True)
y_mask = np.isin(yID, tID, invert=True)

t_se = np.r_[0,np.bincount(grp_t,t_mask).astype(int).cumsum()]
y_se = np.r_[0,np.bincount(grp_y,y_mask).astype(int).cumsum()]

Y = yc[y_mask].tolist()
T = tc[t_mask].tolist()

A = pd.Series([T[i:j] for (i,j) in zip(t_se[:-1],t_se[1:])])
R = pd.Series([Y[i:j] for (i,j) in zip(y_se[:-1],y_se[1:])])

Une optimisation supplémentaire est possible aux étapes de calcul t_masket y_mask,np.searchsorted pourrait être réutilisée.

Nous pourrions également utiliser une simple affectation de tableau comme alternative à l' isinétape pour obtenir t_masket y_mask, comme ça -

M = max(tID.max(), yID.max())+1
mask = np.empty(M, dtype=bool)

mask[tID] = True
mask[yID] = False
t_mask = mask[tID]

mask[yID] = True
mask[tID] = False
y_mask = mask[yID]
Divakar
la source