Полный 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 (правильно упорядочить верх).
Постановка задачи (важно сразу). Здесь revisits разрешены: цель — предсказать следующий чек-ин, а не обязательно новое место. Поэтому уже посещённое место — валидный target (в отличие от MovieLens, где обычно «remove seen»). Так как на каждого eval-пользователя ровно один post-cutoff target, Recall@K здесь ≡ HitRate@K (попали / не попали в топ-K), а MRR показывает, насколько высоко target в списке.
▸Почему retrieval уже недостаточно
Почему линейный ранкер (логистическая регрессия)
Мы сознательно берём простую логрегрессию на numpy: никакой новой зависимости в инференсе, а её коэффициенты интерпретируемы. Плата за простоту — модель линейна, поэтому весь интеллект уходит в инженерию фич.
Но осторожно с «важностями»: после стандартизации коэффициент — это условный эффект фичи при прочих равных, а не полноценный causal/production importance. При коррелирующих признаках он искажается — например, у transition самый большой коэффициент, но его абляция почти не двигает метрику (сигнал дублируется co-visitation, историей и составом candidate pool). Абляция отвечает на другой вопрос: операционный вклад группы в этом протоколе после переобучения — и её результат тоже зависит от того, чем оставшиеся фичи могут заместить убранный сигнал. «Истинной важности» не даёт ни одна из двух.
▸Формулы: логрегрессия и негативы
Ranking score кандидата: , где , обучаем на бинарную кросс-энтропию. Позитив — реальный следующий чек-ин, негативы сэмплируем из пула кандидатов. Называть это вероятностью нельзя: из-за искусственного баланса 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% случаев, и туда же попадают все недостижимые таргеты.
Мониторинг: данные не стоят на месте
Продовая модель живёт в дрейфующем мире. Режем историю на временные срезы и следим за распределением спроса — метрикой PSI (Population Stability Index):
▸Формула PSI и как читать
по бинам (у нас — децили популярности стабильного каталога). Пороги 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).
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.