Confronto di elenchi in due colonne in ordine di riga


16

Quando si dispone di un Pandas DataFrame in questo modo:

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

Ma con circa 100000 voci, sto cercando di trovare le aggiunte e le rimozioni di tali elenchi nelle due colonne su una base di riga.

È paragonabile a questa domanda: Panda: come confrontare colonne di elenchi per riga in un DataFrame con Panda (non per loop)? ma sto osservando le differenze e il Pandas.applymetodo sembra non essere così veloce per così tante voci. Questo è il codice che sto attualmente utilizzando. Pandas.applycon numpy's setdiff1dmetodo:

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)

Funziona bene, tuttavia sono necessari circa un minuto per 120.000 voci. Quindi c'è un modo più veloce per raggiungere questo obiettivo?


Quanti elementi al massimo (in una singola riga) può contenere una di queste colonne?
thushv89,

2
hai provato i metodi in quel post che hai collegato? in particolare quelli che usano set intersezione, tutto quello che dovresti fare è usare invece set differenza, no?
gold_cy

1
@aws_apprentice questa soluzione è essenzialmente ciò che ha OP qui.
Quang Hoang

Un Pandas DataFrame potrebbe non essere la struttura dati giusta per questo. Puoi condividere un po 'più di background sul programma e sui dati?
AMC

Risposte:


14

Non sono sicuro delle prestazioni, ma alla mancanza di una soluzione migliore questo potrebbe applicarsi:

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) 

Traslochi:

  yesterday
0        {}
1        {}
2       {a}

aggiunte:

  today
0   {c}
1   {b}
2   {b}

2
Questo è molto veloce.
rpanai,

2
Questo è davvero molto veloce. È sceso a circa 2 secondi!
MegaCookie

2
Wow, sono sorpreso anche dalla prestazione dovuta applymap, ma felice che abbia funzionato per te!
Guarda l'

2
Ora, come sappiamo la soluzione di Rook è veloce, qualcuno può spiegarmi. Perché è stato più veloce?
Grijesh Chauhan

7
df['today'].apply(set) - df['yesterday'].apply(set)

Grazie! Questa è la soluzione più leggibile, tuttavia la soluzione di r.ook è leggermente più veloce.
MegaCookie

5

Ti suggerirò di calcolare additionse removalsnello stesso modo applicare.

Genera un esempio più grande

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)

La tua soluzione

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

La tua soluzione su una sola domanda

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

utilizzando set

A meno che le tue liste non siano molto grandi puoi evitare 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 soluzione di @ r.ook

Se sei contento di avere set anziché elenchi come output puoi usare il codice di @ 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 soluzione di @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

e alla fine puoi aggiungere .apply(list)per ottenere lo stesso output


1
Fantastico confronto che hai fatto!
MegaCookie

1

Eccone uno con l'idea di scaricare la parte di calcolo su strumenti NumPy vettorializzati. Raccoglieremo tutti i dati in matrici singole per ciascuna intestazione, eseguiremo tutte le corrispondenze richieste su NumPy e infine ridimensioneremo le voci di riga richieste. Sul NumPy che esegue la parte di sollevamento pesante, utilizzeremo l'hashing in base agli ID e agli ID di gruppo all'interno di ciascun gruppo che utilizza np.searchsorted. Stiamo anche facendo uso di numeri poiché quelli sono più veloci con NumPy. L'implementazione sarebbe simile a questa:

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

Ulteriore ottimizzazione è possibile nelle fasi di calcolo t_maske y_mask, dovenp.searchsorted potrebbe essere riutilizzato.

Potremmo anche usare una semplice assegnazione di array come alternativa al isinpassaggio da ottenere t_maske y_mask, in questo modo -

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]
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.