Comparar listas en dos columnas de forma eficiente en filas

16

Al tener un Pandas DataFrame como este:

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

Pero con alrededor de 100 000 entradas, estoy buscando encontrar las adiciones y eliminaciones de esas listas en las dos columnas en forma de fila.

Es comparable a esta pregunta: Pandas: ¿Cómo comparar columnas de listas en fila en un DataFrame con Pandas (no para bucle)? pero estoy mirando las diferencias, y el Pandas.applymétodo no parece ser tan rápido para tantas entradas. Este es el código que estoy usando actualmente. Pandas.applycon numpy's setdiff1dmétodo:

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)

Esto funciona bien, sin embargo, toma alrededor de un minuto para 120 000 entradas. Entonces, ¿hay una manera más rápida de lograr esto?

MegaCookie
fuente
¿Cuántos elementos como máximo (en una sola fila) podría contener una de estas columnas?
thushv89
2
¿Has probado los métodos en esa publicación que vinculaste? específicamente los que usan la intersección establecida, todo lo que tendría que hacer es usar la diferencia establecida, ¿no?
gold_cy
1
@aws_apprentice esa solución es esencialmente lo que OP tiene aquí.
Quang Hoang
Un Pandas DataFrame puede no ser la estructura de datos correcta para esto. ¿Puedes compartir un poco más de antecedentes sobre el programa y los datos?
AMC

Respuestas:

14

No estoy seguro sobre el rendimiento, pero a falta de una mejor solución, esto podría aplicarse:

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) 

Mudanzas:

  yesterday
0        {}
1        {}
2       {a}

Adiciones:

  today
0   {c}
1   {b}
2   {b}
torre
fuente
2
Esto es muy rapido.
rpanai
2
Esto es realmente muy rápido. ¡Se redujo a unos 2 segundos!
MegaCookie
2
Wow, también estoy sorprendido por el rendimiento applymap, pero me alegro de que te haya funcionado.
r.ook
2
Ahora, como sabemos, la solución de Rook es rápida. ¿Puede alguien explicarme? ¿Por qué fue más rápido?
Grijesh Chauhan
7
df['today'].apply(set) - df['yesterday'].apply(set)
Andreas K.
fuente
¡Gracias! Esta es, creo, la solución más legible, sin embargo, la solución de r.ook es un poco más rápida.
MegaCookie
5

Le sugeriré que calcule additionsy removalsdentro de la misma solicitud.

Genera un ejemplo más 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)

Tu solución

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

Su solución en una sola solicitud

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

Utilizando set

A menos que sus listas sean muy grandes, puede evitar 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 solución de @ r.ook

Si está contento de tener conjuntos en lugar de listas como salida, puede usar el código 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 solución 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

y eventualmente puedes agregar .apply(list)para obtener tu mismo resultado

rpanai
fuente
1
¡Genial comparación que hiciste!
MegaCookie
1

Aquí hay uno con la idea de descargar la parte de cálculo a las herramientas vectorizadas de NumPy. Recopilaremos todos los datos en matrices individuales para cada encabezado, realizaremos todas las coincidencias requeridas en NumPy y finalmente volveremos a las entradas de fila requeridas. En el NumPy que realiza la parte de levantamiento pesado, usaremos hashing en función de las ID de grupo y las ID dentro de cada grupo que use np.searchsorted. También estamos haciendo uso de números, ya que son más rápidos con NumPy. La implementación se vería así:

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

Es posible una mayor optimización en los pasos para calcular t_masky y_maskdónde np.searchsortedpodría usarse nuevamente.

También podríamos usar una simple asignación de matriz como una alternativa al isinpaso para obtener t_masky y_mask, así:

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
fuente