Listen in zwei Spalten zeilenweise effizient vergleichen

16

Wenn Sie einen Pandas DataFrame wie diesen haben:

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

Aber mit ungefähr 100 000 Einträgen suche ich nach zeilenweisen Hinzufügungen und Entfernungen dieser Listen in den beiden Spalten.

Es ist vergleichbar mit dieser Frage: Pandas: Wie vergleiche ich Spalten von Listen zeilenweise in einem DataFrame mit Pandas (nicht für Schleife)? aber ich betrachte die Unterschiede, und die Pandas.applyMethode scheint für so viele Einträge nicht so schnell zu sein. Dies ist der Code, den ich derzeit verwende. Pandas.applymit numpy's setdiff1dMethode:

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)

Dies funktioniert einwandfrei, dauert jedoch ungefähr eine Minute für 120 000 Einträge. Gibt es einen schnelleren Weg, dies zu erreichen?

MegaCookie
quelle
Wie viele Elemente maximal (in einer einzelnen Zeile) darf eine dieser Spalten enthalten?
thushv89
2
Haben Sie die Methoden in dem von Ihnen verlinkten Beitrag ausprobiert? speziell diejenigen, die Set-Schnittpunkte verwenden, alles, was Sie tun müssten, ist stattdessen Set-Differenz zu verwenden, nein?
gold_cy
1
@aws_apprentice diese Lösung ist im Wesentlichen das, was OP hier hat.
Quang Hoang
Ein Pandas DataFrame ist möglicherweise nicht die richtige Datenstruktur dafür. Können Sie etwas mehr Hintergrundinformationen über das Programm und die Daten geben?
AMC

Antworten:

14

Ich bin mir nicht sicher über die Leistung, aber mangels einer besseren Lösung könnte dies zutreffen:

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) 

Umzüge:

  yesterday
0        {}
1        {}
2       {a}

Ergänzungen:

  today
0   {c}
1   {b}
2   {b}
Turm
quelle
2
Das geht sehr schnell.
Rpanai
2
Das ist in der Tat sehr schnell. Es kam auf ca. 2 Sekunden!
MegaCookie
2
Wow, ich bin auch von der Leistung überrascht, aber ich bin applymapfroh, dass es für dich geklappt hat!
r.ook
2
Nun, da wir wissen, dass die Lösung von Rook schnell ist, kann mir jemand erklären. Warum war es schneller?
Grijesh Chauhan
7
df['today'].apply(set) - df['yesterday'].apply(set)
Andreas K.
quelle
Vielen Dank! Dies ist meiner Meinung nach die am besten lesbare Lösung, jedoch ist die Lösung von r.ook etwas schneller.
MegaCookie
5

Ich werde Ihnen vorschlagen, zu berechnen additionsund removalsinnerhalb derselben anzuwenden.

Generieren Sie ein größeres Beispiel

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)

Ihre Lösung

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

Ihre Lösung auf einmal anwenden

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

Verwenden von set

Wenn Ihre Listen nicht sehr groß sind, können Sie dies vermeiden 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

@ r.ooks Lösung

Wenn Sie gerne Sets anstelle von Listen als Ausgabe haben, können Sie den Code von @ r.ook verwenden

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

@Andreas Ks Lösung

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

und Sie können schließlich hinzufügen .apply(list), um die gleiche Ausgabe zu erhalten

rpanai
quelle
1
Cooler Vergleich, den du gemacht hast!
MegaCookie
1

Hier ist eine mit der Idee, Computerteile in vektorisierte NumPy-Tools zu verlagern. Wir werden alle Daten für jeden Header in einzelnen Arrays sammeln, alle erforderlichen Übereinstimmungen für NumPy durchführen und schließlich zu den erforderlichen Zeileneinträgen zurückkehren. Auf dem NumPy, der den schweren Hebeteil ausführt, verwenden wir Hashing basierend auf Gruppen-IDs und IDs innerhalb jeder Gruppe mit np.searchsorted. Wir verwenden auch Zahlen, da diese mit NumPy schneller sind. Die Implementierung würde ungefähr so ​​aussehen -

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:])])

Eine weitere Optimierung ist in den zu berechnenden Schritten t_maskund y_maskwo möglichnp.searchsorted wieder verwendet werden könnten.

Wir könnten auch eine einfache Array-Zuweisung als Alternative zu dem isinzu erhaltenden Schritt verwendent_mask und y_maskso -

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
quelle