Реалізація 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 тижні з гарантією конфіденційності. Замовте консультацію — обговоримо деталі вашого проєкту та підготуємо індивідуальну пропозицію.







