Рейтинг: выбывшие между собой не сравниваются
Решение владельца: в партии все выбывшие проиграли и одинаково слабы, миров у них нет, поэтому пара двух выбывших в сумму не входит — ни 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:
@@ -7,9 +7,10 @@
|
||||
E_ab = 1 / (1 + 10^((R_b − R_a) / D)) ожидание по рейтингам ДО партии
|
||||
S_ab = 1 / 0.5 / 0 выше / поровну / ниже
|
||||
Δ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) — вес размера стола,
|
||||
M — множитель отрыва (темп, цели, миры; близость по типу победы — только у пар
|
||||
с победителем). Недостающий признак партии подставляется типичным и не влияет на M.
|
||||
Пары двух выбывших в сумму не входят (#91), остальные — все. K_i спускается от K_MAX
|
||||
у новичка до K_MIN за K_GAMES партий, G(N) — вес размера стола, M — множитель отрыва
|
||||
(темп, цели, миры; близость по типу победы — только у пар с победителем). Недостающий
|
||||
признак партии подставляется типичным и не влияет на M.
|
||||
|
||||
Модуль — только константы и чистые функции без БД: калибровка на реальных данных —
|
||||
правка констант, пересчёт выполняется сам (рейтинг — функция упорядоченной истории).
|
||||
@@ -168,6 +169,10 @@ def rate_match(
|
||||
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}
|
||||
for a, b in combinations(m.seats, 2):
|
||||
if a.eliminated and b.eliminated:
|
||||
# Выбывшие между собой не сравниваются: в этой партии все они проиграли, а миров
|
||||
# у них нет (решение владельца, #91). Нормировка на N − 1 остаётся прежней.
|
||||
continue
|
||||
if a.place > b.place:
|
||||
a, b = b, a
|
||||
s_ab = 0.5 if a.place == b.place else 1.0
|
||||
|
||||
@@ -68,7 +68,7 @@ EXAMPLES = [
|
||||
_seat(C, 3, 1, 0, eliminated=True), _seat(D, 3, 0, 0, eliminated=True)),
|
||||
"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)),
|
||||
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}),
|
||||
@@ -101,6 +101,23 @@ def test_last_standing_counts_full_objective_gap():
|
||||
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"
|
||||
@@ -147,6 +164,7 @@ def test_constants_match_reference(sim):
|
||||
assert dict(p.closeness) == scoring.CLOSENESS
|
||||
assert sim.BOARD_TILES == scoring.BOARD_TILES
|
||||
assert not p.autocorr
|
||||
assert p.skip_eliminated_pairs # выбывшие между собой не сравниваются (#91)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scenario", ["сигнал", "клубы", "рост"])
|
||||
@@ -174,8 +192,8 @@ def test_replay_matches_reference_season(sim, scenario, stripped):
|
||||
|
||||
|
||||
def test_monotone_and_zero_sum(sim):
|
||||
"""Победитель без ничьей не теряет, последний без ничьей не получает; при равных K
|
||||
сумма изменений за партию — ноль."""
|
||||
"""Победитель без ничьей не теряет, последний без ничьей и любой выбывший не получают;
|
||||
при равных K сумма изменений за партию — ноль."""
|
||||
cfg = sim.SCENARIOS["сигнал"]
|
||||
_skill, matches = sim.generate_season(cfg["seed"] + 7, 200, True)
|
||||
ids: dict[str, int] = {}
|
||||
@@ -188,6 +206,8 @@ def test_monotone_and_zero_sum(sim):
|
||||
delta, _ = rate_match({}, veterans, rm)
|
||||
places = [s.place 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:
|
||||
continue
|
||||
if s.place == 1:
|
||||
|
||||
Reference in New Issue
Block a user