← К портфолио SERP Rank Lab preview
Ситуация
Метапоиск зарабатывает на позиции оффера, а не на самой продаже — значит ранжирование и монетизация измеряются не одним и тем же числом
Задача
Построить субстрат измерения: честный CTR, ranking-метрики и ридаут A/B-эксперимента поверх политики ранжирования
Действия
Синтетическая выдача над реальными парами маршрут/цена, PBM-модель клика, IPW и EM, NDCG/MRR, delta-method, CUPED, mSPRT — всё воспроизводимо из фиксированного seed
Результат
NDCG и revenue расходятся на 38%, наивный CTR занижен вчетверо, а наивный peeking раздут до 14.5% против 0.25% у всегда-валидного mSPRT
Стек
PythonDuckDBStreamlitNumPy / SciPypytestuv
Исходники Смотреть на GitHub → Обновлено: Oct 11, 2026
Содержание
  1. Ситуация
  2. Задача
  3. Действия
  4. Слои
  5. Методы
  6. Запуск
  7. Результат
  8. Ограничения
  9. Документация
  10. Частые вопросы

SERP Rank Lab

▶ Схема потока данных — самодостаточный HTML, открывается в браузере — без установки и сборки

Ситуация

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

Офлайн-метрика релевантности и деньги при этом не совпадают: политика, которая выглядит лучше по NDCG, может приносить меньше комиссии.

Задача

От меня требовалось построить субстрат измерения для этой поверхности: честный (не смещённый позицией) CTR, ranking-метрики и ридаут A/B-эксперимента поверх политики ранжирования — и связать их в один воспроизводимый пайплайн.

Действия

Слои

Слой Файлы Роль
Данные src/data_ingest.py, sql/01_normalize.sql booking-фид → строка на сегмент; нормализация заказов и событий
Сессии src/sessions.py, sql/02_sessions.sql правило сессии: 30 минут неактивности + граница суток
Выдача src/serp_sim.py синтетические страницы над реальными парами маршрут/цена; PBM-модель клика; экономика CPA/CPC
Оценка src/ranking_eval.py, sql/03_funnel.sql, sql/04_revenue_per_search.sql несмещённый CTR, NDCG/MRR, interleaving, revenue per search
Эксперимент src/stats.py, src/experiment.py SRM, A/A, delta-method, CUPED, mSPRT, peeking
Поверхность app.py, run.py Streamlit-дашборд; единая точка входа пайплайна

Методы

  • Позиционное смещение (PBM). Вероятность просмотра падает с позицией как (1/k)^η; клик = examined × relevance.
  • Несмещённый CTR. Inverse-propensity weighting по Horvitz–Thompson, кривая известна или подобрана EM.
  • Ratio-метрики. Revenue per search — отношение сумм, поэтому дисперсию считает delta-method, а не наивный per-unit t-test.
  • Последовательное тестирование. Фиксированный горизонт против всегда-валидного mSPRT для количественной оценки цены подглядывания.

Запуск

uv sync --all-groups
uv run python run.py --stage all      # ingest → sessions → serp → sql → rank → experiment → business
uv run pytest -q                      # 110 тестов (108 pass, 2 skip без данных-соседа)
uv run streamlit run app.py           # дашборд на :8501

Результат

Проверка Результат Чтение
NDCG@10 против revenue/search relevance-first: 1.000 / 868.9, revenue-first: 0.932 / 1,196.6 политики движутся в разные стороны — revenue на 38% выше
Наивный против позиционно-свободного CTR 0.163 против 0.629 занижение примерно вчетверо
SRM против плана 90/10 χ² = 0.119, p = 0.730 сплит корректен
Ratio-метрика, delta-method против наивной SE 69.0 против 88.5 наивный SE шире в 1.28 раза
Peeking, фиксированный горизонт FPR 14.5% за десять взглядов 2.9× от номинала
Peeking, mSPRT FPR 0.25% всегда-валидный порог держит уровень

Пайплайн проходит end-to-end из фиксированного seed; 108 тестов проходят, 2 пропускаются без опциональных данных-соседа.

Ограничения

Синтетический слой выдачи похож на реальный трафик только как предположение — настоящего лога показов для этой поверхности нет, поэтому все ranking- и A/B-числа получены на модели, а не измерены. Два «острова» реальных данных (заказы и сегменты бронирования) не имеют внешнего ключа к кликстриму и репортятся отдельно, никогда не сливаясь с моделируемой выдачей. Сырые сторонние данные не коммитятся: в репозитории только загрузчик, генератор выдачи и SQL.

Документация

Частые вопросы

Почему offline-метрика ранжирования плохой релизный гейт?

Политика, максимизирующая NDCG@10, набирает больше кликов, но зарабатывает на 38% меньше за поиск, чем revenue-first. Узкое место — revenue per search, а не офлайн-скор релевантности.

Насколько искажён наивный CTR?

Вчетверо: 0.163 против позиционно-свободного 0.629. Дело в позиционном смещении, а не в метрике; лечится inverse-propensity weighting.

Чем опасны повторные подглядывания?

На фиксированном горизонте false-positive rate растёт до 14.5% за десять взглядов. Всегда-валидный mSPRT держит его на 0.25%, но ценой более широкого порога.

Сколько стоит сплит 90/10?

2.78 раза больше трафика, чем у 50/50, при той же мощности: 55,196 против 19,871 пользователя.

Что показал CUPED на реальных данных?

На i.i.d.-пользователях ковариата не даёт выигрыша (корреляция около нуля), а на коррелированном контроле возвращает теоретические 11.2% снижения дисперсии.