Skip to main content

ranklens

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

Модель поменяли — стало ли лучше, где и почему? ranklens отвечает на это из таблиц pandas одной строкой: разница с интервалом, p- и q-value, минимальный заметный эффект (MDE), разбивка по сегментам с поправкой на множественные сравнения. Метрики совпадают с trec_eval до 1e-9.

import ranklens as rl

rl.compare(current, new, qrels, ["ndcg@10"], by="device")

Типичные задачи

Примеры — на синтетическом каталоге из docs/examples/catalog.py: запросы, товары и оценки выдуманы, но устроены как в настоящем поиске. На главной странице документации они выполняются при каждой сборке.

    query                            product  grade history  base_s0  new_s0
0  молоко            Молоко Луговое 3,2% 1 л      3     нет     0.92    1.08
1  молоко  Молоко Северная ферма 2,5% 930 мл      2     нет     0.97    1.04

Эффект виден только в сегменте. Новая модель знает, покупал ли пользователь товар раньше, — такой признак есть только у пользователей с историей покупок (около 9% запросов). У каждой модели два сида: они усредняются по запросу.

rl.compare([base_s0, base_s1], [new_s0, new_s1], qrels, ["ndcg@10"], by="history")
                 n_queries   delta  ci_low  ci_high     mde  q_value
metric  history
ndcg@10 есть            29  0.0387  0.0190   0.0616  0.0308   0.0006
        нет            291 -0.0010 -0.0034   0.0015  0.0035   0.4194

В среднем по всем запросам прирост +0.0026 не отличим от нуля и меньше MDE (0.0046) — минимального эффекта, который эти запросы способны показать. Разбивка находит, где он.

Две версии выдачи размечены разными партиями асессоров.

rl.agreement(batch_cur, batch_new)
# Agreement(2495 common pairs: exact 88.8%, mean grade A 1.801, B 1.913, shift +0.112)
qrels = rl.merge_qrels(batch_cur, batch_new)  # одна разметка для обеих сторон
rl.compare(current, new_version, qrels, ["ndcg@5", "ndcg@10", "err@10"])
         n_queries   delta  ci_low  ci_high     mde  q_value
metric
ndcg@5         320 -0.0019 -0.0039   0.0000  0.0028   0.0801
ndcg@10        320  0.0629  0.0559   0.0699  0.0099   0.0003
err@10         320  0.0019 -0.0016   0.0054  0.0051   0.2966

Вторая партия мягче, поэтому сравниваем на общей разметке. NDCG@10 значимо выше, а метрики верха выдачи — нет: новая версия улучшает позиции 6–10.

Почему: вклад признаков в метрику. Модель — CatBoost, LightGBM, XGBoost, scikit-learn, PyTorch, ONNX или любая функция скоринга.

ex = rl.explainer(model, df, query="query", doc="product", relevance="grade")
ex.importance("ndcg@10")  # насколько падает метрика, если перемешать признак
ex.worst(20)  # худшие запросы и признаки, которые их подвели
               drop of ndcg@10
feature
text_match              0.1548
bought_before           0.0066
popularity              0.0046
price_rank              0.0006

Установка

pip install "ranklens[viz]"

Extras: viz (графики и HTML-отчёты), arrow (parquet, Feather), models (CatBoost, LightGBM, XGBoost, scikit-learn), neural (ONNX). PyTorch ставится по инструкции pytorch.org. Свежий main — pip install "ranklens @ git+https://github.com/AlexandrBaranov7/ranklens".

Данные и командная строка

rl.run и rl.qrels берут таблицу pandas, polars или pyarrow с одной строкой на пару «запрос — документ»; порядок — по скору модели (score=), по позиции (rank=) или порядку строк. Несколько выдач на запрос (по магазинам, сессиям) — list_id=.

qrels = rl.qrels(labels, query="qid", doc="sku", relevance="grade")
current = rl.run(df_a, query="qid", doc="sku", rank="pos", segments="device")
new = rl.run(df_b, query="qid", doc="sku", rank="pos", segments="device")

Те же сравнения из терминала — для файлов и CI:

ranklens eval --run run.trec --qrels qrels.trec --metrics ndcg@10 mrr@10
ranklens compare --baseline a.csv --candidate b.csv --qrels qrels.csv \
    --metrics ndcg@10 mrr@10 --segments device locale --report compare.html
ranklens explain --model model.cbm --features features.csv --top-worst 20
metric   device   locale  queries  mean A  mean B  delta    95% CI              p       q
ndcg@10  desktop  ru      60       0.5352  0.5352  +0.0000  [+0.0000, +0.0000]  1.0000  1.0000
ndcg@10  mobile   en      60       0.6490  0.6329  -0.0161  [-0.0302, -0.0038]  0.0655  0.0982
ndcg@10  mobile   ru      60       0.6137  0.6926  +0.0789  [+0.0409, +0.1234]  0.0005  0.0015  *

--report compare.html пишет один файл со всеми графиками, который открывается без интернета: таблица, покрытие разметкой, разница по сегментам с интервалами (значимые после поправки — синим), распределение поразапросных разниц, метрика по cutoff, перемещения документов из топ-10.

