← к модулям

Полный implicit-трек: ранкер + мониторинг + bias (D2b)

Над retrieval из D2a ставим настоящий feature-rich ранкер (гео / время / последовательность / персональная частота), а затем честно разбираем: откуда берётся его качество (revisit vs novel), какие фичи несут сигнал, как данные дрейфуют и какие смещения создаёт персонализация. Переход от «модель считает» к production-мышлению.

⚠ Ранкер обучается и оценивается офлайн на реальном логе (~2.02M событий); страница показывает готовый маленький артефакт (VPS не обучает и сырьё не трогает). Ранкер — логистическая регрессия на numpy, поэтому весь интеллект в инженерии фич.

загрузка артефакта ранкера…

Теория простым языком

В D2a мы построили retrieval на реальном логе. Но retrieval только собирает кандидатов «пошире»; финальный порядок решает ranker. Здесь мы ставим над retrieval полноценный feature-rich ранкер на настоящих сигналах чек-инов — и, что важнее, честно разбираем, почему он выглядит сильным, где реально слаб, как он дрейфует и какие смещения создаёт. Это переход от «модель считает» к production-мышлению.

Двухстадийная воронка на реальных данных

Та же идея, что в системном дизайне (Фаза 7), но теперь ранкер получает настоящие фичи, а не только CF-скор. Retrieval дёшев и нацелен на recall (не потерять нужное); ranker дорог и нацелен на precision (правильно упорядочить верх).

Feature-rich rankingдля каждой пары «пользователь × кандидат» собираем вектор фич из истории и ранжируем обученной моделью. На POI-данных ожидаемо сильны персональная частота, гео, время суток и переходы между местами — но ожидание надо проверять: у нас лифт дали только персональные и (частично) последовательные фичи, а абляции гео и часов дают около нуля.

Постановка задачи (важно сразу). Здесь revisits разрешены: цель — предсказать следующий чек-ин, а не обязательно новое место. Поэтому уже посещённое место — валидный target (в отличие от MovieLens, где обычно «remove seen»). Так как на каждого eval-пользователя ровно один post-cutoff target, Recall@K здесь ≡ HitRate@K (попали / не попали в топ-K), а MRR показывает, насколько высоко target в списке.

Почему retrieval уже недостаточно
БылоRetrieval (D2a) собирал кандидатов по простым сигналам.
ПроблемаRetrieval плохо сортирует верх и не учитывает богатый контекст.
ИдеяНад кандидатами поставить feature-rich ранкер.
Стало лучшеМожно использовать гео, время, recency, персональную частоту, переходы между местами.
Осталось слабымВысокий Recall может держаться на revisits; модель дрейфует; персонализация концентрирует выдачу — у нас на личной истории, а не в голове каталога.
ДальшеНелинейные ранкеры, sequential-модели, debiasing, online-мониторинг (в следующих модулях).

Почему линейный ранкер (логистическая регрессия)

Мы сознательно берём простую логрегрессию на numpy: никакой новой зависимости в инференсе, а её коэффициенты интерпретируемы. Плата за простоту — модель линейна, поэтому весь интеллект уходит в инженерию фич.

Но осторожно с «важностями»: после стандартизации коэффициент — это условный эффект фичи при прочих равных, а не полноценный causal/production importance. При коррелирующих признаках он искажается — например, у transition самый большой коэффициент, но его абляция почти не двигает метрику (сигнал дублируется co-visitation, историей и составом candidate pool). Абляция отвечает на другой вопрос: операционный вклад группы в этом протоколе после переобучения — и её результат тоже зависит от того, чем оставшиеся фичи могут заместить убранный сигнал. «Истинной важности» не даёт ни одна из двух.

Формулы: логрегрессия и негативы

