Мне нужно знать вероятность совместной продажи одинаковых товаров на основе истории продаж, отформатированной следующим образом:
pd.DataFrame({"sale_id": [1, 1, 1, 2, 2, 3, 3, 3, 3, 4],
"item": ["A", "B", "C", "A", "C", "A", "D", "E", "C", "B"],
"qty": [1, 4, 3, 2, 8, 3, 6, 5, 12, 9]})
sale_id Item Qty
1 A 1
1 B 4
1 C 3
2 A 2
2 C 8
3 A 3
3 D 6
3 E 5
3 C 12
4 B 9
Я хочу построить матрицу следующим образом:
Я попытался повернуть фрейм данных и использовать pd.DataFrame.corr() с пользовательским вызываемым объектом, но у меня закончилась оперативная память, вызвав:
pd.pivot_table(df, index = "sales_id", columns = "item")
Фактический фрейм данных, который я использую, имеет длину 700 000 строк и 20 000 различных элементов.
К сожалению, я не могу, фрейм данных содержит конфиденциальные данные, но я отредактировал вопрос с кодом, чтобы сгенерировать фрейм данных, который я использовал в качестве примера.
Легко. Вы уже сделали что-нибудь или вы полностью застряли?
Я попытался повернуть элементы в столбцы, а затем выполнить итерацию по фрейму данных, подсчитывая, сколько раз A и B происходят на одном и том же sale_id. Но у меня закончилась оперативная память, просто повернув.
Это хорошо, чтобы включить в свой вопрос. Вам нужно объяснить, что вас больше всего беспокоит пространственная сложность алгоритма.
Спасибо, я снова отредактировал вопрос, чтобы лучше объяснить мою проблему.
Более быстрый способ сделать ваш поворот будет: df.set_index(['sale_id', 'item']).unstack()






