Ефективне порівняння списків у двох стовпцях


16

Якщо у вас є такий DataFrame Pandas:

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: Як порівнювати стовпці списків рядно в DataFrame з 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 записів потрібно близько хвилини. То чи існує більш швидкий спосіб досягти цього?


Скільки елементів, що містять максимум (в одному рядку), може містити один із цих стовпців?
thushv89

2
ви спробували методи на цій посаді, яку ви пов’язали? конкретно ті, що використовують набір перетину, все, що вам потрібно зробити, це використовувати різницю заданих натомість, ні?
gold_cy

1
@aws_apprentice це рішення - це по суті те, що тут має ОП.
Куанг Хоанг

DataFrame Pandas може не бути правильною структурою даних для цього. Чи можете ви поділитися трохи більше інформації про програму та дані?
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}

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)щоб отримати той самий вихід


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]
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.