Вероятность повторной продажи одних и тех же предметов в фрейме данных pandas

Мне нужно знать вероятность совместной продажи одинаковых товаров на основе истории продаж, отформатированной следующим образом:

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

Я хочу построить матрицу следующим образом:

Вероятность повторной продажи одних и тех же предметов в фрейме данных pandas

Я попытался повернуть фрейм данных и использовать pd.DataFrame.corr() с пользовательским вызываемым объектом, но у меня закончилась оперативная память, вызвав:

pd.pivot_table(df, index = "sales_id", columns = "item")

Фактический фрейм данных, который я использую, имеет длину 700 000 строк и 20 000 различных элементов.

Хотели бы вы добавить источник вашего фрейма данных, чтобы людям не приходилось заново создавать его вручную?

pavel 13.05.2022 01:49

К сожалению, я не могу, фрейм данных содержит конфиденциальные данные, но я отредактировал вопрос с кодом, чтобы сгенерировать фрейм данных, который я использовал в качестве примера.

Renan Klehm 13.05.2022 01:58

Легко. Вы уже сделали что-нибудь или вы полностью застряли?

pavel 13.05.2022 02:11

Я попытался повернуть элементы в столбцы, а затем выполнить итерацию по фрейму данных, подсчитывая, сколько раз A и B происходят на одном и том же sale_id. Но у меня закончилась оперативная память, просто повернув.

Renan Klehm 13.05.2022 02:15

Это хорошо, чтобы включить в свой вопрос. Вам нужно объяснить, что вас больше всего беспокоит пространственная сложность алгоритма.

pavel 13.05.2022 02:17

Спасибо, я снова отредактировал вопрос, чтобы лучше объяснить мою проблему.

Renan Klehm 13.05.2022 02:23

Более быстрый способ сделать ваш поворот будет: df.set_index(['sale_id', 'item']).unstack()

BeRT2me 13.05.2022 02:57
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
7
63
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я считаю, что стандартный алгоритм совместной фильтрации будет выглядеть примерно так:

  • сначала вам нужно сгруппировать ваши данные по sale_id и объединить значения в столбце элемента.
  • Затем для каждой группы нужно создать набор предметов, которые были куплены вместе.
  • Затем, наконец, вам нужно создать все возможные комбинации существующих элементов в виде набора и выполнить пересечение с вашими фактическими наборами элементов.

Это то, что все это выглядит для меня. Это должно иметь линейную пространственную сложность, и я уверен, что ее еще можно улучшить, но она может работать.

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 минут, но не похоже, что он собирается исчерпать мою оперативную память, принимая ваш ответ только по объяснению алгоритма.

Renan Klehm 13.05.2022 03:11

@RenanKlehm, временная сложность этого алгоритма довольно низкая, но в целом временная сложность для совместной фильтрации довольно плохая.

pavel 13.05.2022 03:27

@pavel Я обрисовываю в своем ответе небольшое изменение в вашем ответе, которое может значительно ускорить процесс ~

BeRT2me 13.05.2022 06:48
Ответ принят как подходящий

Я решил найти чисто 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)

Затем начинается трассировка времени/памяти:

  1. Единственный метод панд:
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)
  1. Оптимизированная версия метода Павла:

Единственные отличия заключаются в извлечении только 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)
  1. Оригинальный метод:
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%~

BeRT2me 13.05.2022 07:06

На самом деле ваше решение на основе pandas кажется лучшим для решения проблем с памятью, с которыми столкнулся OP, хотя с точки зрения баланса производительности/памяти решение 2, конечно, является лучшим. Самая большая проблема заключается в том, что у OP есть 2000 продуктов, поэтому это примерно 2e6 различных комбинаций размера 2. Так что это займет немного времени.

pavel 13.05.2022 07:12

Я не получил никакой разницы с этим небольшим набором данных, но иногда производительность можно улучшить, изменив строковые столбцы с большим количеством повторений на категориальный тип. Ака df.item = df.item.astype('category'), это также почти всегда снижает использование памяти DataFrame ~

BeRT2me 13.05.2022 07:37

Интересно, что в моем фрейме данных с 20000 различных элементов первый метод, использующий только Pandas, выполнялся всего за 1 минуту, тогда как другие методы все еще зависали в цикле на несколько минут. Я предполагаю, что у Pandas может быть какая-то оптимизация, работающая за капотом.

Renan Klehm 13.05.2022 12:26

@РенанКлем. да, это довольно странно. Вполне вероятно, что улучшение производительности связано с выполнением кода вне интерпретатора Python и в первую очередь на C, что и сделали бы pandas. Общая сложность алгоритма примерно одинакова, но панды, работающие на C за капотом, предлагают значительный прирост.

pavel 13.05.2022 12:32

Другие вопросы по теме