Data Science
Рекомендации на корзинах зоомагазина
Не учебный датасет: 4743 настоящих заказа за два с половиной года из проекта, который лежит рядом в этом же портфолио. Сервис учится на них и подсказывает товары прямо на карточке в витрине.
Как это устроено
- FP-Growth даёт правила «берут вместе» с внятными метриками: support, confidence, lift. Но покрывает не всех — редкие товары не набирают поддержки
- Поэтому вторая модель, item-item по косинусной близости: соседи есть у 93% товаров, которые вообще встречались в заказах
- Выдача берёт правила, потом добирает соседями, и у каждой рекомендации указано, откуда она взялась
- Витрина от сервиса не зависит: таймаут две секунды, запасной путь по таксономии, ответ кэшируется на час
Запрашиваю сервис…
Методика
Откуда данные
Таблица заказов предыдущей версии Zoo-Land — того самого сайта, который лежит рядом в портфолио как «исходная точка миграции». 4743 заказа за 2,7 года, 1271 товар. Для обучения берутся корзины с двумя и более позициями: их 3382, по одиночным заказам ассоциативные правила строить не из чего.
База обезличена скриптом до того, как попала в разработку: имена, телефоны и адреса заменены. Состав корзин при этом сохранён — ровно то, что нужно market-basket анализу.
Какие метрики и почему им можно верить
- У каждого правила выводятся support, confidence и lift — все три, а не только удобная. Порог support 0,004 подобран вручную; это честно названо ограничением, а не «оптимальным значением»
- Покрытие указано рядом с цифрами: правила закрывают 195 товаров из 1271, остальное добирает item-item — 1238 товаров с соседями. Без покрытия любая метрика выглядит полной
- Качество кластеризации мерится adjusted Rand index против дерева категорий магазина. Он равен −0,017, и это не провал: кластеры оказались марочными, а каталог построен по животным. Модель нашла структуру, которой в каталоге нет
- RFM считается по 12,6% заказов, и эта доля стоит в самом ответе API. Причина — в блоке ниже
Две метрики, которые я отверг сам
Обе выглядели отличными результатами, и обе были неправдой. Показываю их потому, что метрика, которой никто не проверял, — это украшение, а не измерение.
чистота кластеров 0,975
Цифра отличная, но рядом стоял Rand index около нуля — сочетание слишком хорошее, чтобы быть правдой. Оказалось, что категории я брал только из корней дерева (parent = 0), а товары висят на листьях: 419 товаров из 445 в крупнейшем кластере попадали в класс «без категории». Чистота мерила однородность пустого класса. После подъёма от листа к корню стало 0,872 — и цифра начала означать то, что должна.
1000 покупателей, 100% повторных покупок
Ключом покупателя я взял телефон: в обезличенной базе их ровно 1000, и это выглядело готовым псевдонимом. Но обезличивание выводит телефон из id заказа (id MOD 1000) — заказы, различающиеся на 1000, получают один номер. «1000 покупателей со стопроцентными повторными покупками» — это просто 4743 / 1000. Пересчитал по user_id: 160 покупателей, 599 заказов, покрытие 12,6%.
Общий вывод: обезличивание сохранило состав корзин — то, что нужно рекомендациям, — и уничтожило идентичность покупателя, которая нужна RFM. Одни и те же данные годятся для одной задачи и не годятся для другой, и это стоит проверять до того, как цифра попадёт в отчёт.
Переобучение и что бывает, когда рекомендаций нет
Расписания переобучения нет — и это не недоделка. Датасет исторический: заказы за 2021–2023 годы из архива магазина, новые не появляются. Переобучать по графику нечего, поэтому пересчёт запускается руками (train.py, analyze.py), а служба перезапускается после — модели читаются один раз на старте. Появится живой поток заказов — понадобится и расписание, и сравнение метрик до и после.
Покрытие: честные две цифры
В каталоге 2771 товар, но в заказах встречались 1271 — остальные ни разу никто не купил. Рекомендации есть у 1187 товаров: это 93% от тех, что вообще попадали в корзины, и 42,8% каталога. Вторая цифра важнее первой: на большей части карточек сервис молчит, и это нормальное поведение, а не сбой.
Три способа остаться без ответа
- Товар вне покрытия — сервис отвечает 200 и пустым списком, а не ошибкой. Витрина в этом случае показывает подбор по таксономии, и посетитель разницы не замечает
- Файлов моделей нет — /health отдаёт «no model» вместо «ok». Ручки продолжают отвечать пустыми списками, поэтому витрина деградирует так же мягко
- Служба остановлена — витрина ждёт две секунды и уходит на запасной путь. Проверено остановкой службы, а не только чтением кода
Почему сервис работает только на одном магазине
В портфолио есть второй магазин — Melanj, пряжа. Витрина у него такая же, и место под рекомендации на карточке товара готово. Подставить их туда всё равно нельзя, и причина не в коде.
- Модель обучена на корзинах зоомагазина. Её соседи и правила — это конкретные id зоотоваров; к каталогу пряжи они отношения не имеют
- Обучить вторую модель на корзинах Melanj не выйдет: таблицы заказов в той базе нет вовсе, истории покупок не существует
Поэтому рекомендации стоят там, где есть история покупок, а на Melanj похожие товары по-прежнему подбираются по марке. Это осознанное решение, а не недоделка: подставить сюда чужие товары было бы демонстрацией, которая работает только на скриншоте.
Что данные не дают
База обезличена скриптом, и он сохранил состав корзин — ровно то, что нужно market-basket анализу, — но уничтожил идентичность покупателя: телефон в ней выводится из id заказа. Поэтому RFM считается только по авторизованным пользователям, а это 12,6% заказов. Цифра покрытия стоит рядом с сегментами намеренно: без неё выборка выглядела бы полной.