ranklens 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ranklens-0.2.0/LICENSE +21 -0
- ranklens-0.2.0/PKG-INFO +302 -0
- ranklens-0.2.0/README.md +263 -0
- ranklens-0.2.0/pyproject.toml +138 -0
- ranklens-0.2.0/src/ranklens/__init__.py +43 -0
- ranklens-0.2.0/src/ranklens/__main__.py +3 -0
- ranklens-0.2.0/src/ranklens/aggregate/__init__.py +5 -0
- ranklens-0.2.0/src/ranklens/aggregate/segments.py +171 -0
- ranklens-0.2.0/src/ranklens/api/__init__.py +24 -0
- ranklens-0.2.0/src/ranklens/api/compare.py +254 -0
- ranklens-0.2.0/src/ranklens/api/data.py +256 -0
- ranklens-0.2.0/src/ranklens/api/explain.py +160 -0
- ranklens-0.2.0/src/ranklens/cli.py +715 -0
- ranklens-0.2.0/src/ranklens/core/__init__.py +88 -0
- ranklens-0.2.0/src/ranklens/core/exceptions.py +310 -0
- ranklens-0.2.0/src/ranklens/core/feedback.py +108 -0
- ranklens-0.2.0/src/ranklens/core/protocols.py +21 -0
- ranklens-0.2.0/src/ranklens/core/registry.py +180 -0
- ranklens-0.2.0/src/ranklens/core/result.py +310 -0
- ranklens-0.2.0/src/ranklens/core/types.py +81 -0
- ranklens-0.2.0/src/ranklens/explain/__init__.py +22 -0
- ranklens-0.2.0/src/ranklens/explain/adapters/__init__.py +56 -0
- ranklens-0.2.0/src/ranklens/explain/adapters/base.py +94 -0
- ranklens-0.2.0/src/ranklens/explain/adapters/libraries.py +129 -0
- ranklens-0.2.0/src/ranklens/explain/adapters/neural.py +96 -0
- ranklens-0.2.0/src/ranklens/explain/data.py +66 -0
- ranklens-0.2.0/src/ranklens/explain/diagnose.py +61 -0
- ranklens-0.2.0/src/ranklens/explain/grouping.py +61 -0
- ranklens-0.2.0/src/ranklens/explain/listwise.py +102 -0
- ranklens-0.2.0/src/ranklens/explain/loading.py +83 -0
- ranklens-0.2.0/src/ranklens/explain/pairwise.py +112 -0
- ranklens-0.2.0/src/ranklens/io/__init__.py +35 -0
- ranklens-0.2.0/src/ranklens/io/arrow.py +90 -0
- ranklens-0.2.0/src/ranklens/io/clicklog.py +215 -0
- ranklens-0.2.0/src/ranklens/io/errors.py +38 -0
- ranklens-0.2.0/src/ranklens/io/features.py +163 -0
- ranklens-0.2.0/src/ranklens/io/formats.py +247 -0
- ranklens-0.2.0/src/ranklens/io/paired.py +91 -0
- ranklens-0.2.0/src/ranklens/io/readers.py +218 -0
- ranklens-0.2.0/src/ranklens/io/schema.py +123 -0
- ranklens-0.2.0/src/ranklens/metrics/__init__.py +33 -0
- ranklens-0.2.0/src/ranklens/metrics/base.py +57 -0
- ranklens-0.2.0/src/ranklens/metrics/builtin.py +24 -0
- ranklens-0.2.0/src/ranklens/metrics/err_rbp.py +93 -0
- ranklens-0.2.0/src/ranklens/metrics/evaluation.py +104 -0
- ranklens-0.2.0/src/ranklens/metrics/judged.py +33 -0
- ranklens-0.2.0/src/ranklens/metrics/map_mrr.py +82 -0
- ranklens-0.2.0/src/ranklens/metrics/ndcg.py +49 -0
- ranklens-0.2.0/src/ranklens/metrics/rbo.py +34 -0
- ranklens-0.2.0/src/ranklens/metrics/vectorized.py +56 -0
- ranklens-0.2.0/src/ranklens/offpolicy/__init__.py +24 -0
- ranklens-0.2.0/src/ranklens/offpolicy/diagnostics.py +50 -0
- ranklens-0.2.0/src/ranklens/offpolicy/estimators.py +227 -0
- ranklens-0.2.0/src/ranklens/offpolicy/events.py +232 -0
- ranklens-0.2.0/src/ranklens/offpolicy/propensity.py +88 -0
- ranklens-0.2.0/src/ranklens/offpolicy/simulator.py +130 -0
- ranklens-0.2.0/src/ranklens/py.typed +0 -0
- ranklens-0.2.0/src/ranklens/report/__init__.py +30 -0
- ranklens-0.2.0/src/ranklens/report/atomic.py +81 -0
- ranklens-0.2.0/src/ranklens/report/html.py +71 -0
- ranklens-0.2.0/src/ranklens/report/markdown.py +44 -0
- ranklens-0.2.0/src/ranklens/report/sections.py +26 -0
- ranklens-0.2.0/src/ranklens/stats/__init__.py +52 -0
- ranklens-0.2.0/src/ranklens/stats/bootstrap.py +104 -0
- ranklens-0.2.0/src/ranklens/stats/chunking.py +19 -0
- ranklens-0.2.0/src/ranklens/stats/compare.py +64 -0
- ranklens-0.2.0/src/ranklens/stats/multiple.py +57 -0
- ranklens-0.2.0/src/ranklens/stats/permutation.py +51 -0
- ranklens-0.2.0/src/ranklens/stats/power.py +69 -0
- ranklens-0.2.0/src/ranklens/stats/ppi.py +152 -0
- ranklens-0.2.0/src/ranklens/viz/__init__.py +19 -0
- ranklens-0.2.0/src/ranklens/viz/charts.py +174 -0
ranklens-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AlexandrBaranov7
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ranklens-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ranklens
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Offline evaluation, statistical comparison and explanation of ranking quality
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Keywords: ranking,information-retrieval,ndcg,evaluation,off-policy,learning-to-rank
|
|
8
|
+
Author: AlexandrBaranov7
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Provides-Extra: arrow
|
|
21
|
+
Provides-Extra: models
|
|
22
|
+
Provides-Extra: neural
|
|
23
|
+
Provides-Extra: viz
|
|
24
|
+
Requires-Dist: catboost ; extra == "models"
|
|
25
|
+
Requires-Dist: lightgbm ; extra == "models"
|
|
26
|
+
Requires-Dist: numpy (>=1.26)
|
|
27
|
+
Requires-Dist: onnxruntime (>=1.17) ; extra == "neural"
|
|
28
|
+
Requires-Dist: pandas (>=2.2)
|
|
29
|
+
Requires-Dist: plotly (>=5.18) ; extra == "viz"
|
|
30
|
+
Requires-Dist: pyarrow (>=16) ; extra == "arrow"
|
|
31
|
+
Requires-Dist: scikit-learn ; extra == "models"
|
|
32
|
+
Requires-Dist: xgboost ; extra == "models"
|
|
33
|
+
Project-URL: Changelog, https://github.com/AlexandrBaranov7/ranklens/blob/main/CHANGELOG.md
|
|
34
|
+
Project-URL: Documentation, https://alexandrbaranov7.github.io/ranklens/
|
|
35
|
+
Project-URL: Homepage, https://github.com/AlexandrBaranov7/ranklens
|
|
36
|
+
Project-URL: Issues, https://github.com/AlexandrBaranov7/ranklens/issues
|
|
37
|
+
Description-Content-Type: text/markdown
|
|
38
|
+
|
|
39
|
+
<h1>
|
|
40
|
+
<picture>
|
|
41
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/brand/logo-wordmark-dark.svg">
|
|
42
|
+
<img src="https://alexandrbaranov7.github.io/ranklens/images/brand/logo-wordmark.svg" alt="ranklens" height="64">
|
|
43
|
+
</picture>
|
|
44
|
+
</h1>
|
|
45
|
+
|
|
46
|
+
[](https://github.com/AlexandrBaranov7/ranklens/actions/workflows/ci.yml)
|
|
47
|
+
[](https://pypi.org/project/ranklens/)
|
|
48
|
+
[](https://alexandrbaranov7.github.io/ranklens/)
|
|
49
|
+
|
|
50
|
+
Модель поменяли — стало ли лучше, **где** и **почему**? ranklens отвечает на это из
|
|
51
|
+
таблиц pandas одной строкой: разница с интервалом, p- и q-value, минимальный заметный
|
|
52
|
+
эффект (MDE), разбивка по сегментам с поправкой на множественные сравнения. Метрики
|
|
53
|
+
совпадают с trec_eval до 1e-9.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
import ranklens as rl
|
|
57
|
+
|
|
58
|
+
rl.compare(current, new, qrels, ["ndcg@10"], by="device")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Типичные задачи
|
|
62
|
+
|
|
63
|
+
Примеры — на синтетическом каталоге из `docs/examples/catalog.py`: запросы, товары
|
|
64
|
+
и оценки выдуманы, но устроены как в настоящем поиске. На
|
|
65
|
+
[главной странице документации](https://alexandrbaranov7.github.io/ranklens/) они
|
|
66
|
+
выполняются при каждой сборке.
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
query product grade history base_s0 new_s0
|
|
70
|
+
0 молоко Молоко Луговое 3,2% 1 л 3 нет 0.92 1.08
|
|
71
|
+
1 молоко Молоко Северная ферма 2,5% 930 мл 2 нет 0.97 1.04
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Эффект виден только в сегменте.** Новая модель знает, покупал ли пользователь товар
|
|
75
|
+
раньше, — такой признак есть только у пользователей с историей покупок (около 9%
|
|
76
|
+
запросов). У каждой модели два сида: они усредняются по запросу.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
rl.compare([base_s0, base_s1], [new_s0, new_s1], qrels, ["ndcg@10"], by="history")
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
n_queries delta ci_low ci_high mde q_value
|
|
84
|
+
metric history
|
|
85
|
+
ndcg@10 есть 29 0.0387 0.0190 0.0616 0.0308 0.0006
|
|
86
|
+
нет 291 -0.0010 -0.0034 0.0015 0.0035 0.4194
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
В среднем по всем запросам прирост +0.0026 не отличим от нуля и меньше MDE (0.0046) —
|
|
90
|
+
минимального эффекта, который эти запросы способны показать. Разбивка находит, где он.
|
|
91
|
+
|
|
92
|
+
**Две версии выдачи размечены разными партиями асессоров.**
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
rl.agreement(batch_cur, batch_new)
|
|
96
|
+
# Agreement(2495 common pairs: exact 88.8%, mean grade A 1.801, B 1.913, shift +0.112)
|
|
97
|
+
qrels = rl.merge_qrels(batch_cur, batch_new) # одна разметка для обеих сторон
|
|
98
|
+
rl.compare(current, new_version, qrels, ["ndcg@5", "ndcg@10", "err@10"])
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
n_queries delta ci_low ci_high mde q_value
|
|
103
|
+
metric
|
|
104
|
+
ndcg@5 320 -0.0019 -0.0039 0.0000 0.0028 0.0801
|
|
105
|
+
ndcg@10 320 0.0629 0.0559 0.0699 0.0099 0.0003
|
|
106
|
+
err@10 320 0.0019 -0.0016 0.0054 0.0051 0.2966
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Вторая партия мягче, поэтому сравниваем на общей разметке. NDCG@10 значимо выше,
|
|
110
|
+
а метрики верха выдачи — нет: новая версия улучшает позиции 6–10.
|
|
111
|
+
|
|
112
|
+
**Почему: вклад признаков в метрику.** Модель — CatBoost, LightGBM, XGBoost,
|
|
113
|
+
scikit-learn, PyTorch, ONNX или любая функция скоринга.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
ex = rl.explainer(model, df, query="query", doc="product", relevance="grade")
|
|
117
|
+
ex.importance("ndcg@10") # насколько падает метрика, если перемешать признак
|
|
118
|
+
ex.worst(20) # худшие запросы и признаки, которые их подвели
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
```text
|
|
122
|
+
drop of ndcg@10
|
|
123
|
+
feature
|
|
124
|
+
text_match 0.1548
|
|
125
|
+
bought_before 0.0066
|
|
126
|
+
popularity 0.0046
|
|
127
|
+
price_rank 0.0006
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Установка
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pip install "ranklens[viz]"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Extras: `viz` (графики и HTML-отчёты), `arrow` (parquet, Feather), `models` (CatBoost,
|
|
137
|
+
LightGBM, XGBoost, scikit-learn), `neural` (ONNX). PyTorch ставится по инструкции
|
|
138
|
+
pytorch.org. Свежий `main` — `pip install "ranklens @ git+https://github.com/AlexandrBaranov7/ranklens"`.
|
|
139
|
+
|
|
140
|
+
## Данные и командная строка
|
|
141
|
+
|
|
142
|
+
`rl.run` и `rl.qrels` берут таблицу pandas, polars или pyarrow с одной строкой на пару
|
|
143
|
+
«запрос — документ»; порядок — по скору модели (`score=`), по позиции (`rank=`) или
|
|
144
|
+
порядку строк. Несколько выдач на запрос (по магазинам, сессиям) — `list_id=`.
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
qrels = rl.qrels(labels, query="qid", doc="sku", relevance="grade")
|
|
148
|
+
current = rl.run(df_a, query="qid", doc="sku", rank="pos", segments="device")
|
|
149
|
+
new = rl.run(df_b, query="qid", doc="sku", rank="pos", segments="device")
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Те же сравнения из терминала — для файлов и CI:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
ranklens eval --run run.trec --qrels qrels.trec --metrics ndcg@10 mrr@10
|
|
156
|
+
ranklens compare --baseline a.csv --candidate b.csv --qrels qrels.csv \
|
|
157
|
+
--metrics ndcg@10 mrr@10 --segments device locale --report compare.html
|
|
158
|
+
ranklens explain --model model.cbm --features features.csv --top-worst 20
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```text
|
|
162
|
+
metric device locale queries mean A mean B delta 95% CI p q
|
|
163
|
+
ndcg@10 desktop ru 60 0.5352 0.5352 +0.0000 [+0.0000, +0.0000] 1.0000 1.0000
|
|
164
|
+
ndcg@10 mobile en 60 0.6490 0.6329 -0.0161 [-0.0302, -0.0038] 0.0655 0.0982
|
|
165
|
+
ndcg@10 mobile ru 60 0.6137 0.6926 +0.0789 [+0.0409, +0.1234] 0.0005 0.0015 *
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`--report compare.html` пишет один файл со всеми графиками, который открывается без
|
|
169
|
+
интернета: таблица, покрытие разметкой, разница по сегментам с интервалами (значимые
|
|
170
|
+
после поправки — синим), распределение поразапросных разниц, метрика по cutoff,
|
|
171
|
+
перемещения документов из топ-10.
|
|
172
|
+
|
|
173
|
+

|
|
174
|
+
|
|
175
|
+
Подробнее — [«Первые шаги»](https://alexandrbaranov7.github.io/ranklens/tutorial/).
|
|
176
|
+
|
|
177
|
+
| Что | Где |
|
|
178
|
+
|---|---|
|
|
179
|
+
| Таблицы pandas/polars → сравнение одной строкой: сиды, сегменты, MDE, покрытие разметки | `import ranklens as rl` |
|
|
180
|
+
| NDCG, MAP, MRR (умолчания trec_eval), ERR, RBP, RBO, judged@k; свои метрики — плагином | `ranklens.metrics` |
|
|
181
|
+
| Парный bootstrap по запросам, перестановочный тест, Бенджамини — Хохберг, MDE | `ranklens.stats` |
|
|
182
|
+
| Сравнение по сегментам с q-values | `ranklens.aggregate` |
|
|
183
|
+
| Неточная разметка: согласие партий, общая разметка, PPI++ (дешёвая proxy-разметка + gold-выборка), judged@k | `rl.agreement`, `rl.merge_qrels`, `ranklens.stats` |
|
|
184
|
+
| Off-policy: IPS и SNIPS по логам кликов, PBM, симулятор с известной истиной, ESS | `ranklens.offpolicy` |
|
|
185
|
+
| Объяснение: Шепли для пары документов, вклад признаков в метрику, группы коррелированных признаков, худшие запросы | `rl.explainer`, `ranklens.explain` |
|
|
186
|
+
| Графики и самодостаточные HTML/Markdown-отчёты | `ranklens.viz`, `ranklens.report` |
|
|
187
|
+
|
|
188
|
+
## Почему не существующие библиотеки
|
|
189
|
+
|
|
190
|
+
- **[pytrec_eval](https://github.com/cvangysel/pytrec_eval)** — обёртка над trec_eval:
|
|
191
|
+
быстрый эталон метрик, десятки мер. ranklens сверяется с ним golden-тестами и
|
|
192
|
+
проигрывает ему в числе мер. Но pytrec_eval отвечает на «сколько», а не на «отличимо
|
|
193
|
+
ли это от шума» — сравнения, интервалов и сегментов в нём нет.
|
|
194
|
+
- **[ir_measures](https://github.com/terrierteam/ir_measures)** — единый интерфейс
|
|
195
|
+
к нескольким реализациям мер; хорош, когда нужна именно мера из конкретного инструмента.
|
|
196
|
+
Статистику сравнения и объяснения он не делает.
|
|
197
|
+
- **[ranx](https://github.com/AmenRa/ranx)** умеет и метрики, и статистическое
|
|
198
|
+
сравнение прогонов, и fusion — ближайший аналог в части сравнения. ranklens добавляет
|
|
199
|
+
то, чего в нём нет: поправку FDR по сегментам, MDE, PPI для неточной разметки,
|
|
200
|
+
off-policy оценку по кликам и объяснение модели по метрике.
|
|
201
|
+
|
|
202
|
+
ranklens не заменяет их в скорости и полноте набора мер — он закрывает путь от
|
|
203
|
+
«цифра изменилась» до «вот где и почему».
|
|
204
|
+
|
|
205
|
+
## Архитектура
|
|
206
|
+
|
|
207
|
+
Слои с проверяемыми границами: `core` (типы, результаты, протоколы, исключения — только
|
|
208
|
+
stdlib) → `io`, `metrics`, `stats`, `viz` → `aggregate`, `offpolicy`, `explain`, `report`
|
|
209
|
+
→ `api` (`import ranklens as rl`) и `cli`. `tests/test_architecture.py` разбирает AST каждого модуля и падает, если слой
|
|
210
|
+
импортирует то, что ему не положено, или тяжёлую библиотеку не лениво. Прогоны читаются
|
|
211
|
+
потоком (генератор, в памяти — одна выдача), разметка — в словарь для поиска за O(1).
|
|
212
|
+
Подробно — [«Устройство»](https://alexandrbaranov7.github.io/ranklens/design/).
|
|
213
|
+
|
|
214
|
+
## Выбор структур данных и средств языка
|
|
215
|
+
|
|
216
|
+
| Требование | Где | Почему так |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `list` | буфер строк текущего запроса в `iter_run`, `iter_clicklog` | дописывание в конец — амортизированное O(1); собранная выдача сразу становится `tuple` |
|
|
219
|
+
| `tuple` | `RankedList.docs`, `SegmentKey`, `Impression.positions`, вклады признаков | порядок — это позиция; неизменяем и хешируем: поле frozen-объекта и ключ словаря. `list` в frozen dataclass менялся бы на месте |
|
|
220
|
+
| `dict` | `Qrels: dict[QueryId, dict[DocId, float]]`, реестр метрик, `ADAPTERS` | поиск оценки пары запрос–документ за O(1) в горячем цикле; реестры вместо цепочек `if` — точки расширения |
|
|
221
|
+
| `set` | дубликаты документов в `RankedList`, общие запросы двух прогонов, повторы в ридерах | проверка дубликатов O(n) вместо O(n²); общие запросы — ровно `a.keys() & b.keys()` |
|
|
222
|
+
| генератор | `iter_run`, `iter_clicklog`, `iter_records` | в памяти — строки одного запроса: на отсортированном входе память постоянна, на сгруппированном растёт на ~200 байт на запрос (id уже прочитанных); тесты меряют пик `tracemalloc` на файле в 10 раз больше |
|
|
223
|
+
| свой итератор | `PairedRuns` (`__iter__`/`__next__`) | merge-join двух отсортированных потоков; `zip` молча спарил бы разные запросы, когда в одном прогоне запрос пропущен |
|
|
224
|
+
| своё исключение | иерархия `RankLensError`: `DataError`, `ConfigError`, `ModelError`, `StatisticalError` | у ошибок поля (`line_no`, `path`, `column`) и подсказки («did you mean 'ndcg'?»); аргументы в `super().__init__` — иначе не переживают pickle (тест) |
|
|
225
|
+
| осмысленная обработка | `ErrorCollector`: битые строки пропускаются и считаются; `--strict` останавливает | два сценария — разведка данных и CI-гейт; коды выхода 0 / 1 конфигурация / 2 аргументы / 3 данные |
|
|
226
|
+
| контекстный менеджер | `AtomicReport` | временный файл в той же директории, `fsync`, `os.replace` только без исключения; при ошибке или `Ctrl-C` остаётся прежний отчёт. `with open()` сначала обнулил бы цель |
|
|
227
|
+
| аннотации + mypy | весь пакет и тесты, mypy `strict`, `py.typed` | ошибки типов ловятся в CI до тестов; пакет отдаёт типы пользователям |
|
|
228
|
+
| pytest | 750+ тестов, покрытие ~99%, порог 90% | точные значения, property-based (Hypothesis), golden против trec_eval, статистическая валидация на симуляциях |
|
|
229
|
+
| `pyproject.toml` | PEP 621, `[project.scripts]`, entry points `ranklens.metrics`, extras | одна точка для метаданных, команды, плагинов и необязательных зависимостей |
|
|
230
|
+
| установка pip | задание CI `install` | собранный wheel ставится **обычным pip** в чистое окружение, и `scripts/tutorial_smoke.py` проходит «Первые шаги» вне репозитория |
|
|
231
|
+
| pandas + агрегация | `offpolicy/events.py`: `merge`, `groupby().max()`, `explode`, `value_counts`; `aggregate` — таблица по сегментам | склейка событий показов и реакций — табличная операция; сегменты отдаются DataFrame |
|
|
232
|
+
|
|
233
|
+
Неизменяемость результатов: `frozen=True, slots=True` + `tuple` + `MappingProxyType` +
|
|
234
|
+
read-only массивы numpy — `frozen` сам по себе запрещает только присваивание полей.
|
|
235
|
+
|
|
236
|
+
## Почему Protocol, а не ABC
|
|
237
|
+
|
|
238
|
+
Метрика — любой вызываемый объект `(ranked, judgements, k) -> float`, в том числе обычная
|
|
239
|
+
функция; модель для объяснений — любой объект с `feature_names`, `score`,
|
|
240
|
+
`native_importance`. Это `typing.Protocol`: структурная типизация, реализации не
|
|
241
|
+
наследуются от наших классов, и чужую модель или функцию не нужно встраивать в иерархию.
|
|
242
|
+
С ABC каждую сторонюю модель пришлось бы оборачивать в подкласс. Имя метрики хранится
|
|
243
|
+
в спецификации (`ndcg@10`): у функции нет атрибута `name`, и протокол с ним её бы
|
|
244
|
+
не принял. Новая модель — класс-адаптер и одна строка в `ADAPTERS`; адаптер находится
|
|
245
|
+
по MRO класса модели, поэтому подкласс `LGBMRanker` из чужого кода тоже распознаётся.
|
|
246
|
+
|
|
247
|
+
## Почему ресемплинг по запросам
|
|
248
|
+
|
|
249
|
+
Единица наблюдения — запрос. Пусть 100 запросов по 10 документов. Ресемплинг строк
|
|
250
|
+
считал бы, что наблюдений 1000, хотя документы одной выдачи зависимы — интервал вышел бы
|
|
251
|
+
примерно в √10 ≈ 3 раза уже правды. Поэтому bootstrap берёт с возвращением **запросы**
|
|
252
|
+
и усредняет поразапросные разницы `M_B(q) − M_A(q)`; перестановочный тест меняет знак
|
|
253
|
+
разницы целиком у запроса. Проверено симуляцией: 95%-й интервал покрывает истинную
|
|
254
|
+
разницу в 95.3% из 1000 повторов. Вывод — в
|
|
255
|
+
[«Статистике сравнения»](https://alexandrbaranov7.github.io/ranklens/math/statistics/).
|
|
256
|
+
|
|
257
|
+
## Ограничения
|
|
258
|
+
|
|
259
|
+
- **Вход должен быть сгруппирован по запросу** (строки запроса подряд), для `PairedRuns` —
|
|
260
|
+
отсортирован; иначе ошибка, а не тихая потеря запросов. Разметка грузится в память целиком.
|
|
261
|
+
- **Неразмеченный документ считается нерелевантным** (как в trec_eval): прогон, который
|
|
262
|
+
приносит много нового, проигрывает из-за разметки. `compare` показывает judged@k
|
|
263
|
+
и предупреждает о разрыве; PPI++ лечит систематическую ошибку разметки при наличии
|
|
264
|
+
случайной gold-выборки.
|
|
265
|
+
- **Перцентильный bootstrap-интервал** на малых выборках бывает уже нужного; поэтому
|
|
266
|
+
p-value — из перестановочного теста, а меньше 20 запросов — предупреждение.
|
|
267
|
+
- **Off-policy:** PBM с фиксированными propensity не описывает каскадное поведение;
|
|
268
|
+
неверные propensity смещают IPS; документы, которых старая модель не показывала,
|
|
269
|
+
не оцениваются никакими весами. SNIPS здесь — отношение (классический SNIPS
|
|
270
|
+
на PBM-веса не переносится) и оценивает долю, а не клики.
|
|
271
|
+
- **Объяснения:** точки «наполовину A, наполовину B» и перемешанные признаки бывают
|
|
272
|
+
нереалистичными; Шепли точен до 12 признаков, дальше — по выборке порядков;
|
|
273
|
+
текстовые кросс-энкодеры и LLM-реранкеры (без матрицы признаков) не объясняются
|
|
274
|
+
([#34](https://github.com/AlexandrBaranov7/ranklens/issues/34),
|
|
275
|
+
[#35](https://github.com/AlexandrBaranov7/ranklens/issues/35)).
|
|
276
|
+
- **Модели из pickle** исполняют код при загрузке — только с `--allow-pickle`
|
|
277
|
+
([SECURITY.md](https://github.com/AlexandrBaranov7/ranklens/blob/main/SECURITY.md)).
|
|
278
|
+
|
|
279
|
+
## Roadmap
|
|
280
|
+
|
|
281
|
+
- Doubly Robust оценщик и Regression-EM для оценки propensity по логам; каскадные
|
|
282
|
+
click-модели.
|
|
283
|
+
- PPI в сравнении по сегментам и в CLI; стратифицированная gold-выборка.
|
|
284
|
+
- Объяснение текстовых реранкеров ([#34](https://github.com/AlexandrBaranov7/ranklens/issues/34)),
|
|
285
|
+
смещения LLM-реранкеров ([#35](https://github.com/AlexandrBaranov7/ranklens/issues/35)),
|
|
286
|
+
GPU-бэкенд метрик ([#17](https://github.com/AlexandrBaranov7/ranklens/issues/17)).
|
|
287
|
+
|
|
288
|
+
## Разработка
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
git clone https://github.com/AlexandrBaranov7/ranklens.git
|
|
292
|
+
cd ranklens
|
|
293
|
+
poetry install --all-extras # Poetry >= 2.4
|
|
294
|
+
poetry run pytest
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Правила — в [CONTRIBUTING.md](https://github.com/AlexandrBaranov7/ranklens/blob/main/CONTRIBUTING.md), изменения — в [CHANGELOG.md](https://github.com/AlexandrBaranov7/ranklens/blob/main/CHANGELOG.md).
|
|
298
|
+
|
|
299
|
+
## Лицензия
|
|
300
|
+
|
|
301
|
+
MIT
|
|
302
|
+
|
ranklens-0.2.0/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<h1>
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/brand/logo-wordmark-dark.svg">
|
|
4
|
+
<img src="https://alexandrbaranov7.github.io/ranklens/images/brand/logo-wordmark.svg" alt="ranklens" height="64">
|
|
5
|
+
</picture>
|
|
6
|
+
</h1>
|
|
7
|
+
|
|
8
|
+
[](https://github.com/AlexandrBaranov7/ranklens/actions/workflows/ci.yml)
|
|
9
|
+
[](https://pypi.org/project/ranklens/)
|
|
10
|
+
[](https://alexandrbaranov7.github.io/ranklens/)
|
|
11
|
+
|
|
12
|
+
Модель поменяли — стало ли лучше, **где** и **почему**? ranklens отвечает на это из
|
|
13
|
+
таблиц pandas одной строкой: разница с интервалом, p- и q-value, минимальный заметный
|
|
14
|
+
эффект (MDE), разбивка по сегментам с поправкой на множественные сравнения. Метрики
|
|
15
|
+
совпадают с trec_eval до 1e-9.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import ranklens as rl
|
|
19
|
+
|
|
20
|
+
rl.compare(current, new, qrels, ["ndcg@10"], by="device")
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Типичные задачи
|
|
24
|
+
|
|
25
|
+
Примеры — на синтетическом каталоге из `docs/examples/catalog.py`: запросы, товары
|
|
26
|
+
и оценки выдуманы, но устроены как в настоящем поиске. На
|
|
27
|
+
[главной странице документации](https://alexandrbaranov7.github.io/ranklens/) они
|
|
28
|
+
выполняются при каждой сборке.
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
query product grade history base_s0 new_s0
|
|
32
|
+
0 молоко Молоко Луговое 3,2% 1 л 3 нет 0.92 1.08
|
|
33
|
+
1 молоко Молоко Северная ферма 2,5% 930 мл 2 нет 0.97 1.04
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**Эффект виден только в сегменте.** Новая модель знает, покупал ли пользователь товар
|
|
37
|
+
раньше, — такой признак есть только у пользователей с историей покупок (около 9%
|
|
38
|
+
запросов). У каждой модели два сида: они усредняются по запросу.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
rl.compare([base_s0, base_s1], [new_s0, new_s1], qrels, ["ndcg@10"], by="history")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
n_queries delta ci_low ci_high mde q_value
|
|
46
|
+
metric history
|
|
47
|
+
ndcg@10 есть 29 0.0387 0.0190 0.0616 0.0308 0.0006
|
|
48
|
+
нет 291 -0.0010 -0.0034 0.0015 0.0035 0.4194
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
В среднем по всем запросам прирост +0.0026 не отличим от нуля и меньше MDE (0.0046) —
|
|
52
|
+
минимального эффекта, который эти запросы способны показать. Разбивка находит, где он.
|
|
53
|
+
|
|
54
|
+
**Две версии выдачи размечены разными партиями асессоров.**
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
rl.agreement(batch_cur, batch_new)
|
|
58
|
+
# Agreement(2495 common pairs: exact 88.8%, mean grade A 1.801, B 1.913, shift +0.112)
|
|
59
|
+
qrels = rl.merge_qrels(batch_cur, batch_new) # одна разметка для обеих сторон
|
|
60
|
+
rl.compare(current, new_version, qrels, ["ndcg@5", "ndcg@10", "err@10"])
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
n_queries delta ci_low ci_high mde q_value
|
|
65
|
+
metric
|
|
66
|
+
ndcg@5 320 -0.0019 -0.0039 0.0000 0.0028 0.0801
|
|
67
|
+
ndcg@10 320 0.0629 0.0559 0.0699 0.0099 0.0003
|
|
68
|
+
err@10 320 0.0019 -0.0016 0.0054 0.0051 0.2966
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Вторая партия мягче, поэтому сравниваем на общей разметке. NDCG@10 значимо выше,
|
|
72
|
+
а метрики верха выдачи — нет: новая версия улучшает позиции 6–10.
|
|
73
|
+
|
|
74
|
+
**Почему: вклад признаков в метрику.** Модель — CatBoost, LightGBM, XGBoost,
|
|
75
|
+
scikit-learn, PyTorch, ONNX или любая функция скоринга.
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
ex = rl.explainer(model, df, query="query", doc="product", relevance="grade")
|
|
79
|
+
ex.importance("ndcg@10") # насколько падает метрика, если перемешать признак
|
|
80
|
+
ex.worst(20) # худшие запросы и признаки, которые их подвели
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
drop of ndcg@10
|
|
85
|
+
feature
|
|
86
|
+
text_match 0.1548
|
|
87
|
+
bought_before 0.0066
|
|
88
|
+
popularity 0.0046
|
|
89
|
+
price_rank 0.0006
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Установка
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install "ranklens[viz]"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Extras: `viz` (графики и HTML-отчёты), `arrow` (parquet, Feather), `models` (CatBoost,
|
|
99
|
+
LightGBM, XGBoost, scikit-learn), `neural` (ONNX). PyTorch ставится по инструкции
|
|
100
|
+
pytorch.org. Свежий `main` — `pip install "ranklens @ git+https://github.com/AlexandrBaranov7/ranklens"`.
|
|
101
|
+
|
|
102
|
+
## Данные и командная строка
|
|
103
|
+
|
|
104
|
+
`rl.run` и `rl.qrels` берут таблицу pandas, polars или pyarrow с одной строкой на пару
|
|
105
|
+
«запрос — документ»; порядок — по скору модели (`score=`), по позиции (`rank=`) или
|
|
106
|
+
порядку строк. Несколько выдач на запрос (по магазинам, сессиям) — `list_id=`.
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
qrels = rl.qrels(labels, query="qid", doc="sku", relevance="grade")
|
|
110
|
+
current = rl.run(df_a, query="qid", doc="sku", rank="pos", segments="device")
|
|
111
|
+
new = rl.run(df_b, query="qid", doc="sku", rank="pos", segments="device")
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Те же сравнения из терминала — для файлов и CI:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
ranklens eval --run run.trec --qrels qrels.trec --metrics ndcg@10 mrr@10
|
|
118
|
+
ranklens compare --baseline a.csv --candidate b.csv --qrels qrels.csv \
|
|
119
|
+
--metrics ndcg@10 mrr@10 --segments device locale --report compare.html
|
|
120
|
+
ranklens explain --model model.cbm --features features.csv --top-worst 20
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```text
|
|
124
|
+
metric device locale queries mean A mean B delta 95% CI p q
|
|
125
|
+
ndcg@10 desktop ru 60 0.5352 0.5352 +0.0000 [+0.0000, +0.0000] 1.0000 1.0000
|
|
126
|
+
ndcg@10 mobile en 60 0.6490 0.6329 -0.0161 [-0.0302, -0.0038] 0.0655 0.0982
|
|
127
|
+
ndcg@10 mobile ru 60 0.6137 0.6926 +0.0789 [+0.0409, +0.1234] 0.0005 0.0015 *
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`--report compare.html` пишет один файл со всеми графиками, который открывается без
|
|
131
|
+
интернета: таблица, покрытие разметкой, разница по сегментам с интервалами (значимые
|
|
132
|
+
после поправки — синим), распределение поразапросных разниц, метрика по cutoff,
|
|
133
|
+
перемещения документов из топ-10.
|
|
134
|
+
|
|
135
|
+

|
|
136
|
+
|
|
137
|
+
Подробнее — [«Первые шаги»](https://alexandrbaranov7.github.io/ranklens/tutorial/).
|
|
138
|
+
|
|
139
|
+
| Что | Где |
|
|
140
|
+
|---|---|
|
|
141
|
+
| Таблицы pandas/polars → сравнение одной строкой: сиды, сегменты, MDE, покрытие разметки | `import ranklens as rl` |
|
|
142
|
+
| NDCG, MAP, MRR (умолчания trec_eval), ERR, RBP, RBO, judged@k; свои метрики — плагином | `ranklens.metrics` |
|
|
143
|
+
| Парный bootstrap по запросам, перестановочный тест, Бенджамини — Хохберг, MDE | `ranklens.stats` |
|
|
144
|
+
| Сравнение по сегментам с q-values | `ranklens.aggregate` |
|
|
145
|
+
| Неточная разметка: согласие партий, общая разметка, PPI++ (дешёвая proxy-разметка + gold-выборка), judged@k | `rl.agreement`, `rl.merge_qrels`, `ranklens.stats` |
|
|
146
|
+
| Off-policy: IPS и SNIPS по логам кликов, PBM, симулятор с известной истиной, ESS | `ranklens.offpolicy` |
|
|
147
|
+
| Объяснение: Шепли для пары документов, вклад признаков в метрику, группы коррелированных признаков, худшие запросы | `rl.explainer`, `ranklens.explain` |
|
|
148
|
+
| Графики и самодостаточные HTML/Markdown-отчёты | `ranklens.viz`, `ranklens.report` |
|
|
149
|
+
|
|
150
|
+
## Почему не существующие библиотеки
|
|
151
|
+
|
|
152
|
+
- **[pytrec_eval](https://github.com/cvangysel/pytrec_eval)** — обёртка над trec_eval:
|
|
153
|
+
быстрый эталон метрик, десятки мер. ranklens сверяется с ним golden-тестами и
|
|
154
|
+
проигрывает ему в числе мер. Но pytrec_eval отвечает на «сколько», а не на «отличимо
|
|
155
|
+
ли это от шума» — сравнения, интервалов и сегментов в нём нет.
|
|
156
|
+
- **[ir_measures](https://github.com/terrierteam/ir_measures)** — единый интерфейс
|
|
157
|
+
к нескольким реализациям мер; хорош, когда нужна именно мера из конкретного инструмента.
|
|
158
|
+
Статистику сравнения и объяснения он не делает.
|
|
159
|
+
- **[ranx](https://github.com/AmenRa/ranx)** умеет и метрики, и статистическое
|
|
160
|
+
сравнение прогонов, и fusion — ближайший аналог в части сравнения. ranklens добавляет
|
|
161
|
+
то, чего в нём нет: поправку FDR по сегментам, MDE, PPI для неточной разметки,
|
|
162
|
+
off-policy оценку по кликам и объяснение модели по метрике.
|
|
163
|
+
|
|
164
|
+
ranklens не заменяет их в скорости и полноте набора мер — он закрывает путь от
|
|
165
|
+
«цифра изменилась» до «вот где и почему».
|
|
166
|
+
|
|
167
|
+
## Архитектура
|
|
168
|
+
|
|
169
|
+
Слои с проверяемыми границами: `core` (типы, результаты, протоколы, исключения — только
|
|
170
|
+
stdlib) → `io`, `metrics`, `stats`, `viz` → `aggregate`, `offpolicy`, `explain`, `report`
|
|
171
|
+
→ `api` (`import ranklens as rl`) и `cli`. `tests/test_architecture.py` разбирает AST каждого модуля и падает, если слой
|
|
172
|
+
импортирует то, что ему не положено, или тяжёлую библиотеку не лениво. Прогоны читаются
|
|
173
|
+
потоком (генератор, в памяти — одна выдача), разметка — в словарь для поиска за O(1).
|
|
174
|
+
Подробно — [«Устройство»](https://alexandrbaranov7.github.io/ranklens/design/).
|
|
175
|
+
|
|
176
|
+
## Выбор структур данных и средств языка
|
|
177
|
+
|
|
178
|
+
| Требование | Где | Почему так |
|
|
179
|
+
|---|---|---|
|
|
180
|
+
| `list` | буфер строк текущего запроса в `iter_run`, `iter_clicklog` | дописывание в конец — амортизированное O(1); собранная выдача сразу становится `tuple` |
|
|
181
|
+
| `tuple` | `RankedList.docs`, `SegmentKey`, `Impression.positions`, вклады признаков | порядок — это позиция; неизменяем и хешируем: поле frozen-объекта и ключ словаря. `list` в frozen dataclass менялся бы на месте |
|
|
182
|
+
| `dict` | `Qrels: dict[QueryId, dict[DocId, float]]`, реестр метрик, `ADAPTERS` | поиск оценки пары запрос–документ за O(1) в горячем цикле; реестры вместо цепочек `if` — точки расширения |
|
|
183
|
+
| `set` | дубликаты документов в `RankedList`, общие запросы двух прогонов, повторы в ридерах | проверка дубликатов O(n) вместо O(n²); общие запросы — ровно `a.keys() & b.keys()` |
|
|
184
|
+
| генератор | `iter_run`, `iter_clicklog`, `iter_records` | в памяти — строки одного запроса: на отсортированном входе память постоянна, на сгруппированном растёт на ~200 байт на запрос (id уже прочитанных); тесты меряют пик `tracemalloc` на файле в 10 раз больше |
|
|
185
|
+
| свой итератор | `PairedRuns` (`__iter__`/`__next__`) | merge-join двух отсортированных потоков; `zip` молча спарил бы разные запросы, когда в одном прогоне запрос пропущен |
|
|
186
|
+
| своё исключение | иерархия `RankLensError`: `DataError`, `ConfigError`, `ModelError`, `StatisticalError` | у ошибок поля (`line_no`, `path`, `column`) и подсказки («did you mean 'ndcg'?»); аргументы в `super().__init__` — иначе не переживают pickle (тест) |
|
|
187
|
+
| осмысленная обработка | `ErrorCollector`: битые строки пропускаются и считаются; `--strict` останавливает | два сценария — разведка данных и CI-гейт; коды выхода 0 / 1 конфигурация / 2 аргументы / 3 данные |
|
|
188
|
+
| контекстный менеджер | `AtomicReport` | временный файл в той же директории, `fsync`, `os.replace` только без исключения; при ошибке или `Ctrl-C` остаётся прежний отчёт. `with open()` сначала обнулил бы цель |
|
|
189
|
+
| аннотации + mypy | весь пакет и тесты, mypy `strict`, `py.typed` | ошибки типов ловятся в CI до тестов; пакет отдаёт типы пользователям |
|
|
190
|
+
| pytest | 750+ тестов, покрытие ~99%, порог 90% | точные значения, property-based (Hypothesis), golden против trec_eval, статистическая валидация на симуляциях |
|
|
191
|
+
| `pyproject.toml` | PEP 621, `[project.scripts]`, entry points `ranklens.metrics`, extras | одна точка для метаданных, команды, плагинов и необязательных зависимостей |
|
|
192
|
+
| установка pip | задание CI `install` | собранный wheel ставится **обычным pip** в чистое окружение, и `scripts/tutorial_smoke.py` проходит «Первые шаги» вне репозитория |
|
|
193
|
+
| pandas + агрегация | `offpolicy/events.py`: `merge`, `groupby().max()`, `explode`, `value_counts`; `aggregate` — таблица по сегментам | склейка событий показов и реакций — табличная операция; сегменты отдаются DataFrame |
|
|
194
|
+
|
|
195
|
+
Неизменяемость результатов: `frozen=True, slots=True` + `tuple` + `MappingProxyType` +
|
|
196
|
+
read-only массивы numpy — `frozen` сам по себе запрещает только присваивание полей.
|
|
197
|
+
|
|
198
|
+
## Почему Protocol, а не ABC
|
|
199
|
+
|
|
200
|
+
Метрика — любой вызываемый объект `(ranked, judgements, k) -> float`, в том числе обычная
|
|
201
|
+
функция; модель для объяснений — любой объект с `feature_names`, `score`,
|
|
202
|
+
`native_importance`. Это `typing.Protocol`: структурная типизация, реализации не
|
|
203
|
+
наследуются от наших классов, и чужую модель или функцию не нужно встраивать в иерархию.
|
|
204
|
+
С ABC каждую сторонюю модель пришлось бы оборачивать в подкласс. Имя метрики хранится
|
|
205
|
+
в спецификации (`ndcg@10`): у функции нет атрибута `name`, и протокол с ним её бы
|
|
206
|
+
не принял. Новая модель — класс-адаптер и одна строка в `ADAPTERS`; адаптер находится
|
|
207
|
+
по MRO класса модели, поэтому подкласс `LGBMRanker` из чужого кода тоже распознаётся.
|
|
208
|
+
|
|
209
|
+
## Почему ресемплинг по запросам
|
|
210
|
+
|
|
211
|
+
Единица наблюдения — запрос. Пусть 100 запросов по 10 документов. Ресемплинг строк
|
|
212
|
+
считал бы, что наблюдений 1000, хотя документы одной выдачи зависимы — интервал вышел бы
|
|
213
|
+
примерно в √10 ≈ 3 раза уже правды. Поэтому bootstrap берёт с возвращением **запросы**
|
|
214
|
+
и усредняет поразапросные разницы `M_B(q) − M_A(q)`; перестановочный тест меняет знак
|
|
215
|
+
разницы целиком у запроса. Проверено симуляцией: 95%-й интервал покрывает истинную
|
|
216
|
+
разницу в 95.3% из 1000 повторов. Вывод — в
|
|
217
|
+
[«Статистике сравнения»](https://alexandrbaranov7.github.io/ranklens/math/statistics/).
|
|
218
|
+
|
|
219
|
+
## Ограничения
|
|
220
|
+
|
|
221
|
+
- **Вход должен быть сгруппирован по запросу** (строки запроса подряд), для `PairedRuns` —
|
|
222
|
+
отсортирован; иначе ошибка, а не тихая потеря запросов. Разметка грузится в память целиком.
|
|
223
|
+
- **Неразмеченный документ считается нерелевантным** (как в trec_eval): прогон, который
|
|
224
|
+
приносит много нового, проигрывает из-за разметки. `compare` показывает judged@k
|
|
225
|
+
и предупреждает о разрыве; PPI++ лечит систематическую ошибку разметки при наличии
|
|
226
|
+
случайной gold-выборки.
|
|
227
|
+
- **Перцентильный bootstrap-интервал** на малых выборках бывает уже нужного; поэтому
|
|
228
|
+
p-value — из перестановочного теста, а меньше 20 запросов — предупреждение.
|
|
229
|
+
- **Off-policy:** PBM с фиксированными propensity не описывает каскадное поведение;
|
|
230
|
+
неверные propensity смещают IPS; документы, которых старая модель не показывала,
|
|
231
|
+
не оцениваются никакими весами. SNIPS здесь — отношение (классический SNIPS
|
|
232
|
+
на PBM-веса не переносится) и оценивает долю, а не клики.
|
|
233
|
+
- **Объяснения:** точки «наполовину A, наполовину B» и перемешанные признаки бывают
|
|
234
|
+
нереалистичными; Шепли точен до 12 признаков, дальше — по выборке порядков;
|
|
235
|
+
текстовые кросс-энкодеры и LLM-реранкеры (без матрицы признаков) не объясняются
|
|
236
|
+
([#34](https://github.com/AlexandrBaranov7/ranklens/issues/34),
|
|
237
|
+
[#35](https://github.com/AlexandrBaranov7/ranklens/issues/35)).
|
|
238
|
+
- **Модели из pickle** исполняют код при загрузке — только с `--allow-pickle`
|
|
239
|
+
([SECURITY.md](https://github.com/AlexandrBaranov7/ranklens/blob/main/SECURITY.md)).
|
|
240
|
+
|
|
241
|
+
## Roadmap
|
|
242
|
+
|
|
243
|
+
- Doubly Robust оценщик и Regression-EM для оценки propensity по логам; каскадные
|
|
244
|
+
click-модели.
|
|
245
|
+
- PPI в сравнении по сегментам и в CLI; стратифицированная gold-выборка.
|
|
246
|
+
- Объяснение текстовых реранкеров ([#34](https://github.com/AlexandrBaranov7/ranklens/issues/34)),
|
|
247
|
+
смещения LLM-реранкеров ([#35](https://github.com/AlexandrBaranov7/ranklens/issues/35)),
|
|
248
|
+
GPU-бэкенд метрик ([#17](https://github.com/AlexandrBaranov7/ranklens/issues/17)).
|
|
249
|
+
|
|
250
|
+
## Разработка
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
git clone https://github.com/AlexandrBaranov7/ranklens.git
|
|
254
|
+
cd ranklens
|
|
255
|
+
poetry install --all-extras # Poetry >= 2.4
|
|
256
|
+
poetry run pytest
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Правила — в [CONTRIBUTING.md](https://github.com/AlexandrBaranov7/ranklens/blob/main/CONTRIBUTING.md), изменения — в [CHANGELOG.md](https://github.com/AlexandrBaranov7/ranklens/blob/main/CHANGELOG.md).
|
|
260
|
+
|
|
261
|
+
## Лицензия
|
|
262
|
+
|
|
263
|
+
MIT
|