Эффективное сравнение списков в двух столбцах

16

При использовании Pandas DataFrame, как это:

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

Но с примерно 100 000 записей я ищу, чтобы найти и удалить эти списки в двух столбцах построчно.

Это сравнимо с этим вопросом: Pandas: Как сравнивать столбцы списков по строкам в кадре данных с Pandas (не для цикла)? но я смотрю на различия, и Pandas.applyметод, кажется, не такой быстрый для такого количества записей. Это код, который я сейчас использую. Pandas.applyс numpy's setdiff1dметодом:

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)

Это прекрасно работает, однако для 120 000 записей требуется около минуты. Так есть ли более быстрый способ сделать это?

MegaCookie
источник
Сколько элементов в максимуме (в одной строке) может содержать один из этих столбцов?
thushv89
2
Вы пробовали методы в этом посте, который вы связали? в частности, те, которые используют пересечение множеств, все, что вам нужно сделать, это использовать вместо этого разность множеств, нет?
gold_cy
1
@aws_apprentice это решение, по сути, то, что OP имеет здесь.
Куанг Хоанг
Pandas DataFrame может быть неправильной структурой данных для этого. Можете ли вы рассказать немного больше о программе и данных?
AMC

Ответы:

14

Не уверен насчет производительности, но из-за отсутствия лучшего решения это может быть применимо:

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) 

Удаления:

  yesterday
0        {}
1        {}
2       {a}

дополнения:

  today
0   {c}
1   {b}
2   {b}
r.ook
источник
2
Это очень быстро
rpanai
2
Это действительно очень быстро. Дошло до 2 секунд!
MegaCookie
2
Вау, я тоже удивлен работой applymap, но рад, что она сработала для вас!
r.ook
2
Теперь, когда мы знаем, что решение ладьи быстрое, может кто-нибудь объяснить мне. Почему это было быстрее?
Грижеш Чаухан
7
df['today'].apply(set) - df['yesterday'].apply(set)
Андреас К.
источник
Спасибо! Я думаю, что это наиболее читаемое решение, однако решение r.ook немного быстрее.
MegaCookie
5

Я предложу вам посчитать additionsи в removalsпределах этого же подать заявку.

Создайте пример большего

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)

Ваше решение

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

Ваше решение по одной заявке

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

С помощью set

Если ваши списки не очень большие, вы можете избежать 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.ook

Если вы довольны, что в качестве выходных данных вместо наборов используются списки, вы можете использовать код @ 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

Решение @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

и вы можете в конечном итоге добавить, .apply(list)чтобы получить свой же вывод

rpanai
источник
1
Классное сравнение, которое вы сделали!
MegaCookie
1

Вот один из них с идеей разгрузки вычислительной части в векторизованные инструменты NumPy. Мы соберем все данные в отдельные массивы для каждого заголовка, выполним все необходимые сопоставления в NumPy и, наконец, вернемся к нужным элементам строки. На NumPy, который выполняет тяжелую часть, мы будем использовать хеширование на основе идентификаторов групп и идентификаторов внутри каждой группы np.searchsorted. Мы также используем числа, так как они быстрее с NumPy. Реализация будет выглядеть примерно так -

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

Дальнейшая оптимизация возможна на этапах вычисления t_maskи y_mask, где np.searchsortedможет использоваться снова.

Мы также могли бы использовать простое присвоение массива в качестве альтернативы isinшагу, чтобы получить t_maskи y_mask, как это так -

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
источник