Если вы когда-нибудь задумывались, почему в Магните или Пятерочке пельмени лежат рядом с майонезом, а хлеб с молоком? Все, что происходит в крупных сетевых магазинах происходит не просто так :-)
Анализ покупательской корзины (market basket analysis) отвечает на простой вопрос: какие товары люди покупают вместе? Зная это, можно:
располагать связанные товары рядом, это как раз пример выше, он же кросс-мерчандайзинг;
собирать комплекты и акции: купи пельмени и получи майонез в подарок;
рекомендовать товары в мобильном приложении (при добавлении в корзину пельменей у покупателя появляется рекомендация: с этим товаром также покупают майонез).
В этой статье мы разберем одну из типичных задач вида «Анализ покупательской корзины» на учебных данных, с кодом SQL, со всей необходимой математикой и с примером выводов. Задача не боевая, а лишь учебный пример.
1. Исходные данные
Представим, что у нас есть интернет-магазин продуктов и данные хранятся в двух таблицах:
Таблица transactions (справочник со всеми чеками):
transaction_id | date | customer_id |
|---|---|---|
1001 | 2026-02-01 | 501 |
1002 | 2026-02-01 | 502 |
1003 | 2026-02-02 | 501 |
Таблица transaction_items (состав всех чеков из справочника):
transaction_id | product_id | product_name | quantity | price |
|---|---|---|---|---|
1001 | 101 | Хлеб Бородинский | 1 | 45.00 |
1001 | 102 | Молоко Простоквашино | 2 | 89.00 |
1001 | 103 | Пельмени Цезарь | 1 | 120.00 |
1002 | 101 | Хлеб Бородинский | 1 | 45.00 |
1002 | 104 | Колбаса Докторская | 1 | 180.00 |
1003 | 102 | Молоко Простоквашино | 1 | 89.00 |
1003 | 103 | Пельмени Цезарь | 2 | 120.00 |
1003 | 105 | Майонез Слобода | 1 | 70.00 |
Это классический транзакционный формат данных, он встречается почти всегда и везде, в нашем примере на один чек (транзакцию) может быть несколько строк, по одной на каждый купленный товар.
2. Постановка задачи
Категорийный менеджер просит ответить на вопросы:
Какие пары товаров чаще всего встречаются в одном чеке?
Для каждого товара, что еще кладут в корзину вместе с ним?
Какие сочетания товаров приносят больше всего выручки?
Будем решать задачу на чистом SQL.
3. Простая статистика: частотность пар
Для начала найдем все пары товаров, которые встречаются в одном чеке.
3.1. Джойн таблицы на саму себя (self-join) для поиска пар
Ключевая идея: соединяем таблицу transaction_items саму с собой по transaction_id, чтобы для каждого чека получить все возможные комбинации товаров внутри него .
SELECT ti1.transaction_id, ti1.product_id AS product_a, ti1.product_name AS product_a_name, ti2.product_id AS product_b, ti2.product_name AS product_b_name FROM transaction_items ti1 JOIN transaction_items ti2 ON ti1.transaction_id = ti2.transaction_id AND ti1.product_id < ti2.product_id -- убираем дубли и самосоединения WHERE ti1.transaction_id IN (1001, 1002, 1003) -- для примера ORDER BY ti1.transaction_id;
Результат:
transaction_id | product_a | product_a_name | product_b | product_b_name |
|---|---|---|---|---|
1001 | 101 | Хлеб Бородинский | 102 | Молоко Простоквашино |
1001 | 101 | Хлеб Бородинский | 103 | Пельмени Цезарь |
1001 | 102 | Молоко Простоквашино | 103 | Пельмени Цезарь |
1002 | 101 | Хлеб Бородинский | 104 | Колбаса Докторская |
1003 | 102 | Молоко Простоквашино | 103 | Пельмени Цезарь |
1003 | 102 | Молоко Простоквашино | 105 | Майонез Слобода |
1003 | 103 | Пельмени Цезарь | 105 | Майонез Слобода |
Что мы сделали:
Ограничение ti1.product_id < ti2.product_id гарантирует, что пара (хлеб, молоко) попадет в результат только один раз, а не дважды как (хлеб, молоко) и (молоко, хлеб).
Условие WHERE ti1.transaction_id IN (1001, 1002, 1003) - выбираем три чека для примера.
В чеке 1001 с тремя товарами получилось 3 пары (формула сочетаний: C(3,2) = 3).
3.2. Подсчет частоты пар
Теперь сгруппируем и посчитаем, сколько раз каждая пара встретилась:
SELECT ti1.product_id AS product_a_id, MAX(ti1.product_name) AS product_a_name, -- берем максимум, он все равно один ti2.product_id AS product_b_id, MAX(ti2.product_name) AS product_b_name, COUNT(*) AS times_together -- считаем строки FROM transaction_items ti1 JOIN transaction_items ti2 ON ti1.transaction_id = ti2.transaction_id AND ti1.product_id < ti2.product_id GROUP BY ti1.product_id, ti2.product_id ORDER BY times_together DESC;
Результат:
product_a_id | product_a_name | product_b_id | product_b_name | times_together |
|---|---|---|---|---|
102 | Молоко Простоквашино | 103 | Пельмени Цезарь | 2 |
101 | Хлеб Бородинский | 102 | Молоко Простоквашино | 1 |
101 | Хлеб Бородинский | 103 | Пельмени Цезарь | 1 |
101 | Хлеб Бородинский | 104 | Колбаса Докторская | 1 |
102 | Молоко Простоквашино | 105 | Майонез Слобода | 1 |
103 | Пельмени Цезарь | 105 | Майонез Слобода | 1 |
Инсайт: пельмени и молоко встречаются вместе в 2 чеках из 3, а хлеб с молоком - только в одном, на таком малом наборе выводы делать рано, но думаю, что базовая логика понятна.
4. Метрики качества ассоциативных правил
Простая частота встречаемости – это не единственная метрика, в анализе корзины используют три ключевых показателя, рассмотрим каждый из них.
4.1. Поддержка (Support)
Support – это доля транзакций, содержащих данный набор товаров. Для набора (X, Y):
Support(X, Y) = (число транзакций с X и Y) / (общее число транзакций)
На наших данных (3 транзакции):
Support(пельмени, молоко) = 2/3 = 0,67
Support(хлеб, молоко) = 1/3 = 0,33
4.2. Достоверность (Confidence)
Confidence – это условная вероятность: если купили X, то купят и Y .
Confidence(X → Y) = Support(X, Y) / Support(X)
Сначала нужно посчитать частоту каждого товара отдельно:
SELECT product_id, product_name, COUNT(DISTINCT transaction_id) AS transaction_count FROM transaction_items GROUP BY product_id, product_name;
Результат:
product_id | product_name | transaction_count |
|---|---|---|
101 | Хлеб Бородинский | 2 |
102 | Молоко Простоквашино | 2 |
103 | Пельмени Цезарь | 2 |
104 | Колбаса Докторская | 1 |
105 | Майонез Слобода | 1 |
Теперь считаем Confidence для правила пельмени + молоко:
Support(пельмени, молоко) = 2
Support(пельмени) = 2
Confidence = 2/2 = 1,0 (100%)
То есть если купили пельмени, то в 100% случаев купили и молоко (на примере наших трех чеков).
4.3. Лифт (Lift)
Lift показывает, насколько вероятнее покупка Y при покупке X по сравнению со случайной покупкой Y.
Lift(X → Y) = Confidence(X → Y) / Support(Y)
Для правила пельмени + молоко:
Confidence = 1,0
Support(молоко) = 2/3 = 0,67
Lift = 1,0 / 0,67 = 1,5
Интерпретация метрики:
Lift = 1 — независимы (покупка X не влияет на покупку Y)
Lift > 1 — положительная связь (X увеличивает вероятность Y)
Lift < 1 — отрицательная связь (X уменьшает вероятность Y)
Lift = 1,5 означает, что при покупке пельменей вероятность купить молоко в 1,5 раза выше, чем просто случайная вероятность покупки молока.
4.4. Финальный запрос со всеми метриками
Соберем все метрики в одном запросе:
WITH product_freq AS ( -- частотность каждого товара SELECT product_id, COUNT(DISTINCT transaction_id) AS product_freq FROM transaction_items GROUP BY product_id ), total_transactions AS ( -- общее число чеков SELECT COUNT(DISTINCT transaction_id) AS total FROM transaction_items ), pair_counts AS ( -- частотность пар SELECT ti1.product_id AS a_id, ti2.product_id AS b_id, COUNT(*) AS pair_freq FROM transaction_items ti1 JOIN transaction_items ti2 ON ti1.transaction_id = ti2.transaction_id AND ti1.product_id < ti2.product_id GROUP BY ti1.product_id, ti2.product_id ) SELECT p1.product_name AS product_a, p2.product_name AS product_b, pc.pair_freq, ROUND(100.0 * pc.pair_freq / tt.total, 2) AS support_percent, ROUND(100.0 * pc.pair_freq / pf_a.product_freq, 2) AS confidence_percent, ROUND((pc.pair_freq * tt.total) / (pf_a.product_freq * pf_b.product_freq), 2) AS lift FROM pair_counts pc CROSS JOIN total_transactions tt JOIN products p1 ON pc.a_id = p1.product_id JOIN products p2 ON pc.b_id = p2.product_id JOIN product_freq pf_a ON pc.a_id = pf_a.product_id JOIN product_freq pf_b ON pc.b_id = pf_b.product_id ORDER BY lift DESC;
5. Продвинутый анализ: когда больше двух товаров
Чеки могут содержать больше двух товаров, также найдем тройки товаров, которые часто встречаются вместе, логика такая же только становится больше джойнов. В целом логика останется неизмененой и при большем количестве товаров, просто джойнов будет становится больше.
-- тройки товаров SELECT ti1.product_id AS a_id, ti2.product_id AS b_id, ti3.product_id AS c_id, COUNT(*) AS triple_freq FROM transaction_items ti1 JOIN transaction_items ti2 ON ti1.transaction_id = ti2.transaction_id AND ti1.product_id < ti2.product_id JOIN transaction_items ti3 ON ti1.transaction_id = ti3.transaction_id AND ti2.product_id < ti3.product_id GROUP BY ti1.product_id, ti2.product_id, ti3.product_id;
Условия ti1.product_id < ti2.product_id AND ti2.product_id < ti3.product_id также, как в предыдущем примере с парами обеспечивают уникальность комбинации (избегаем перестановок).
6. Анализ с учетом выручки
Менеджеру важно не только что с чем, но и сколько денег нам все это приносит, поэтому добавим стоимость.
6.1. Ищем товары с максимальной выручкой в парах
WITH pair_revenue AS ( SELECT ti1.transaction_id, ti1.product_id AS a_id, ti2.product_id AS b_id, ti1.price * ti1.quantity + ti2.price * ti2.quantity AS pair_sum FROM transaction_items ti1 JOIN transaction_items ti2 ON ti1.transaction_id = ti2.transaction_id AND ti1.product_id < ti2.product_id ) SELECT p1.product_name AS product_a, p2.product_name AS product_b, COUNT(*) AS times_together, SUM(pr.pair_sum) AS total_revenue_from_pairs, AVG(pr.pair_sum) AS avg_check_with_this_pair FROM pair_revenue pr JOIN products p1 ON pr.a_id = p1.product_id JOIN products p2 ON pr.b_id = p2.product_id GROUP BY p1.product_name, p2.product_name ORDER BY total_revenue_from_pairs DESC;
6.2. Считаем долю кросс-продаж
Для каждого товара посчитаем, какую долю выручки он нам приносит в сочетании с другими товарами:
WITH product_total AS ( SELECT product_id, SUM(price * quantity) AS total_revenue FROM transaction_items GROUP BY product_id ), pair_revenue_for_product AS ( SELECT ti1.product_id AS product_id, SUM(ti1.price * ti1.quantity + ti2.price * ti2.quantity) AS revenue_in_pairs FROM transaction_items ti1 JOIN transaction_items ti2 ON ti1.transaction_id = ti2.transaction_id AND ti1.product_id < ti2.product_id GROUP BY ti1.product_id ) SELECT p.product_name, pt.total_revenue, COALESCE(prfp.revenue_in_pairs, 0) AS revenue_in_pairs, ROUND(100.0 * COALESCE(prfp.revenue_in_pairs, 0) / pt.total_revenue, 2) AS cross_sell_share_percent FROM products p JOIN product_total pt ON p.product_id = pt.product_id LEFT JOIN pair_revenue_for_product prfp ON p.product_id = prfp.product_id ORDER BY cross_sell_share_percent DESC;
7. Полный пайплайн задачи
Теперь на больших синтетических данных (20 чеков) посмотрим, как это работает в реальности.
Пример данных (транзакции):
Транзакции с 1 по 20, где:
Чек 1: молоко, хлеб, пельмени
Чек 2: молоко, хлеб, масло
Чек 3: молоко, пельмени, майонез, кетчуп
... и так далее
Результат анализа (топ-5 пар по лифту):
Товар А | Товар В | Вместе | Поддержка, % | Достоверность, % | Лифт |
|---|---|---|---|---|---|
Пельмени | Майонез | 8 | 40,0 | 80,0 | 2,67 |
Пельмени | Кетчуп | 7 | 35,0 | 70,0 | 2,33 |
Хлеб | Масло | 5 | 25,0 | 50,0 | 2,00 |
Молоко | Хлеб | 6 | 30,0 | 42,9 | 1,43 |
Молоко | Пельмени | 7 | 35,0 | 50,0 | 1,25 |
Пример выводов по данным:
Пельмени и майонез – у нас самая сильная связка, лифт 2,67 означает, что при покупке пельменей вероятность купить майонез почти в 3 раза выше случайной, их нужно размещать рядом, это хорошая возможность для кросс-продажи.
Пельмени и кетчуп – это тоже достаточно сильная связка (лифт у пары 2,33), значит рядом с пельменями нам нужна зона соусов.
Хлеб и масло - казалось бы, классическая пара продуктов, но лифт ниже 2,0, поэтому возможно, масло покупают и просто так и наша интуиция нас подводит.
Молоко и хлеб – пара встречаются достаточно часто, но лифт близок к 1, что говорит нам о том, что зависимость слабая, значит это просто популярные товары, которые покупают независимо.
Пример рекомендация для менеджера:
Создать пельменный островок: пельмени (заморозка) + рядом или над холодильником стеллаж с майонезом, кетчупом, соусами, это увеличит кросс-продажи и средний чек на 1 клиента.
8. Еще один полезный запрос
Для каждого товара покажем топ-3 сопутствующих товара:
WITH pairs_with_rank AS ( --подзапрос с рейтингом SELECT a_id, b_id, pair_freq, ROW_NUMBER() OVER(PARTITION BY a_id ORDER BY pair_freq DESC) AS rn FROM pair_counts ) --итог с ограничением на топ 3 SELECT p1.product_name AS product, p2.product_name AS frequently_bought_with, pair_freq FROM pairs_with_rank pwr JOIN products p1 ON pwr.a_id = p1.product_id JOIN products p2 ON pwr.b_id = p2.product_id WHERE rn <= 3 ORDER BY product, rn;
Вместо заключения
Мы разобрали один из множества вариантов анализа покупательской корзины, который можно провести на чистом SQL. Но важно помнить, что результат аналитики – это не просто цифры и это не просто понимание, что покупают наши клиенты, это возможность увеличения среднего чека, повышение эффек��ивности выкладки товаров и возможности для персонализации рекомендаций клиентам.
📚Статья с примерами задач из аналитики ассортимента https://habr.com/ru/articles/1005262/
✔️Больше про будни и задачи аналитика данных в моем тг 🌸Таня и Данные📊
