Если вы когда-нибудь задумывались, почему в Магните или Пятерочке пельмени лежат рядом с майонезом, а хлеб с молоком? Все, что происходит в крупных сетевых магазинах происходит не просто так :-)
Анализ покупательской корзины (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/
✔️Больше про будни и задачи аналитика данных в моем тг ?Таня и Данные?
Комментарии (3)

Akina
02.03.2026 05:39Продвинутый анализ: когда больше двух товаров
А вот тут у вас не лучшее, ну или во всяком случае не универсальное, решение. Потому что вы заявляете "больше", но используете хардкод - ровно три, ни больше ни меньше. В то время как я лично ожидал увидеть универсальное решение - подходящее и для двух, и для трёх, и для четырёх, а в оптимуме - ещё и одновременно. Это было бы красиво...
Жаль, что у вас не Постгресс, он позволяет создать такое универсальное решение достаточно эффективным - на основании данных из отдельных транзакций сгенерировать все возможные сочетания (скажем, в формате массива идентификаторов продуктов), затем по вхождению связать, агрегировать, и провести анализ показателей для полученных групп. Для SQLite и SQL Server придётся делать то же через CSV или там JSON, что будет не так эффективно.

uaggster
02.03.2026 05:39Посмотрите цикл лекций "Специалиста" имеющиеся в открытом доступе, а именно пару лекций Федора Самородова "Реляционное деление":
https://www.specialist.ru/center/video/417/relyacionnoe-delenie-v-sql-chastj-2
https://www.youtube.com/watch?v=WugkIMlnIk8
Там именно об этом, с разных сторон, и подробно.
Kwisatz
Бытует мнение (не мое) что товары, которые чаще всего покупают в паре, должны быть максимально далеко друг от друга. Можете подтвердить или опровергнуть?