Рейтинг: выбывшие между собой не сравниваются

Решение владельца: в партии все выбывшие проиграли и одинаково слабы, миров у них
нет, поэтому пара двух выбывших в сумму не входит — ни S − E, ни множитель отрыва.
Пары с невыбывшими считаются как прежде, нормировка на N − 1 тоже: при равных
рейтингах результат не меняется. Слабый выбывший больше не получает рейтинг за счёт
сильных выбывших.

Эталон — флаг skip_eliminated_pairs у предложенной системы; документ: 4.3, 4.4,
4.9, пример 5, итоги 7.3 пересчитаны (сдвиг в третьем знаке), решение 8 в разделе 9;
справка. #91

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LqSoRj99iwVEH5U5fnZgsd
This commit is contained in:
2026-09-18 23:41:29 +03:00
co-authored by Claude Opus 5
parent 5035ee41e6
commit 63a2cddd5e
5 changed files with 85 additions and 35 deletions
+8 -3
View File
@@ -7,9 +7,10 @@
E_ab = 1 / (1 + 10^((R_b − R_a) / D)) ожидание по рейтингам ДО партии E_ab = 1 / (1 + 10^((R_b − R_a) / D)) ожидание по рейтингам ДО партии
S_ab = 1 / 0.5 / 0 выше / поровну / ниже S_ab = 1 / 0.5 / 0 выше / поровну / ниже
ΔR_i = K_i · G(N) / (N − 1) · Σ_j M_ij · (S_ij − E_ij) ΔR_i = K_i · G(N) / (N − 1) · Σ_j M_ij · (S_ij − E_ij)
K_i спускается от K_MAX у новичка до K_MIN за K_GAMES партий, G(N) — вес размера стола, Пары двух выбывших в сумму не входят (#91), остальные — все. K_i спускается от K_MAX
M — множитель отрыва (темп, цели, миры; близость по типу победы — только у пар у новичка до K_MIN за K_GAMES партий, G(N) — вес размера стола, M — множитель отрыва
с победителем). Недостающий признак партии подставляется типичным и не влияет на M. (темп, цели, миры; близость по типу победы — только у пар с победителем). Недостающий
признак партии подставляется типичным и не влияет на M.
Модуль — только константы и чистые функции без БД: калибровка на реальных данных — Модуль — только константы и чистые функции без БД: калибровка на реальных данных —
правка констант, пересчёт выполняется сам (рейтинг — функция упорядоченной истории). правка констант, пересчёт выполняется сам (рейтинг — функция упорядоченной истории).
@@ -168,6 +169,10 @@ def rate_match(
r = {s.user_id: ratings.get(s.user_id, R0) for s in m.seats} r = {s.user_id: ratings.get(s.user_id, R0) for s in m.seats}
k = {s.user_id: k_factor(games.get(s.user_id, 0)) for s in m.seats} k = {s.user_id: k_factor(games.get(s.user_id, 0)) for s in m.seats}
for a, b in combinations(m.seats, 2): for a, b in combinations(m.seats, 2):
if a.eliminated and b.eliminated:
# Выбывшие между собой не сравниваются: в этой партии все они проиграли, а миров
# у них нет (решение владельца, #91). Нормировка на N − 1 остаётся прежней.
continue
if a.place > b.place: if a.place > b.place:
a, b = b, a a, b = b, a
s_ab = 0.5 if a.place == b.place else 1.0 s_ab = 0.5 if a.place == b.place else 1.0
+23 -3
View File
@@ -68,7 +68,7 @@ EXAMPLES = [
_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", end_round=7, "objectives", end_round=7,
), ),
{A: 9.27, B: 5.85, C: -7.78, D: -7.35}), {A: 9.27, B: 5.85, C: -7.6, D: -7.53}),
("6a", dict.fromkeys(range(A, F + 1), 1500), _vets(*range(A, F + 1)), ("6a", dict.fromkeys(range(A, F + 1), 1500), _vets(*range(A, F + 1)),
RatedMatch(SIX, "objectives", end_round=8, nine_rounds_rule=True), RatedMatch(SIX, "objectives", end_round=8, nine_rounds_rule=True),
{A: 12.0, B: 7.2, C: 2.4, D: -2.4, E: -7.2, F: -12.0}), {A: 12.0, B: 7.2, C: 2.4, D: -2.4, E: -7.2, F: -12.0}),
@@ -101,6 +101,23 @@ def test_last_standing_counts_full_objective_gap():
assert standing - ordinary == pytest.approx(16 * 0.5 * 0.5) assert standing - ordinary == pytest.approx(16 * 0.5 * 0.5)
def test_eliminated_are_not_compared_with_each_other():
"""Выбывшие между собой не сравниваются (#91): слабый выбывший среди сильных не
получает рейтинг, а рейтинги прочих выбывших на его изменение не влияют."""
six = range(A, F + 1)
seats = (_seat(A, 1),) + tuple(_seat(u, 2, eliminated=True) for u in six if u != A)
match = RatedMatch(seats, "last_standing")
strong = {**dict.fromkeys(six, 1800), F: 1200}
delta = rate_match(strong, _vets(*six), match)[0]
assert delta[F] < 0
# Сильные выбывшие → слабые: у F и у победителя ничего не меняется от этого.
weak = {**dict.fromkeys(six, 1200), A: 1800}
again = rate_match(weak, _vets(*six), match)[0]
assert again[F] == pytest.approx(delta[F])
# Победителю сила соперников по-прежнему важна: против слабых он получает меньше.
assert again[A] < delta[A]
# ─── Сверка с эталоном ─────────────────────────────────────────────────────── # ─── Сверка с эталоном ───────────────────────────────────────────────────────
SIMULATE = Path(__file__).resolve().parents[2] / "docs" / "rating" / "simulate.py" SIMULATE = Path(__file__).resolve().parents[2] / "docs" / "rating" / "simulate.py"
@@ -147,6 +164,7 @@ def test_constants_match_reference(sim):
assert dict(p.closeness) == scoring.CLOSENESS assert dict(p.closeness) == scoring.CLOSENESS
assert sim.BOARD_TILES == scoring.BOARD_TILES assert sim.BOARD_TILES == scoring.BOARD_TILES
assert not p.autocorr assert not p.autocorr
assert p.skip_eliminated_pairs # выбывшие между собой не сравниваются (#91)
@pytest.mark.parametrize("scenario", ["сигнал", "клубы", "рост"]) @pytest.mark.parametrize("scenario", ["сигнал", "клубы", "рост"])
@@ -174,8 +192,8 @@ def test_replay_matches_reference_season(sim, scenario, stripped):
def test_monotone_and_zero_sum(sim): def test_monotone_and_zero_sum(sim):
"""Победитель без ничьей не теряет, последний без ничьей не получает; при равных K """Победитель без ничьей не теряет, последний без ничьей и любой выбывший не получают;
сумма изменений за партию — ноль.""" при равных K сумма изменений за партию — ноль."""
cfg = sim.SCENARIOS["сигнал"] cfg = sim.SCENARIOS["сигнал"]
_skill, matches = sim.generate_season(cfg["seed"] + 7, 200, True) _skill, matches = sim.generate_season(cfg["seed"] + 7, 200, True)
ids: dict[str, int] = {} ids: dict[str, int] = {}
@@ -188,6 +206,8 @@ def test_monotone_and_zero_sum(sim):
delta, _ = rate_match({}, veterans, rm) delta, _ = rate_match({}, veterans, rm)
places = [s.place for s in rm.seats] places = [s.place for s in rm.seats]
for s in rm.seats: for s in rm.seats:
if s.eliminated:
assert delta[s.user_id] < 0
if places.count(s.place) > 1: if places.count(s.place) > 1:
continue continue
if s.place == 1: if s.place == 1:
+42 -23
View File
@@ -26,9 +26,11 @@
влияет, но слабее темпа. влияет, но слабее темпа.
- **Шкала — классический Elo.** Старт 1500, разница 400 пунктов — шансы 10:1. Пример из - **Шкала — классический Elo.** Старт 1500, разница 400 пунктов — шансы 10:1. Пример из
задачи «60 против 40» в ней — 1600 против 1400. задачи «60 против 40» в ней — 1600 против 1400.
- **Монотонность.** Единоличный победитель никогда не теряет рейтинг, единоличное - **Выбывшие между собой не сравниваются.** Все они проиграли, а миров у них нет: пара двух
последнее место никогда его не приносит. При общем месте — ничьей за 1-е или нескольких выбывших в расчёт не входит. Сила соперников при этом важна, как и прежде (4.4).
выбывших — пара внутри ничьей может сдвинуть рейтинг в любую сторону (4.9). - **Монотонность.** Единоличный победитель и выбывшие всегда движутся в свою сторону: первый
не теряет рейтинг, вторые его не получают. Внутри ничьей невыбывших — за 1-е или
последнее место — пара может сдвинуть рейтинг в любую сторону (4.9).
- **Старая история.** Пересчитывается по тем же формулам: у партий без новых полей - **Старая история.** Пересчитывается по тем же формулам: у партий без новых полей
признаки берутся нейтральными. признаки берутся нейтральными.
@@ -160,7 +162,8 @@ E_{ab} = \frac{1}{1 + 10^{(R_b - R_a)/D}}
### 4.3. Фактический результат пары ### 4.3. Фактический результат пары
`S_ab = 1`, если `a` занял место выше `b`; `0.5`, если места равны; `0` — если ниже. `S_ab = 1`, если `a` занял место выше `b`; `0.5`, если места равны; `0` — если ниже.
Выбывшие делят последнее место, как и сейчас (`match_service._resolve_finish_places`). Выбывшие делят последнее место, как и сейчас (`match_service._resolve_finish_places`),
но между собой не сравниваются (4.4).
### 4.4. Изменение рейтинга ### 4.4. Изменение рейтинга
@@ -171,6 +174,14 @@ E_{ab} = \frac{1}{1 + 10^{(R_b - R_a)/D}}
Все рейтинги в формуле — **до** партии. Деление на `N − 1` приводит сумму по соперникам Все рейтинги в формуле — **до** партии. Деление на `N − 1` приводит сумму по соперникам
к «средней дуэли», размер стола затем добавляется явно через `G(N)`. к «средней дуэли», размер стола затем добавляется явно через `G(N)`.
**Выбывшие между собой не сравниваются** (решение владельца, #91): если `i` и `j` оба
выбыли, их пара в сумму не входит — ни `S − E`, ни множитель отрыва. В этой партии они
все проиграли, то есть оказались одинаково слабы, а миров у выбывших нет. Пары выбывшего
с невыбывшими, в том числе с победителем, считаются как обычно: сила соперников важна.
Нормировка на `N − 1` не меняется, поэтому при равных рейтингах результат прежний — такая
пара и раньше давала `S − E = 0`. Уходит только перекос, при котором слабый выбывший
получал рейтинг за счёт сильных выбывших.
### 4.5. Коэффициент K — скорость изменения ### 4.5. Коэффициент K — скорость изменения
```math ```math
@@ -237,7 +248,10 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
### 4.9. Свойства ### 4.9. Свойства
- **Монотонность.** 1-е место без ничьей даёт `S − E > 0` во всех парах, а `M > 0` — значит, - **Монотонность.** 1-е место без ничьей даёт `S − E > 0` во всех парах, а `M > 0` — значит,
рейтинг растёт. Последнее место без ничьей всегда уменьшает рейтинг. рейтинг растёт. Последнее место без ничьей всегда уменьшает рейтинг. Выбывший — тоже:
его пары с выбывшими не считаются, а каждому невыбывшему он проиграл. Внутри ничьей
невыбывших `S = 0.5`, и знак `S − E` зависит от рейтингов: сильный игрок, поделивший
1-е место со слабым, может потерять.
- **Сумма-ноль.** `M_ab = M_ba`, поэтому при равных K сумма изменений за партию равна нулю - **Сумма-ноль.** `M_ab = M_ba`, поэтому при равных K сумма изменений за партию равна нулю
и рейтинг не раздувается. Когда K разные (новичок и ветеран), сумма не нулевая — и рейтинг не раздувается. Когда K разные (новичок и ветеран), сумма не нулевая —
это сделано намеренно (пример 7). В симуляции среднее по лиге за 300 партий сдвигается это сделано намеренно (пример 7). В симуляции среднее по лиге за 300 партий сдвигается
@@ -353,16 +367,16 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
| A–D | 1 | 0.640 | 1 | 1 | 1 + 0.25 + 0.25 = **1.500** | 0.540 | | 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–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 | | 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 | — | — | — | — | оба выбыли — пара не считается (4.4) | 0 |
- `ΔR_A = 6.67 · (0.300 + 0.551 + 0.540)` = **+9.27** - `ΔR_A = 6.67 · (0.300 + 0.551 + 0.540)` = **+9.27**
- `ΔR_B = 6.67 · (−0.300 + 0.589 + 0.589)` = **+5.85** - `ΔR_B = 6.67 · (−0.300 + 0.589 + 0.589)` = **+5.85**
- `ΔR_C = 6.67 · (−0.551 − 0.589 − 0.027)` = **−7.78** - `ΔR_C = 6.67 · (−0.551 − 0.589)` = **−7.60**
- `ΔR_D = 6.67 · (−0.540 − 0.589 + 0.027)` = **−7.35** - `ΔR_D = 6.67 · (−0.540 − 0.589)` = **−7.53**
Итого: A и B близки друг к другу по целям и мирам, поэтому эта пара весит 0.7. Отрыв обоих Итого: A и B близки друг к другу по целям и мирам, поэтому эта пара весит 0.7. Отрыв обоих
от выбывших огромный — эти пары весят 1.25–1.5. C и D поделили место, но C сильнее по от выбывших огромный — эти пары весят 1.25–1.5. C и D между собой не сравниваются: оба
рейтингу, поэтому немного уступает D. проиграли всем невыбывшим. C теряет чуть больше, потому что от более сильного ждали большего.
### Пример 6. Стол на 6 и хоумрул 9 раундов ### Пример 6. Стол на 6 и хоумрул 9 раундов
@@ -436,7 +450,9 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
### 7.3. Результаты: 200 сезонов на сценарий ### 7.3. Результаты: 200 сезонов на сценарий
«Без новых полей» — предложенная система на той же истории, но без раунда, целей и миров: «Без новых полей» — предложенная система на той же истории, но без раунда, целей и миров:
так будет считаться история, накопленная до #23. так будет считаться история, накопленная до #23. Строки предложенной системы пересчитаны
с правилом «выбывшие между собой не сравниваются» (#91). Оно сдвинуло метрики лишь
в третьем знаке, у остальных систем цифры прежние.
**Сигнал** (потолок точности 0.6843) **Сигнал** (потолок точности 0.6843)
@@ -444,8 +460,8 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|
| League Points (сейчас) | 0.6683 | — | 0.913 | 0.660 | 0.769 | 0.854 | — | — | 6.91 | | 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 | 47.4 | 0.81 | 5.24 | | Elo, чистый | 0.6691 | 0.2089 | 0.911 | 0.656 | 0.770 | 0.852 | 47.4 | 0.81 | 5.24 |
| **Предложенная** | **0.6721** | **0.2077** | **0.929** | **0.711** | **0.807** | **0.881** | **41.9** | **0.95** | 5.95 | | **Предложенная** | **0.6721** | **0.2077** | **0.929** | **0.712** | **0.807** | **0.881** | **41.9** | **0.95** | 5.95 |
| Предложенная, без новых полей | 0.6701 | 0.2085 | 0.916 | 0.677 | 0.782 | 0.864 | 47.4 | 0.79 | 5.90 | | Предложенная, без новых полей | 0.6701 | 0.2085 | 0.916 | 0.677 | 0.783 | 0.864 | 47.2 | 0.79 | 5.90 |
**Клубы** (потолок 0.6308) **Клубы** (потолок 0.6308)
@@ -453,8 +469,8 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|
| League Points (сейчас) | 0.6045 | — | 0.525 | 0.303 | 0.391 | 0.463 | — | — | 8.06 | | 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 | 102.6 | 0.40 | 5.71 | | Elo, чистый | 0.6063 | 0.2341 | 0.632 | 0.326 | 0.440 | 0.535 | 102.6 | 0.40 | 5.71 |
| **Предложенная** | **0.6102** | 0.2336 | **0.644** | **0.356** | **0.463** | **0.555** | **100.3** | **0.47** | 6.53 | | **Предложенная** | **0.6101** | 0.2336 | **0.645** | **0.356** | **0.463** | **0.555** | **100.3** | **0.47** | 6.53 |
| Предложенная, без новых полей | 0.6074 | **0.2332** | 0.633 | 0.336 | 0.440 | 0.534 | 103.3 | 0.38 | 6.41 | | Предложенная, без новых полей | 0.6073 | **0.2332** | 0.633 | 0.336 | 0.440 | 0.535 | 103.3 | 0.38 | 6.41 |
**Рост** (потолок 0.6864) **Рост** (потолок 0.6864)
@@ -462,8 +478,8 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|
| League Points (сейчас) | 0.6688 | — | 0.915 | 0.631 | 0.732 | 0.824 | — | — | 6.91 | | 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 | 48.0 | 0.81 | 5.25 | | Elo, чистый | 0.6687 | 0.2086 | 0.916 | 0.632 | 0.733 | 0.825 | 48.0 | 0.81 | 5.25 |
| **Предложенная** | **0.6726** | **0.2076** | **0.929** | **0.685** | **0.774** | **0.851** | **42.7** | **0.95** | 5.96 | | **Предложенная** | **0.6726** | **0.2076** | **0.929** | **0.685** | **0.775** | **0.850** | **42.7** | **0.95** | 5.96 |
| Предложенная, без новых полей | 0.6697 | 0.2081 | 0.922 | 0.651 | 0.750 | 0.835 | 47.6 | 0.79 | 5.91 | | Предложенная, без новых полей | 0.6697 | 0.2081 | 0.922 | 0.651 | 0.750 | 0.835 | 47.5 | 0.79 | 5.91 |
**Шум** (потолок 0.6894) **Шум** (потолок 0.6894)
@@ -471,16 +487,16 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|
| League Points (сейчас) | 0.6737 | — | 0.916 | 0.656 | 0.761 | 0.848 | — | — | 6.89 | | 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 | **46.1** | **0.82** | 5.23 | | 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.643 | 0.750 | 0.844 | 48.6 | 0.81 | 5.91 | | Предложенная | 0.6728 | 0.2076 | 0.908 | 0.643 | 0.750 | 0.844 | 48.5 | 0.81 | 5.91 |
| Предложенная, без новых полей | **0.6743** | **0.2070** | **0.919** | **0.667** | **0.775** | **0.856** | 47.4 | 0.78 | 5.88 | | Предложенная, без новых полей | **0.6744** | **0.2070** | **0.919** | **0.667** | **0.776** | **0.856** | 47.3 | 0.78 | 5.88 |
Парные разности (среднее ± стандартная ошибка по 200 сезонам): Парные разности (среднее ± стандартная ошибка по 200 сезонам):
| Сценарий | Brier: предложенная − чистый Elo | Точность: предложенная − чистый Elo | Точность: предложенная − сейчас | | Сценарий | Brier: предложенная − чистый Elo | Точность: предложенная − чистый Elo | Точность: предложенная − сейчас |
|---|---|---|---| |---|---|---|---|
| сигнал | −0.0011 ± 0.0001 | +0.30 ± 0.06 п.п. | +0.37 ± 0.07 п.п. | | сигнал | −0.0011 ± 0.0001 | +0.30 ± 0.06 п.п. | +0.38 ± 0.07 п.п. |
| клубы | −0.0005 ± 0.0002 | +0.38 ± 0.08 п.п. | +0.57 ± 0.10 п.п. | | клубы | −0.0005 ± 0.0002 | +0.38 ± 0.08 п.п. | +0.56 ± 0.10 п.п. |
| рост | −0.0010 ± 0.0001 | +0.39 ± 0.07 п.п. | +0.37 ± 0.06 п.п. | | рост | −0.0010 ± 0.0001 | +0.39 ± 0.06 п.п. | +0.38 ± 0.06 п.п. |
| шум | +0.0006 ± 0.0001 | −0.12 ± 0.06 п.п. | −0.10 ± 0.07 п.п. | | шум | +0.0006 ± 0.0001 | −0.12 ± 0.06 п.п. | −0.10 ± 0.07 п.п. |
Выводы: Выводы:
@@ -491,7 +507,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.644): чемпион слабой группы система упорядочивает игроков заметно хуже (ρ 0.525 против 0.645): чемпион слабой группы
у неё стоит рядом с чемпионом сильной. у неё стоит рядом с чемпионом сильной.
3. **Если отрыв — шум**, предложенная система теряет 0.1 п.п. точности и 0.0006 Brier — 3. **Если отрыв — шум**, предложенная система теряет 0.1 п.п. точности и 0.0006 Brier —
цена лишней волатильности. Это худший из рассмотренных случаев: в остальных сценариях цена лишней волатильности. Это худший из рассмотренных случаев: в остальных сценариях
@@ -507,6 +523,8 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
`simulate.py --grid` перебирает коэффициенты на **других** 40 сезонах каждого сценария. `simulate.py --grid` перебирает коэффициенты на **других** 40 сезонах каждого сценария.
Критерий — средний Brier, меньше — лучше. Разница в 0.0001 — примерно граница шума. Критерий — средний Brier, меньше — лучше. Разница в 0.0001 — примерно граница шума.
Перебор выполнен до правила «выбывшие между собой не сравниваются» (#91) и не
переигрывался: правило сдвигает метрики лишь в третьем знаке (7.3).
**Этап 1. K чистого Elo** (все четыре сценария). Спуск K за 20 партий лучше, чем за 10. **Этап 1. K чистого Elo** (все четыре сценария). Спуск K за 20 партий лучше, чем за 10.
Выгоден высокий K новичка и низкий K ветерана. Выгоден высокий K новичка и низкий K ветерана.
@@ -733,6 +751,7 @@ M_{ab} = \operatorname{clamp}(A_{ab},\; 0.5,\; 2.0) \cdot c_{ab}
| 5 | Затухание за неактивность | **Не добавлять** | без изменений | | 5 | Затухание за неактивность | **Не добавлять** | без изменений |
| 6 | Минимум партий для топа | **Оставить 10** | без изменений: `MIN_GAMES = 10` | | 6 | Минимум партий для топа | **Оставить 10** | без изменений: `MIN_GAMES = 10` |
| 7 | Групповой рейтинг отдельной цепочкой (после внедрения, #80, 2026-09-15) | **Убрать.** Рейтинг единый; на странице группы — показатели по партиям группы, на главной и в профиле — общие | раздел 8, «Расчёт» | | 7 | Групповой рейтинг отдельной цепочкой (после внедрения, #80, 2026-09-15) | **Убрать.** Рейтинг единый; на странице группы — показатели по партиям группы, на главной и в профиле — общие | раздел 8, «Расчёт» |
| 8 | Сравнивать ли выбывших между собой (после ревью, #87 → #91, 2026-09-18) | **Нет.** Все они проиграли и одинаково слабы в этой партии, миров у них нет; при расчёте победителя сила соперников по-прежнему важна | 4.3, 4.4, 4.9, пример 5, итоги 7.3 (подбор 7.4 не переигрывался) |
Открытых вопросов по предложению не осталось. Калибровка коэффициентов на реальных данных — Открытых вопросов по предложению не осталось. Калибровка коэффициентов на реальных данных —
после внедрения #23 (раздел 8, «Калибровка»). после внедрения #23 (раздел 8, «Калибровка»).
+8 -2
View File
@@ -93,6 +93,9 @@ class Params:
m_min: float = 0.5 m_min: float = 0.5
m_max: float = 2.0 m_max: float = 2.0
autocorr: bool = False # поправка на автокорреляцию (см. документ) autocorr: bool = False # поправка на автокорреляцию (см. документ)
# Выбывшие между собой не сравниваются: пара двух выбывших не входит в сумму
# (решение владельца, #91). У систем для сравнения — как было.
skip_eliminated_pairs: bool = False
def closeness_for(self, reason: str | None) -> float: def closeness_for(self, reason: str | None) -> float:
return dict(self.closeness).get(reason, 1.0) if reason else 1.0 return dict(self.closeness).get(reason, 1.0) if reason else 1.0
@@ -181,6 +184,8 @@ def rate_match(
k = {s.player: k_factor(games.get(s.player, 0), p) for s in m.seats} k = {s.player: k_factor(games.get(s.player, 0), p) for s in m.seats}
delta = {s.player: 0.0 for s in m.seats} delta = {s.player: 0.0 for s in m.seats}
for a, b in combinations(m.seats, 2): for a, b in combinations(m.seats, 2):
if p.skip_eliminated_pairs and a.eliminated and b.eliminated:
continue
if a.place > b.place: if a.place > b.place:
a, b = b, a a, b = b, a
s_ab = 0.5 if a.place == b.place else 1.0 s_ab = 0.5 if a.place == b.place else 1.0
@@ -550,6 +555,7 @@ PROPOSED = Params(
w_obj=0.5, w_obj=0.5,
w_worlds=0.5, w_worlds=0.5,
closeness=CLOSENESS, closeness=CLOSENESS,
skip_eliminated_pairs=True,
) )
@@ -594,7 +600,7 @@ def examples() -> list[tuple[str, str, dict, dict, Match]]:
("4e", "Разгром: 3-й раунд, 2:0 цели, 8:2 миров", ("4e", "Разгром: 3-й раунд, 2:0 цели, 8:2 миров",
{"A": 1500, "B": 1500}, _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": 1550, "B": 1500, "C": 1480, "D": 1450}, _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),
@@ -625,7 +631,7 @@ EXPECTED: dict[str, dict[str, float]] = {
"4c": {"A": 4.8, "B": -4.8}, "4c": {"A": 4.8, "B": -4.8},
"4d": {"A": 3.4, "B": -3.4}, "4d": {"A": 3.4, "B": -3.4},
"4e": {"A": 16.0, "B": -16.0}, "4e": {"A": 16.0, "B": -16.0},
"5": {"A": 9.27, "B": 5.85, "C": -7.78, "D": -7.35}, "5": {"A": 9.27, "B": 5.85, "C": -7.6, "D": -7.53},
"6a": {"A": 12.0, "B": 7.2, "C": 2.4, "D": -2.4, "E": -7.2, "F": -12.0}, "6a": {"A": 12.0, "B": 7.2, "C": 2.4, "D": -2.4, "E": -7.2, "F": -12.0},
"6b": {"A": 10.29, "B": 7.54, "C": 2.74, "D": -2.06, "E": -6.86, "F": -11.66}, "6b": {"A": 10.29, "B": 7.54, "C": 2.74, "D": -2.06, "E": -6.86, "F": -11.66},
"7": {"A": 32.0, "B": -8.0}, "7": {"A": 32.0, "B": -8.0},
+4 -4
View File
@@ -54,7 +54,8 @@ export function HelpPage() {
<p className="small" style={{ marginTop: 10 }}> <p className="small" style={{ marginTop: 10 }}>
где <b>N</b> — число игроков, сумма — по всем соперникам. Поэтому победа над сильным где <b>N</b> — число игроков, сумма — по всем соперникам. Поэтому победа над сильным
приносит больше, чем над слабым, а поражение от слабого отнимает больше. Выбывшие приносит больше, чем над слабым, а поражение от слабого отнимает больше. Выбывшие
делят последнее место. делят последнее место, но между собой не сравниваются: в этой партии все они
проиграли, так что пара двух выбывших рейтинг не двигает.
</p> </p>
</div> </div>
@@ -94,9 +95,8 @@ export function HelpPage() {
</p> </p>
<p className="small" style={{ marginBottom: 0 }}> <p className="small" style={{ marginBottom: 0 }}>
Единоличный победитель никогда не теряет рейтинг, а единоличное последнее место Единоличный победитель никогда не теряет рейтинг, а единоличное последнее место
никогда его не приносит. При общем месте (ничья за 1-е, несколько выбывших) и выбывание никогда его не приносят. Невыбывшие, поделившие место, сравниваются
игроки внутри ничьей сравниваются между собой как в ничьей: слабый может между собой как в ничьей: слабый может получить рейтинг, сильный — потерять.
получить рейтинг, сильный — потерять.
</p> </p>
</div> </div>