Отчёт сравнения: таблица и разница по сегментам

Подробнее — «Первые шаги».

Что Где
Таблицы pandas/polars → сравнение одной строкой: сиды, сегменты, MDE, покрытие разметки import ranklens as rl
NDCG, MAP, MRR (умолчания trec_eval), ERR, RBP, RBO, judged@k; свои метрики — плагином ranklens.metrics
Парный bootstrap по запросам, перестановочный тест, Бенджамини — Хохберг, MDE ranklens.stats
Сравнение по сегментам с q-values ranklens.aggregate
Неточная разметка: согласие партий, общая разметка, PPI++ (дешёвая proxy-разметка + gold-выборка), judged@k rl.agreement, rl.merge_qrels, ranklens.stats
Off-policy: IPS и SNIPS по логам кликов, PBM, симулятор с известной истиной, ESS ranklens.offpolicy
Объяснение: Шепли для пары документов, вклад признаков в метрику, группы коррелированных признаков, худшие запросы rl.explainer, ranklens.explain
Графики и самодостаточные HTML/Markdown-отчёты ranklens.viz, ranklens.report

Почему не существующие библиотеки

  • pytrec_eval — обёртка над trec_eval: быстрый эталон метрик, десятки мер. ranklens сверяется с ним golden-тестами и проигрывает ему в числе мер. Но pytrec_eval отвечает на «сколько», а не на «отличимо ли это от шума» — сравнения, интервалов и сегментов в нём нет.
  • ir_measures — единый интерфейс к нескольким реализациям мер; хорош, когда нужна именно мера из конкретного инструмента. Статистику сравнения и объяснения он не делает.
  • ranx умеет и метрики, и статистическое сравнение прогонов, и fusion — ближайший аналог в части сравнения. ranklens добавляет то, чего в нём нет: поправку FDR по сегментам, MDE, PPI для неточной разметки, off-policy оценку по кликам и объяснение модели по метрике.

ranklens не заменяет их в скорости и полноте набора мер — он закрывает путь от «цифра изменилась» до «вот где и почему».

Архитектура

Слои с проверяемыми границами: core (типы, результаты, протоколы, исключения — только stdlib) → io, metrics, stats, viz → aggregate, offpolicy, explain, report → api (import ranklens as rl) и cli. tests/test_architecture.py разбирает AST каждого модуля и падает, если слой импортирует то, что ему не положено, или тяжёлую библиотеку не лениво. Прогоны читаются потоком (генератор, в памяти — одна выдача), разметка — в словарь для поиска за O(1). Подробно — «Устройство».

Выбор структур данных и средств языка

Требование Где Почему так
list буфер строк текущего запроса в iter_run, iter_clicklog дописывание в конец — амортизированное O(1); собранная выдача сразу становится tuple
tuple RankedList.docs, SegmentKey, Impression.positions, вклады признаков порядок — это позиция; неизменяем и хешируем: поле frozen-объекта и ключ словаря. list в frozen dataclass менялся бы на месте
dict Qrels: dict[QueryId, dict[DocId, float]], реестр метрик, ADAPTERS поиск оценки пары запрос–документ за O(1) в горячем цикле; реестры вместо цепочек if — точки расширения
set дубликаты документов в RankedList, общие запросы двух прогонов, повторы в ридерах проверка дубликатов O(n) вместо O(n²); общие запросы — ровно a.keys() & b.keys()
генератор iter_run, iter_clicklog, iter_records в памяти — строки одного запроса: на отсортированном входе память постоянна, на сгруппированном растёт на ~200 байт на запрос (id уже прочитанных); тесты меряют пик tracemalloc на файле в 10 раз больше
свой итератор PairedRuns (__iter__/__next__) merge-join двух отсортированных потоков; zip молча спарил бы разные запросы, когда в одном прогоне запрос пропущен
своё исключение иерархия RankLensError: DataError, ConfigError, ModelError, StatisticalError у ошибок поля (line_no, path, column) и подсказки («did you mean 'ndcg'?»); аргументы в super().__init__ — иначе не переживают pickle (тест)
осмысленная обработка ErrorCollector: битые строки пропускаются и считаются; --strict останавливает два сценария — разведка данных и CI-гейт; коды выхода 0 / 1 конфигурация / 2 аргументы / 3 данные
контекстный менеджер AtomicReport временный файл в той же директории, fsync, os.replace только без исключения; при ошибке или Ctrl-C остаётся прежний отчёт. with open() сначала обнулил бы цель
аннотации + mypy весь пакет и тесты, mypy strict, py.typed ошибки типов ловятся в CI до тестов; пакет отдаёт типы пользователям
pytest 750+ тестов, покрытие ~99%, порог 90% точные значения, property-based (Hypothesis), golden против trec_eval, статистическая валидация на симуляциях
pyproject.toml PEP 621, [project.scripts], entry points ranklens.metrics, extras одна точка для метаданных, команды, плагинов и необязательных зависимостей
установка pip задание CI install собранный wheel ставится обычным pip в чистое окружение, и scripts/tutorial_smoke.py проходит «Первые шаги» вне репозитория
pandas + агрегация offpolicy/events.py: merge, groupby().max(), explode, value_counts; aggregate — таблица по сегментам склейка событий показов и реакций — табличная операция; сегменты отдаются DataFrame

