Реализация AI-системы рекомендаций Cross-Sell для продаж
К нам обратился e-commerce-проект с 50 000 SKU: правило «к принтеру — картридж» давало всего 8% конверсии. Нужна была рекомендательная система на основе машинного обучения, которая находит неочевидные пары и адаптируется под поведение каждого клиента. Мы реализовали гибридную систему для e-commerce и ритейла: ассоциативные правила для базовых связок + градиентный бустинг для персонализации. Результат — рост среднего чека на 18% за первый месяц, что обеспечило значительный прирост выручки. Рассказываю, как это работает.
Проблемы, которые решаем
Статические правила не видят скрытых закономерностей. Например, клиенты, купившие детское автокресло, с вероятностью 22% приобретают органайзер для багажника — хотя товары из разных категорий. ML находит такие паттерны через ассоциативные правила (Lift > 2.0). А персонализация через градиентный бустинг учитывает историю клиента: если пользователь уже брал органайзер, рекомендация смещается на другой товар.
Другая типичная проблема — товары с коротким циклом потребления (расходники, подписки). Здесь требуется предсказание следующей покупки. Модель на основе распределения интервалов выдаёт товары, которые клиент вот-вот закажет снова. Так мы увеличиваем повторные продажи без промоакций.
Третья проблема — холодный старт для новых товаров. Если у товара нет истории покупок, правила Apriori не сработают. Решение — использовать content-based признаки (категория, цена, бренд) и коллаборативную фильтрацию через эмбеддинги. Для этого мы обогащаем модель градиентного бустинга фичами товара.
Как мы строим модель CrossSellRecommender
Market Basket Analysis + персонализация
Гибридная архитектура: сначала Apriori даёт сырые правила, затем градиентный бустинг ранжирует кандидатов под конкретного пользователя. Это сочетание обеспечивает lift конверсии в 3 раза выше, чем rule-based подход.
import pandas as pd
import numpy as np
from mlxtend.frequent_patterns import apriori, association_rules
from mlxtend.preprocessing import TransactionEncoder
from sklearn.ensemble import GradientBoostingClassifier
class CrossSellRecommender:
def __init__(self, min_support: float = 0.01, min_confidence: float = 0.1):
self.min_support = min_support
self.min_confidence = min_confidence
self.rules = None
self.cross_sell_map = {}
self.personalization_model = None
def fit_association_rules(self, orders_df: pd.DataFrame,
order_col: str = "order_id",
item_col: str = "item_id"):
"""Поиск ассоциативных правил через Apriori"""
# Корзины транзакций
baskets = orders_df.groupby(order_col)[item_col].apply(list).tolist()
te = TransactionEncoder()
te_array = te.fit_transform(baskets)
df_encoded = pd.DataFrame(te_array, columns=te.columns_)
# Частые наборы
frequent_itemsets = apriori(
df_encoded,
min_support=self.min_support,
use_colnames=True,
max_len=3
)
# Правила ассоциации
self.rules = association_rules(
frequent_itemsets,
metric="lift",
min_threshold=1.2
)
self.rules = self.rules[self.rules['confidence'] >= self.min_confidence]
self.rules = self.rules.sort_values('lift', ascending=False)
# Маппинг: item → список рекомендаций с метриками
for _, rule in self.rules.iterrows():
for antecedent in rule['antecedents']:
if antecedent not in self.cross_sell_map:
self.cross_sell_map[antecedent] = []
for consequent in rule['consequents']:
if antecedent != consequent:
self.cross_sell_map[antecedent].append({
'item_id': consequent,
'confidence': rule['confidence'],
'lift': rule['lift'],
'support': rule['support']
})
# Сортировка по lift
for item in self.cross_sell_map:
self.cross_sell_map[item].sort(key=lambda x: x['lift'], reverse=True)
def recommend_cross_sell(self, cart_items: list[str],
user_history: list[str] = None,
n: int = 5) -> list[dict]:
"""Cross-sell для текущей корзины"""
candidates = {}
for item_id in cart_items:
related = self.cross_sell_map.get(item_id, [])
for rec in related:
rec_id = rec['item_id']
# Пропускаем уже в корзине или в истории
if rec_id in cart_items:
continue
if user_history and rec_id in user_history:
continue
if rec_id not in candidates:
candidates[rec_id] = {'score': 0, 'triggers': []}
candidates[rec_id]['score'] += rec['lift']
candidates[rec_id]['triggers'].append(item_id)
# Нормализация
if not candidates:
return []
sorted_candidates = sorted(
[{'item_id': k, **v} for k, v in candidates.items()],
key=lambda x: x['score'],
reverse=True
)
return sorted_candidates[:n]
def get_complementary_categories(self, category: str) -> list[str]:
"""Взаимодополняющие категории"""
category_rules = self.rules[
self.rules['antecedents'].apply(lambda x: category in str(x))
]['consequents'].apply(lambda x: list(x)).explode().value_counts()
return category_rules.head(5).index.tolist()
Временные паттерны: следующий заказ
class NextPurchasePredictor:
"""Предсказание следующей покупки на основе истории"""
def predict_next_items(self, user_id: str,
purchase_history: list[dict],
catalog_features: pd.DataFrame) -> list[tuple]:
"""
purchase_history: [{item_id, date, quantity, category}]
Возвращает: [(item_id, probability)]
"""
if len(purchase_history) < 3:
return []
# Паттерн повторных покупок
item_intervals = {}
for i in range(1, len(purchase_history)):
item = purchase_history[i]['item_id']
prev_same = [h for h in purchase_history[:i] if h['item_id'] == item]
if prev_same:
days_between = (
pd.to_datetime(purchase_history[i]['date']) -
pd.to_datetime(prev_same[-1]['date'])
).days
if item not in item_intervals:
item_intervals[item] = []
item_intervals[item].append(days_between)
# Предсказание повторных покупок
predictions = []
last_purchase_date = pd.to_datetime(purchase_history[-1]['date'])
today = pd.Timestamp.now()
days_since_last = (today - last_purchase_date).days
for item_id, intervals in item_intervals.items():
avg_interval = np.mean(intervals)
std_interval = np.std(intervals) if len(intervals) > 1 else avg_interval * 0.3
# Вероятность через normal distribution
from scipy.stats import norm
prob = norm.cdf(days_since_last + 7, avg_interval, std_interval + 1)
prob -= norm.cdf(days_since_last - 7, avg_interval, std_interval + 1)
prob = min(max(prob, 0), 1)
if prob > 0.1:
predictions.append((item_id, prob))
return sorted(predictions, key=lambda x: x[1], reverse=True)[:10]
Подробнее о настройке Apriori
Association rules с min_support=0.01, min_confidence=0.1 обычно дают 500-5000 значимых правил на 100K заказов. Lift > 2.0 — сильная ассоциация. Для подбора гиперпараметров используем кросс-валидацию: разбиваем данные по времени (train/test) и выбираем min_support, min_confidence по максимальному lift на валидационной выборке.
Как обучить модель CrossSellRecommender на своих данных
- Подготовка данных. Соберите историю заказов с колонками: order_id, item_id, date. Минимальное количество транзакций — 10 000.
-
Запустите Apriori. Используйте метод
fit_association_rulesсmin_support=0.01иmin_confidence=0.1. Это даст ~1000 правил. -
Настройте параметры. Проверьте распределение lift и confidence. Если правил слишком много, увеличьте
min_supportдо 0.02 илиmin_confidenceдо 0.2. - Оцените качество. Используйте метрики precision@k и recall@k на отложенной выборке. Cross-sell через правила даёт средний uplift корзины 15-25%. Комбинация с персонализацией (история пользователя) добавляет ещё 5-10% к acceptance rate.
Как измерить качество рекомендаций?
Основные offline-метрики: precision@k, recall@k, lift@k. Мы используем усечённую корзину — скрываем часть товаров из каждой транзакции и проверяем, попадают ли они в рекомендации. Онлайн-метрики — acceptance rate, AOV uplift, доля корзин с cross-sell. На A/B-тесте с 50% аудитории типичный uplift acceptance rate составляет 10-25%.
Почему гибридная модель эффективнее rule-based?
Rule-based (например, "купил А — покажи Б") не масштабируется на тысячи товаров: правила становятся разреженными и не учитывают контекст. ML находит ассоциации с высоким lift даже для редких пар. Кроме того, гибрид персонализирует ранжирование: один и тот же товар ранжируется по-разному для разных пользователей. В наших проектах acceptance rate гибрида в 3 раза выше, чем у правил.
Этапы внедрения
| Этап | Что делаем | Результат | Длительность |
|---|---|---|---|
| 1. Аудит данных | Проверяем полноту и качество: количество SKU, длину истории, частоту заказов | Дашборд метрик, список пропусков | 1-2 дня |
| 2. Построение пайплайна | ETL для сбора данных из CRM, очистка, агрегация корзин | Датасет orders_df | 3-4 дня |
| 3. Обучение правил | Apriori, подбор min_support и min_confidence через кросс-валидацию |
Файл правил (.pkl) | 1-2 дня |
| 4. Персонализация | Обучаем градиентный бустинг на истории пользователей | Ранжирующая модель | 3-5 дней |
| 5. A/B-тест | 50% аудитории — старая система, 50% — новая. Считаем конверсию и AOV | Отчёт с uplift | 7-14 дней |
| 6. Продакшн-деплой | Интеграция через REST API, мониторинг latency p99 | Документация, дашборд | 2-3 дня |
Что входит в результат
- Документация: описание правил, метрик, инструкция для обновления модели.
- Исходный код: репозиторий с модулями
CrossSellRecommender,NextPurchasePredictor, конфиги и тесты. - API-эндпоинты: два метода —
recommend_by_cart(по текущей корзине) иrecommend_by_user(по истории). - Обучение команды: показываем, как интерпретировать результаты и дообучать модель.
- Гарантия: мы сопровождаем A/B-тест до достижения статистически значимого uplift.
Как оценить ROI для вашего бизнеса
| Метрика | Типичное значение | Как рассчитываем |
|---|---|---|
| Прирост среднего чека | 15-25% | (AOV_тест - AOV_контроль) / AOV_контроль * 100% |
| Acceptance rate | 5-12% | Клики по рекомендациям / показы |
| Доля корзин с cross-sell | 30-50% | Корзины с ≥1 рекомендованным товаром / всего корзин |
| Время окупаемости | 2-4 месяца | Стоимость внедрения / ежемесячный прирост выручки |
Чтобы оценить потенциал cross-sell для вашего ассортимента, свяжитесь с нами. Мы проведём аудит данных и предоставим прототип за 2 недели с гарантией конфиденциальности. Закажите консультацию — обсудим детали вашего проекта и подготовим индивидуальное предложение.







