Рейтинг: байесовское сглаживание по опыту; новички — до 10 партий

Проблема: средний балл не учитывал число игр — новичок с 10 удачными
партиями обходил ветерана с сотнями стабильных.

Теперь score = (C·m + сумма очков) / (C + игр) × 100 при C=10, m=0.5:
к реальным партиям «дописываются» 10 виртуальных со средним 0.5, рейтинг
стартует около 50 и с опытом сходится к чистому среднему (формула IMDB).
Применено в топах и профиле; разбивка по фракциям осталась на чистом
среднем (служебная метрика «лучшая/худшая», сглаживание давило бы её к 50).
MIN_GAMES 5 -> 10 («Новички» во всех топах — до 10 партий; подписи берут
порог из API). Справка обновлена, добавлен тест (62 passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-17 03:04:48 +03:00
co-authored by Claude Fable 5
parent ddbcd74058
commit ebb368686d
4 changed files with 72 additions and 8 deletions
+39
View File
@@ -0,0 +1,39 @@
"""Сглаживание рейтинга: score = (C·m + сумма очков) / (C + игр) × 100, C=10, m=0.5."""
from __future__ import annotations
from fastapi.testclient import TestClient
from tests.conftest import add_group_member, create_finished_match, csrf_headers, login
def test_leaderboard_and_profile_score_are_smoothed(client: TestClient, engine):
me = login(client, "Хозяин")
gid = client.post(
"/api/groups", json={"name": "Группа", "expansion_ids": []}, headers=csrf_headers(client)
).json()["id"]
b = add_group_member(engine, gid, "Гость")
factions = client.get(f"/api/groups/{gid}/factions").json()
f1, f2 = factions[0]["id"], factions[1]["id"]
create_finished_match(
client,
gid,
[
{"user_id": me["id"], "faction_id": f1, "place": 1},
{"user_id": b, "faction_id": f2, "place": 2},
],
)
board = client.get("/api/stats/leaderboard").json()
by_id = {e["user_id"]: e for e in board["entries"] + board["provisional"]}
# Победитель: 1 очко за партию → (10·0.5 + 1) / (10 + 1) × 100 = 54.5.
# Проигравший: 0 очков → (10·0.5 + 0) / 11 × 100 = 45.5.
assert by_id[me["id"]]["score"] == 54.5
assert by_id[b]["score"] == 45.5
# 1 игра < MIN_GAMES=10 → оба пока «Новички», ранжированный топ пуст.
assert board["entries"] == []
assert board["min_games"] == 10
# Профиль показывает тот же сглаженный рейтинг, что и топ.
prof = client.get("/api/users/me/stats").json()
assert prof["overall"]["score"] == 54.5