Перейти к содержанию

ranklens

Модель поменяли — стало ли лучше, где и почему? 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).

Данные — обычная таблица с одной строкой на пару «запрос — товар»: оценка асессора, признаки и скоры моделей.

import ranklens as rl
from catalog import searches  # синтетический каталог

df = searches()
print(df[["query", "product", "grade", "history", "base_s0", "new_s0"]].head(5).round(2))
    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
2  молоко  Молоко безлактозное Утро 1,5% 1 л      2     нет     0.98    1.05
3  молоко  Молоко топлёное Луговое 4% 950 мл      2     нет     0.67    0.74
4  молоко           Молоко овсяное Зерно 1 л      2     нет    -0.20   -0.21

Эффект виден только в сегменте

Новая модель знает, покупал ли пользователь товар раньше. Такой признак есть только у пользователей с историей покупок — это около 9% запросов. У каждой модели два сида обучения: они усредняются по запросу, чтобы шум обучения не выдавался за эффект.

qrels = rl.qrels(df, query="query", doc="product", relevance="grade")


def run(score):
    return rl.run(df, query="query", doc="product", score=score, segments="history")


base, new = [run("base_s0"), run("base_s1")], [run("new_s0"), run("new_s1")]

columns = ["n_queries", "delta", "ci_low", "ci_high", "mde", "q_value"]
print(rl.compare(base, new, qrels, ["ndcg@10"]).table[columns].round(4), "\n")
print(rl.compare(base, new, qrels, ["ndcg@10"], by="history").table[columns].round(4))
         n_queries   delta  ci_low  ci_high     mde  q_value
metric                                                      
ndcg@10        320  0.0026 -0.0005   0.0059  0.0046   0.1131 

                 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

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

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

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

from catalog import two_versions

current_df, new_df = two_versions()
batch_cur = rl.qrels(current_df, query="query", doc="product", relevance="grade")
batch_new = rl.qrels(new_df, query="query", doc="product", relevance="grade")
print(rl.agreement(batch_cur, batch_new), "\n")

both = rl.merge_qrels(batch_cur, batch_new)  # одна разметка для обеих сторон
current = rl.run(current_df, query="query", doc="product", rank="pos")
new_version = rl.run(new_df, query="query", doc="product", rank="pos")
result = rl.compare(current, new_version, both, ["ndcg@5", "ndcg@10", "err@10"])
print(result.table[columns].round(4))
Agreement(2495 common pairs: exact 88.8%, mean grade A 1.801, B 1.913, shift +0.112) 

         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 значимо выше, а NDCG@5 и ERR@10, которые смотрят в основном на верх выдачи, — нет: новые товары улучшают позиции 6–10, первые карточки почти не изменились. Какая метрика главная, решает то, сколько карточек пользователи реально просматривают.

Почему: вклад признаков в метрику

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

from catalog import FEATURES, new_model

from ranklens.explain import CallableAdapter

model = CallableAdapter(new_model, FEATURES)
ex = rl.explainer(model, df, query="query", doc="product", relevance="grade")
print(ex.importance("ndcg@10", n_repeats=3).round(4), "\n")  # если перемешать признак
print(ex.worst(3)[["ndcg@10", "cause 1"]].round(3))  # худшие запросы и что их подвело
               drop of ndcg@10
feature                       
text_match              0.1548
bought_before           0.0066
popularity              0.0046
price_rank              0.0006 

                         ndcg@10            cause 1
query                                              
сыр моцарелла на завтра    0.661  popularity -0.127
оливковое масло сегодня    0.664  text_match -0.062
сыр моцарелла маленький    0.666  popularity -0.040

Дальше — «Первые шаги»: файлы и CLI, отчёт с графиками, объяснение модели.

Что ещё умеет

  • Метрики: ndcg, map, mrr, err, rbp, judged — по умолчанию как в trec_eval, плюс rbo для сравнения двух порядков без разметки; своя метрика — плагином.
  • Данные: таблицы pandas, polars, pyarrow; файлы csv, tsv, jsonl, TREC (в том числе сжатые), parquet, Feather — потоком, в памяти одна выдача.
  • Неточная разметка: покрытие judged@k для обеих сторон с предупреждением о разрыве; PPI++ — дешёвая разметка всего плюс небольшая gold-выборка.
  • Клики: IPS и SNIPS с поправкой на позицию, диагностика весов (ESS), симулятор с известной истиной — рецепт.
  • Отчёты: самодостаточный HTML или Markdown с таблицей и графиками.
  • CLI: ranklens eval, compare, explain — для файлов и гейтов качества в CI.

Чего ranklens не делает

  • не обучает модели — только оценивает;
  • не отвечает, различаются ли порядки двух выдач: одинаковый NDCG при разном порядке вполне возможен, для этого есть rbo;
  • не заменяет онлайн-эксперимент: оффлайн-метрика считается на фиксированной разметке.

Установка

pip install "ranklens[viz]"

Extras: viz (графики и отчёты), arrow (parquet, Feather), models (CatBoost, LightGBM, XGBoost, scikit-learn), neural (ONNX).