Неизменяемость результатов: frozen=True, slots=True + tuple + MappingProxyType + read-only массивы numpy — frozen сам по себе запрещает только присваивание полей.

Почему Protocol, а не ABC

Метрика — любой вызываемый объект (ranked, judgements, k) -> float, в том числе обычная функция; модель для объяснений — любой объект с feature_names, score, native_importance. Это typing.Protocol: структурная типизация, реализации не наследуются от наших классов, и чужую модель или функцию не нужно встраивать в иерархию. С ABC каждую сторонюю модель пришлось бы оборачивать в подкласс. Имя метрики хранится в спецификации (ndcg@10): у функции нет атрибута name, и протокол с ним её бы не принял. Новая модель — класс-адаптер и одна строка в ADAPTERS; адаптер находится по MRO класса модели, поэтому подкласс LGBMRanker из чужого кода тоже распознаётся.

Почему ресемплинг по запросам

Единица наблюдения — запрос. Пусть 100 запросов по 10 документов. Ресемплинг строк считал бы, что наблюдений 1000, хотя документы одной выдачи зависимы — интервал вышел бы примерно в √10 ≈ 3 раза уже правды. Поэтому bootstrap берёт с возвращением запросы и усредняет поразапросные разницы M_B(q) − M_A(q); перестановочный тест меняет знак разницы целиком у запроса. Проверено симуляцией: 95%-й интервал покрывает истинную разницу в 95.3% из 1000 повторов. Вывод — в «Статистике сравнения».

Ограничения

  • Вход должен быть сгруппирован по запросу (строки запроса подряд), для PairedRuns — отсортирован; иначе ошибка, а не тихая потеря запросов. Разметка грузится в память целиком.
  • Неразмеченный документ считается нерелевантным (как в trec_eval): прогон, который приносит много нового, проигрывает из-за разметки. compare показывает judged@k и предупреждает о разрыве; PPI++ лечит систематическую ошибку разметки при наличии случайной gold-выборки.
  • Перцентильный bootstrap-интервал на малых выборках бывает уже нужного; поэтому p-value — из перестановочного теста, а меньше 20 запросов — предупреждение.
  • Off-policy: PBM с фиксированными propensity не описывает каскадное поведение; неверные propensity смещают IPS; документы, которых старая модель не показывала, не оцениваются никакими весами. SNIPS здесь — отношение (классический SNIPS на PBM-веса не переносится) и оценивает долю, а не клики.
  • Объяснения: точки «наполовину A, наполовину B» и перемешанные признаки бывают нереалистичными; Шепли точен до 12 признаков, дальше — по выборке порядков; текстовые кросс-энкодеры и LLM-реранкеры (без матрицы признаков) не объясняются (#34, #35).
  • Модели из pickle исполняют код при загрузке — только с --allow-pickle (SECURITY.md).

Roadmap

  • Doubly Robust оценщик и Regression-EM для оценки propensity по логам; каскадные click-модели.
  • PPI в сравнении по сегментам и в CLI; стратифицированная gold-выборка.
  • Объяснение текстовых реранкеров (#34), смещения LLM-реранкеров (#35), GPU-бэкенд метрик (#17).

Разработка

git clone https://github.com/AlexandrBaranov7/ranklens.git
cd ranklens
poetry install --all-extras   # Poetry >= 2.4
poetry run pytest

Правила — в CONTRIBUTING.md, изменения — в CHANGELOG.md.

Лицензия

MIT

Metadata

Release files for ranklens 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ranklens 0.2.0
File Size Uploaded Provenance Actions
ranklens-0.2.0.tar.gz 95.2 kB With provenance Details for ranklens-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for ranklens 0.2.0
File Interpreter ABI Platform Actions
ranklens-0.2.0-py3-none-any.whl Python 3 none any Details for ranklens-0.2.0-py3-none-any.whl

Total release size: 214.6 kB

Release files / ranklens-0.2.0.tar.gz

Download URL ranklens-0.2.0.tar.gz
Size 95.2 kB
Tags Source
SHA-256 checksum
How to use checksums
c032a9cbd2717dd03469c04de53eb73741c493bdc3f094a7cf80d5c6ca0bfbbb
BLAKE2b-256 checksum
How to use checksums
3022f8136ecde257b0bb0aa5061e3e92eb507eacf46e548b359b848362bd51bf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Release files / ranklens-0.2.0-py3-none-any.whl

Download URL ranklens-0.2.0-py3-none-any.whl
Size 119.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
1eb5749579b396d320e014252e93fc3e7e08274889e551c9abfddaef613038bb
BLAKE2b-256 checksum
How to use checksums
997a0a0e5714797aff2a6215b02badc0fb2597f405187bd96538152325114bc6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

Security for ranklens 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for ranklens-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 5, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page