Ranking score кандидата: s(u,i)=σ(wx+b)s(u,i) = \sigma(w^\top x + b), где σ(z)=11+ez\sigma(z)=\tfrac{1}{1+e^{-z}}, обучаем на бинарную кросс-энтропию. Позитив — реальный следующий чек-ин, негативы сэмплируем из пула кандидатов. Называть это вероятностью нельзя: из-за искусственного баланса 1:8 уровень выхода смещён, и без коррекции на сэмплинг плюс калибровки продуктовой вероятностью визита он не является. Для сортировки этого хватает — порядок монотонен; для порогов и экономики показа нет.

Позитивов в обучающей выборке ~11% — это баланс сэмплированных пар (1 позитив на 8 сэмплированных негативов), не реальная продуктовая конверсия и не естественная доля позитивов.

Leakage checklist — что мы проверили

Утечка в ранкинге прячется в фичах. Наш чеклист:

  • candidate-генераторы считаются только по train-истории;
  • personal_freq, seen, recency, transition, covis, pop — только из прошлого (до таргета);
  • стандартизация (scaler) фитится только на ranker-train, не на test;
  • negative sampling не использует test-target как информацию;
  • координаты и часы места (geo, hour) — квазистатичные метаданные, но агрегируем их только по train: иначе будущие (test) чек-ины утекли бы в фичи. Холодным местам даём train-медианную локацию как нейтральный fallback.
  • ранкер учится point-in-time: скользящие фолды внутри train, каждый строит все индексы на t ≤ cutoff и размечает первое событие следующего окна. Прежняя схема («последний train-чек-ин каждого юзера = позитив») держала вне фич только его собственное будущее, а глобальные агрегаты всё ещё собирались по всему train — то есть ранняя метка видела чужое будущее;
  • query-time — последнее известное событие пользователя, а не timestamp таргета: иначе постановка молча превращается в «дано, что запрос сделан в момент t».

Главная ловушка: revisit ≠ качество

В eval-когорте D2b 93% таргетов — повторные визиты (1408 из 1518; в D2a своя когорта — там 89%, и смешивать эти два числа нельзя). Ранкер с персональным prior («человек возвращается туда, где уже был») получает высокий Recall@10 почти даром. Поэтому мы разбиваем метрику на revisit и novel: на повторных местах ранкер силён (0.99), а на по-настоящему новых — обваливается до 0.01 и проигрывает даже наивной co-visitation (0.11), то есть простой со-встречаемости мест. Потолок у этих срезов тоже разный: revisit достижим всегда (история — источник кандидатов), novel — лишь в 43% случаев, и туда же попадают все недостижимые таргеты.

Пример. Высокий общий балл — свойство задачи (revisit-доминированной), а не «ума» ранкера. Всегда спрашивайте: метрика измеряет способность рекомендовать или способность угадать привычку? Именно поэтому абляция «только crowd-сигналы» (pop + covis) обрушивает ранкер до 0.21 — причём это ниже, чем даёт одна co-visitation: весь лифт несут персональные и последовательные фичи, а пара crowd-фич вдобавок ломает порядок из-за отрицательного веса популярности (разбор — в §6 лаборатории).

Мониторинг: данные не стоят на месте

Продовая модель живёт в дрейфующем мире. Режем историю на временные срезы и следим за распределением спроса — метрикой PSI (Population Stability Index):

Формула PSI и как читать

PSI=i(piqi)ln ⁣piqi\text{PSI}=\sum_i (p_i - q_i)\,\ln\!\frac{p_i}{q_i} по бинам (у нас — децили популярности стабильного каталога). Пороги 0.1 и 0.25 — отраслевые практические эвристики, а не критерии статистической значимости: PSI выше 0.25 — повод пойти разбираться, а не доказательство, что пора переобучать. Решение о ретрейне принимают вместе с просадкой качества на свежих метках и продуктовыми показателями.

Мы намеренно разделили два вида дрейфа: PSI меряет перераспределение спроса внутри существующего каталога, а приток новых мест (new-item share) — оборот каталога. Смешивать их — раздувать одно число.

