2 Commits
Author SHA1 Message Date
NotBigGhostandClaude Opus 5 28846c561c Рейтинг: документ по решениям владельца
Поле дуэли 2×3 в таблице правил и примерах; шкала 1500 в формулах, примерах
и пределах изменений; таблицы 7.3 и 7.4 — из новых прогонов simulate.py.
Раздел 8 для #23: правило last_standing (автоматически при одном невыбывшем,
проверка на сервере, бэкфилл истории), рейтинг целым числом, столбцы общего
топа; устаревший абзац про CLAUDE.md заменён отсылкой к HelpPage.tsx.
Раздел 9 «Открытые вопросы» стал «Решениями владельца». #22

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
2026-09-14 21:11:25 +03:00
NotBigGhostandClaude Opus 5 af7d2b358d Рейтинг: поле 2×3 для дуэли и шкала 1500 в эталонной реализации
Решения владельца по #22: у дуэли поле 2×3 (6 тайлов, 6.6 мира на игрока),
шкала — классический Elo (R0 = 1500, D = 400, K 64 → 16; чистый Elo — 96 → 16).
Примеры и assert переведены в новую шкалу, сетки K в --grid умножены на 10,
|ΔR| League Points в сравнении приводится к шкале 1500. Добавлена проверка,
что шкала не влияет на точность, Brier и ρ. #22

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
2026-09-14 21:07:21 +03:00
2 changed files with 256 additions and 194 deletions
+180 -140
View File
@@ -1,13 +1,14 @@
# Новая рейтинговая система # Новая рейтинговая система
> **Статус:** предложение на утверждение (#22). Реализация — #23. > **Статус:** предложение на утверждение (#22). Решения владельца по открытым вопросам
> (2026-09-14) внесены — [раздел 9](#9-решения-владельца). Реализация — #23.
> **Приложение:** [`simulate.py`](simulate.py) — эталонная реализация формул, все примеры > **Приложение:** [`simulate.py`](simulate.py) — эталонная реализация формул, все примеры
> этого документа (с `assert`), симуляция и перебор коэффициентов. Только стандартная > этого документа (с `assert`), симуляция и перебор коэффициентов. Только стандартная
> библиотека Python, вывод воспроизводим: > библиотека Python, вывод воспроизводим:
> >
> ```bash > ```bash
> python docs/rating/simulate.py # примеры + сравнение систем (≈20 с) > python docs/rating/simulate.py # примеры + сравнение систем (≈1.5 мин)
> python docs/rating/simulate.py --grid # примеры + подбор коэффициентов (≈5 мин) > python docs/rating/simulate.py --grid # примеры + подбор коэффициентов (≈7 мин)
> ``` > ```
## Коротко ## Коротко
@@ -23,8 +24,8 @@
- **Множитель отрыва.** Размер изменения зависит от того, *как* партия сыграна: на каком - **Множитель отрыва.** Размер изменения зависит от того, *как* партия сыграна: на каком
раунде закончилась, какой отрыв по целям и мирам, какой тип победы. Размер стола тоже раунде закончилась, какой отрыв по целям и мирам, какой тип победы. Размер стола тоже
влияет, но слабее темпа. влияет, но слабее темпа.
- **Шкала прежняя.** Старт 50, как в нынешнем топе. Пример из задачи «60 против 40» - **Шкала — классический Elo.** Старт 1500, разница 400 пунктов — шансы 10:1. Пример из
ложится в неё буквально. задачи «60 против 40» в ней — 1600 против 1400.
- **Монотонность.** Победитель никогда не теряет рейтинг, последнее место никогда не - **Монотонность.** Победитель никогда не теряет рейтинг, последнее место никогда не
приносит. приносит.
- **Старая история.** Пересчитывается по тем же формулам: у партий без новых полей - **Старая история.** Пересчитывается по тем же формулам: у партий без новых полей
@@ -45,7 +46,7 @@
6. [Примеры расчётов](#6-примеры-расчётов) 6. [Примеры расчётов](#6-примеры-расчётов)
7. [Проверка на симуляции](#7-проверка-на-симуляции) 7. [Проверка на симуляции](#7-проверка-на-симуляции)
8. [Что потребуется в реализации (#23)](#8-что-потребуется-в-реализации-23) 8. [Что потребуется в реализации (#23)](#8-что-потребуется-в-реализации-23)
9. [Открытые вопросы](#9-открытые-вопросы) 9. [Решения владельца](#9-решения-владельца)
--- ---
@@ -68,10 +69,10 @@
| # | Проблема | Пример | | # | Проблема | Пример |
|---|---|---| |---|---|---|
| 1 | **Сила соперника не учитывается.** | Победа над лидером топа и над новичком — одинаковые 1.0. Кто играет в основном со слабыми, копит рейтинг быстрее. В симуляции с тремя группами разной силы текущая система упорядочивает игроков с ρ = 0.525, предложенная — с 0.645 (раздел 7). | | 1 | **Сила соперника не учитывается.** | Победа над лидером топа и над новичком — одинаковые 1.0. Кто играет в основном со слабыми, копит рейтинг быстрее. В симуляции с тремя группами разной силы текущая система упорядочивает игроков с ρ = 0.525, предложенная — с 0.644 (раздел 7). |
| 2 | **Размер стола не учитывается.** | 1-е место в дуэли — 1.0, 1-е место за столом на пятерых — тоже 1.0, хотя обыграны четверо. | | 2 | **Размер стола не учитывается.** | 1-е место в дуэли — 1.0, 1-е место за столом на пятерых — тоже 1.0, хотя обыграны четверо. |
| 3 | **Ход партии не учитывается.** | Разгром на 3-м раунде (2:0 по целям, 8:2 по мирам) и победа на тай-брейке по мирам в конце 8-го раунда — одинаковые 1.0. | | 3 | **Ход партии не учитывается.** | Разгром на 3-м раунде (2:0 по целям, 8:2 по мирам) и победа на тай-брейке по мирам в конце 8-го раунда — одинаковые 1.0. |
| 4 | **Рейтинг помнит всю историю с одним весом.** | Игрок, проигравший первые 10 дуэлей и выигравший следующие 10, имеет (5 + 10) / 30 · 100 = **50** — как середняк, хотя сейчас сильнее всех. В сценарии «рост» симуляции ρ после 10 партий у текущей системы 0.732, у предложенной 0.773. | | 4 | **Рейтинг помнит всю историю с одним весом.** | Игрок, проигравший первые 10 дуэлей и выигравший следующие 10, имеет (5 + 10) / 30 · 100 = **50** — как середняк, хотя сейчас сильнее всех. В сценарии «рост» симуляции ρ после 10 партий у текущей системы 0.732, у предложенной 0.774. |
## 2. Исходные данные: правила игры ## 2. Исходные данные: правила игры
@@ -90,7 +91,7 @@
| `worlds` — по мирам | цели поровну, больше дружественных миров | близкая | | `worlds` — по мирам | цели поровну, больше дружественных миров | близкая |
| `plastic` — по пластику | цели и миры поровну, больше отрядов на поле | очень близкая | | `plastic` — по пластику | цели и миры поровну, больше отрядов на поле | очень близкая |
| `resources` — по ресурсам | всё выше поровну; **домашнее правило** | самая близкая | | `resources` — по ресурсам | всё выше поровну; **домашнее правило** | самая близкая |
| `last_standing` — последний выживший | все соперники устранены (нет миров); **предлагается добавить** | разгром | | `last_standing` — последний выживший | все соперники устранены (нет миров); **новая причина** (решение владельца, раздел 9) | разгром |
Если поровну вообще всё, победа общая (в приложении — ничья за 1-е место). Если поровну вообще всё, победа общая (в приложении — ничья за 1-е место).
@@ -103,7 +104,7 @@
| N | Поле | Тайлов | Миров на поле (×2.2) | Миров на игрока `W(N)/N` | | N | Поле | Тайлов | Миров на поле (×2.2) | Миров на игрока `W(N)/N` |
|---|---|---|---|---| |---|---|---|---|---|
| 2 | 2×2 | 4 | 8.8 | 4.40 | | 2 | 2×3 | 6 | 13.2 | 6.60 |
| 3 | 3×3 | 9 | 19.8 | 6.60 | | 3 | 3×3 | 9 | 19.8 | 6.60 |
| 4 | 3×4 | 12 | 26.4 | 6.60 | | 4 | 3×4 | 12 | 26.4 | 6.60 |
| 5 | 4×4 | 16 | 35.2 | 7.04 | | 5 | 4×4 | 16 | 35.2 | 7.04 |
@@ -136,9 +137,16 @@
### 4.1. Шкала ### 4.1. Шкала
Стартовый рейтинг **R₀ = 50**, масштаб **D = 40**: разница 40 пунктов означает шансы 10:1. Стартовый рейтинг **R₀ = 1500**, масштаб **D = 400**: разница 400 пунктов означает шансы 10:1.
Это классический Elo, делённый на 10 (`R_Elo = 10·R + 1000`): 1500 ↔ 50, 1600 ↔ 60. Это классическая шкала Elo (решение владельца, раздел 9).
Шкала совпадает по порядку величин с нынешним топом.
С нынешним топом (около 50) она не совпадает. Для сопоставления чисел в этом документе
используется соответствие `R = 10·score + 1000`: 50 ↔ 1500, 60 ↔ 1600. Пример «60 против 40»
из #22 — это 1600 против 1400. Формулы от выбора шкалы не зависят. Если пересчитать рейтинги
по этому соответствию, а `D` и `K` умножить на 10, ожидания, порядок игроков и качество
прогноза не изменятся — в 10 раз вырастут только изменения рейтинга и его разброс.
`simulate.py` это проверяет: на одних и тех же сезонах обе шкалы дают одинаковые точность,
Brier и ρ.
### 4.2. Ожидаемый результат пары ### 4.2. Ожидаемый результат пары
@@ -146,7 +154,7 @@
E_{ab} = \frac{1}{1 + 10^{(R_b - R_a)/D}} E_{ab} = \frac{1}{1 + 10^{(R_b - R_a)/D}}
``` ```
`E_ab` — вероятность, что `a` окажется выше `b`. При 60 против 40 — 0.760, при равных — 0.500. `E_ab` — вероятность, что `a` окажется выше `b`. При 1600 против 1400 — 0.760, при равных — 0.500.
### 4.3. Фактический результат пары ### 4.3. Фактический результат пары
@@ -168,8 +176,8 @@ E_{ab} = \frac{1}{1 + 10^{(R_b - R_a)/D}}
K_i = K_{\min} + (K_{\max} - K_{\min}) \cdot \max\!\left(0,\; 1 - \frac{n_i}{n_K}\right) 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`, `n_i` — сколько завершённых партий игрок сыграл до этой. Новичок стартует с `K_max = 64`,
к 20-й партии K линейно спускается до `K_min = 1.6`. Так новичок быстро находит свой к 20-й партии K линейно спускается до `K_min = 16`. Так новичок быстро находит свой
уровень, а рейтинг опытного игрока не скачет от одной партии. уровень, а рейтинг опытного игрока не скачет от одной партии.
Эта схема заменяет нынешние «10 виртуальных партий». Минимум партий для топа Эта схема заменяет нынешние «10 виртуальных партий». Минимум партий для топа
@@ -232,9 +240,9 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
- **Сумма-ноль.** `M_ab = M_ba`, поэтому при равных K сумма изменений за партию равна нулю - **Сумма-ноль.** `M_ab = M_ba`, поэтому при равных K сумма изменений за партию равна нулю
и рейтинг не раздувается. Когда K разные (новичок и ветеран), сумма не нулевая — и рейтинг не раздувается. Когда K разные (новичок и ветеран), сумма не нулевая —
это сделано намеренно (пример 7). В симуляции среднее по лиге за 300 партий сдвигается это сделано намеренно (пример 7). В симуляции среднее по лиге за 300 партий сдвигается
не больше чем на 0.1 пункта, в сценарии «рост» — на −0.7 при разбросе силы игроков ±14. не больше чем на 0.8 пункта, в сценарии «рост» — на −7 при разбросе силы игроков ±140.
- **Ограниченность.** Изменение за партию не больше `K·G·2.0`: 3.2 у ветерана в дуэли, - **Ограниченность.** Изменение за партию не больше `K·G·2.0`: 32 у ветерана в дуэли,
12.8 у новичка в дуэли, 19.2 у новичка за столом на шестерых. Это теоретические пределы 128 у новичка в дуэли, 192 у новичка за столом на шестерых. Это теоретические пределы
для разгромной победы, которой почти никто не ждал (`E ≈ 0`); против равных — вдвое меньше. для разгромной победы, которой почти никто не ждал (`E ≈ 0`); против равных — вдвое меньше.
- **Детерминизм.** Рейтинг — функция упорядоченной истории партий. Пересчёт с нуля всегда - **Детерминизм.** Рейтинг — функция упорядоченной истории партий. Пересчёт с нуля всегда
даёт тот же результат. даёт тот же результат.
@@ -243,69 +251,70 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
| Параметр | Значение | Откуда | | Параметр | Значение | Откуда |
|---|---|---| |---|---|---|
| `R₀`, `D` | 50, 40 | шкала (4.1) | | `R₀`, `D` | 1500, 400 | шкала (4.1), решение владельца |
| `K_max`, `K_min`, `n_K` | 6.4, 1.6, 20 | перебор (7.4): выигрыш на «сигнале» без потерь на «шуме» | | `K_max`, `K_min`, `n_K` | 64, 16, 20 | перебор (7.4): выигрыш на «сигнале» без потерь на «шуме» |
| `w_N` (стол) | 0.5 | требование «слабее темпа»; в переборе 0.25–0.5 равноценны | | `w_N` (стол) | 0.5 | требование «слабее темпа»; в переборе 0.25–0.5 равноценны |
| `w_τ` (темп) | 1.0 | требование 1 («значительно ценнее»); в переборе безопасен до 1.0, вред — с 2.0 | | `w_τ` (темп) | 1.0 | требование 1 («значительно ценнее»); в переборе безопасен до 1.0, вред — с 2.0 |
| `w_o` (цели) | 0.5 | требование 4; равен весу миров, пока нет данных, что один из признаков информативнее (7.4) | | `w_o` (цели) | 0.5 | требование 4; равен весу миров, пока нет данных, что один из признаков информативнее (7.4) |
| `w_w` (миры) | 0.5 | требование 4; в переборе 0.5 — лучший вес единственного признака отрыва | | `w_w` (миры) | 0.5 | требование 4; в переборе 0.5 — лучший вес единственного признака отрыва |
| `c` (близость) | 1 / 0.85 / 0.7 / 0.6 / 1 | требование 5, экспертная оценка по порядку тай-брейков; симуляция не подтверждает и не опровергает (7.5) | | `c` (близость) | 1 / 0.85 / 0.7 / 0.6 / 1 | требование 5, экспертная оценка по порядку тай-брейков, **утверждена владельцем**; симуляция не подтверждает и не опровергает (7.5) |
| `clamp` | [0.5, 2.0] | страховка от выбросов | | `clamp` | [0.5, 2.0] | страховка от выбросов |
## 5. Требования → решения ## 5. Требования → решения
| Требование из #22 | Механизм | Эффект (ветеран против равного, дуэль) | | Требование из #22 | Механизм | Эффект (ветеран против равного, дуэль) |
|---|---|---| |---|---|---|
| 1. Темп: 4 цели за 2 раунда ценнее, чем к концу 8-го | признак `τ`, вес 1.0 — самый большой из весов | победа на 3-м раунде +1.15, на 8-м +0.58 — **вдвое** (пример 2) | | 1. Темп: 4 цели за 2 раунда ценнее, чем к концу 8-го | признак `τ`, вес 1.0 — самый большой из весов | победа на 3-м раунде +11.2, на 8-м +5.5 — **вдвое** (пример 2) |
| 2. Размер стола, но слабее темпа | `G(N)`, вес 0.5 | 1-е место: дуэль +0.80, пятеро +1.10, шестеро +1.20 — **до ×1.5**, меньше, чем ×2 у темпа (примеры 3, 6) | | 2. Размер стола, но слабее темпа | `G(N)`, вес 0.5 | 1-е место: дуэль +8.0, пятеро +11.0, шестеро +12.0 — **до ×1.5**, меньше, чем ×2 у темпа (примеры 3, 6) |
| 3. Разница рейтингов с соперником | ожидание `E` | 60 побеждает 40: +0.38; 40 побеждает 60: +1.22 — **втрое** больше (пример 1) | | 3. Разница рейтингов с соперником | ожидание `E` | 1600 побеждает 1400: +3.8; 1400 побеждает 1600: +12.2 — **втрое** больше (пример 1) |
| 4. Цели и миры на конец партии | признаки `o`, `w` | стол на 4: пары с выбывшими весят 1.25–1.5, пара лидеров — 0.7 (пример 5) | | 4. Цели и миры на конец партии | признаки `o`, `w` | стол на 4: пары с выбывшими весят 1.25–1.5, пара лидеров — 0.7 (пример 5) |
| 5. Тип победы | близость `c` | без деталей: по целям +0.80 → по мирам +0.68 → по пластику +0.56 → по ресурсам +0.48 (пример 4) | | 5. Тип победы | близость `c` | без деталей: по целям +8.0 → по мирам +6.8 → по пластику +5.6 → по ресурсам +4.8 (пример 4) |
| Веса параметров (из треб. 2) | единый множитель `M` с весами | весь диапазон по отрыву: от +0.34 (самая близкая партия) до +1.60 (разгром) — **×4.7** (пример 4) | | Веса параметров (из треб. 2) | единый множитель `M` с весами | весь диапазон по отрыву: от +3.4 (самая близкая партия) до +16.0 (разгром) — **×4.7** (пример 4) |
## 6. Примеры расчётов ## 6. Примеры расчётов
Все игроки опытные (40 партий, `K = 1.6`), если не сказано иное. Числа совпадают Все игроки опытные (40 партий, `K = 16`), если не сказано иное. Числа совпадают
с выводом `simulate.py` — скрипт проверяет их через `assert`. Промежуточные значения с выводом `simulate.py` — скрипт проверяет их через `assert`. Промежуточные значения
здесь округлены. здесь округлены. В приложении рейтинг показывается целым числом, а в расчёте хранится
без округления (раздел 8).
### Пример 1. Дуэль 60 против 40 (треб. 3) ### Пример 1. Дуэль 1600 против 1400 (треб. 3)
Партия из старой истории: только места и тип `objectives`, так что `M = 1`. Партия из старой истории: только места и тип `objectives`, так что `M = 1`.
- `E(A выше B) = 1 / (1 + 10^(−20/40)) = 1 / (1 + 0.316) = 0.760`. - `E(A выше B) = 1 / (1 + 10^(−200/400)) = 1 / (1 + 0.316) = 0.760`.
- **1a. Побеждает сильный A:** `ΔR_A = 1.6 · 1 · (1 − 0.760) = +0.384`, у B −0.384. - **1a. Побеждает сильный A:** `ΔR_A = 16 · 1 · (1 − 0.760) = +3.84`, у B −3.84.
- **1b. Побеждает слабый B:** `ΔR_B = 1.6 · 1 · (1 − 0.240) = +1.216`, у A −1.216. - **1b. Побеждает слабый B:** `ΔR_B = 16 · 1 · (1 − 0.240) = +12.16`, у A −12.16.
Неожиданная победа приносит втрое больше ожидаемой. Неожиданная победа приносит втрое больше ожидаемой.
### Пример 2. Быстрая и медленная победа (треб. 1) ### Пример 2. Быстрая и медленная победа (треб. 1)
Равные (50 и 50), дуэль, победа по целям 2:1, миры 5:4. Разница — только раунд. Равные (1500 и 1500), дуэль на поле 2×3, победа по целям 2:1, миры 5:4. Разница — только раунд.
| | Раунд 3 (2a) | Раунд 8 (2b) | | | Раунд 3 (2a) | Раунд 8 (2b) |
|---|---|---| |---|---|---|
| темп `τ = (8 − r)/7` | 0.714 | 0.000 | | темп `τ = (8 − r)/7` | 0.714 | 0.000 |
| `w_τ(τ − 1/7)` | +0.571 | −0.143 | | `w_τ(τ − 1/7)` | +0.571 | −0.143 |
| цели `o = 1/2` → `0.5·(0.5 − 0.5)` | 0 | 0 | | цели `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 | | миры `w = 1/6.6 = 0.152` → `0.5·(0.152 − 0.5)` | −0.174 | −0.174 |
| `A = M` | 1.435 | 0.721 | | `A = M` | 1.397 | 0.683 |
| `ΔR_A = 1.6 · M · (1 − 0.5)` | **+1.148** | **+0.577** | | `ΔR_A = 16 · M · (1 − 0.5)` | **+11.18** | **+5.46** |
### Пример 3. Размер стола (треб. 2) ### Пример 3. Размер стола (треб. 2)
Все по 50, партии без деталей (`M = 1`). Все по 1500, партии без деталей (`M = 1`).
- **3a. Дуэль:** `ΔR_1 = 1.6 · 1 · 0.5 = +0.800`. - **3a. Дуэль:** `ΔR_1 = 16 · 1 · 0.5 = +8.00`.
- **3b. Стол на 5:** `G(5) = 1 + 0.5·3/4 = 1.375`, множитель перед суммой — `1.6·1.375/4 = 0.55`. - **3b. Стол на 5:** `G(5) = 1 + 0.5·3/4 = 1.375`, множитель перед суммой — `16·1.375/4 = 5.5`.
| Место | Σ (S − E) по 4 соперникам | ΔR | Сейчас (League Points) | | Место | Σ (S − E) по 4 соперникам | ΔR | Сейчас (League Points) |
|---|---|---|---| |---|---|---|---|
| 1 | 4·0.5 = 2.0 | **+1.100** | 1.00 | | 1 | 4·0.5 = 2.0 | **+11.00** | 1.00 |
| 2 | −0.5 + 3·0.5 = 1.0 | +0.550 | 0.75 | | 2 | −0.5 + 3·0.5 = 1.0 | +5.50 | 0.75 |
| 3 | 0 | 0.000 | 0.50 | | 3 | 0 | 0.00 | 0.50 |
| 4 | −1.0 | −0.550 | 0.25 | | 4 | −1.0 | −5.50 | 0.25 |
| 5 | −2.0 | −1.100 | 0.00 | | 5 | −2.0 | −11.00 | 0.00 |
### Пример 4. Тип победы и близость партии (треб. 5) ### Пример 4. Тип победы и близость партии (треб. 5)
@@ -313,26 +322,26 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
| Вариант | τ | o | w | A | c | M | ΔR победителя | | Вариант | τ | o | w | A | c | M | ΔR победителя |
|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|
| по целям, без деталей (= 3a) | μ | μ | μ | 1.000 | 1.00 | 1.000 | +0.800 | | по целям, без деталей (= 3a) | μ | μ | μ | 1.000 | 1.00 | 1.000 | +8.00 |
| 4a: по мирам, без деталей | μ | μ | μ | 1.000 | 0.85 | 0.850 | +0.680 | | 4a: по мирам, без деталей | μ | μ | μ | 1.000 | 0.85 | 0.850 | +6.80 |
| 4b: по пластику, без деталей | μ | μ | μ | 1.000 | 0.70 | 0.700 | +0.560 | | 4b: по пластику, без деталей | μ | μ | μ | 1.000 | 0.70 | 0.700 | +5.60 |
| 4c: по ресурсам, без деталей | μ | μ | μ | 1.000 | 0.60 | 0.600 | +0.480 | | 4c: по ресурсам, без деталей | μ | μ | μ | 1.000 | 0.60 | 0.600 | +4.80 |
| 4d: по мирам на 8-м раунде, цели 2:2, миры 6:5 | 0 | 0 | 0.227 | 0.471 → **0.5** | 0.85 | 0.425 | **+0.340** | | 4d: по мирам на 8-м раунде, цели 2:2, миры 6:5 | 0 | 0 | 0.152 | 0.433 → **0.5** | 0.85 | 0.425 | **+3.40** |
| 4e: разгром — 3-й раунд, цели 2:0, миры 8:2 | 0.714 | 1 | 1 | 2.071 → **2.0** | 1.00 | 2.000 | **+1.600** | | 4e: разгром — 3-й раунд, цели 2:0, миры 8:2 | 0.714 | 1 | 0.909 | 2.026 → **2.0** | 1.00 | 2.000 | **+16.00** |
В 4d и 4e сработала страховка `clamp`. В 4d и 4e сработала страховка `clamp`.
### Пример 5. Стол на 4 с выбывшими (треб. 4) ### Пример 5. Стол на 4 с выбывшими (треб. 4)
Партия закончилась на 7-м раунде по целям. `G(4) = 1.25`, множитель перед суммой — Партия закончилась на 7-м раунде по целям. `G(4) = 1.25`, множитель перед суммой —
`1.6·1.25/3 = 0.667`, честная доля миров — 6.6. `16·1.25/3 = 6.67`, честная доля миров — 6.6.
| Игрок | Рейтинг | Место | Цели | Миры | | Игрок | Рейтинг | Место | Цели | Миры |
|---|---|---|---|---| |---|---|---|---|---|
| A | 55 | 1 | 4 | 8 | | A | 1550 | 1 | 4 | 8 |
| B | 50 | 2 | 3 | 7 | | B | 1500 | 2 | 3 | 7 |
| C | 48 | 3 (выбыл) | 1 | 0 | | C | 1480 | 3 (выбыл) | 1 | 0 |
| D | 45 | 3 (выбыл) | 0 | 0 | | D | 1450 | 3 (выбыл) | 0 | 0 |
Темп `τ = 1/7` совпадает с типичным, его слагаемое равно 0. Темп `τ = 1/7` совпадает с типичным, его слагаемое равно 0.
@@ -345,10 +354,10 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
| B–D | 1 | 0.571 | 3/4 | 1 | **1.375** | 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 | | 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_A = 6.67 · (0.300 + 0.551 + 0.540)` = **+9.27**
- `ΔR_B = 0.667 · (−0.300 + 0.589 + 0.589)` = **+0.585** - `ΔR_B = 6.67 · (−0.300 + 0.589 + 0.589)` = **+5.85**
- `ΔR_C = 0.667 · (−0.551 − 0.589 − 0.027)` = **−0.778** - `ΔR_C = 6.67 · (−0.551 − 0.589 − 0.027)` = **−7.78**
- `ΔR_D = 0.667 · (−0.540 − 0.589 + 0.027)` = **−0.735** - `ΔR_D = 6.67 · (−0.540 − 0.589 + 0.027)` = **−7.35**
Итого: A и B близки друг к другу по целям и мирам, поэтому эта пара весит 0.7. Отрыв обоих Итого: A и B близки друг к другу по целям и мирам, поэтому эта пара весит 0.7. Отрыв обоих
от выбывших огромный — эти пары весят 1.25–1.5. C и D поделили место, но C сильнее по от выбывших огромный — эти пары весят 1.25–1.5. C и D поделили место, но C сильнее по
@@ -356,15 +365,15 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
### Пример 6. Стол на 6 и хоумрул 9 раундов ### Пример 6. Стол на 6 и хоумрул 9 раундов
Все по 50, партия закончилась **на 8-м раунде**, других деталей нет. Все по 1500, партия закончилась **на 8-м раунде**, других деталей нет.
`G(6) = 1.5`, множитель перед суммой — `1.6·1.5/5 = 0.48`. `G(6) = 1.5`, множитель перед суммой — `16·1.5/5 = 4.8`.
| | 6a: хоумрул включён, `R_max = 9` | 6b: хоумрул выключен, `R_max = 8` | | | 6a: хоумрул включён, `R_max = 9` | 6b: хоумрул выключен, `R_max = 8` |
|---|---|---| |---|---|---|
| темп `τ` | (9 − 8)/8 = 0.125 | (8 − 8)/7 = 0 | | темп `τ` | (9 − 8)/8 = 0.125 | (8 − 8)/7 = 0 |
| типичный `μ_τ` | 1/8 = 0.125 | 1/7 = 0.143 | | типичный `μ_τ` | 1/8 = 0.125 | 1/7 = 0.143 |
| `M` пар с победителем | 1.000 — обычная партия | 0.857 — затянутая | | `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 | | ΔR по местам 1…6 | +12.00, +7.20, +2.40, −2.40, −7.20, −12.00 | +10.29, +7.54, +2.74, −2.06, −6.86, −11.66 |
Конец на 8-м раунде при лимите 9 — это «предпоследний раунд», то есть типичная партия. Конец на 8-м раунде при лимите 9 — это «предпоследний раунд», то есть типичная партия.
При лимите 8 — затянутая партия, и победа весит меньше. Поэтому лимит раундов снимается При лимите 8 — затянутая партия, и победа весит меньше. Поэтому лимит раундов снимается
@@ -372,9 +381,9 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
### Пример 7. Новичок против ветерана ### Пример 7. Новичок против ветерана
Оба по 50; A — новичок (0 партий, `K = 6.4`), B — 40 партий (`K = 1.6`). A побеждает. Оба по 1500; A — новичок (0 партий, `K = 64`), B — 40 партий (`K = 16`). A побеждает.
`ΔR_A = 6.4 · 0.5` = **+3.2**, `ΔR_B = 1.6 · (−0.5)` = **−0.8**. `ΔR_A = 64 · 0.5` = **+32**, `ΔR_B = 16 · (−0.5)` = **−8**.
О силе новичка ещё ничего не известно, поэтому его рейтинг двигается быстро. Ветеран О силе новичка ещё ничего не известно, поэтому его рейтинг двигается быстро. Ветеран
теряет как за обычное поражение от равного. теряет как за обычное поражение от равного.
@@ -391,7 +400,7 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
| Игроки | 12 со старта, ещё по 2 на 1/3 и 2/3 сезона; активность у каждого своя (0.5–1.5) | | Игроки | 12 со старта, ещё по 2 на 1/3 и 2/3 сезона; активность у каждого своя (0.5–1.5) |
| Сезон | 300 партий; хоумрул 9 раундов включён в половине сезонов | | Сезон | 300 партий; хоумрул 9 раундов включён в половине сезонов |
| Размер стола | 2 — 45%, 3 — 25%, 4 — 20%, 5 — 7%, 6 — 3% | | Размер стола | 2 — 45%, 3 — 25%, 4 — 20%, 5 — 7%, 6 — 3% |
| Сила | `θ ~ N(0, 150)`; в шкале рейтинга ≈ ±14 | | Сила | `θ ~ N(0, 150)`; в шкале рейтинга ≈ ±140 |
| Производительность в партии | `θ + N(0, 225)` — кубы, карты, ошибки; места — по производительности | | Производительность в партии | `θ + N(0, 225)` — кубы, карты, ошибки; места — по производительности |
| Детали партии | из отрыва производительности: раунд (чем больше отрыв, тем раньше конец), цели, миры, тип победы, выбывание. Получается 81% побед по целям, 10% по мирам, 4% по пластику, 1.4% по ресурсам, 3% последний выживший; чаще всего конец на 7–8 раунде | | Детали партии | из отрыва производительности: раунд (чем больше отрыв, тем раньше конец), цели, миры, тип победы, выбывание. Получается 81% побед по целям, 10% по мирам, 4% по пластику, 1.4% по ресурсам, 3% последний выживший; чаще всего конец на 7–8 раунде |
@@ -417,6 +426,8 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
- **Наклон** — регрессия рейтинга на истинную силу: 1.0 — разброс честный, меньше — - **Наклон** — регрессия рейтинга на истинную силу: 1.0 — разброс честный, меньше —
рейтинги сжаты к середине. рейтинги сжаты к середине.
- **|ΔR|** — средний модуль изменения за партию у игроков с 20+ партиями (волатильность). - **|ΔR|** — средний модуль изменения за партию у игроков с 20+ партиями (волатильность).
У League Points рейтинг в шкале 0–100, его |ΔR| приведён к шкале 1500 умножением на 10
(соответствие из 4.1).
Сравнение идёт на одних и тех же сезонах (парные разности). Коэффициенты подбирались Сравнение идёт на одних и тех же сезонах (парные разности). Коэффициенты подбирались
на других сезонах (7.4), так что это проверка вне выборки подбора. на других сезонах (7.4), так что это проверка вне выборки подбора.
@@ -430,46 +441,46 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
| Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | \|ΔR\| | | Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | \|ΔR\| |
|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|
| League Points (сейчас) | 0.6683 | — | 0.913 | 0.660 | 0.769 | 0.854 | — | — | 0.69 | | League Points (сейчас) | 0.6683 | — | 0.913 | 0.660 | 0.769 | 0.854 | — | — | 6.91 |
| Elo, чистый | 0.6691 | 0.2089 | 0.911 | 0.656 | 0.770 | 0.852 | 4.74 | 0.81 | 0.52 | | Elo, чистый | 0.6691 | 0.2089 | 0.911 | 0.656 | 0.770 | 0.852 | 47.4 | 0.81 | 5.24 |
| **Предложенная** | **0.6723** | **0.2077** | **0.928** | **0.711** | **0.807** | **0.881** | **4.19** | **0.95** | 0.60 | | **Предложенная** | **0.6721** | **0.2077** | **0.929** | **0.711** | **0.807** | **0.881** | **41.9** | **0.95** | 5.95 |
| Предложенная, без новых полей | 0.6701 | 0.2085 | 0.916 | 0.677 | 0.782 | 0.864 | 4.74 | 0.79 | 0.59 | | Предложенная, без новых полей | 0.6701 | 0.2085 | 0.916 | 0.677 | 0.782 | 0.864 | 47.4 | 0.79 | 5.90 |
**Клубы** (потолок 0.6308) **Клубы** (потолок 0.6308)
| Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | \|ΔR\| | | Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | \|ΔR\| |
|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|
| League Points (сейчас) | 0.6045 | — | 0.525 | 0.303 | 0.391 | 0.463 | — | — | 0.81 | | League Points (сейчас) | 0.6045 | — | 0.525 | 0.303 | 0.391 | 0.463 | — | — | 8.06 |
| Elo, чистый | 0.6063 | 0.2341 | 0.632 | 0.326 | 0.440 | 0.535 | 10.26 | 0.40 | 0.57 | | Elo, чистый | 0.6063 | 0.2341 | 0.632 | 0.326 | 0.440 | 0.535 | 102.6 | 0.40 | 5.71 |
| **Предложенная** | **0.6104** | 0.2336 | **0.645** | **0.354** | **0.462** | **0.554** | **10.03** | **0.47** | 0.65 | | **Предложенная** | **0.6102** | 0.2336 | **0.644** | **0.356** | **0.463** | **0.555** | **100.3** | **0.47** | 6.53 |
| Предложенная, без новых полей | 0.6074 | **0.2332** | 0.633 | 0.336 | 0.440 | 0.534 | 10.33 | 0.38 | 0.64 | | Предложенная, без новых полей | 0.6074 | **0.2332** | 0.633 | 0.336 | 0.440 | 0.534 | 103.3 | 0.38 | 6.41 |
**Рост** (потолок 0.6864) **Рост** (потолок 0.6864)
| Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | \|ΔR\| | | Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | \|ΔR\| |
|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|
| League Points (сейчас) | 0.6688 | — | 0.915 | 0.631 | 0.732 | 0.824 | — | — | 0.69 | | League Points (сейчас) | 0.6688 | — | 0.915 | 0.631 | 0.732 | 0.824 | — | — | 6.91 |
| Elo, чистый | 0.6687 | 0.2086 | 0.916 | 0.632 | 0.733 | 0.825 | 4.81 | 0.81 | 0.53 | | Elo, чистый | 0.6687 | 0.2086 | 0.916 | 0.632 | 0.733 | 0.825 | 48.0 | 0.81 | 5.25 |
| **Предложенная** | **0.6725** | **0.2076** | **0.928** | **0.685** | **0.773** | **0.850** | **4.27** | **0.95** | 0.60 | | **Предложенная** | **0.6726** | **0.2076** | **0.929** | **0.685** | **0.774** | **0.851** | **42.7** | **0.95** | 5.96 |
| Предложенная, без новых полей | 0.6697 | 0.2081 | 0.922 | 0.651 | 0.750 | 0.835 | 4.76 | 0.79 | 0.59 | | Предложенная, без новых полей | 0.6697 | 0.2081 | 0.922 | 0.651 | 0.750 | 0.835 | 47.6 | 0.79 | 5.91 |
**Шум** (потолок 0.6894) **Шум** (потолок 0.6894)
| Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | \|ΔR\| | | Система | Точность | Brier | ρ | ρ@5 | ρ@10 | ρ@20 | RMSE | Наклон | \|ΔR\| |
|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|
| League Points (сейчас) | 0.6737 | — | 0.916 | 0.656 | 0.761 | 0.848 | — | — | 0.69 | | League Points (сейчас) | 0.6737 | — | 0.916 | 0.656 | 0.761 | 0.848 | — | — | 6.89 |
| Elo, чистый | 0.6739 | **0.2070** | 0.917 | 0.659 | 0.765 | 0.851 | **4.61** | **0.82** | 0.52 | | Elo, чистый | 0.6739 | **0.2070** | 0.917 | 0.659 | 0.765 | 0.851 | **46.1** | **0.82** | 5.23 |
| Предложенная | 0.6727 | 0.2076 | 0.908 | 0.641 | 0.751 | 0.844 | 4.85 | 0.81 | 0.59 | | Предложенная | 0.6727 | 0.2076 | 0.908 | 0.643 | 0.750 | 0.844 | 48.6 | 0.81 | 5.91 |
| Предложенная, без новых полей | **0.6743** | **0.2070** | **0.919** | **0.667** | **0.775** | **0.856** | 4.74 | 0.78 | 0.59 | | Предложенная, без новых полей | **0.6743** | **0.2070** | **0.919** | **0.667** | **0.775** | **0.856** | 47.4 | 0.78 | 5.88 |
Парные разности (среднее ± стандартная ошибка по 200 сезонам): Парные разности (среднее ± стандартная ошибка по 200 сезонам):
| Сценарий | Brier: предложенная − чистый Elo | Точность: предложенная − чистый Elo | Точность: предложенная − сейчас | | Сценарий | Brier: предложенная − чистый Elo | Точность: предложенная − чистый Elo | Точность: предложенная − сейчас |
|---|---|---|---| |---|---|---|---|
| сигнал | −0.0011 ± 0.0001 | +0.33 ± 0.06 п.п. | +0.40 ± 0.07 п.п. | | сигнал | −0.0011 ± 0.0001 | +0.30 ± 0.06 п.п. | +0.37 ± 0.07 п.п. |
| клубы | −0.0005 ± 0.0002 | +0.40 ± 0.08 п.п. | +0.59 ± 0.10 п.п. | | клубы | −0.0005 ± 0.0002 | +0.38 ± 0.08 п.п. | +0.57 ± 0.10 п.п. |
| рост | −0.0010 ± 0.0001 | +0.38 ± 0.07 п.п. | +0.37 ± 0.06 п.п. | | рост | −0.0010 ± 0.0001 | +0.39 ± 0.07 п.п. | +0.37 ± 0.06 п.п. |
| шум | +0.0006 ± 0.0001 | −0.13 ± 0.07 п.п. | −0.11 ± 0.07 п.п. | | шум | +0.0006 ± 0.0001 | −0.12 ± 0.06 п.п. | −0.10 ± 0.07 п.п. |
Выводы: Выводы:
@@ -479,7 +490,7 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
Рейтинг меньше сжат к середине (наклон 0.95 против 0.81): сильные игроки быстрее Рейтинг меньше сжат к середине (наклон 0.95 против 0.81): сильные игроки быстрее
отрываются от середняков. отрываются от середняков.
2. **Сила соперников — главное преимущество Elo над текущей системой.** В «клубах» текущая 2. **Сила соперников — главное преимущество Elo над текущей системой.** В «клубах» текущая
система упорядочивает игроков заметно хуже (ρ 0.525 против 0.645): чемпион слабой группы система упорядочивает игроков заметно хуже (ρ 0.525 против 0.644): чемпион слабой группы
у неё стоит рядом с чемпионом сильной. у неё стоит рядом с чемпионом сильной.
3. **Если отрыв — шум**, предложенная система теряет 0.1 п.п. точности и 0.0006 Brier — 3. **Если отрыв — шум**, предложенная система теряет 0.1 п.п. точности и 0.0006 Brier —
цена лишней волатильности. Это худший из рассмотренных случаев: в остальных сценариях цена лишней волатильности. Это худший из рассмотренных случаев: в остальных сценариях
@@ -499,16 +510,17 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
**Этап 1. K чистого Elo** (все четыре сценария). Спуск K за 20 партий лучше, чем за 10. **Этап 1. K чистого Elo** (все четыре сценария). Спуск K за 20 партий лучше, чем за 10.
Выгоден высокий K новичка и низкий K ветерана. Выгоден высокий K новичка и низкий K ветерана.
| `K_max` \ `K_min` (спуск за 20 партий) | 1.6 | 2.4 | 3.2 | | `K_max` \ `K_min` (спуск за 20 партий) | 16 | 24 | 32 |
|---|---|---|---| |---|---|---|---|
| 4.8 | 0.21697 | 0.21708 | 0.21775 | | 48 | 0.21697 | 0.21708 | 0.21775 |
| 6.4 | 0.21649 | 0.21675 | 0.21751 | | 64 | 0.21649 | 0.21675 | 0.21751 |
| 9.6 | **0.21639** | 0.21669 | 0.21747 | | 96 | **0.21639** | 0.21669 | 0.21747 |
| 12.8 | 0.21682 | 0.21703 | 0.21775 | | 128 | 0.21682 | 0.21703 | 0.21775 |
| 16.0 | 0.21748 | 0.21755 | 0.21819 | | 160 | 0.21748 | 0.21755 | 0.21819 |
Лучший вариант со спуском за 10 партий — 0.21676 (9.6 / 2.4). Для «чистого Elo» Лучший вариант со спуском за 10 партий — 0.21676 (96 / 24). Для «чистого Elo»
в сравнении 7.3 взяты K = 9.6 / 1.6 / 20. в сравнении 7.3 взяты K = 96 / 16 / 20. Чистый Elo не использует миры, поэтому поле 2×3
на этот этап не повлияло.
**Этап 2. Веса отрыва** при K этапа 1: 162 комбинации (`w_N` ∈ {0, 0.25, 0.5}; **Этап 2. Веса отрыва** при K этапа 1: 162 комбинации (`w_N` ∈ {0, 0.25, 0.5};
`w_τ`, `w_o`, `w_w` ∈ {0, 0.5, 1}; близость да/нет); сценарии «сигнал», «клубы», «рост». `w_τ`, `w_o`, `w_w` ∈ {0, 0.5, 1}; близость да/нет); сценарии «сигнал», «клубы», «рост».
@@ -518,13 +530,13 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
| Brier | Brier «шум» | `w_N` | `w_τ` | `w_o` | `w_w` | близость | | Brier | Brier «шум» | `w_N` | `w_τ` | `w_o` | `w_w` | близость |
|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|
| 0.21801 | 0.20992 | 0.25 | 0 | 0 | 0.5 | да | | 0.21797 | 0.20991 | 0.25 | 0 | 0 | 0.5 | да |
| 0.21804 | 0.20986 | 0.5 | 0 | 0 | 0.5 | да | | 0.21801 | 0.20985 | 0.5 | 0 | 0 | 0.5 | да |
| 0.21807 | 0.21010 | 0 | 0 | 0 | 0.5 | да | | 0.21802 | 0.21009 | 0 | 0 | 0 | 0.5 | да |
| 0.21807 | 0.20992 | 0.5 | 0.5 | 0 | 0 | да | | 0.21804 | 0.21005 | 0.25 | 0.5 | 0 | 0.5 | да |
| 0.21807 | 0.21005 | 0.25 | 0.5 | 0 | 0.5 | да | | 0.21804 | 0.20994 | 0.25 | 0 | 0 | 0.5 | нет |
| … | | | | | | | | … | | | | | | |
| 0.22000 | | | | | | худшая комбинация | | 0.21995 | | | | | | худшая комбинация |
Лучшие комбинации выигрывают у отсутствия множителя около 0.0005, худшая проигрывает Лучшие комбинации выигрывают у отсутствия множителя около 0.0005, худшая проигрывает
0.0015. Оптимум очень пологий: первые десять вариантов умещаются в 0.0001. 0.0015. Оптимум очень пологий: первые десять вариантов умещаются в 0.0001.
@@ -541,14 +553,14 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
| `K_max` | `K_min` | Brier (сигнальные) | Brier «шум» | Brier с поправкой на автокорреляцию | | `K_max` | `K_min` | Brier (сигнальные) | Brier «шум» | Brier с поправкой на автокорреляцию |
|---|---|---|---|---| |---|---|---|---|---|
| 4.8 | 1.2 | **0.21760** | 0.21063 | 0.21757 | | 48 | 12 | **0.21759** | 0.21063 | 0.21755 |
| 4.8 | 1.6 | 0.21780 | 0.21049 | 0.21775 | | 48 | 16 | 0.21778 | 0.21049 | 0.21773 |
| 6.4 | 1.2 | 0.21781 | 0.21027 | 0.21773 | | 64 | 12 | 0.21779 | 0.21026 | 0.21771 |
| **6.4** | **1.6** | 0.21799 | **0.21022** | 0.21790 | | **64** | **16** | 0.21797 | **0.21022** | 0.21788 |
| 8.0 | 1.6 | 0.21840 | 0.21028 | 0.21828 | | 80 | 16 | 0.21838 | 0.21028 | 0.21826 |
- **Выбор 6.4 / 1.6.** Против чистого Elo он даёт −0.00047 на сигнальных сценариях и - **Выбор 64 / 16.** Против чистого Elo он даёт −0.00049 на сигнальных сценариях и
+0.00005 на «шуме». Вариант 4.8 / 1.2 выигрывает больше (−0.00086), но на «шуме» +0.00005 на «шуме». Вариант 48 / 12 выигрывает больше (−0.00087), но на «шуме»
проигрывает +0.00046. Выбран вариант, который не теряет, если гипотеза ТЗ о значении проигрывает +0.00046. Выбран вариант, который не теряет, если гипотеза ТЗ о значении
отрыва не подтвердится. отрыва не подтвердится.
- **Поправка на автокорреляцию** (FiveThirtyEight: фаворит закономерно побеждает с большим - **Поправка на автокорреляцию** (FiveThirtyEight: фаворит закономерно побеждает с большим
@@ -556,20 +568,20 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
включена: лишняя сложность для ручного расчёта при нулевом эффекте. включена: лишняя сложность для ручного расчёта при нулевом эффекте.
**Этап 4. Чувствительность.** Меняется один параметр, остальные — как в предложении **Этап 4. Чувствительность.** Меняется один параметр, остальные — как в предложении
(Brier 0.21799, «шум» 0.21022). ρ@10 здесь — среднее по трём сценариям, включая «клубы», (Brier 0.21797, «шум» 0.21022). ρ@10 здесь — среднее по трём сценариям, включая «клубы»,
поэтому оно ниже, чем в 7.3. поэтому оно ниже, чем в 7.3.
| Параметр | Значение | Brier (сигнальные) | ρ@10 | Brier «шум» | | Параметр | Значение | 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_N` | 0 / 0.25 / **0.5** / 1.0 | 0.21803 / 0.21796 / **0.21797** / 0.21819 | 0.663 / 0.668 / **0.674** / 0.680 | 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_τ` | 0 / 0.25 / 0.5 / **1.0** / 2.0 | 0.21771 / 0.21775 / 0.21781 / **0.21797** / 0.21826 | 0.672 / 0.672 / 0.673 / **0.674** / 0.667 | 0.20988 / 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_o` | 0 / 0.25 / **0.5** / 1.0 / 2.0 | 0.21767 / 0.21778 / **0.21797** / 0.21838 / 0.21892 | 0.666 / 0.668 / **0.674** / 0.674 / 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 | | `w_w` | 0 / 0.25 / **0.5** / 1.0 / 2.0 | 0.21781 / 0.21785 / **0.21797** / 0.21827 / 0.21873 | 0.667 / 0.671 / **0.674** / 0.673 / 0.669 | 0.21003 / 0.21011 / **0.21022** / 0.21037 / 0.21054 |
| `c` | нет / **предложенная** / вдвое сильнее | 0.21801 / **0.21799** / 0.21801 | 0.674 / **0.673** / 0.671 | 0.21016 / **0.21022** / 0.21031 | | `c` | нет / **предложенная** / вдвое сильнее | 0.21799 / **0.21797** / 0.21799 | 0.673 / **0.674** / 0.672 | 0.21016 / **0.21022** / 0.21031 |
Итог: Итог:
- Веса в диапазоне 0–1 безопасны: изменение вдвое сдвигает Brier не больше чем на 0.00042 - Веса в диапазоне 0–1 безопасны: изменение вдвое сдвигает Brier не больше чем на 0.00041
(`w_o` 0.5 → 1.0). Вред начинается с 2.0 — поэтому ни один вес не выше 1. (`w_o` 0.5 → 1.0). Вред начинается с 2.0 — поэтому ни один вес не выше 1.
- Вес размера стола полезен: от 0 до 0.5 растёт ρ@10 и падает Brier на «шуме». - Вес размера стола полезен: от 0 до 0.5 растёт ρ@10 и падает Brier на «шуме».
- Близость по типу победы в симуляции нейтральна (±0.00002). - Близость по типу победы в симуляции нейтральна (±0.00002).
@@ -578,7 +590,7 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
- **Доказывает:** формулы корректны и устойчивы, не раздувают рейтинг, быстро сходятся, - **Доказывает:** формулы корректны и устойчивы, не раздувают рейтинг, быстро сходятся,
правильно «сшивают» группы разной силы. Выбранные веса лежат в пологой области: изменение правильно «сшивают» группы разной силы. Выбранные веса лежат в пологой области: изменение
любого веса вдвое в любую сторону сдвигает Brier не больше чем на 0.00042. Если признаки любого веса вдвое в любую сторону сдвигает Brier не больше чем на 0.00041. Если признаки
партии окажутся бесполезны, потеря мала. партии окажутся бесполезны, потеря мала.
- **Не доказывает:** - **Не доказывает:**
- что в *реальных* партиях Forbidden Stars ранний конец, отрыв по целям и мирам связаны - что в *реальных* партиях Forbidden Stars ранний конец, отрыв по целям и мирам связаны
@@ -608,13 +620,19 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
| `matches` | `end_round` | int NULL, `1 ≤ end_round ≤ R_max` | раунд, в котором партия закончилась | | `matches` | `end_round` | int NULL, `1 ≤ end_round ≤ R_max` | раунд, в котором партия закончилась |
| `match_participants` | `objectives` | int NULL, ≥ 0 | маркеры целей на конец партии | | `match_participants` | `objectives` | int NULL, ≥ 0 | маркеры целей на конец партии |
| `match_participants` | `worlds` | int NULL, ≥ 0 | дружественные миры на конец партии; у выбывшего 0 | | `match_participants` | `worlds` | int NULL, ≥ 0 | дружественные миры на конец партии; у выбывшего 0 |
| `matches.win_reason` | + `last_standing` | CHECK | новая причина победы | | `matches.win_reason` | + `last_standing` | CHECK | новая причина победы (раздел 9) |
`R_max` в партии не хранится, а вычисляется: `9`, если `nine_rounds_rule` и `N ≥ 5`, иначе `8`. `R_max` в партии не хранится, а вычисляется: `9`, если `nine_rounds_rule` и `N ≥ 5`, иначе `8`.
Число участников может поменяться при правке партии, а снимок правила — нет. Число участников может поменяться при правке партии, а снимок правила — нет.
Миграция идемпотентна, как `0003`: ALTER только при отсутствии столбца, `render_as_batch` Миграция идемпотентна, как `0003`: ALTER только при отсутствии столбца, `render_as_batch`
для CHECK. **Бэкфилл не нужен:** NULL — это «нет данных», и формулы его учитывают (4.8). для CHECK. Новые столбцы задним числом не заполняются: NULL — это «нет данных», и формулы
его учитывают (4.8).
**Бэкфилл нужен только для `last_standing`.** У завершённых партий, где невыбывший участник
ровно один, миграция ставит `win_reason = last_standing`. Тогда старые партии не нарушают
правило из раздела «Ввод» при правке, а в рейтинге считаются так же, как новые: отрыв
победителя по целям = 1 (4.7). Бэкфилл идемпотентен: повторный запуск ничего не меняет.
### Ввод ### Ввод
@@ -626,6 +644,25 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
и т.п.) лучше показывать предупреждением, а не отказом. и т.п.) лучше показывать предупреждением, а не отказом.
- Настройки группы: галочка рядом с дополнениями (`PUT /groups/{id}/expansions` или - Настройки группы: галочка рядом с дополнениями (`PUT /groups/{id}/expansions` или
отдельный `PATCH`). отдельный `PATCH`).
- **Причина `last_standing`** (решение владельца):
- правило — «невыбывший участник ровно один» ⇔ `win_reason = last_standing`;
- в форме завершения и в админской правке причина проставляется автоматически, как только
все участники, кроме одного, отмечены выбывшими. Выбор причины при этом заблокирован;
- вернули второго невыбывшего — причина сбрасывается, её нужно выбрать заново;
- в выпадающем списке причин `last_standing` нет: выбрать её вручную нельзя;
- сервер проверяет то же правило в `finish_match` и при правке результатов: несовпадение —
ошибка валидации. Черновик формы (`PUT /matches/{id}/finish-draft`) правило не проверяет,
он хранит незаконченный ввод.
### Отображение
- Рейтинг показывается **целым числом**; в расчёте значения хранятся без округления, иначе
ошибка округления накапливается по цепочке партий.
- Общий топ (`OverallStatsPage.tsx`): столбец «Поб» и сортировка по победам убираются, чтобы
четырёхзначный рейтинг поместился в строку; «Очки» переименовываются в «Рейтинг».
Для единообразия — «Очки (рейтинг)» в `ProfileStatsCard.tsx` и заголовок «Очки игроков
(рейтинг)» в `HelpPage.tsx`.
- Подпись причины `last_standing` в карточке партии и истории — «последний выживший».
### Расчёт ### Расчёт
@@ -647,12 +684,13 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
### Что ломается для пользователей ### Что ломается для пользователей
- Числа рейтинга у всех меняются. Шкала та же (около 50), но это другая величина: не - Числа рейтинга у всех меняются: шкала другая (около 1500 вместо около 50), и это другая
«средний процент очков», а сила относительно соперников. величина — не «средний процент очков», а сила относительно соперников.
- Порядок в топе может измениться — в первую очередь у тех, кто играл в основном со слабыми - Порядок в топе может измениться — в первую очередь у тех, кто играл в основном со слабыми
или сильными соперниками. или сильными соперниками.
- Рейтинг новичка после одной партии меняется заметно сильнее, чем раньше: +3.2 за обычную - Рейтинг новичка после одной партии меняется заметно сильнее, чем раньше: +32 за обычную
победу над равным, до +6.4 за разгром (теоретический предел — 12.8). победу над равным, до +64 за разгром (теоретический предел — 128).
- В общем топе пропадает столбец побед (win rate остаётся).
- **Предложение:** разовое уведомление всем игрокам и короткое пояснение «как считается - **Предложение:** разовое уведомление всем игрокам и короткое пояснение «как считается
рейтинг» на странице топа. рейтинг» на странице топа.
@@ -669,25 +707,27 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
Коэффициенты — константы в одном модуле (как сейчас `scoring.py`): калибровка — это правка Коэффициенты — константы в одном модуле (как сейчас `scoring.py`): калибровка — это правка
констант и пересчёт, без миграций. констант и пересчёт, без миграций.
### CLAUDE.md ### Справка
Раздел «Статистика» уже расходится с кодом: там `MIN_GAMES=3` и формула без сглаживания. Формула текущего рейтинга и пороги продублированы текстом на странице справки
При реализации #23 его нужно переписать под новую систему. (`frontend/src/pages/HelpPage.tsx`). При реализации #23 её нужно переписать под новую
систему: шкала, от чего зависит изменение рейтинга, `MIN_GAMES`, причина `last_standing`.
## 9. Открытые вопросы ## 9. Решения владельца
На утверждение владельцу: Первая версия документа выносила шесть вопросов на решение. Ответы владельца —
[комментарий к PR #65](https://gitea.arseniev.info/NotBigGhost/ForbiddenStarsApp/pulls/65#issuecomment-3466)
(2026-09-14). Там же и в [#22](https://gitea.arseniev.info/NotBigGhost/ForbiddenStarsApp/issues/22#issuecomment-3380)
уточнено поле дуэли: 2×3, а не 2×2 — исправлено в разделе 2, примерах и симуляции.
1. **Шкала отображения.** Предлагается «Elo/10»: старт 50, как сейчас. Альтернатива — | # | Вопрос | Решение | Что изменилось в документе |
классические 1500 (`10·R + 1000`); она привычнее шахматистам, но ломает визуальную |---|---|---|---|
преемственность топа. Формулы от выбора не зависят. | 1 | Шкала отображения: «Elo/10» (старт 50) или классические 1500 | **1500.** В общем топе убрать столбец побед, «Очки» переименовать в «Рейтинг» | 4.1 и все числа примеров и таблиц; раздел 8, «Отображение» |
2. **Коэффициенты близости** по типам победы (1 / 0.85 / 0.7 / 0.6 / 1) — экспертная оценка | 2 | Коэффициенты близости по типам победы (1 / 0.85 / 0.7 / 0.6 / 1) | **Согласованы** | 4.10 — отмечены как утверждённые |
(7.5). Согласны ли с порядком и шагом? | 3 | Новая причина победы `last_standing` | **Добавить.** Ставится автоматически, когда невыбывший ровно один, и не меняется, пока невыбывших меньше двух; в списке выбора её нет | раздел 2; раздел 8 — «Данные» (бэкфилл) и «Ввод» |
3. **Новая причина победы `last_standing`** (все соперники устранены) — добавлять? | 4 | Ввод миров на конец партии | **Оставить** | без изменений: `w_w = 0.5` |
Сейчас такая партия записывается как `objectives`. | 5 | Затухание за неактивность | **Не добавлять** | без изменений |
4. **Ввод миров на конец партии** — самое затратное новое поле: миры нужно пересчитать на | 6 | Минимум партий для топа | **Оставить 10** | без изменений: `MIN_GAMES = 10` |
поле. Если это неудобно за столом, можно оставить только раунд и цели: вес миров перейдёт
к целям (перебор показывает, что система остаётся работоспособной и без `w_w`). Открытых вопросов по предложению не осталось. Калибровка коэффициентов на реальных данных —
5. **Затухание за неактивность** (рейтинг долго не игравшего игрока) в предложение не входит. после внедрения #23 (раздел 8, «Калибровка»).
Нужно ли оно?
6. **Минимум партий для топа.** Сохраняется 10 — нужно ли менять?
+76 -54
View File
@@ -6,6 +6,7 @@
1. Формулы документа в коде (раздел «Эталонная реализация»). #23 может сверять 1. Формулы документа в коде (раздел «Эталонная реализация»). #23 может сверять
с ними свою реализацию. с ними свою реализацию.
2. Пошаговые примеры документа с assert на числа: документ и код не разъедутся. 2. Пошаговые примеры документа с assert на числа: документ и код не разъедутся.
Плюс проверка, что шкала (1500 или прежние 50) не влияет на качество прогноза.
3. Синтетическая лига: игроки со скрытой «истинной» силой, партии на 2–6 человек. 3. Синтетическая лига: игроки со скрытой «истинной» силой, партии на 2–6 человек.
Детали партии (раунд, цели, миры, тип победы) выводятся из отрыва Детали партии (раунд, цели, миры, тип победы) выводятся из отрыва
по производительности. На одних и тех же партиях сравниваются текущий League по производительности. На одних и тех же партиях сравниваются текущий League
@@ -14,8 +15,8 @@
Только стандартная библиотека и фиксированные seed — вывод воспроизводим. Только стандартная библиотека и фиксированные seed — вывод воспроизводим.
python docs/rating/simulate.py # примеры + сравнение систем (≈20 с) python docs/rating/simulate.py # примеры + сравнение систем (≈1.5 мин)
python docs/rating/simulate.py --grid # примеры + перебор K и весов (≈5 мин) python docs/rating/simulate.py --grid # примеры + перебор K и весов (≈7 мин)
""" """
from __future__ import annotations from __future__ import annotations
@@ -29,8 +30,8 @@ from itertools import combinations
# ═══ Правила игры ═══════════════════════════════════════════════════════════════ # ═══ Правила игры ═══════════════════════════════════════════════════════════════
# Размер поля в тайлах по числу игроков (6 игроков — 4×5, ответ владельца в #22). # Размер поля в тайлах по числу игроков (дуэль — 2×3, 6 игроков — 4×5; уточнения владельца в #22).
BOARD_TILES = {2: 4, 3: 9, 4: 12, 5: 16, 6: 20} BOARD_TILES = {2: 6, 3: 9, 4: 12, 5: 16, 6: 20}
WORLDS_PER_TILE = 2.2 WORLDS_PER_TILE = 2.2
BASE_ROUNDS = 8 BASE_ROUNDS = 8
# Хоумрул группы: при 5–6 игроках играется 9 раундов. # Хоумрул группы: при 5–6 игроках играется 9 раундов.
@@ -73,11 +74,11 @@ class Match:
@dataclass(frozen=True) @dataclass(frozen=True)
class Params: class Params:
# Шкала «Elo / 10»: старт 50, разница 40 = шансы 10:1. Классический Elo — R·10 + 1000. # Классическая шкала Elo (решение владельца в PR #65): старт 1500, разница 400 = шансы 10:1.
r0: float = 50.0 # стартовый рейтинг r0: float = 1500.0 # стартовый рейтинг
d: float = 40.0 # масштаб логистики d: float = 400.0 # масштаб логистики
k_max: float = 6.4 # K новичка (0 партий) k_max: float = 64.0 # K новичка (0 партий)
k_min: float = 1.6 # K опытного игрока k_min: float = 16.0 # K опытного игрока
k_games: int = 20 # за сколько партий K линейно спускается от k_max к k_min k_games: int = 20 # за сколько партий K линейно спускается от k_max к k_min
w_table: float = 0.0 # вес размера стола (треб. 2) w_table: float = 0.0 # вес размера стола (треб. 2)
w_tempo: float = 0.0 # вес темпа победы (треб. 1) w_tempo: float = 0.0 # вес темпа победы (треб. 1)
@@ -155,8 +156,8 @@ def pair_multiplier(
add += p.w_worlds * (wor - p.mu_worlds) add += p.w_worlds * (wor - p.mu_worlds)
if p.autocorr and add > 1.0 and not tie: if p.autocorr and add > 1.0 and not tie:
# Поправка FiveThirtyEight (2.2 / (0.001·ΔElo + 2.2)) в шкале Elo/10. # Поправка FiveThirtyEight: 2.2 / (0.001·ΔElo + 2.2).
kappa = 2.2 / (0.01 * (r_a - r_b) + 2.2) kappa = 2.2 / (0.001 * (r_a - r_b) + 2.2)
parts["kappa"] = kappa parts["kappa"] = kappa
add = 1.0 + (add - 1.0) * kappa add = 1.0 + (add - 1.0) * kappa
parts["additive"] = add parts["additive"] = add
@@ -202,6 +203,8 @@ class LeaguePoints:
"""Текущая система (backend/app/services/scoring.py): сглаженное среднее очков за место.""" """Текущая система (backend/app/services/scoring.py): сглаженное среднее очков за место."""
probabilistic = False probabilistic = False
# Рейтинг в шкале 0–100; |ΔR| сравнивается с Elo в пересчёте R_Elo = 10·score + 1000.
move_scale = 10.0
PRIOR_GAMES = 10 PRIOR_GAMES = 10
PRIOR_MEAN = 0.5 PRIOR_MEAN = 0.5
@@ -232,6 +235,7 @@ class Elo:
"""Парный многопользовательский Elo; с нулевыми весами — «чистый» Elo.""" """Парный многопользовательский Elo; с нулевыми весами — «чистый» Elo."""
probabilistic = True probabilistic = True
move_scale = 1.0
def __init__(self, p: Params) -> None: def __init__(self, p: Params) -> None:
self.p = p self.p = p
@@ -253,8 +257,8 @@ class Elo:
SIGMA_SKILL = 150.0 # разброс «истинной» силы игроков SIGMA_SKILL = 150.0 # разброс «истинной» силы игроков
SIGMA_PERF = 225.0 # шум производительности в отдельной партии (кубы, карты, ошибки) SIGMA_PERF = 225.0 # шум производительности в отдельной партии (кубы, карты, ошибки)
# Истинная сила в шкале рейтинга: Φ(Δθ/(σ√2)) ≈ логистика с масштабом d=40 при ΔR ≈ 0.093·Δθ. # Истинная сила в шкале рейтинга: Φ(Δθ/(σ√2)) ≈ логистика с масштабом d=400 при ΔR ≈ 0.93·Δθ.
SKILL_TO_RATING = 1.702 * 40 / (math.log(10) * SIGMA_PERF * math.sqrt(2)) SKILL_TO_RATING = 1.702 * 400 / (math.log(10) * SIGMA_PERF * math.sqrt(2))
TABLE_SIZES = ((2, 0.45), (3, 0.25), (4, 0.20), (5, 0.07), (6, 0.03)) TABLE_SIZES = ((2, 0.45), (3, 0.25), (4, 0.20), (5, 0.07), (6, 0.03))
ELIM_Z = 2.3 # отставание (в σ), при котором игрок может выбыть ELIM_Z = 2.3 # отставание (в σ), при котором игрок может выбыть
ELIM_P = 0.35 # вероятность выбывания при таком отставании ELIM_P = 0.35 # вероятность выбывания при таком отставании
@@ -472,7 +476,7 @@ def evaluate(system, skill: dict[str, float], matches: list[Match], feed=None) -
games[s.player] = games.get(s.player, 0) + 1 games[s.player] = games.get(s.player, 0) + 1
gp = games[s.player] gp = games[s.player]
if gp > 20: if gp > 20:
abs_moves.append(abs(delta[s.player])) abs_moves.append(abs(delta[s.player]) * system.move_scale)
if gp in at_k: if gp in at_k:
at_k[gp][s.player] = system.rating(s.player) at_k[gp][s.player] = system.rating(s.player)
played = [pl for pl in skill if games.get(pl, 0) >= 10] played = [pl for pl in skill if games.get(pl, 0) >= 10]
@@ -530,7 +534,7 @@ def summarize(rows: list[dict]) -> dict[str, tuple[float, float]]:
# ═══ Коэффициенты ═════════════════════════════════════════════════════════════ # ═══ Коэффициенты ═════════════════════════════════════════════════════════════
PLAIN = Params(k_max=9.6, k_min=1.6) # чистый Elo со своими лучшими K (перебор, этап 1) PLAIN = Params(k_max=96.0, k_min=16.0) # чистый Elo со своими лучшими K (перебор, этап 1)
CLOSENESS = ( CLOSENESS = (
("objectives", 1.0), ("objectives", 1.0),
("worlds", 0.85), ("worlds", 0.85),
@@ -564,67 +568,67 @@ def examples() -> list[tuple[str, str, dict, dict, Match]]:
five = [Seat("A", 1), Seat("B", 2), Seat("C", 3), Seat("D", 4), Seat("E", 5)] five = [Seat("A", 1), Seat("B", 2), Seat("C", 3), Seat("D", 4), Seat("E", 5)]
six = [Seat(x, i + 1) for i, x in enumerate("ABCDEF")] six = [Seat(x, i + 1) for i, x in enumerate("ABCDEF")]
return [ return [
("1a", "Дуэль 60 против 40: побеждает сильный", ("1a", "Дуэль 1600 против 1400: побеждает сильный",
{"A": 60, "B": 40}, _vets("A", "B"), duel("A", "B", win_reason="objectives")), {"A": 1600, "B": 1400}, _vets("A", "B"), duel("A", "B", win_reason="objectives")),
("1b", "Дуэль 60 против 40: побеждает слабый", ("1b", "Дуэль 1600 против 1400: побеждает слабый",
{"A": 60, "B": 40}, _vets("A", "B"), duel("B", "A", win_reason="objectives")), {"A": 1600, "B": 1400}, _vets("A", "B"), duel("B", "A", win_reason="objectives")),
("2a", "Быстрая победа: 3-й раунд", ("2a", "Быстрая победа: 3-й раунд",
{"A": 50, "B": 50}, _vets("A", "B"), {"A": 1500, "B": 1500}, _vets("A", "B"),
Match([Seat("A", 1, 2, 5), Seat("B", 2, 1, 4)], "objectives", round=3)), Match([Seat("A", 1, 2, 5), Seat("B", 2, 1, 4)], "objectives", round=3)),
("2b", "Медленная победа: 8-й раунд", ("2b", "Медленная победа: 8-й раунд",
{"A": 50, "B": 50}, _vets("A", "B"), {"A": 1500, "B": 1500}, _vets("A", "B"),
Match([Seat("A", 1, 2, 5), Seat("B", 2, 1, 4)], "objectives", round=8)), Match([Seat("A", 1, 2, 5), Seat("B", 2, 1, 4)], "objectives", round=8)),
("3a", "Первое место в дуэли", ("3a", "Первое место в дуэли",
{"A": 50, "B": 50}, _vets("A", "B"), duel("A", "B", win_reason="objectives")), {"A": 1500, "B": 1500}, _vets("A", "B"), duel("A", "B", win_reason="objectives")),
("3b", "Стол на 5: все места", ("3b", "Стол на 5: все места",
dict.fromkeys("ABCDE", 50), _vets(*"ABCDE"), Match(five, "objectives")), dict.fromkeys("ABCDE", 1500), _vets(*"ABCDE"), Match(five, "objectives")),
("4a", "Тип победы без деталей: по мирам", ("4a", "Тип победы без деталей: по мирам",
{"A": 50, "B": 50}, _vets("A", "B"), duel("A", "B", win_reason="worlds")), {"A": 1500, "B": 1500}, _vets("A", "B"), duel("A", "B", win_reason="worlds")),
("4b", "Тип победы без деталей: по пластику", ("4b", "Тип победы без деталей: по пластику",
{"A": 50, "B": 50}, _vets("A", "B"), duel("A", "B", win_reason="plastic")), {"A": 1500, "B": 1500}, _vets("A", "B"), duel("A", "B", win_reason="plastic")),
("4c", "Тип победы без деталей: по ресурсам", ("4c", "Тип победы без деталей: по ресурсам",
{"A": 50, "B": 50}, _vets("A", "B"), duel("A", "B", win_reason="resources")), {"A": 1500, "B": 1500}, _vets("A", "B"), duel("A", "B", win_reason="resources")),
("4d", "Самая близкая полная партия: по мирам на 8-м раунде, 2:2 цели, 6:5 миров", ("4d", "Самая близкая полная партия: по мирам на 8-м раунде, 2:2 цели, 6:5 миров",
{"A": 50, "B": 50}, _vets("A", "B"), {"A": 1500, "B": 1500}, _vets("A", "B"),
Match([Seat("A", 1, 2, 6), Seat("B", 2, 2, 5)], "worlds", round=8)), Match([Seat("A", 1, 2, 6), Seat("B", 2, 2, 5)], "worlds", round=8)),
("4e", "Разгром: 3-й раунд, 2:0 цели, 8:2 миров", ("4e", "Разгром: 3-й раунд, 2:0 цели, 8:2 миров",
{"A": 50, "B": 50}, _vets("A", "B"), {"A": 1500, "B": 1500}, _vets("A", "B"),
Match([Seat("A", 1, 2, 8), Seat("B", 2, 0, 2)], "objectives", round=3)), Match([Seat("A", 1, 2, 8), Seat("B", 2, 0, 2)], "objectives", round=3)),
("5", "Стол на 4: ничья выбывших, раунд 7", ("5", "Стол на 4: ничья выбывших, раунд 7",
{"A": 55, "B": 50, "C": 48, "D": 45}, _vets(*"ABCD"), {"A": 1550, "B": 1500, "C": 1480, "D": 1450}, _vets(*"ABCD"),
Match( Match(
[Seat("A", 1, 4, 8), Seat("B", 2, 3, 7), [Seat("A", 1, 4, 8), Seat("B", 2, 3, 7),
Seat("C", 3, 1, 0, eliminated=True), Seat("D", 3, 0, 0, eliminated=True)], Seat("C", 3, 1, 0, eliminated=True), Seat("D", 3, 0, 0, eliminated=True)],
"objectives", round=7, "objectives", round=7,
)), )),
("6a", "Стол на 6, конец на 8-м раунде, хоумрул 9 раундов включён", ("6a", "Стол на 6, конец на 8-м раунде, хоумрул 9 раундов включён",
dict.fromkeys("ABCDEF", 50), _vets(*"ABCDEF"), dict.fromkeys("ABCDEF", 1500), _vets(*"ABCDEF"),
Match(six, "objectives", round=8, nine_rounds=True)), Match(six, "objectives", round=8, nine_rounds=True)),
("6b", "Стол на 6, конец на 8-м раунде, хоумрул выключен", ("6b", "Стол на 6, конец на 8-м раунде, хоумрул выключен",
dict.fromkeys("ABCDEF", 50), _vets(*"ABCDEF"), dict.fromkeys("ABCDEF", 1500), _vets(*"ABCDEF"),
Match(six, "objectives", round=8, nine_rounds=False)), Match(six, "objectives", round=8, nine_rounds=False)),
("7", "Новичок (0 партий) побеждает ветерана, оба 50", ("7", "Новичок (0 партий) побеждает ветерана, оба 1500",
{"A": 50, "B": 50}, {"A": 0, "B": VETERAN}, duel("A", "B", win_reason="objectives")), {"A": 1500, "B": 1500}, {"A": 0, "B": VETERAN}, duel("A", "B", win_reason="objectives")),
] ]
# Изменения рейтинга в примерах (округление до 0.001) — те же числа стоят в документе. # Изменения рейтинга в примерах (округление до 0.01) — те же числа стоят в документе.
EXPECTED: dict[str, dict[str, float]] = { EXPECTED: dict[str, dict[str, float]] = {
"1a": {"A": 0.384, "B": -0.384}, "1a": {"A": 3.84, "B": -3.84},
"1b": {"B": 1.216, "A": -1.216}, "1b": {"B": 12.16, "A": -12.16},
"2a": {"A": 1.148, "B": -1.148}, "2a": {"A": 11.18, "B": -11.18},
"2b": {"A": 0.577, "B": -0.577}, "2b": {"A": 5.46, "B": -5.46},
"3a": {"A": 0.8, "B": -0.8}, "3a": {"A": 8.0, "B": -8.0},
"3b": {"A": 1.1, "B": 0.55, "C": 0.0, "D": -0.55, "E": -1.1}, "3b": {"A": 11.0, "B": 5.5, "C": 0.0, "D": -5.5, "E": -11.0},
"4a": {"A": 0.68, "B": -0.68}, "4a": {"A": 6.8, "B": -6.8},
"4b": {"A": 0.56, "B": -0.56}, "4b": {"A": 5.6, "B": -5.6},
"4c": {"A": 0.48, "B": -0.48}, "4c": {"A": 4.8, "B": -4.8},
"4d": {"A": 0.34, "B": -0.34}, "4d": {"A": 3.4, "B": -3.4},
"4e": {"A": 1.6, "B": -1.6}, "4e": {"A": 16.0, "B": -16.0},
"5": {"A": 0.927, "B": 0.585, "C": -0.778, "D": -0.735}, "5": {"A": 9.27, "B": 5.85, "C": -7.78, "D": -7.35},
"6a": {"A": 1.2, "B": 0.72, "C": 0.24, "D": -0.24, "E": -0.72, "F": -1.2}, "6a": {"A": 12.0, "B": 7.2, "C": 2.4, "D": -2.4, "E": -7.2, "F": -12.0},
"6b": {"A": 1.029, "B": 0.754, "C": 0.274, "D": -0.206, "E": -0.686, "F": -1.166}, "6b": {"A": 10.29, "B": 7.54, "C": 2.74, "D": -2.06, "E": -6.86, "F": -11.66},
"7": {"A": 3.2, "B": -0.8}, "7": {"A": 32.0, "B": -8.0},
} }
@@ -632,7 +636,7 @@ def run_examples(p: Params = PROPOSED, verbose: bool = True) -> None:
for key, title, ratings, games, m in examples(): for key, title, ratings, games, m in examples():
trace: list = [] trace: list = []
delta = rate_match(ratings, games, m, p, trace) delta = rate_match(ratings, games, m, p, trace)
got = {pl: round(v, 3) for pl, v in delta.items()} got = {pl: round(v, 2) for pl, v in delta.items()}
if verbose: if verbose:
n = len(m.seats) n = len(m.seats)
print(f"\n### Пример {key}. {title}\n") print(f"\n### Пример {key}. {title}\n")
@@ -651,6 +655,23 @@ def run_examples(p: Params = PROPOSED, verbose: bool = True) -> None:
print("\nВсе примеры совпадают с документом.") print("\nВсе примеры совпадают с документом.")
def check_scale_invariance(seasons: int = 1) -> None:
"""Шкала «50 / 40» (R = 10·score + 1000, D и K ÷10) и шкала 1500 дают одинаковые
точность, Brier и ρ; изменения рейтинга различаются ровно в 10 раз (документ, 4.1)."""
for cfg in SCENARIOS.values():
for s in range(seasons):
skill, matches = generate_season(
cfg["seed"] + s, SEASON_MATCHES, cfg["informative"], cfg["clubs"], cfg["learning"]
)
for p in (PROPOSED, PLAIN):
small = replace(p, r0=(p.r0 - 1000) / 10, d=p.d / 10, k_max=p.k_max / 10, k_min=p.k_min / 10)
big, tiny = evaluate(Elo(p), skill, matches), evaluate(Elo(small), skill, matches)
for key in ("acc", "brier", "rho", *(f"rho@{k}" for k in CHECKPOINTS)):
assert math.isclose(big[key], tiny[key], abs_tol=1e-9), f"шкала: {key}"
assert math.isclose(big["move"], 10 * tiny["move"], rel_tol=1e-9), "шкала: |ΔR|"
print("Шкала 1500 и шкала 50 дают одинаковые точность, Brier и ρ.")
# ═══ Сценарии запуска ═════════════════════════════════════════════════════════ # ═══ Сценарии запуска ═════════════════════════════════════════════════════════
SEASON_MATCHES = 300 SEASON_MATCHES = 300
@@ -750,8 +771,8 @@ def grid(seasons: int) -> None:
print("|---|---|---|---|") print("|---|---|---|---|")
k_res = [] k_res = []
for k_games in (10, 20): for k_games in (10, 20):
for k_max in (4.8, 6.4, 9.6, 12.8, 16.0): for k_max in (48.0, 64.0, 96.0, 128.0, 160.0):
for k_min in (1.6, 2.4, 3.2): for k_min in (16.0, 24.0, 32.0):
p = replace(PLAIN, k_max=k_max, k_min=k_min, k_games=k_games) p = replace(PLAIN, k_max=k_max, k_min=k_min, k_games=k_games)
b = brier(p, SCENARIOS) b = brier(p, SCENARIOS)
k_res.append((b, k_max, k_min, k_games)) k_res.append((b, k_max, k_min, k_games))
@@ -790,8 +811,8 @@ def grid(seasons: int) -> None:
print("| k_max | k_min | k_games | autocorr | Brier (сигнальные) | Brier «шум» |") print("| k_max | k_min | k_games | autocorr | Brier (сигнальные) | Brier «шум» |")
print("|---|---|---|---|---|---|") print("|---|---|---|---|---|---|")
for k_games2 in (10, 20): for k_games2 in (10, 20):
for k_max2 in (4.8, 6.4, 8.0): for k_max2 in (48.0, 64.0, 80.0):
for k_min2 in (1.2, 1.6, 2.4): for k_min2 in (12.0, 16.0, 24.0):
for ac in (False, True): for ac in (False, True):
p = replace(PROPOSED, k_max=k_max2, k_min=k_min2, k_games=k_games2, autocorr=ac) p = replace(PROPOSED, k_max=k_max2, k_min=k_min2, k_games=k_games2, autocorr=ac)
print( print(
@@ -829,6 +850,7 @@ def main() -> None:
args = ap.parse_args() args = ap.parse_args()
print("# Примеры расчётов") print("# Примеры расчётов")
run_examples() run_examples()
check_scale_invariance()
if args.grid: if args.grid:
grid(args.grid_seasons) grid(args.grid_seasons)
return return