Comparando listas em duas colunas de maneira eficiente em linhas

16

Ao ter um DataFrame do Pandas assim:

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

Porém, com cerca de 100.000 entradas, estou procurando encontrar as adições e remoções dessas listas nas duas colunas em uma linha.

É comparável a esta pergunta: Pandas: Como comparar colunas de listas em linhas em um DataFrame com Pandas (não para loop)? mas estou observando as diferenças, e o Pandas.applymétodo parece não ser tão rápido para tantas entradas. Este é o código que estou usando no momento. Pandas.applycom o 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)

Isso funciona bem, no entanto, leva cerca de um minuto para 120 000 entradas. Então, existe uma maneira mais rápida de conseguir isso?

MegaCookie
fonte
Quantos itens no máximo (em uma única linha) uma dessas colunas pode conter?
thushv89 8/01
2
você tentou os métodos nesse post que você vinculou? especificamente aqueles que usam a interseção de conjuntos, tudo o que você precisa fazer é usar a diferença de conjuntos, não?
gold_cy
11
@aws_apprentice essa solução é essencialmente o que o OP tem aqui.
Quang Hoang
Um DataFrame do Pandas pode não ser a estrutura de dados correta para isso. Você pode compartilhar um pouco mais de informações sobre o programa e os dados?
AMC

Respostas:

14

Não tenho certeza sobre o desempenho, mas com a falta de uma solução melhor, isso pode se aplicar:

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) 

Remoções:

  yesterday
0        {}
1        {}
2       {a}

Aditivos:

  today
0   {c}
1   {b}
2   {b}
torre
fonte
2
Isto é muito rápido.
rpanai 8/01
2
Isso é realmente muito rápido. Tudo se reduziu a cerca de 2 segundos!
MegaCookie 8/01
2
Uau, estou surpreso com o desempenho também devido a applymap, mas feliz que funcionou para você!
r.ook 8/01
2
Agora, como sabemos que a solução da torre é rápida, alguém pode me explicar. Por que foi mais rápido?
Grijesh Chauhan
7
df['today'].apply(set) - df['yesterday'].apply(set)
Andreas K.
fonte
Obrigado! Esta é a solução mais legível, no entanto, a solução da r.ook é um pouco mais rápida.
MegaCookie
5

Vou sugerir que você calcule additionse removalsdentro da mesma aplicação.

Gere um exemplo maior

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)

Sua solução

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

Sua solução em uma única aplicação

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

Usando set

A menos que suas listas sejam muito grandes, você pode 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

Solução de @ r.ook

Se você está feliz por ter conjuntos em vez de listas como saída, você pode usar o 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

A solução 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

e você pode eventualmente adicionar .apply(list)para obter a mesma saída

rpanai
fonte
11
Comparação legal que você fez!
MegaCookie 8/01
1

Aqui está um com a idéia de descarregar parte de computação para ferramentas NumPy vetorizadas. Reuniremos todos os dados em matrizes únicas para cada cabeçalho, executaremos toda a correspondência necessária no NumPy e, finalmente, voltaremos às entradas de linha necessárias. No NumPy que faz a parte de trabalho pesado, usaremos o hash com base nos IDs e IDs de cada grupo usando np.searchsorted. Também estamos usando números, pois esses são mais rápidos com o NumPy. A implementação seria algo parecido com isto -

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

É possível uma otimização adicional nas etapas de cálculo t_maske y_mask, onde np.searchsortedpoderia ser usado novamente.

Também poderíamos usar uma atribuição de matriz simples como uma alternativa à isinetapa a ser obtida t_maske y_mask, assim:

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
fonte