На Brightkite оба сигнала высоки: спрос уезжает, каталог обновляется на 15–25% за срез. Что из этого следует аккуратно: нужно расследование и контроль качества на свежих метках, а также темпоральная валидация (B2) вместо разового офлайн-замера. Чего PSI сам по себе не доказывает — что модель уже деградировала и что ретрейн нужен с какой-то конкретной частотой: это решается замером качества во времени и ценой переобучения, а не порогом на распределении.

Смещения: цена персонализации

Оговорка о природе метрик. В датасете нет impressions, поэтому «экспозиция» тут — offline proxy: распределение того, что модель поставила бы в топ-K, а не реальные показы пользователю. Настоящую экспозицию (и, значит, честный exposure-bias) можно измерить только в production-логах с impressions.

Сильные стороны

  • Персональный ранкер релевантнее в среднем — ловит привычки и контекст.
  • Гео и часовые профили ЛЕГКО добавить — но на этих данных их абляции дают около нуля, так что «правдоподобно здесь и сейчас» пока не подтверждено; и часа запроса в датасете нет вовсе.

Слабые стороны

  • Выдача концентрируется — но НЕ в голове каталога (10.4% при базовых 10%), а по личной истории: Gini 0.74 при том, что большинство мест не показывается никогда.
  • Значимая доля топ-10 — уже посещённые места; при деплое без exploration это создаёт РИСК петли обратной связи и фильтр-пузыря (сам пузырь офлайн не измерен).
  • Новое и нишевое почти не всплывает — нужен explore/diversity и debiasing (Фаза 14).
D2b — воронка целиком (офлайн)
cands = retrieve(user)              # popularity ∪ covis ∪ transition ∪ geo ∪ history
X     = features(user, cands)      # personal_freq, seen, recency=exp(-days/30), pop, covis, transition, geo=exp(-km/5), hour
s     = sigmoid(X @ w + b)         # ranking score (НЕ вероятность: баланс 1:8 без калибровки)
topK  = cands[lexsort((cands, -s))][:K]   # ties — по item_id, не по порядку пула
# оценка: Recall@K / MRR + разрез revisit vs novel
# мониторинг: PSI по срезам; bias: head-share, Gini, revisit-подкрепление

⚠️ Что может пойти не так

  • Верить высокому общему Recall на next-POI: он держится на revisits — всегда делайте разрез revisit/novel.
  • Считать линейный ранкер «умным»: без персональных и последовательных фич остаётся crowd-пара pop+covis, и она ранжирует хуже, чем одна co-visitation.
  • Учить ранкер на фичах, посчитанных с включённым таргетом/будущим — утечка; фичи только из прошлого.
  • Мерить офлайн один раз: PSI показывает сильный дрейф — модель устаревает без ретрейна.
  • Игнорировать экспозиционное смещение: персонализация концентрирует выдачу — на этих данных не в голове каталога, а на личной истории пользователя.
  • Сравнивать ранжирующие метрики, не сверив контракт: бюджеты источников, политика недостижимого позитива и схема негативов двигают число не меньше модели.
  • Считать одиночную фичу, которая ранжирует лучше обученной модели, случайностью: обычно это расхождение objective (BCE на сэмплированных парах) с метрикой (Recall@10 на полном пуле).

🧠 Проверь себя: Ранкер даёт Recall@10 = 0.92, а co-visitation — 0.33. Значит ли это, что ранкер «втрое лучше рекомендует места»?

Где это в дорожной карте

Это D2b — полный офлайн-трек на implicit-данных: над retrieval из D2a встали feature-rich ranking, мониторинг дрейфа и анализ смещений — production-подобный пайплайн от событий до ранжирования и наблюдаемости. Дальше: нелинейные ранкеры и sequential-модели (Фазы 8–9), честная контрфактическая оценка и debiasing (Фаза 14), capstone.