diff --git a/docs/rating/rating-system.md b/docs/rating/rating-system.md new file mode 100644 index 0000000..e55e4e6 --- /dev/null +++ b/docs/rating/rating-system.md @@ -0,0 +1,693 @@ +# Новая рейтинговая система + +> **Статус:** предложение на утверждение (#22). Реализация — #23. +> **Приложение:** [`simulate.py`](simulate.py) — эталонная реализация формул, все примеры +> этого документа (с `assert`), симуляция и перебор коэффициентов. Только стандартная +> библиотека Python, вывод воспроизводим: +> +> ```bash +> python docs/rating/simulate.py # примеры + сравнение систем (≈20 с) +> python docs/rating/simulate.py --grid # примеры + подбор коэффициентов (≈5 мин) +> ``` + +## Коротко + +Сейчас рейтинг — среднее очков за место. Он не видит ни силы соперников, ни хода партии, +а первое место в дуэли и за столом на пятерых стоит одинаково. + +Предлагается **многопользовательский Elo с множителем отрыва**: + +- **Разбор на дуэли.** Партия раскладывается на пары игроков. Каждая пара — дуэль, исход + которой сравнивается с ожидаемым по разнице рейтингов: победа над сильным даёт больше, + чем над слабым. +- **Множитель отрыва.** Размер изменения зависит от того, *как* партия сыграна: на каком + раунде закончилась, какой отрыв по целям и мирам, какой тип победы. Размер стола тоже + влияет, но слабее темпа. +- **Шкала прежняя.** Старт 50, как в нынешнем топе. Пример из задачи «60 против 40» + ложится в неё буквально. +- **Монотонность.** Победитель никогда не теряет рейтинг, последнее место никогда не + приносит. +- **Старая история.** Пересчитывается по тем же формулам: у партий без новых полей + признаки берутся нейтральными. + +На синтетической лиге система лучше текущей и чистого Elo во всех сценариях, где отрыв +действительно связан с силой игроков. Например, после 10 партий она правильнее упорядочивает +игроков: ρ = 0.807 против 0.769 у текущей. Там, где отрыв — чистый шум, она уступает +чистому Elo около 0.1 п.п. точности. Подробности — в [разделе 7](#7-проверка-на-симуляции). + +## Содержание + +1. [Как считается сейчас и что с этим не так](#1-как-считается-сейчас-и-что-с-этим-не-так) +2. [Исходные данные: правила игры](#2-исходные-данные-правила-игры) +3. [Выбор модели](#3-выбор-модели) +4. [Формулы](#4-формулы) +5. [Требования → решения](#5-требования--решения) +6. [Примеры расчётов](#6-примеры-расчётов) +7. [Проверка на симуляции](#7-проверка-на-симуляции) +8. [Что потребуется в реализации (#23)](#8-что-потребуется-в-реализации-23) +9. [Открытые вопросы](#9-открытые-вопросы) + +--- + +## 1. Как считается сейчас и что с этим не так + +`backend/app/services/scoring.py`. За партию на N игроков участник получает League Points: + +```math +\text{points} = \frac{N - \text{place} - (\text{tie}-1)/2}{N-1} +``` + +1-е место — 1.0, последнее — 0.0; равные места делят очки. Рейтинг — сглаженное среднее +с 10 «виртуальными» партиями по 0.5, в топ попадают игроки от 10 партий: + +```math +\text{score} = \frac{10 \cdot 0.5 + \sum \text{points}}{10 + \text{games}} \cdot 100 +``` + +Проблемы на цифрах: + +| # | Проблема | Пример | +|---|---|---| +| 1 | **Сила соперника не учитывается.** | Победа над лидером топа и над новичком — одинаковые 1.0. Кто играет в основном со слабыми, копит рейтинг быстрее. В симуляции с тремя группами разной силы текущая система упорядочивает игроков с ρ = 0.525, предложенная — с 0.645 (раздел 7). | +| 2 | **Размер стола не учитывается.** | 1-е место в дуэли — 1.0, 1-е место за столом на пятерых — тоже 1.0, хотя обыграны четверо. | +| 3 | **Ход партии не учитывается.** | Разгром на 3-м раунде (2:0 по целям, 8:2 по мирам) и победа на тай-брейке по мирам в конце 8-го раунда — одинаковые 1.0. | +| 4 | **Рейтинг помнит всю историю с одним весом.** | Игрок, проигравший первые 10 дуэлей и выигравший следующие 10, имеет (5 + 10) / 30 · 100 = **50** — как середняк, хотя сейчас сильнее всех. В сценарии «рост» симуляции ρ после 10 партий у текущей системы 0.732, у предложенной 0.773. | + +## 2. Исходные данные: правила игры + +По справочнику (стр. 8, 11, 16) и уточнениям владельца в #22. + +**Победа.** Игрок, собравший маркеры целей в количестве, равном числу игроков N, побеждает. +Цели собираются в фазе обновления, поэтому партия заканчивается на границе раунда. +Если к концу последнего раунда никто не набрал N, побеждает тот, у кого больше целей. + +**Тай-брейки** (от менее близкой партии к более близкой) — это и есть типы победы +в приложении: + +| Тип (`win_reason`) | Когда | Близость партии | +|---|---|---| +| `objectives` — по целям | больше всех целей | обычная | +| `worlds` — по мирам | цели поровну, больше дружественных миров | близкая | +| `plastic` — по пластику | цели и миры поровну, больше отрядов на поле | очень близкая | +| `resources` — по ресурсам | всё выше поровну; **домашнее правило** | самая близкая | +| `last_standing` — последний выживший | все соперники устранены (нет миров); **предлагается добавить** | разгром | + +Если поровну вообще всё, победа общая (в приложении — ничья за 1-е место). + +**Лимит раундов `R_max`.** По правилам 8. **Домашнее правило:** за столом на 5–6 игроков +играется 9 раундов. Правило включается галочкой в настройках группы. + +**Миры.** В тексте #22 они названы «системами». По справочнику *система* — это целый тайл +из четырёх областей, а область с планетой — *мир*. Дальше используется термин справочника. +Размер поля и «честная доля» миров на игрока: + +| N | Поле | Тайлов | Миров на поле (×2.2) | Миров на игрока `W(N)/N` | +|---|---|---|---|---| +| 2 | 2×2 | 4 | 8.8 | 4.40 | +| 3 | 3×3 | 9 | 19.8 | 6.60 | +| 4 | 3×4 | 12 | 26.4 | 6.60 | +| 5 | 4×4 | 16 | 35.2 | 7.04 | +| 6 | 4×5 | 20 | 44.0 | 7.33 | + +## 3. Выбор модели + +Что нужно от модели: + +- учитывать силу соперников (треб. 3); +- работать для столов на 2–6 игроков с ничьими и выбывшими; +- принимать дополнительные признаки партии (треб. 1, 2, 4, 5); +- считаться вручную — чтобы игроки могли проверить, почему рейтинг изменился именно так; +- работать на малых данных: десятки игроков и сотни партий; +- обходиться без внешних зависимостей: вся реализация — пара десятков строк. + +| Модель | Сила соперника | 2–6 игроков | Признаки партии | Ручной расчёт | Вывод | +|---|---|---|---|---|---| +| League Points (сейчас) | нет | да | только место | да | не выполняет треб. 1–5 | +| Elo, парный | да | да, через пары | через множитель K | да | **база** | +| Glicko-2 | да, плюс неопределённость | только через пары, как Elo; нужны рейтинговые периоды | нет | тяжело | сложность без выигрыша на наших объёмах | +| TrueSkill / OpenSkill | да, плюс неопределённость | да, нативно | нет — только порядок мест | нет | не принимает отрыв без самодельных надстроек | + +Выбран **парный Elo с множителем отрыва**. Отрыв умножает K, а фактический результат пары +остаётся 1/0.5/0. Можно было бы вшить отрыв в сам результат (например, близкая победа = +0.6), но тогда фаворит, выигравший близко, *терял бы* рейтинг за победу. Множитель K +сохраняет монотонность: победа всегда в плюс, отрыв влияет только на размер. + +## 4. Формулы + +### 4.1. Шкала + +Стартовый рейтинг **R₀ = 50**, масштаб **D = 40**: разница 40 пунктов означает шансы 10:1. +Это классический Elo, делённый на 10 (`R_Elo = 10·R + 1000`): 1500 ↔ 50, 1600 ↔ 60. +Шкала совпадает по порядку величин с нынешним топом. + +### 4.2. Ожидаемый результат пары + +```math +E_{ab} = \frac{1}{1 + 10^{(R_b - R_a)/D}} +``` + +`E_ab` — вероятность, что `a` окажется выше `b`. При 60 против 40 — 0.760, при равных — 0.500. + +### 4.3. Фактический результат пары + +`S_ab = 1`, если `a` занял место выше `b`; `0.5`, если места равны; `0` — если ниже. +Выбывшие делят последнее место, как и сейчас (`match_service._resolve_finish_places`). + +### 4.4. Изменение рейтинга + +```math +\Delta R_i = \frac{K_i \cdot G(N)}{N-1} \sum_{j \ne i} M_{ij} \, (S_{ij} - E_{ij}) +``` + +Все рейтинги в формуле — **до** партии. Деление на `N − 1` приводит сумму по соперникам +к «средней дуэли», размер стола затем добавляется явно через `G(N)`. + +### 4.5. Коэффициент K — скорость изменения + +```math +K_i = K_{\min} + (K_{\max} - K_{\min}) \cdot \max\!\left(0,\; 1 - \frac{n_i}{n_K}\right) +``` + +`n_i` — сколько завершённых партий игрок сыграл до этой. Новичок стартует с `K_max = 6.4`, +к 20-й партии K линейно спускается до `K_min = 1.6`. Так новичок быстро находит свой +уровень, а рейтинг опытного игрока не скачет от одной партии. + +Эта схема заменяет нынешние «10 виртуальных партий». Минимум партий для топа +(`MIN_GAMES = 10`) сохраняется. К 10-й партии предложенная система упорядочивает игроков +лучше текущей: ρ 0.807 против 0.769 в сценарии «сигнал» (раздел 7). + +### 4.6. Вес размера стола (треб. 2) + +```math +G(N) = 1 + w_N \cdot \frac{N-2}{4}, \qquad w_N = 0.5 +``` + +`G` = 1.0 для дуэли, 1.25 для четверых, 1.5 для шестерых. + +### 4.7. Множитель отрыва пары + +Признаки пары (a — выше или наравне с b), каждый нормирован в [0, 1]: + +| Признак | Формула | Для каких пар | Типичное значение μ | +|---|---|---|---| +| темп `τ` (треб. 1) | `(R_max − раунд) / (R_max − 1)` | только пары с победителем | `1/(R_max − 1)` — конец в предпоследнем раунде | +| отрыв по целям `o` (треб. 4) | `max(0, цели_a − цели_b) / N` | все | 0.5 | +| отрыв по мирам `w` (треб. 4) | `min(1, max(0, миры_a − миры_b) / (W(N)/N))` | все | 0.5 | + +Для пар с равными местами разница берётся по модулю. При победе `last_standing` отрыв +победителя по целям считается равным 1: все соперники устранены, сколько бы маркеров +у них ни было. + +```math +A_{ab} = 1 + w_\tau (\tau - \mu_\tau) + w_o (o - 0.5) + w_w (w - 0.5) +``` + +```math +M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab} +``` + +- **Центрирование.** Благодаря вычитанию μ партия с типичными признаками получает `M = 1` — + то есть обычный Elo. Разгром поднимает множитель, близкая партия его снижает. +- **Ограничение.** `clamp` ставит страховку: одна партия не может весить больше чем вдвое + или меньше чем вдвое против обычной. +- **Близость по типу победы `c`** (треб. 5) действует только на пары с победителем: тип + победы описывает борьбу за первое место, а не за второе или третье. + +| `win_reason` | `objectives` | `worlds` | `plastic` | `resources` | `last_standing` | +|---|---|---|---|---|---| +| `c` | 1.00 | 0.85 | 0.70 | 0.60 | 1.00 | + +### 4.8. Партии без новых полей + +У партий из истории (и у любых, где поле не заполнено) нет раунда, целей или миров. +Недостающий признак подставляется **типичным значением μ**: его слагаемое в `A` равно нулю. +Тип победы в истории есть, поэтому близость `c` работает всегда. Для старой партии формула +сводится к чистому Elo с учётом размера стола и типа победы. Заполнять историю задним числом +не обязательно. + +### 4.9. Свойства + +- **Монотонность.** 1-е место без ничьей даёт `S − E > 0` во всех парах, а `M > 0` — значит, + рейтинг растёт. Последнее место без ничьей всегда уменьшает рейтинг. +- **Сумма-ноль.** `M_ab = M_ba`, поэтому при равных K сумма изменений за партию равна нулю + и рейтинг не раздувается. Когда K разные (новичок и ветеран), сумма не нулевая — + это сделано намеренно (пример 7). В симуляции среднее по лиге за 300 партий сдвигается + не больше чем на 0.1 пункта, в сценарии «рост» — на −0.7 при разбросе силы игроков ±14. +- **Ограниченность.** Изменение за партию не больше `K·G·2.0`: 3.2 у ветерана в дуэли, + 12.8 у новичка в дуэли, 19.2 у новичка за столом на шестерых. Это теоретические пределы + для разгромной победы, которой почти никто не ждал (`E ≈ 0`); против равных — вдвое меньше. +- **Детерминизм.** Рейтинг — функция упорядоченной истории партий. Пересчёт с нуля всегда + даёт тот же результат. + +### 4.10. Коэффициенты + +| Параметр | Значение | Откуда | +|---|---|---| +| `R₀`, `D` | 50, 40 | шкала (4.1) | +| `K_max`, `K_min`, `n_K` | 6.4, 1.6, 20 | перебор (7.4): выигрыш на «сигнале» без потерь на «шуме» | +| `w_N` (стол) | 0.5 | требование «слабее темпа»; в переборе 0.25–0.5 равноценны | +| `w_τ` (темп) | 1.0 | требование 1 («значительно ценнее»); в переборе безопасен до 1.0, вред — с 2.0 | +| `w_o` (цели) | 0.5 | требование 4; равен весу миров, пока нет данных, что один из признаков информативнее (7.4) | +| `w_w` (миры) | 0.5 | требование 4; в переборе 0.5 — лучший вес единственного признака отрыва | +| `c` (близость) | 1 / 0.85 / 0.7 / 0.6 / 1 | требование 5, экспертная оценка по порядку тай-брейков; симуляция не подтверждает и не опровергает (7.5) | +| `clamp` | [0.5, 2.0] | страховка от выбросов | + +## 5. Требования → решения + +| Требование из #22 | Механизм | Эффект (ветеран против равного, дуэль) | +|---|---|---| +| 1. Темп: 4 цели за 2 раунда ценнее, чем к концу 8-го | признак `τ`, вес 1.0 — самый большой из весов | победа на 3-м раунде +1.15, на 8-м +0.58 — **вдвое** (пример 2) | +| 2. Размер стола, но слабее темпа | `G(N)`, вес 0.5 | 1-е место: дуэль +0.80, пятеро +1.10, шестеро +1.20 — **до ×1.5**, меньше, чем ×2 у темпа (примеры 3, 6) | +| 3. Разница рейтингов с соперником | ожидание `E` | 60 побеждает 40: +0.38; 40 побеждает 60: +1.22 — **втрое** больше (пример 1) | +| 4. Цели и миры на конец партии | признаки `o`, `w` | стол на 4: пары с выбывшими весят 1.25–1.5, пара лидеров — 0.7 (пример 5) | +| 5. Тип победы | близость `c` | без деталей: по целям +0.80 → по мирам +0.68 → по пластику +0.56 → по ресурсам +0.48 (пример 4) | +| Веса параметров (из треб. 2) | единый множитель `M` с весами | весь диапазон по отрыву: от +0.34 (самая близкая партия) до +1.60 (разгром) — **×4.7** (пример 4) | + +## 6. Примеры расчётов + +Все игроки опытные (40 партий, `K = 1.6`), если не сказано иное. Числа совпадают +с выводом `simulate.py` — скрипт проверяет их через `assert`. Промежуточные значения +здесь округлены. + +### Пример 1. Дуэль 60 против 40 (треб. 3) + +Партия из старой истории: только места и тип `objectives`, так что `M = 1`. + +- `E(A выше B) = 1 / (1 + 10^(−20/40)) = 1 / (1 + 0.316) = 0.760`. +- **1a. Побеждает сильный A:** `ΔR_A = 1.6 · 1 · (1 − 0.760) = +0.384`, у B −0.384. +- **1b. Побеждает слабый B:** `ΔR_B = 1.6 · 1 · (1 − 0.240) = +1.216`, у A −1.216. + +Неожиданная победа приносит втрое больше ожидаемой. + +### Пример 2. Быстрая и медленная победа (треб. 1) + +Равные (50 и 50), дуэль, победа по целям 2:1, миры 5:4. Разница — только раунд. + +| | Раунд 3 (2a) | Раунд 8 (2b) | +|---|---|---| +| темп `τ = (8 − r)/7` | 0.714 | 0.000 | +| `w_τ(τ − 1/7)` | +0.571 | −0.143 | +| цели `o = 1/2` → `0.5·(0.5 − 0.5)` | 0 | 0 | +| миры `w = 1/4.4 = 0.227` → `0.5·(0.227 − 0.5)` | −0.136 | −0.136 | +| `A = M` | 1.435 | 0.721 | +| `ΔR_A = 1.6 · M · (1 − 0.5)` | **+1.148** | **+0.577** | + +### Пример 3. Размер стола (треб. 2) + +Все по 50, партии без деталей (`M = 1`). + +- **3a. Дуэль:** `ΔR_1 = 1.6 · 1 · 0.5 = +0.800`. +- **3b. Стол на 5:** `G(5) = 1 + 0.5·3/4 = 1.375`, множитель перед суммой — `1.6·1.375/4 = 0.55`. + +| Место | Σ (S − E) по 4 соперникам | ΔR | Сейчас (League Points) | +|---|---|---|---| +| 1 | 4·0.5 = 2.0 | **+1.100** | 1.00 | +| 2 | −0.5 + 3·0.5 = 1.0 | +0.550 | 0.75 | +| 3 | 0 | 0.000 | 0.50 | +| 4 | −1.0 | −0.550 | 0.25 | +| 5 | −2.0 | −1.100 | 0.00 | + +### Пример 4. Тип победы и близость партии (треб. 5) + +Равные, дуэль. + +| Вариант | τ | o | w | A | c | M | ΔR победителя | +|---|---|---|---|---|---|---|---| +| по целям, без деталей (= 3a) | μ | μ | μ | 1.000 | 1.00 | 1.000 | +0.800 | +| 4a: по мирам, без деталей | μ | μ | μ | 1.000 | 0.85 | 0.850 | +0.680 | +| 4b: по пластику, без деталей | μ | μ | μ | 1.000 | 0.70 | 0.700 | +0.560 | +| 4c: по ресурсам, без деталей | μ | μ | μ | 1.000 | 0.60 | 0.600 | +0.480 | +| 4d: по мирам на 8-м раунде, цели 2:2, миры 6:5 | 0 | 0 | 0.227 | 0.471 → **0.5** | 0.85 | 0.425 | **+0.340** | +| 4e: разгром — 3-й раунд, цели 2:0, миры 8:2 | 0.714 | 1 | 1 | 2.071 → **2.0** | 1.00 | 2.000 | **+1.600** | + +В 4d и 4e сработала страховка `clamp`. + +### Пример 5. Стол на 4 с выбывшими (треб. 4) + +Партия закончилась на 7-м раунде по целям. `G(4) = 1.25`, множитель перед суммой — +`1.6·1.25/3 = 0.667`, честная доля миров — 6.6. + +| Игрок | Рейтинг | Место | Цели | Миры | +|---|---|---|---|---| +| A | 55 | 1 | 4 | 8 | +| B | 50 | 2 | 3 | 7 | +| C | 48 | 3 (выбыл) | 1 | 0 | +| D | 45 | 3 (выбыл) | 0 | 0 | + +Темп `τ = 1/7` совпадает с типичным, его слагаемое равно 0. + +| Пара | S | E | o | w | A = M | M·(S − E) | +|---|---|---|---|---|---|---| +| A–B | 1 | 0.571 | 1/4 | 1/6.6 = 0.152 | 1 − 0.125 − 0.174 = **0.701** | 0.300 | +| A–C | 1 | 0.599 | 3/4 | 1 | 1 + 0.125 + 0.25 = **1.375** | 0.551 | +| A–D | 1 | 0.640 | 1 | 1 | 1 + 0.25 + 0.25 = **1.500** | 0.540 | +| B–C | 1 | 0.529 | 2/4 | 1 | 1 + 0 + 0.25 = **1.250** | 0.589 | +| B–D | 1 | 0.571 | 3/4 | 1 | **1.375** | 0.589 | +| C–D | 0.5 | 0.543 | 1/4 (по модулю) | 0 | 1 − 0.125 − 0.25 = **0.625** | −0.027 | + +- `ΔR_A = 0.667 · (0.300 + 0.551 + 0.540)` = **+0.927** +- `ΔR_B = 0.667 · (−0.300 + 0.589 + 0.589)` = **+0.585** +- `ΔR_C = 0.667 · (−0.551 − 0.589 − 0.027)` = **−0.778** +- `ΔR_D = 0.667 · (−0.540 − 0.589 + 0.027)` = **−0.735** + +Итого: A и B близки друг к другу по целям и мирам, поэтому эта пара весит 0.7. Отрыв обоих +от выбывших огромный — эти пары весят 1.25–1.5. C и D поделили место, но C сильнее по +рейтингу, поэтому немного уступает D. + +### Пример 6. Стол на 6 и хоумрул 9 раундов + +Все по 50, партия закончилась **на 8-м раунде**, других деталей нет. +`G(6) = 1.5`, множитель перед суммой — `1.6·1.5/5 = 0.48`. + +| | 6a: хоумрул включён, `R_max = 9` | 6b: хоумрул выключен, `R_max = 8` | +|---|---|---| +| темп `τ` | (9 − 8)/8 = 0.125 | (8 − 8)/7 = 0 | +| типичный `μ_τ` | 1/8 = 0.125 | 1/7 = 0.143 | +| `M` пар с победителем | 1.000 — обычная партия | 0.857 — затянутая | +| ΔR по местам 1…6 | +1.200, +0.720, +0.240, −0.240, −0.720, −1.200 | +1.029, +0.754, +0.274, −0.206, −0.686, −1.166 | + +Конец на 8-м раунде при лимите 9 — это «предпоследний раунд», то есть типичная партия. +При лимите 8 — затянутая партия, и победа весит меньше. Поэтому лимит раундов снимается +в партию при её создании (раздел 8). + +### Пример 7. Новичок против ветерана + +Оба по 50; A — новичок (0 партий, `K = 6.4`), B — 40 партий (`K = 1.6`). A побеждает. + +`ΔR_A = 6.4 · 0.5` = **+3.2**, `ΔR_B = 1.6 · (−0.5)` = **−0.8**. + +О силе новичка ещё ничего не известно, поэтому его рейтинг двигается быстро. Ветеран +теряет как за обычное поражение от равного. + +## 7. Проверка на симуляции + +### 7.1. Модель лиги + +У настоящих партий пока нет раундов, целей и миров, поэтому система проверяется на +синтетической лиге, где «истинная» сила игроков известна. + +| Параметр | Значение | +|---|---| +| Игроки | 12 со старта, ещё по 2 на 1/3 и 2/3 сезона; активность у каждого своя (0.5–1.5) | +| Сезон | 300 партий; хоумрул 9 раундов включён в половине сезонов | +| Размер стола | 2 — 45%, 3 — 25%, 4 — 20%, 5 — 7%, 6 — 3% | +| Сила | `θ ~ N(0, 150)`; в шкале рейтинга ≈ ±14 | +| Производительность в партии | `θ + N(0, 225)` — кубы, карты, ошибки; места — по производительности | +| Детали партии | из отрыва производительности: раунд (чем больше отрыв, тем раньше конец), цели, миры, тип победы, выбывание. Получается 81% побед по целям, 10% по мирам, 4% по пластику, 1.4% по ресурсам, 3% последний выживший; чаще всего конец на 7–8 раунде | + +Сценарии: + +| Сценарий | Что проверяет | +|---|---| +| **сигнал** | базовый: отрыв связан с разницей сил | +| **шум** | места те же, но величина отрыва случайна и с силой не связана — сколько система теряет, если признаки ничего не говорят | +| **клубы** | три группы разной силы (−150 / 0 / +150), 90% партий внутри своей — умеет ли общий рейтинг «сшить» группы | +| **рост** | новички стартуют слабее на 0–200 и догоняют с опытом (×1/e за 15 партий) — успевает ли рейтинг за ростом игрока | + +### 7.2. Метрики + +- **Точность** — доля пар без ничьих во второй половине сезона, где *до* партии рейтинг + выше у занявшего место выше. Потолок — тот же прогноз по истинной силе. +- **Brier** — `(1 − E)²` по тем же парам, меньше — лучше. Показывает, насколько честны + сами вероятности; есть только у Elo-систем. +- **ρ** — ранговая корреляция Спирмена рейтинга на конец сезона с истинной силой + (игроки с 10+ партиями). +- **ρ@k** — то же сразу после k-й партии игрока: как быстро рейтинг «находит» игрока. +- **RMSE** — ошибка рейтинга относительно истинной силы в пунктах шкалы. +- **Наклон** — регрессия рейтинга на истинную силу: 1.0 — разброс честный, меньше — + рейтинги сжаты к середине. +- **|ΔR|** — средний модуль изменения за партию у игроков с 20+ партиями (волатильность). + +Сравнение идёт на одних и тех же сезонах (парные разности). Коэффициенты подбирались +на других сезонах (7.4), так что это проверка вне выборки подбора. + +### 7.3. Результаты: 200 сезонов на сценарий + +«Без новых полей» — предложенная система на той же истории, но без раунда, целей и миров: +так будет считаться история, накопленная до #23. + +**Сигнал** (потолок точности 0.6843) + +| Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | \|ΔR\| | +|---|---|---|---|---|---|---|---|---|---| +| League Points (сейчас) | 0.6683 | — | 0.913 | 0.660 | 0.769 | 0.854 | — | — | 0.69 | +| Elo, чистый | 0.6691 | 0.2089 | 0.911 | 0.656 | 0.770 | 0.852 | 4.74 | 0.81 | 0.52 | +| **Предложенная** | **0.6723** | **0.2077** | **0.928** | **0.711** | **0.807** | **0.881** | **4.19** | **0.95** | 0.60 | +| Предложенная, без новых полей | 0.6701 | 0.2085 | 0.916 | 0.677 | 0.782 | 0.864 | 4.74 | 0.79 | 0.59 | + +**Клубы** (потолок 0.6308) + +| Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | \|ΔR\| | +|---|---|---|---|---|---|---|---|---|---| +| League Points (сейчас) | 0.6045 | — | 0.525 | 0.303 | 0.391 | 0.463 | — | — | 0.81 | +| Elo, чистый | 0.6063 | 0.2341 | 0.632 | 0.326 | 0.440 | 0.535 | 10.26 | 0.40 | 0.57 | +| **Предложенная** | **0.6104** | 0.2336 | **0.645** | **0.354** | **0.462** | **0.554** | **10.03** | **0.47** | 0.65 | +| Предложенная, без новых полей | 0.6074 | **0.2332** | 0.633 | 0.336 | 0.440 | 0.534 | 10.33 | 0.38 | 0.64 | + +**Рост** (потолок 0.6864) + +| Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | \|ΔR\| | +|---|---|---|---|---|---|---|---|---|---| +| League Points (сейчас) | 0.6688 | — | 0.915 | 0.631 | 0.732 | 0.824 | — | — | 0.69 | +| Elo, чистый | 0.6687 | 0.2086 | 0.916 | 0.632 | 0.733 | 0.825 | 4.81 | 0.81 | 0.53 | +| **Предложенная** | **0.6725** | **0.2076** | **0.928** | **0.685** | **0.773** | **0.850** | **4.27** | **0.95** | 0.60 | +| Предложенная, без новых полей | 0.6697 | 0.2081 | 0.922 | 0.651 | 0.750 | 0.835 | 4.76 | 0.79 | 0.59 | + +**Шум** (потолок 0.6894) + +| Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | \|ΔR\| | +|---|---|---|---|---|---|---|---|---|---| +| League Points (сейчас) | 0.6737 | — | 0.916 | 0.656 | 0.761 | 0.848 | — | — | 0.69 | +| Elo, чистый | 0.6739 | **0.2070** | 0.917 | 0.659 | 0.765 | 0.851 | **4.61** | **0.82** | 0.52 | +| Предложенная | 0.6727 | 0.2076 | 0.908 | 0.641 | 0.751 | 0.844 | 4.85 | 0.81 | 0.59 | +| Предложенная, без новых полей | **0.6743** | **0.2070** | **0.919** | **0.667** | **0.775** | **0.856** | 4.74 | 0.78 | 0.59 | + +Парные разности (среднее ± стандартная ошибка по 200 сезонам): + +| Сценарий | Brier: предложенная − чистый Elo | Точность: предложенная − чистый Elo | Точность: предложенная − сейчас | +|---|---|---|---| +| сигнал | −0.0011 ± 0.0001 | +0.33 ± 0.06 п.п. | +0.40 ± 0.07 п.п. | +| клубы | −0.0005 ± 0.0002 | +0.40 ± 0.08 п.п. | +0.59 ± 0.10 п.п. | +| рост | −0.0010 ± 0.0001 | +0.38 ± 0.07 п.п. | +0.37 ± 0.06 п.п. | +| шум | +0.0006 ± 0.0001 | −0.13 ± 0.07 п.п. | −0.11 ± 0.07 п.п. | + +Выводы: + +1. **Если отрыв отражает силу** (а требования #22 исходят именно из этого), предложенная + система лучше обеих альтернатив по всем метрикам качества. Сильнее всего она выигрывает + в скорости: после 5 партий ρ = 0.71 против 0.66 у текущей, после 10 — 0.81 против 0.77. + Рейтинг меньше сжат к середине (наклон 0.95 против 0.81): сильные игроки быстрее + отрываются от середняков. +2. **Сила соперников — главное преимущество Elo над текущей системой.** В «клубах» текущая + система упорядочивает игроков заметно хуже (ρ 0.525 против 0.645): чемпион слабой группы + у неё стоит рядом с чемпионом сильной. +3. **Если отрыв — шум**, предложенная система теряет 0.1 п.п. точности и 0.0006 Brier — + цена лишней волатильности. Это худший из рассмотренных случаев: в остальных сценариях + она чистому Elo не уступает. +4. **История без новых полей** считается как чистый Elo с учётом стола и типа победы. + По точности и Brier она не хуже чистого Elo ни в одном сценарии. +5. **Абсолютные разности точности малы** (доли процента), потому что партия Forbidden Stars + сама по себе сильно случайна: даже знание истинной силы угадывает порядок пары лишь + в 68% случаев. Все системы близки к этому потолку. Качество рейтинга лучше видно + по ρ@k и RMSE, чем по точности. + +### 7.4. Подбор коэффициентов + +`simulate.py --grid` перебирает коэффициенты на **других** 40 сезонах каждого сценария. +Критерий — средний Brier, меньше — лучше. Разница в 0.0001 — примерно граница шума. + +**Этап 1. K чистого Elo** (все четыре сценария). Спуск K за 20 партий лучше, чем за 10. +Выгоден высокий K новичка и низкий K ветерана. + +| `K_max` \ `K_min` (спуск за 20 партий) | 1.6 | 2.4 | 3.2 | +|---|---|---|---| +| 4.8 | 0.21697 | 0.21708 | 0.21775 | +| 6.4 | 0.21649 | 0.21675 | 0.21751 | +| 9.6 | **0.21639** | 0.21669 | 0.21747 | +| 12.8 | 0.21682 | 0.21703 | 0.21775 | +| 16.0 | 0.21748 | 0.21755 | 0.21819 | + +Лучший вариант со спуском за 10 партий — 0.21676 (9.6 / 2.4). Для «чистого Elo» +в сравнении 7.3 взяты K = 9.6 / 1.6 / 20. + +**Этап 2. Веса отрыва** при K этапа 1: 162 комбинации (`w_N` ∈ {0, 0.25, 0.5}; +`w_τ`, `w_o`, `w_w` ∈ {0, 0.5, 1}; близость да/нет); сценарии «сигнал», «клубы», «рост». + +Без множителя (чистый Elo с K этапа 1): Brier **0.21846**, на «шуме» **0.21017**. +Лучшие комбинации: + +| Brier | Brier «шум» | `w_N` | `w_τ` | `w_o` | `w_w` | близость | +|---|---|---|---|---|---|---| +| 0.21801 | 0.20992 | 0.25 | 0 | 0 | 0.5 | да | +| 0.21804 | 0.20986 | 0.5 | 0 | 0 | 0.5 | да | +| 0.21807 | 0.21010 | 0 | 0 | 0 | 0.5 | да | +| 0.21807 | 0.20992 | 0.5 | 0.5 | 0 | 0 | да | +| 0.21807 | 0.21005 | 0.25 | 0.5 | 0 | 0.5 | да | +| … | | | | | | | +| 0.22000 | | | | | | худшая комбинация | + +Лучшие комбинации выигрывают у отсутствия множителя около 0.0005, худшая проигрывает +0.0015. Оптимум очень пологий: первые десять вариантов умещаются в 0.0001. + +Перебор оставляет **один** признак отрыва из трёх. Это ожидаемо: в генераторе раунд, цели +и миры выводятся из одного и того же отрыва производительности, второй признак не добавляет +информации и лишь увеличивает разброс. +Реальная игра так не устроена: в ней ранний конец, счёт целей и контроль миров — разные +стороны партии. Поэтому веса признаков заданы требованиями #22 в пределах безопасной +зоны (этап 4), а не взяты из вершины перебора. + +**Этап 3. K для предложенных весов.** Множитель в среднем чуть больше 1, поэтому K нужен +меньше, чем у чистого Elo. Спуск за 20 партий: + +| `K_max` | `K_min` | Brier (сигнальные) | Brier «шум» | Brier с поправкой на автокорреляцию | +|---|---|---|---|---| +| 4.8 | 1.2 | **0.21760** | 0.21063 | 0.21757 | +| 4.8 | 1.6 | 0.21780 | 0.21049 | 0.21775 | +| 6.4 | 1.2 | 0.21781 | 0.21027 | 0.21773 | +| **6.4** | **1.6** | 0.21799 | **0.21022** | 0.21790 | +| 8.0 | 1.6 | 0.21840 | 0.21028 | 0.21828 | + +- **Выбор 6.4 / 1.6.** Против чистого Elo он даёт −0.00047 на сигнальных сценариях и + +0.00005 на «шуме». Вариант 4.8 / 1.2 выигрывает больше (−0.00086), но на «шуме» + проигрывает +0.00046. Выбран вариант, который не теряет, если гипотеза ТЗ о значении + отрыва не подтвердится. +- **Поправка на автокорреляцию** (FiveThirtyEight: фаворит закономерно побеждает с большим + отрывом, и без поправки его рейтинг раздувается) даёт около 0.0001. В формулу она не + включена: лишняя сложность для ручного расчёта при нулевом эффекте. + +**Этап 4. Чувствительность.** Меняется один параметр, остальные — как в предложении +(Brier 0.21799, «шум» 0.21022). ρ@10 здесь — среднее по трём сценариям, включая «клубы», +поэтому оно ниже, чем в 7.3. + +| Параметр | Значение | Brier (сигнальные) | ρ@10 | Brier «шум» | +|---|---|---|---|---| +| `w_N` | 0 / 0.25 / **0.5** / 1.0 | 0.21805 / 0.21798 / **0.21799** / 0.21821 | 0.663 / 0.668 / **0.673** / 0.679 | 0.21072 / 0.21041 / **0.21022** / 0.21011 | +| `w_τ` | 0 / 0.25 / 0.5 / **1.0** / 2.0 | 0.21774 / 0.21778 / 0.21783 / **0.21799** / 0.21828 | 0.671 / 0.670 / 0.672 / **0.673** / 0.665 | 0.20989 / 0.20995 / 0.21003 / **0.21022** / 0.21050 | +| `w_o` | 0 / 0.25 / **0.5** / 1.0 / 2.0 | 0.21770 / 0.21780 / **0.21799** / 0.21841 / 0.21894 | 0.667 / 0.670 / **0.673** / 0.672 / 0.671 | 0.21003 / 0.21010 / **0.21022** / 0.21042 / 0.21073 | +| `w_w` | 0 / 0.25 / **0.5** / 1.0 / 2.0 | 0.21781 / 0.21786 / **0.21799** / 0.21832 / 0.21883 | 0.667 / 0.670 / **0.673** / 0.672 / 0.668 | 0.21003 / 0.21011 / **0.21022** / 0.21038 / 0.21059 | +| `c` | нет / **предложенная** / вдвое сильнее | 0.21801 / **0.21799** / 0.21801 | 0.674 / **0.673** / 0.671 | 0.21016 / **0.21022** / 0.21031 | + +Итог: + +- Веса в диапазоне 0–1 безопасны: изменение вдвое сдвигает Brier не больше чем на 0.00042 + (`w_o` 0.5 → 1.0). Вред начинается с 2.0 — поэтому ни один вес не выше 1. +- Вес размера стола полезен: от 0 до 0.5 растёт ρ@10 и падает Brier на «шуме». +- Близость по типу победы в симуляции нейтральна (±0.00002). + +### 7.5. Что симуляция доказывает и что нет + +- **Доказывает:** формулы корректны и устойчивы, не раздувают рейтинг, быстро сходятся, + правильно «сшивают» группы разной силы. Выбранные веса лежат в пологой области: изменение + любого веса вдвое в любую сторону сдвигает Brier не больше чем на 0.00042. Если признаки + партии окажутся бесполезны, потеря мала. +- **Не доказывает:** + - что в *реальных* партиях Forbidden Stars ранний конец, отрыв по целям и мирам связаны + с разницей сил так, как заложено в генераторе. Это допущение, на котором стоит и само + ТЗ; + - какой из признаков отрыва (раунд, цели, миры) информативнее: в генераторе все три + выводятся из одного отрыва и дублируют друг друга. + + Проверить это можно только на реальных данных после внедрения #23 (раздел 8, + «Калибровка»). +- **Коэффициенты близости `c`** симуляция не подтверждает и не опровергает: при записанных + целях и мирах тип победы почти не добавляет информации. Значения — экспертная оценка + по порядку тай-брейков. Их главная роль — старые партии, где тип победы — единственный + признак хода игры. + +## 8. Что потребуется в реализации (#23) + +Изменение **ломающее** (`Compat/Breaking`): у всех игроков меняются числа рейтинга и, +вероятно, порядок в топе. + +### Данные (миграция `0014_*`) + +| Где | Поле | Тип | Смысл | +|---|---|---|---| +| `groups` | `nine_rounds_rule` | bool, default false | галочка «9 раундов при 5–6 игроках» | +| `matches` | `nine_rounds_rule` | bool, default false | **снимок** настройки группы при создании партии: смена настройки не должна переписывать историю (пример 6) | +| `matches` | `end_round` | int NULL, `1 ≤ end_round ≤ R_max` | раунд, в котором партия закончилась | +| `match_participants` | `objectives` | int NULL, ≥ 0 | маркеры целей на конец партии | +| `match_participants` | `worlds` | int NULL, ≥ 0 | дружественные миры на конец партии; у выбывшего 0 | +| `matches.win_reason` | + `last_standing` | CHECK | новая причина победы | + +`R_max` в партии не хранится, а вычисляется: `9`, если `nine_rounds_rule` и `N ≥ 5`, иначе `8`. +Число участников может поменяться при правке партии, а снимок правила — нет. + +Миграция идемпотентна, как `0003`: ALTER только при отсутствии столбца, `render_as_batch` +для CHECK. **Бэкфилл не нужен:** NULL — это «нет данных», и формулы его учитывают (4.8). + +### Ввод + +- Форма завершения (`MatchDetailPage`, `match_service.finish_match`, черновик + `MatchFinishDraft`) и админская правка (`AdminMatchEdit`): раунд окончания и у каждого + участника цели и миры. Поля необязательные — пропуск лучше выдумки. +- Серверная валидация — только диапазоны и явные противоречия: у выбывшего миры = 0; + раунд ≤ `R_max`. Подсказки о согласованности (тип `worlds` при неравных целях лидеров + и т.п.) лучше показывать предупреждением, а не отказом. +- Настройки группы: галочка рядом с дополнениями (`PUT /groups/{id}/expansions` или + отдельный `PATCH`). + +### Расчёт + +- Рейтинг — функция упорядоченной истории, поэтому он **пересчитывается проигрыванием** + завершённых партий по порядку (`played_at`, `finished_at`, `id`), а не агрегатом SQL. + Данных мало: сотни партий, микросекунды на пару. Существующий принцип «считается вживую» + сохраняется, кэш можно ввести позже с инвалидацией по уже существующим SSE-событиям. +- **Две цепочки:** общий рейтинг — по всем партиям приложения, групповой — по партиям + группы. Это разные числа, как и сейчас. +- Правка или удаление прошлой партии автоматически меняет всё после неё: при пересчёте + с нуля отдельной логики не нужно. +- Эталон — `rate_match` в `simulate.py`. Примеры из раздела 6 стоит перенести в тесты + бэкенда как есть. +- Смежные метрики на старых очках места: + - «лучшая партия» в профиле (`user_match_list(best_only)`) → партия с наибольшим ΔR; + - «лучшая/худшая фракция» → средний `S − E` на фракции: насколько игрок на ней + выступает выше ожидания, без привязки к рейтингу; + - `win_rate`, `avg_place`, «форма» — без изменений. + +### Что ломается для пользователей + +- Числа рейтинга у всех меняются. Шкала та же (около 50), но это другая величина: не + «средний процент очков», а сила относительно соперников. +- Порядок в топе может измениться — в первую очередь у тех, кто играл в основном со слабыми + или сильными соперниками. +- Рейтинг новичка после одной партии меняется заметно сильнее, чем раньше: +3.2 за обычную + победу над равным, до +6.4 за разгром (теоретический предел — 12.8). +- **Предложение:** разовое уведомление всем игрокам и короткое пояснение «как считается + рейтинг» на странице топа. + +### Калибровка после внедрения + +Когда наберётся ~100 партий с заполненными раундом, целями и мирами: + +- типичные значения `μ` заменить средними по реальным партиям; +- повторить перебор весов из `simulate.py` на реальной истории: критерий — Brier прогноза + следующей партии; +- проверить главное допущение: есть ли у ранних побед и большого отрыва связь с последующими + результатами игроков. + +Коэффициенты — константы в одном модуле (как сейчас `scoring.py`): калибровка — это правка +констант и пересчёт, без миграций. + +### CLAUDE.md + +Раздел «Статистика» уже расходится с кодом: там `MIN_GAMES=3` и формула без сглаживания. +При реализации #23 его нужно переписать под новую систему. + +## 9. Открытые вопросы + +На утверждение владельцу: + +1. **Шкала отображения.** Предлагается «Elo/10»: старт 50, как сейчас. Альтернатива — + классические 1500 (`10·R + 1000`); она привычнее шахматистам, но ломает визуальную + преемственность топа. Формулы от выбора не зависят. +2. **Коэффициенты близости** по типам победы (1 / 0.85 / 0.7 / 0.6 / 1) — экспертная оценка + (7.5). Согласны ли с порядком и шагом? +3. **Новая причина победы `last_standing`** (все соперники устранены) — добавлять? + Сейчас такая партия записывается как `objectives`. +4. **Ввод миров на конец партии** — самое затратное новое поле: миры нужно пересчитать на + поле. Если это неудобно за столом, можно оставить только раунд и цели: вес миров перейдёт + к целям (перебор показывает, что система остаётся работоспособной и без `w_w`). +5. **Затухание за неактивность** (рейтинг долго не игравшего игрока) в предложение не входит. + Нужно ли оно? +6. **Минимум партий для топа.** Сохраняется 10 — нужно ли менять?