Я считаю, что стандартный алгоритм совместной фильтрации будет выглядеть примерно так:
Это то, что все это выглядит для меня. Это должно иметь линейную пространственную сложность, и я уверен, что ее еще можно улучшить, но она может работать.
from itertools import combinations
import pandas as pd
df = pd.DataFrame({"sale_id": [1, 1, 1, 2, 2, 3, 3, 3, 3, 4],
"item": ["A", "B", "C", "A", "C", "A", "D", "E", "C", "B"],
"qty": [1, 4, 3, 2, 8, 3, 6, 5, 12, 9]})
# we don't care about quantity
df = df.loc[:, ['sale_id', 'item']]
# Get all the unique sets of items sold
grp = df.groupby('sale_id').transform(lambda x: ''.join(x))
purchases = grp['item'].apply(lambda x: ''.join(set(x))).unique()
# create all possible two-item pairs, then iterate over them
# adding 1 to the value of dictionary when the purchase
# matches the combination
unique_items = df.item.unique()
res = {}
for c in combinations(unique_items, 2):
c = set(c)
res[frozenset(c)] = 0
for i in purchases:
if c.intersection(i) == c:
res[frozenset(c)] += 1
# get percentages
for k, v in res.items():
res[k] = v / purchases.shape[0]
Выход:
{frozenset({'A', 'B'}): 0.25,
frozenset({'A', 'C'}): 0.75,
frozenset({'A', 'D'}): 0.25,
frozenset({'A', 'E'}): 0.25,
frozenset({'B', 'C'}): 0.25,
frozenset({'B', 'D'}): 0.0,
frozenset({'B', 'E'}): 0.0,
frozenset({'C', 'D'}): 0.25,
frozenset({'C', 'E'}): 0.25,
frozenset({'D', 'E'}): 0.25}
Код выполняет первый цикл уже около 20 минут, но не похоже, что он собирается исчерпать мою оперативную память, принимая ваш ответ только по объяснению алгоритма.
@RenanKlehm, временная сложность этого алгоритма довольно низкая, но в целом временная сложность для совместной фильтрации довольно плохая.
@pavel Я обрисовываю в своем ответе небольшое изменение в вашем ответе, которое может значительно ускорить процесс ~
Я решил найти чисто Pandas-способ ведения дел, и в итоге также довольно много оптимизировал метод Павла.
Прежде всего, что я делаю:
df = pd.DataFrame({"sale_id": [1, 1, 1, 2, 2, 3, 3, 3, 3, 4],
"item": ["A", "B", "C", "A", "C", "A", "D", "E", "C", "B"],
"qty": [1, 4, 3, 2, 8, 3, 6, 5, 12, 9]})
df.drop('qty', axis=1, inplace=True)
Затем начинается трассировка времени/памяти:
values = df.groupby('sale_id')['item'].agg(lambda x: [*combinations(sorted(x), 2)]).explode().value_counts()
denom = df['sale_id'].nunique()
output = (values/denom).reindex(combinations(df['item'].unique(), 2), fill_value=0)
output
Вывод, синхронизация и использование памяти:
(A, B) 0.25
(A, C) 0.75
(A, D) 0.25
(A, E) 0.25
(B, C) 0.25
(B, D) 0.00
(B, E) 0.00
(C, D) 0.25
(C, E) 0.25
(D, E) 0.25
Name: item, dtype: float64
475 µs ± 13.9 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
(End, Peek) Memory: (5442, 16440)
Единственные отличия заключаются в извлечении только item из groupby, поэтому создается Series, а не DataFrame, используется agg вместо transform и не используется set, поскольку после agg в этом нет необходимости.
grp = df.groupby('sale_id')['item'].agg(lambda x: ''.join(x))
purchases = grp.apply(lambda x: ''.join(x)).unique()
unique_items = df.item.unique()
res = {}
for c in combinations(unique_items, 2):
c = set(c)
res[frozenset(c)] = 0
for i in purchases:
if c.intersection(i) == c:
res[frozenset(c)] += 1
for k, v in res.items():
res[k] = v / purchases.shape[0]
res
Вывод, синхронизация и использование памяти:
{frozenset({'A', 'B'}): 0.25,
frozenset({'A', 'C'}): 0.75,
frozenset({'A', 'D'}): 0.25,
frozenset({'A', 'E'}): 0.25,
frozenset({'B', 'C'}): 0.25,
frozenset({'B', 'D'}): 0.0,
frozenset({'B', 'E'}): 0.0,
frozenset({'C', 'D'}): 0.25,
frozenset({'C', 'E'}): 0.25,
frozenset({'D', 'E'}): 0.25}
276 µs ± 6.59 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
(End, Peek) Memory: (7643, 19402)
grp = df.groupby('sale_id').transform(lambda x: ''.join(x))
purchases = grp['item'].apply(lambda x: ''.join(set(x))).unique()
unique_items = df.item.unique()
res = {}
for c in combinations(unique_items, 2):
c = set(c)
res[frozenset(c)] = 0
for i in purchases:
if c.intersection(i) == c:
res[frozenset(c)] += 1
for k, v in res.items():
res[k] = v / purchases.shape[0]
res
Использование времени и памяти:
1.01 ms ± 30.6 µs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)
(End, Peek) Memory: (15847, 27630)
Использовались следующие методы: %timeit в блокноте Jupyter и tracemalloc для трассировки памяти.
@pavel Нашел еще два изменения, сократив время еще на 50%~
На самом деле ваше решение на основе pandas кажется лучшим для решения проблем с памятью, с которыми столкнулся OP, хотя с точки зрения баланса производительности/памяти решение 2, конечно, является лучшим. Самая большая проблема заключается в том, что у OP есть 2000 продуктов, поэтому это примерно 2e6 различных комбинаций размера 2. Так что это займет немного времени.
Я не получил никакой разницы с этим небольшим набором данных, но иногда производительность можно улучшить, изменив строковые столбцы с большим количеством повторений на категориальный тип. Ака df.item = df.item.astype('category'), это также почти всегда снижает использование памяти DataFrame ~
Интересно, что в моем фрейме данных с 20000 различных элементов первый метод, использующий только Pandas, выполнялся всего за 1 минуту, тогда как другие методы все еще зависали в цикле на несколько минут. Я предполагаю, что у Pandas может быть какая-то оптимизация, работающая за капотом.
@РенанКлем. да, это довольно странно. Вполне вероятно, что улучшение производительности связано с выполнением кода вне интерпретатора Python и в первую очередь на C, что и сделали бы pandas. Общая сложность алгоритма примерно одинакова, но панды, работающие на C за капотом, предлагают значительный прирост.
Хотели бы вы добавить источник вашего фрейма данных, чтобы людям не приходилось заново создавать его вручную?