Как оценить рекомендации, если товар ещё не появился в каталоге

Команда проверяет рекомендательную модель на покупках следующего месяца. Она хорошо угадывает товары, но в верхних позициях встречаются новинки, появившиеся в каталоге позже даты рекомендации. В офлайн-таблице это выглядит как удачное предсказание. На реальном сайте такую карточку нельзя было показать.

В рекомендательной задаче важны не только дата взаимодействия и разбиение пользователей. Нужно восстановить множество товаров, из которого сервис действительно выбирал на тот момент. Доступность, публикация карточки и ассортимент региона влияют и на выдачу, и на смысл проверочной метрики.

Что было известно в момент показа

Сохраните дату расчёта, товары-кандидаты и признаки на эту дату. Покупки после неё могут служить целевым событием, но не источником популярности, цены или описания для обучения прошлой модели. Если популярность посчитана по всему месяцу, завтрашние лидеры продаж незаметно помогают вчерашней выдаче.

Для начала достаточно каталога с датой публикации. В реальном сервисе добавятся интервалы снятия с продажи, регион и ограничения доставки. Нынешний статус карточки не заменяет исторический. Товар, доступный сегодня, мог отсутствовать в момент оценки; удалённая сегодня карточка могла быть обычным кандидатом месяц назад.

Невозможный товар меняет два места расчёта

Во-первых, из ранжирования надо убрать недоступных кандидатов до выбора top-k. Удаление после обрезания выдачи сократит список и даст модели меньше шансов. Во-вторых, нужно явно определить знаменатель recall. Если покупка произошла у товара, который на дату рекомендации ещё не существовал в каталоге, историческая модель не могла его предложить.

Можно считать качество среди доступных будущих покупок и отдельно долю покупок, достижимых для такого сценария. Второй показатель не даёт спрятать ограничения каталога за красивым recall. Когда сервис должен продвигать ещё не продающиеся новинки по анонсам, это уже другой контракт доступности и другой набор данных.

from datetime import date
asof = date(2026, 9, 1)
published = {
    'A': date(2026, 8, 1), 'B': date(2026, 8, 20),
    'C': date(2026, 9, 5), 'D': date(2026, 7, 1),
}
scores = {'A': .8, 'B': .7, 'C': .95, 'D': .6}
future_bought = {'B', 'C'}
eligible = {item for item, day in published.items() if day <= asof}
ranked = sorted(scores, key=lambda x: (-scores[x], x))
naive = ranked[:2]
valid = [x for x in ranked if x in eligible][:2]
reachable = future_bought & eligible
hits = len(set(valid) & reachable)
recall = hits/len(reachable) if reachable else None
coverage = len(reachable)/len(future_bought) if future_bought else None
print('naive_top2', naive, 'eligible_top2', valid)
print('reachable_truth', sorted(reachable))
print('eligible_recall_at_2', recall, 'target_reachability', coverage)
assert naive == ['C', 'A'] and valid == ['A', 'B']
assert recall == 1. and coverage == .5

Без ограничения top-2 содержит C и A. Товар C появится только через четыре дня, поэтому такой показ невозможен. После фильтрации кандидатов получаем A и B. Среди доступных будущих покупок найдена одна из одной, recall@2 равен 1. При этом доступна лишь половина всех будущих покупок: достижимость цели составляет 0,5.

Для пользователей без достижимых покупок recall не определён. Сохраните их число отдельной строкой. Не превращайте отсутствие знаменателя в нулевое качество и не исключайте пользователей из отчёта без объяснения. Продуктовой команде важно понимать, какую часть аудитории вообще оценивает итоговое среднее.

Как сравнить две модели честно

Выдавайте обеим моделям одинаковое историческое множество кандидатов и одинаковый набор пользователей. Зафиксируйте размер выдачи, правила исключения уже купленных товаров и разрешение равных score. Иначе изменение каталога или фильтра выдачи будет выглядеть как улучшение ранжирования.

Проверяйте несколько последовательных дат. Одна дата с большим запуском новинок может сильно отличаться от обычной недели. В каждом окне обучение использует только прошлое, каталог восстанавливается на дату расчёта, а покупки наблюдаются в одинаковом будущем горизонте. Так сравнение отражает будущий рабочий цикл обновления модели.

Разделите результаты для старых и новых товаров. Новинка, уже опубликованная к моменту рекомендации, допустима, но у неё может не быть истории покупок. Это холодный старт. Если модель использует только взаимодействия, заранее определите резервный способ ранжирования таких карточек, например по доступным характеристикам или согласованному правилу продвижения.

Офлайн-попадание в будущую покупку не доказывает рост продаж от показа. Пользователь мог купить товар и без рекомендации. Исторический тест удобен для отсева слабых вариантов и проверки достижимости, а влияние нового блока на поведение требует отдельного продуктового эксперимента.

Что передать в разработку

Вместе с моделью сохраните правила кандидатов и схему их исторического восстановления. Добавьте проверку: каждый рекомендованный товар существовал и был разрешён на дату расчёта. Полезно хранить причину исключения, чтобы при падении числа кандидатов отличить проблему каталога от изменения модели.

Уточните также источник score в исторической выдаче. Фильтрация по дате публикации исправляет множество кандидатов, но не очищает уже обученную на будущем модель. Все взаимодействия и производные признаки, использованные при получении score, должны предшествовать дате расчёта. Сохраните дату обучающей выборки рядом с каждой проверяемой выдачей. Тогда проверка доступности товара дополнит временной split, а не создаст ложное ощущение устранённой утечки.

Для работы с временными данными и воспроизводимой приёмкой подходит платный курс «ML Engineer / Data Scientist: табличные модели в продакшн». В программе вы готовите признаки, строите валидацию без утечек и переносите модель в batch-инференс и API. Откройте программу курса, чтобы собрать общий pipeline, где момент доступности данных проверяется так же внимательно, как итоговая метрика.

Продолжить на практике

  • ML Engineer / Data Scientist: табличные модели в продакшн ↗

    Практический курс для тех, кто хочет получить навыки ML Engineer / Data Scientist на реальных табличных задачах. В курсе Вы не просто обучите CatBoost/LightGBM, а соберёте полноценный production-style пайплайн: подготовка данных, feature engineering (генерация признаков), корректная валидация, защита от leakage (утечек), тюнинг через Optuna, интерпретация через SHAP, batch-инференс и REST API для предсказаний.