1014 lines
46 KiB
Python
1014 lines
46 KiB
Python
"""Core Elo/MMR rebuild engine backed by real historical signals."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from collections import defaultdict
|
|
from datetime import datetime, timezone
|
|
from statistics import pstdev
|
|
from typing import Iterable
|
|
|
|
from .config import get_historical_data_source_kind
|
|
from .data_sources import (
|
|
SOURCE_KIND_PUBLIC_SCOREBOARD,
|
|
SOURCE_KIND_RCON,
|
|
build_source_attempt,
|
|
build_source_policy,
|
|
get_rcon_historical_read_model,
|
|
)
|
|
from .elo_mmr_models import (
|
|
CAPABILITY_APPROXIMATE,
|
|
CAPABILITY_EXACT,
|
|
CAPABILITY_UNAVAILABLE,
|
|
DEFAULT_BASE_MMR,
|
|
ELO_K_FACTOR,
|
|
FULL_QUALITY_DURATION_SECONDS,
|
|
FULL_QUALITY_PLAYER_COUNT,
|
|
MIN_VALID_MATCH_DURATION_SECONDS,
|
|
MIN_VALID_PLAYER_PARTICIPATION_RATIO,
|
|
MIN_VALID_PLAYER_PARTICIPATION_SECONDS,
|
|
MIN_VALID_MATCH_PLAYERS,
|
|
MONTHLY_ACTIVITY_TARGET_HOURS,
|
|
MONTHLY_ACTIVITY_TARGET_MATCHES,
|
|
MONTHLY_MIN_TIME_SECONDS,
|
|
MONTHLY_MIN_VALID_MATCHES,
|
|
build_signal,
|
|
summarize_accuracy,
|
|
)
|
|
from .elo_mmr_storage import (
|
|
get_elo_mmr_player_profile,
|
|
initialize_elo_mmr_storage,
|
|
list_elo_mmr_monthly_rankings,
|
|
replace_elo_mmr_state,
|
|
)
|
|
from .historical_storage import ALL_SERVERS_SLUG, initialize_historical_storage
|
|
from .rcon_historical_read_model import get_rcon_historical_competitive_match_context
|
|
from .sqlite_utils import connect_sqlite_readonly
|
|
from .writer_lock import backend_writer_lock, build_writer_lock_holder
|
|
|
|
|
|
SCOPE_ALL_SERVERS = ALL_SERVERS_SLUG
|
|
QUALITY_BUCKET_HIGH = "high"
|
|
QUALITY_BUCKET_MEDIUM = "medium"
|
|
QUALITY_BUCKET_LOW = "low"
|
|
ROLE_BUCKET_SUPPORT = "support"
|
|
ROLE_BUCKET_OFFENSE = "offense"
|
|
ROLE_BUCKET_DEFENSE = "defense"
|
|
ROLE_BUCKET_COMBAT = "combat"
|
|
ROLE_BUCKET_GENERALIST = "generalist"
|
|
MONTHLY_MIN_AVG_PARTICIPATION_RATIO = 0.45
|
|
MONTHLY_RANK_WEIGHT_COMPETITIVE_GAIN = 0.70
|
|
MONTHLY_RANK_WEIGHT_MATCH_SCORE = 0.14
|
|
MONTHLY_RANK_WEIGHT_STRENGTH_OF_SCHEDULE = 0.05
|
|
MONTHLY_RANK_WEIGHT_CONSISTENCY = 0.04
|
|
MONTHLY_RANK_WEIGHT_CONFIDENCE = 0.04
|
|
MONTHLY_RANK_WEIGHT_ACTIVITY = 0.03
|
|
EXACT_MODIFIER_K_SHARE = 0.06
|
|
PROXY_MODIFIER_K_SHARE = 0.02
|
|
|
|
ROLE_WEIGHTS = {
|
|
ROLE_BUCKET_SUPPORT: {"combat": 0.18, "objective": 0.18, "utility": 0.42, "discipline": 0.22},
|
|
ROLE_BUCKET_OFFENSE: {"combat": 0.38, "objective": 0.30, "utility": 0.10, "discipline": 0.22},
|
|
ROLE_BUCKET_DEFENSE: {"combat": 0.26, "objective": 0.34, "utility": 0.16, "discipline": 0.24},
|
|
ROLE_BUCKET_COMBAT: {"combat": 0.48, "objective": 0.14, "utility": 0.14, "discipline": 0.24},
|
|
ROLE_BUCKET_GENERALIST: {"combat": 0.34, "objective": 0.22, "utility": 0.20, "discipline": 0.24},
|
|
}
|
|
|
|
|
|
def rebuild_elo_mmr_models(*, db_path=None) -> dict[str, object]:
|
|
"""Rebuild persistent player ratings and monthly rankings from scratch."""
|
|
with backend_writer_lock(holder=build_writer_lock_holder("app.elo_mmr_engine rebuild")):
|
|
resolved_path = initialize_historical_storage(db_path=db_path)
|
|
initialize_elo_mmr_storage(db_path=resolved_path)
|
|
historical_source_policy = _build_historical_source_policy_for_elo()
|
|
rcon_read_model = get_rcon_historical_read_model()
|
|
match_rows = _load_closed_match_rows(db_path=resolved_path)
|
|
grouped_matches = _group_match_rows(match_rows)
|
|
rcon_match_context_cache: dict[tuple[str, str | None, str | None], dict[str, object] | None] = {}
|
|
|
|
ratings_by_scope: dict[str, dict[str, dict[str, object]]] = {SCOPE_ALL_SERVERS: {}}
|
|
player_ratings: list[dict[str, object]] = []
|
|
match_results: list[dict[str, object]] = []
|
|
monthly_checkpoints: list[dict[str, object]] = []
|
|
|
|
for match_group in grouped_matches:
|
|
server_scope = match_group["server_slug"]
|
|
ratings_by_scope.setdefault(server_scope, {})
|
|
rcon_match_context = None
|
|
if rcon_read_model is not None:
|
|
cache_key = (
|
|
str(match_group["server_slug"]),
|
|
str(match_group.get("ended_at")) if match_group.get("ended_at") is not None else None,
|
|
str(match_group.get("map_pretty_name") or match_group.get("map_name") or "")
|
|
or None,
|
|
)
|
|
if cache_key not in rcon_match_context_cache:
|
|
rcon_match_context_cache[cache_key] = get_rcon_historical_competitive_match_context(
|
|
server_key=str(match_group["server_slug"]),
|
|
ended_at=match_group.get("ended_at"),
|
|
map_name=match_group.get("map_pretty_name") or match_group.get("map_name"),
|
|
)
|
|
rcon_match_context = rcon_match_context_cache[cache_key]
|
|
for scope_key in (server_scope, SCOPE_ALL_SERVERS):
|
|
match_results.extend(
|
|
_score_match_for_scope(
|
|
match_group=match_group,
|
|
scope_key=scope_key,
|
|
ratings_by_scope=ratings_by_scope[scope_key],
|
|
rcon_match_context=rcon_match_context,
|
|
)
|
|
)
|
|
|
|
for scope_ratings in ratings_by_scope.values():
|
|
player_ratings.extend(scope_ratings.values())
|
|
|
|
monthly_rankings = _build_monthly_rankings(match_results)
|
|
checkpoint_groups: dict[tuple[str, str], list[dict[str, object]]] = defaultdict(list)
|
|
for row in monthly_rankings:
|
|
checkpoint_groups[(row["scope_key"], row["month_key"])].append(row)
|
|
generated_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
for (scope_key, month_key), rows in checkpoint_groups.items():
|
|
eligible_count = sum(1 for row in rows if row["eligible"])
|
|
exact_ratio = round(
|
|
sum(float(row["capabilities"]["exact_ratio"]) for row in rows) / max(1, len(rows)),
|
|
3,
|
|
)
|
|
approximate_ratio = round(
|
|
sum(float(row["capabilities"]["approximate_ratio"]) for row in rows) / max(1, len(rows)),
|
|
3,
|
|
)
|
|
unavailable_ratio = round(
|
|
sum(float(row["capabilities"]["unavailable_ratio"]) for row in rows) / max(1, len(rows)),
|
|
3,
|
|
)
|
|
partial_count = sum(1 for row in rows if row["accuracy_mode"] == "partial")
|
|
monthly_checkpoints.append(
|
|
{
|
|
"scope_key": scope_key,
|
|
"month_key": month_key,
|
|
"generated_at": generated_at,
|
|
"player_count": len(rows),
|
|
"eligible_player_count": eligible_count,
|
|
"source_policy": historical_source_policy,
|
|
"capabilities_summary": {
|
|
"accuracy_mode": "partial" if partial_count > 0 else "approximate" if approximate_ratio > 0 else "exact",
|
|
"exact_ratio": exact_ratio,
|
|
"approximate_ratio": approximate_ratio,
|
|
"unavailable_ratio": unavailable_ratio,
|
|
"partial_count": partial_count,
|
|
"notes": [
|
|
"Outcome, combat, utility, match validity and player participation use real stored signals.",
|
|
"ObjectiveIndex, role bucket, discipline and strength of schedule rely partly on honest proxies.",
|
|
"LeadershipIndex is not available with the current repository telemetry.",
|
|
],
|
|
},
|
|
}
|
|
)
|
|
|
|
replace_elo_mmr_state(
|
|
player_ratings=player_ratings,
|
|
match_results=match_results,
|
|
monthly_rankings=monthly_rankings,
|
|
monthly_checkpoints=monthly_checkpoints,
|
|
db_path=resolved_path,
|
|
)
|
|
latest_month_by_scope = {
|
|
checkpoint["scope_key"]: checkpoint["month_key"] for checkpoint in monthly_checkpoints
|
|
}
|
|
return {
|
|
"status": "ok",
|
|
"historical_source_policy": historical_source_policy,
|
|
"totals": {
|
|
"matches_scored": len({(row["scope_key"], row["external_match_id"]) for row in match_results}),
|
|
"player_ratings": len(player_ratings),
|
|
"match_results": len(match_results),
|
|
"monthly_rankings": len(monthly_rankings),
|
|
"monthly_checkpoints": len(monthly_checkpoints),
|
|
},
|
|
"latest_month_by_scope": latest_month_by_scope,
|
|
}
|
|
|
|
|
|
def list_elo_mmr_leaderboard_payload(*, server_id: str | None, limit: int) -> dict[str, object]:
|
|
"""Return the current monthly Elo/MMR leaderboard for one scope."""
|
|
scope_key = _normalize_scope_key(server_id)
|
|
result = list_elo_mmr_monthly_rankings(scope_key=scope_key, limit=limit)
|
|
return {
|
|
"scope_key": scope_key,
|
|
"month_key": result["month_key"],
|
|
"found": result["found"],
|
|
"generated_at": result["generated_at"],
|
|
"items": result["items"],
|
|
"source_policy": result["source_policy"] or _build_historical_source_policy_for_elo(),
|
|
"capabilities_summary": result["capabilities_summary"],
|
|
}
|
|
|
|
|
|
def get_elo_mmr_player_payload(*, player_id: str, server_id: str | None) -> dict[str, object] | None:
|
|
"""Return one Elo/MMR player profile."""
|
|
return get_elo_mmr_player_profile(
|
|
player_id=player_id,
|
|
scope_key=_normalize_scope_key(server_id),
|
|
)
|
|
|
|
|
|
def build_arg_parser() -> argparse.ArgumentParser:
|
|
"""Build the CLI parser for Elo/MMR maintenance."""
|
|
parser = argparse.ArgumentParser(
|
|
description="Rebuild or inspect the Elo/MMR monthly ranking system.",
|
|
)
|
|
parser.add_argument(
|
|
"mode",
|
|
choices=("rebuild", "leaderboard", "player"),
|
|
help="rebuild recomputes all persisted Elo/MMR state; leaderboard and player inspect the read model",
|
|
)
|
|
parser.add_argument("--server", dest="server_id", help="optional server scope")
|
|
parser.add_argument("--limit", type=int, default=10, help="max rows for leaderboard mode")
|
|
parser.add_argument("--player", dest="player_id", help="player id or steam id for player mode")
|
|
return parser
|
|
|
|
|
|
def main(argv: Iterable[str] | None = None) -> int:
|
|
"""Run the Elo/MMR CLI."""
|
|
parser = build_arg_parser()
|
|
args = parser.parse_args(list(argv) if argv is not None else None)
|
|
if args.mode == "rebuild":
|
|
print(json.dumps(rebuild_elo_mmr_models(), indent=2))
|
|
return 0
|
|
if args.mode == "leaderboard":
|
|
print(json.dumps(list_elo_mmr_leaderboard_payload(server_id=args.server_id, limit=args.limit), indent=2))
|
|
return 0
|
|
if not args.player_id:
|
|
parser.error("--player is required in player mode")
|
|
print(json.dumps(get_elo_mmr_player_payload(player_id=args.player_id, server_id=args.server_id), indent=2))
|
|
return 0
|
|
|
|
|
|
def _load_closed_match_rows(*, db_path) -> list[dict[str, object]]:
|
|
with connect_sqlite_readonly(db_path) as connection:
|
|
rows = connection.execute(
|
|
"""
|
|
SELECT
|
|
historical_servers.slug AS server_slug,
|
|
historical_servers.display_name AS server_name,
|
|
historical_matches.external_match_id,
|
|
historical_matches.started_at,
|
|
historical_matches.ended_at,
|
|
historical_matches.game_mode,
|
|
historical_matches.allied_score,
|
|
historical_matches.axis_score,
|
|
historical_players.stable_player_key,
|
|
historical_players.display_name AS player_name,
|
|
historical_players.steam_id,
|
|
historical_player_match_stats.team_side,
|
|
historical_player_match_stats.kills,
|
|
historical_player_match_stats.deaths,
|
|
historical_player_match_stats.teamkills,
|
|
historical_player_match_stats.time_seconds,
|
|
historical_player_match_stats.combat,
|
|
historical_player_match_stats.offense,
|
|
historical_player_match_stats.defense,
|
|
historical_player_match_stats.support
|
|
FROM historical_player_match_stats
|
|
INNER JOIN historical_matches
|
|
ON historical_matches.id = historical_player_match_stats.historical_match_id
|
|
INNER JOIN historical_servers
|
|
ON historical_servers.id = historical_matches.historical_server_id
|
|
INNER JOIN historical_players
|
|
ON historical_players.id = historical_player_match_stats.historical_player_id
|
|
WHERE historical_matches.ended_at IS NOT NULL
|
|
ORDER BY historical_matches.ended_at ASC, historical_matches.id ASC, historical_players.id ASC
|
|
"""
|
|
).fetchall()
|
|
return [dict(row) for row in rows]
|
|
|
|
|
|
def _group_match_rows(rows: list[dict[str, object]]) -> list[dict[str, object]]:
|
|
grouped: dict[tuple[str, str], list[dict[str, object]]] = defaultdict(list)
|
|
for row in rows:
|
|
grouped[(str(row["server_slug"]), str(row["external_match_id"]))].append(row)
|
|
items: list[dict[str, object]] = []
|
|
for (server_slug, match_id), players in grouped.items():
|
|
first = players[0]
|
|
items.append(
|
|
{
|
|
"server_slug": server_slug,
|
|
"server_name": first["server_name"],
|
|
"external_match_id": match_id,
|
|
"started_at": first["started_at"],
|
|
"ended_at": first["ended_at"],
|
|
"game_mode": first["game_mode"],
|
|
"allied_score": _safe_int(first["allied_score"]),
|
|
"axis_score": _safe_int(first["axis_score"]),
|
|
"players": players,
|
|
}
|
|
)
|
|
return items
|
|
|
|
|
|
def _score_match_for_scope(
|
|
*,
|
|
match_group: dict[str, object],
|
|
scope_key: str,
|
|
ratings_by_scope: dict[str, dict[str, object]],
|
|
rcon_match_context: dict[str, object] | None = None,
|
|
) -> list[dict[str, object]]:
|
|
players = list(match_group["players"])
|
|
duration_seconds, duration_mode = _resolve_match_duration(
|
|
match_group,
|
|
players,
|
|
rcon_match_context=rcon_match_context,
|
|
)
|
|
quality_factor = _build_quality_factor(
|
|
player_count=max(len(players), int(rcon_match_context.get("peak_players") or 0))
|
|
if rcon_match_context is not None
|
|
else len(players),
|
|
duration_seconds=duration_seconds,
|
|
has_score=match_group.get("allied_score") is not None and match_group.get("axis_score") is not None,
|
|
)
|
|
quality_bucket = _classify_quality_bucket(quality_factor)
|
|
match_valid = duration_seconds >= MIN_VALID_MATCH_DURATION_SECONDS and len(players) >= MIN_VALID_MATCH_PLAYERS
|
|
month_key = str(match_group["ended_at"])[:7]
|
|
max_kills = max(max(_safe_int(player.get("kills")), 0) for player in players) or 1
|
|
max_support = max(max(_safe_int(player.get("support")), 0) for player in players) or 1
|
|
max_combat = max(max(_safe_int(player.get("combat")), 0) for player in players) or 1
|
|
max_objective = max(
|
|
max(_safe_int(player.get("offense")) + _safe_int(player.get("defense")), 0)
|
|
for player in players
|
|
) or 1
|
|
results: list[dict[str, object]] = []
|
|
rating_before_by_player = {
|
|
str(player["stable_player_key"]): float(
|
|
ratings_by_scope.get(str(player["stable_player_key"]), {}).get("current_mmr", DEFAULT_BASE_MMR)
|
|
)
|
|
for player in players
|
|
}
|
|
|
|
for player in players:
|
|
stable_player_key = str(player["stable_player_key"])
|
|
rating_row = ratings_by_scope.setdefault(
|
|
stable_player_key,
|
|
{
|
|
"scope_key": scope_key,
|
|
"stable_player_key": stable_player_key,
|
|
"player_name": player["player_name"],
|
|
"steam_id": player.get("steam_id"),
|
|
"current_mmr": DEFAULT_BASE_MMR,
|
|
"matches_processed": 0,
|
|
"wins": 0,
|
|
"draws": 0,
|
|
"losses": 0,
|
|
"last_match_id": None,
|
|
"last_match_ended_at": None,
|
|
"accuracy_mode": "partial",
|
|
"capabilities": summarize_accuracy([]),
|
|
},
|
|
)
|
|
signals: list[dict[str, object]] = []
|
|
time_seconds = _safe_int(player.get("time_seconds"))
|
|
participation_ratio = _build_participation_ratio(
|
|
time_seconds=time_seconds,
|
|
duration_seconds=duration_seconds,
|
|
)
|
|
player_match_valid = match_valid and _is_player_match_eligible(
|
|
time_seconds=time_seconds,
|
|
participation_ratio=participation_ratio,
|
|
)
|
|
team_outcome = _resolve_team_outcome(
|
|
team_side=str(player.get("team_side") or ""),
|
|
allied_score=_safe_int(match_group.get("allied_score")),
|
|
axis_score=_safe_int(match_group.get("axis_score")),
|
|
)
|
|
outcome_score = _build_outcome_score(
|
|
team_outcome=team_outcome,
|
|
allied_score=_safe_int(match_group.get("allied_score")),
|
|
axis_score=_safe_int(match_group.get("axis_score")),
|
|
)
|
|
signals.append(build_signal("OutcomeScore", CAPABILITY_EXACT, "Derived from team side and final match score."))
|
|
signals.append(build_signal("MatchValidity", CAPABILITY_EXACT, "Uses closed match state, duration and lobby size thresholds."))
|
|
if duration_seconds > 0:
|
|
signals.append(build_signal("PlayerParticipation", CAPABILITY_EXACT, "Uses persisted player time_seconds relative to match duration."))
|
|
|
|
kills = _safe_int(player.get("kills"))
|
|
deaths = max(1, _safe_int(player.get("deaths")))
|
|
combat_raw = _safe_int(player.get("combat"))
|
|
combat_index = round(
|
|
(40.0 * (kills / max_kills))
|
|
+ (35.0 * min(1.0, (kills / deaths) / 3.0))
|
|
+ (25.0 * (combat_raw / max_combat)),
|
|
3,
|
|
)
|
|
signals.append(build_signal("CombatIndex", CAPABILITY_EXACT, "Uses kills, KDA proxy and persisted combat score."))
|
|
|
|
support = _safe_int(player.get("support"))
|
|
utility_index = round(100.0 * (support / max_support), 3) if max_support > 0 else 0.0
|
|
signals.append(build_signal("UtilityIndex", CAPABILITY_EXACT, "Uses persisted support points."))
|
|
|
|
objective_proxy = _safe_int(player.get("offense")) + _safe_int(player.get("defense"))
|
|
objective_index = round(100.0 * (objective_proxy / max_objective), 3) if max_objective > 0 else 0.0
|
|
signals.append(build_signal("ObjectiveIndex", CAPABILITY_APPROXIMATE, "Approximated from offense and defense scoreboard points because no tactical event feed exists yet."))
|
|
|
|
teamkills = _safe_int(player.get("teamkills"))
|
|
completion_component = round(participation_ratio * 100.0, 3)
|
|
discipline_index = round(
|
|
max(
|
|
0.0,
|
|
(88.0 - (teamkills * 18.0)) + (0.12 * completion_component),
|
|
),
|
|
3,
|
|
)
|
|
signals.append(build_signal("DisciplineIndex", CAPABILITY_APPROXIMATE, "Uses exact teamkills plus participation as an honest proxy for leave or AFK risk because direct discipline telemetry is unavailable."))
|
|
leadership_index = None
|
|
signals.append(build_signal("LeadershipIndex", CAPABILITY_UNAVAILABLE, "No leadership-specific telemetry is stored in the repository yet."))
|
|
|
|
role_bucket = _resolve_role_bucket(player)
|
|
signals.append(build_signal("role_bucket", CAPABILITY_APPROXIMATE, "Inferred from the dominant combat/offense/defense/support axis because literal player role is unavailable."))
|
|
if duration_mode == CAPABILITY_EXACT:
|
|
signals.append(build_signal("quality_duration", CAPABILITY_EXACT, "Duration computed from match timestamps."))
|
|
else:
|
|
signals.append(build_signal("quality_duration", CAPABILITY_APPROXIMATE, "Duration approximated from the maximum persisted player time."))
|
|
if rcon_match_context is not None:
|
|
signals.append(
|
|
build_signal(
|
|
"RconCompetitiveWindow",
|
|
CAPABILITY_APPROXIMATE,
|
|
"Uses the closest RCON-backed competitive window for match duration and lobby density when coverage exists.",
|
|
)
|
|
)
|
|
|
|
weights = ROLE_WEIGHTS.get(role_bucket, ROLE_WEIGHTS[ROLE_BUCKET_GENERALIST])
|
|
impact_score = round(
|
|
sum(
|
|
{
|
|
"combat": combat_index,
|
|
"objective": objective_index,
|
|
"utility": utility_index,
|
|
"discipline": discipline_index,
|
|
}[key]
|
|
* weight
|
|
for key, weight in weights.items()
|
|
),
|
|
3,
|
|
)
|
|
team_side = str(player.get("team_side") or "")
|
|
strength_of_schedule_match = _build_strength_of_schedule_match(
|
|
stable_player_key=stable_player_key,
|
|
team_side=team_side,
|
|
players=players,
|
|
rating_before_by_player=rating_before_by_player,
|
|
quality_factor=quality_factor,
|
|
)
|
|
signals.append(build_signal("StrengthOfScheduleMatch", CAPABILITY_APPROXIMATE, "Approximated from opponent average MMR pressure plus match quality because no full roster graph is stored."))
|
|
exact_modifier_index = _build_weighted_modifier_index(
|
|
left_value=combat_index,
|
|
right_value=utility_index,
|
|
left_weight=weights["combat"],
|
|
right_weight=weights["utility"],
|
|
)
|
|
proxy_modifier_index = _build_weighted_modifier_index(
|
|
left_value=objective_index,
|
|
right_value=discipline_index,
|
|
left_weight=weights["objective"],
|
|
right_weight=weights["discipline"],
|
|
)
|
|
effective_score = round(
|
|
(
|
|
(0.60 * outcome_score)
|
|
+ (0.25 * impact_score)
|
|
+ (0.10 * strength_of_schedule_match)
|
|
+ (0.05 * discipline_index)
|
|
)
|
|
* participation_ratio,
|
|
3,
|
|
)
|
|
if not player_match_valid:
|
|
delta_mmr = 0.0
|
|
match_score = 0.0
|
|
expected_result = 0.0
|
|
actual_result = 0.0
|
|
elo_core_delta = 0.0
|
|
performance_modifier_delta = 0.0
|
|
proxy_modifier_delta = 0.0
|
|
else:
|
|
expected_result = _build_expected_result(
|
|
player_rating=rating_before_by_player.get(stable_player_key, DEFAULT_BASE_MMR),
|
|
opponent_average_rating=_resolve_opponent_average_rating(
|
|
stable_player_key=stable_player_key,
|
|
team_side=team_side,
|
|
players=players,
|
|
rating_before_by_player=rating_before_by_player,
|
|
),
|
|
)
|
|
actual_result = _build_actual_result(
|
|
team_outcome=team_outcome,
|
|
allied_score=_safe_int(match_group.get("allied_score")),
|
|
axis_score=_safe_int(match_group.get("axis_score")),
|
|
participation_ratio=participation_ratio,
|
|
)
|
|
exact_modifier_edge = _build_centered_modifier_edge(
|
|
exact_modifier_index,
|
|
participation_ratio=participation_ratio,
|
|
)
|
|
proxy_modifier_edge = _build_centered_modifier_edge(
|
|
proxy_modifier_index,
|
|
participation_ratio=participation_ratio,
|
|
)
|
|
elo_core_delta = round(
|
|
ELO_K_FACTOR * quality_factor * (actual_result - expected_result),
|
|
3,
|
|
)
|
|
exact_modifier_delta = round(
|
|
ELO_K_FACTOR * quality_factor * EXACT_MODIFIER_K_SHARE * exact_modifier_edge,
|
|
3,
|
|
)
|
|
proxy_modifier_delta = round(
|
|
ELO_K_FACTOR * quality_factor * PROXY_MODIFIER_K_SHARE * proxy_modifier_edge,
|
|
3,
|
|
)
|
|
performance_modifier_delta = round(
|
|
exact_modifier_delta + proxy_modifier_delta,
|
|
3,
|
|
)
|
|
delta_mmr = round(elo_core_delta + performance_modifier_delta, 3)
|
|
match_score = round(effective_score * quality_factor, 3)
|
|
signals.append(build_signal("DeltaMMR", CAPABILITY_APPROXIMATE, "Uses Elo-like expected-vs-actual movement plus bounded HLL performance modifiers and honest proxy boundaries."))
|
|
signals.append(build_signal("MatchScore", CAPABILITY_APPROXIMATE, "Uses outcome-first competitive scoring with bounded HLL impact and schedule context, then scales by match quality."))
|
|
capability_summary = summarize_accuracy(signals)
|
|
rating_before = float(rating_row["current_mmr"])
|
|
rating_after = round(rating_before + delta_mmr, 3)
|
|
results.append(
|
|
{
|
|
"scope_key": scope_key,
|
|
"month_key": month_key,
|
|
"external_match_id": match_group["external_match_id"],
|
|
"stable_player_key": stable_player_key,
|
|
"player_name": player["player_name"],
|
|
"steam_id": player.get("steam_id"),
|
|
"server_slug": match_group["server_slug"],
|
|
"server_name": match_group["server_name"],
|
|
"match_ended_at": match_group["ended_at"],
|
|
"match_valid": player_match_valid,
|
|
"quality_factor": quality_factor,
|
|
"quality_bucket": quality_bucket,
|
|
"role_bucket": role_bucket,
|
|
"role_bucket_mode": CAPABILITY_APPROXIMATE,
|
|
"outcome_score": outcome_score,
|
|
"combat_index": combat_index,
|
|
"objective_index": objective_index,
|
|
"objective_index_mode": CAPABILITY_APPROXIMATE,
|
|
"utility_index": utility_index,
|
|
"utility_index_mode": CAPABILITY_EXACT,
|
|
"leadership_index": leadership_index,
|
|
"leadership_index_mode": CAPABILITY_UNAVAILABLE,
|
|
"discipline_index": discipline_index,
|
|
"discipline_index_mode": CAPABILITY_APPROXIMATE,
|
|
"impact_score": impact_score,
|
|
"delta_mmr": delta_mmr,
|
|
"mmr_before": rating_before,
|
|
"mmr_after": rating_after,
|
|
"match_score": match_score,
|
|
"penalty_points": round((teamkills * 2.0) + max(0.0, (0.5 - participation_ratio) * 8.0), 3),
|
|
"capabilities": capability_summary,
|
|
"time_seconds": time_seconds,
|
|
"participation_ratio": participation_ratio,
|
|
"strength_of_schedule_match": strength_of_schedule_match,
|
|
"team_outcome": team_outcome,
|
|
"expected_result": expected_result,
|
|
"actual_result": actual_result,
|
|
"elo_core_delta": elo_core_delta,
|
|
"performance_modifier_delta": performance_modifier_delta,
|
|
"proxy_modifier_delta": proxy_modifier_delta,
|
|
}
|
|
)
|
|
rating_row["current_mmr"] = rating_after
|
|
rating_row["matches_processed"] = int(rating_row["matches_processed"]) + 1
|
|
rating_row["last_match_id"] = match_group["external_match_id"]
|
|
rating_row["last_match_ended_at"] = match_group["ended_at"]
|
|
rating_row["accuracy_mode"] = capability_summary["accuracy_mode"]
|
|
rating_row["capabilities"] = capability_summary
|
|
if team_outcome == "win":
|
|
rating_row["wins"] = int(rating_row["wins"]) + 1
|
|
elif team_outcome == "draw":
|
|
rating_row["draws"] = int(rating_row["draws"]) + 1
|
|
else:
|
|
rating_row["losses"] = int(rating_row["losses"]) + 1
|
|
return results
|
|
|
|
|
|
def _build_monthly_rankings(match_results: list[dict[str, object]]) -> list[dict[str, object]]:
|
|
grouped: dict[tuple[str, str, str], list[dict[str, object]]] = defaultdict(list)
|
|
for row in match_results:
|
|
grouped[(row["scope_key"], row["month_key"], row["stable_player_key"])].append(row)
|
|
|
|
rankings: list[dict[str, object]] = []
|
|
grouped_by_scope_month: dict[tuple[str, str], list[dict[str, object]]] = defaultdict(list)
|
|
for (scope_key, month_key, stable_player_key), rows in grouped.items():
|
|
rows.sort(key=lambda item: (item["match_ended_at"], item["external_match_id"]))
|
|
valid_rows = [row for row in rows if row["match_valid"]]
|
|
total_time_seconds = sum(int(row["time_seconds"] or 0) for row in rows)
|
|
penalty_points = round(sum(float(row["penalty_points"]) for row in rows), 3)
|
|
capability_rows = [row["capabilities"] for row in rows]
|
|
exact_ratio = round(sum(float(item["exact_ratio"]) for item in capability_rows) / max(1, len(capability_rows)), 3)
|
|
approximate_ratio = round(sum(float(item["approximate_ratio"]) for item in capability_rows) / max(1, len(capability_rows)), 3)
|
|
unavailable_ratio = round(sum(float(item["unavailable_ratio"]) for item in capability_rows) / max(1, len(capability_rows)), 3)
|
|
accuracy_mode = "partial" if unavailable_ratio > 0 else "approximate" if approximate_ratio > 0 else "exact"
|
|
avg_match_score = round(sum(float(row["match_score"]) for row in valid_rows) / max(1, len(valid_rows)), 3)
|
|
baseline_mmr = round(float(rows[0]["mmr_before"]), 3)
|
|
current_mmr = round(float(rows[-1]["mmr_after"]), 3)
|
|
mmr_gain = round(current_mmr - baseline_mmr, 3)
|
|
elo_core_gain = round(sum(float(row.get("elo_core_delta") or 0.0) for row in rows), 3)
|
|
performance_modifier_gain = round(
|
|
sum(float(row.get("performance_modifier_delta") or 0.0) for row in rows),
|
|
3,
|
|
)
|
|
proxy_modifier_gain = round(
|
|
sum(float(row.get("proxy_modifier_delta") or 0.0) for row in rows),
|
|
3,
|
|
)
|
|
avg_participation_ratio = round(
|
|
sum(float(row.get("participation_ratio") or 0.0) for row in rows) / max(1, len(rows)),
|
|
3,
|
|
)
|
|
strength_of_schedule = round(
|
|
sum(float(row.get("strength_of_schedule_match") or 0.0) for row in valid_rows) / max(1, len(valid_rows)),
|
|
3,
|
|
)
|
|
consistency = _build_consistency_score(valid_rows)
|
|
activity = _build_activity_score(valid_rows, total_time_seconds)
|
|
confidence = round(
|
|
min(
|
|
100.0,
|
|
(len(valid_rows) / MONTHLY_MIN_VALID_MATCHES) * 35.0
|
|
+ (total_time_seconds / MONTHLY_MIN_TIME_SECONDS) * 30.0
|
|
+ (avg_participation_ratio * 20.0)
|
|
+ (exact_ratio * 15.0),
|
|
),
|
|
3,
|
|
)
|
|
eligible = (
|
|
len(valid_rows) >= MONTHLY_MIN_VALID_MATCHES
|
|
and total_time_seconds >= MONTHLY_MIN_TIME_SECONDS
|
|
and avg_participation_ratio >= MONTHLY_MIN_AVG_PARTICIPATION_RATIO
|
|
)
|
|
eligibility_reason = _build_monthly_eligibility_reason(
|
|
valid_match_count=len(valid_rows),
|
|
total_time_seconds=total_time_seconds,
|
|
avg_participation_ratio=avg_participation_ratio,
|
|
)
|
|
grouped_by_scope_month[(scope_key, month_key)].append(
|
|
{
|
|
"scope_key": scope_key,
|
|
"month_key": month_key,
|
|
"stable_player_key": stable_player_key,
|
|
"player_name": rows[-1]["player_name"],
|
|
"steam_id": rows[-1].get("steam_id"),
|
|
"current_mmr": current_mmr,
|
|
"baseline_mmr": baseline_mmr,
|
|
"mmr_gain": mmr_gain,
|
|
"avg_match_score": avg_match_score,
|
|
"strength_of_schedule": strength_of_schedule,
|
|
"consistency": consistency,
|
|
"activity": activity,
|
|
"confidence": confidence,
|
|
"penalty_points": penalty_points,
|
|
"monthly_rank_score": 0.0,
|
|
"valid_matches": len(valid_rows),
|
|
"total_matches": len(rows),
|
|
"total_time_seconds": total_time_seconds,
|
|
"avg_participation_ratio": avg_participation_ratio,
|
|
"eligible": eligible,
|
|
"eligibility_reason": eligibility_reason,
|
|
"accuracy_mode": accuracy_mode,
|
|
"capabilities": {
|
|
"accuracy_mode": accuracy_mode,
|
|
"exact_ratio": exact_ratio,
|
|
"approximate_ratio": approximate_ratio,
|
|
"unavailable_ratio": unavailable_ratio,
|
|
"signals": [
|
|
build_signal("OutcomeScore", CAPABILITY_EXACT, "Uses final scores and team side."),
|
|
build_signal("CombatIndex", CAPABILITY_EXACT, "Uses historical player stats."),
|
|
build_signal("ObjectiveIndex", CAPABILITY_APPROXIMATE, "Uses offense and defense scores as a tactical proxy."),
|
|
build_signal("UtilityIndex", CAPABILITY_EXACT, "Uses support points."),
|
|
build_signal("LeadershipIndex", CAPABILITY_UNAVAILABLE, "No leadership telemetry exists yet."),
|
|
build_signal("DisciplineIndex", CAPABILITY_APPROXIMATE, "Uses teamkills exactly plus participation as a leave-risk proxy."),
|
|
build_signal("StrengthOfSchedule", CAPABILITY_APPROXIMATE, "Uses opponent average MMR pressure plus match quality, not a full roster graph."),
|
|
build_signal("MonthlyEligibility", CAPABILITY_EXACT, "Uses persisted valid-match count, playtime and participation thresholds."),
|
|
],
|
|
},
|
|
"component_scores": {
|
|
"model_version": "elo-v3-competitive",
|
|
"ranking_formula_version": "elo-v3-competitive-balanced-v1",
|
|
"avg_match_score": avg_match_score,
|
|
"mmr_gain_raw": mmr_gain,
|
|
"elo_core_gain": elo_core_gain,
|
|
"performance_modifier_gain": performance_modifier_gain,
|
|
"proxy_modifier_gain": proxy_modifier_gain,
|
|
"competitive_gain": round(
|
|
elo_core_gain
|
|
+ (0.25 * performance_modifier_gain)
|
|
+ (0.10 * proxy_modifier_gain),
|
|
3,
|
|
),
|
|
"strength_of_schedule": strength_of_schedule,
|
|
"consistency": consistency,
|
|
"activity": activity,
|
|
"confidence": confidence,
|
|
"avg_participation_ratio": avg_participation_ratio,
|
|
"penalty_points": penalty_points,
|
|
},
|
|
}
|
|
)
|
|
|
|
for rows in grouped_by_scope_month.values():
|
|
max_avg = max((row["avg_match_score"] for row in rows), default=1.0) or 1.0
|
|
max_competitive_gain = max(
|
|
(max(0.0, float(row["component_scores"].get("competitive_gain") or 0.0)) for row in rows),
|
|
default=1.0,
|
|
) or 1.0
|
|
max_sos = max((row["strength_of_schedule"] for row in rows), default=1.0) or 1.0
|
|
max_consistency = max((row["consistency"] for row in rows), default=1.0) or 1.0
|
|
max_activity = max((row["activity"] for row in rows), default=1.0) or 1.0
|
|
max_confidence = max((row["confidence"] for row in rows), default=1.0) or 1.0
|
|
for row in rows:
|
|
competitive_gain = max(0.0, float(row["component_scores"].get("competitive_gain") or 0.0))
|
|
normalized_gain = competitive_gain / max_competitive_gain if max_competitive_gain > 0 else 0.0
|
|
row["component_scores"]["normalized_mmr_gain"] = round(normalized_gain * 100.0, 3)
|
|
row["monthly_rank_score"] = round(
|
|
(MONTHLY_RANK_WEIGHT_COMPETITIVE_GAIN * normalized_gain * 100.0)
|
|
+ (MONTHLY_RANK_WEIGHT_MATCH_SCORE * (row["avg_match_score"] / max_avg) * 100.0)
|
|
+ (MONTHLY_RANK_WEIGHT_STRENGTH_OF_SCHEDULE * (row["strength_of_schedule"] / max_sos) * 100.0)
|
|
+ (MONTHLY_RANK_WEIGHT_CONSISTENCY * (row["consistency"] / max_consistency) * 100.0)
|
|
+ (MONTHLY_RANK_WEIGHT_ACTIVITY * (row["activity"] / max_activity) * 100.0)
|
|
+ (MONTHLY_RANK_WEIGHT_CONFIDENCE * (row["confidence"] / max_confidence) * 100.0)
|
|
- row["penalty_points"],
|
|
3,
|
|
)
|
|
rankings.append(row)
|
|
return rankings
|
|
|
|
|
|
def _build_historical_source_policy_for_elo() -> dict[str, object]:
|
|
if get_historical_data_source_kind() != SOURCE_KIND_RCON:
|
|
return build_source_policy(
|
|
primary_source=SOURCE_KIND_PUBLIC_SCOREBOARD,
|
|
selected_source=SOURCE_KIND_PUBLIC_SCOREBOARD,
|
|
source_attempts=[build_source_attempt(source=SOURCE_KIND_PUBLIC_SCOREBOARD, role="primary", status="success")],
|
|
)
|
|
return build_source_policy(
|
|
primary_source=SOURCE_KIND_RCON,
|
|
selected_source="hybrid-rcon-competitive-plus-public-scoreboard",
|
|
fallback_used=True,
|
|
fallback_reason="rcon-competitive-context-primary-but-player-stats-still-require-public-scoreboard-supplement",
|
|
source_attempts=[
|
|
build_source_attempt(
|
|
source=SOURCE_KIND_RCON,
|
|
role="primary",
|
|
status="partial",
|
|
reason="rcon-competitive-context-used-for-match-coverage-and-quality",
|
|
),
|
|
build_source_attempt(
|
|
source=SOURCE_KIND_PUBLIC_SCOREBOARD,
|
|
role="supplemental-fallback",
|
|
status="success",
|
|
reason="public-scoreboard-still-provides-player-level-competitive-stats",
|
|
),
|
|
],
|
|
)
|
|
|
|
|
|
def _resolve_match_duration(
|
|
match_group: dict[str, object],
|
|
players: list[dict[str, object]],
|
|
*,
|
|
rcon_match_context: dict[str, object] | None = None,
|
|
) -> tuple[int, str]:
|
|
if rcon_match_context and int(rcon_match_context.get("duration_seconds") or 0) > 0:
|
|
return int(rcon_match_context["duration_seconds"]), CAPABILITY_APPROXIMATE
|
|
started_at = _parse_optional_timestamp(match_group.get("started_at"))
|
|
ended_at = _parse_optional_timestamp(match_group.get("ended_at"))
|
|
if started_at and ended_at and ended_at >= started_at:
|
|
return int((ended_at - started_at).total_seconds()), CAPABILITY_EXACT
|
|
return max((_safe_int(player.get("time_seconds")) for player in players), default=0), CAPABILITY_APPROXIMATE
|
|
|
|
|
|
def _build_quality_factor(*, player_count: int, duration_seconds: int, has_score: bool) -> float:
|
|
player_component = min(1.0, player_count / FULL_QUALITY_PLAYER_COUNT)
|
|
duration_component = min(1.0, duration_seconds / FULL_QUALITY_DURATION_SECONDS)
|
|
score_component = 1.0 if has_score else 0.7
|
|
return round((0.4 * player_component) + (0.4 * duration_component) + (0.2 * score_component), 3)
|
|
|
|
|
|
def _build_actual_result(
|
|
*,
|
|
team_outcome: str,
|
|
allied_score: int | None,
|
|
axis_score: int | None,
|
|
participation_ratio: float,
|
|
) -> float:
|
|
if team_outcome == "draw":
|
|
base_result = 0.5
|
|
elif team_outcome == "win":
|
|
base_result = 1.0
|
|
else:
|
|
base_result = 0.0
|
|
if allied_score is None or axis_score is None:
|
|
margin_adjustment = 0.0
|
|
else:
|
|
total_score = max(1, allied_score + axis_score)
|
|
margin_ratio = abs(allied_score - axis_score) / total_score
|
|
margin_adjustment = min(0.08, margin_ratio * 0.12)
|
|
if team_outcome == "win":
|
|
adjusted = min(1.0, base_result + margin_adjustment)
|
|
elif team_outcome == "loss":
|
|
adjusted = max(0.0, base_result - margin_adjustment)
|
|
else:
|
|
adjusted = base_result
|
|
return round(0.5 + ((adjusted - 0.5) * participation_ratio), 4)
|
|
|
|
|
|
def _build_weighted_modifier_index(
|
|
*,
|
|
left_value: float,
|
|
right_value: float,
|
|
left_weight: float,
|
|
right_weight: float,
|
|
) -> float:
|
|
total_weight = max(0.001, left_weight + right_weight)
|
|
return round(((left_value * left_weight) + (right_value * right_weight)) / total_weight, 3)
|
|
|
|
|
|
def _build_centered_modifier_edge(index_value: float, *, participation_ratio: float) -> float:
|
|
centered = (index_value - 50.0) / 50.0
|
|
return round(max(-1.0, min(1.0, centered * participation_ratio)), 4)
|
|
|
|
|
|
def _build_participation_ratio(*, time_seconds: int, duration_seconds: int) -> float:
|
|
if duration_seconds <= 0:
|
|
return 0.0
|
|
return round(min(1.0, max(0.0, time_seconds / duration_seconds)), 3)
|
|
|
|
|
|
def _is_player_match_eligible(*, time_seconds: int, participation_ratio: float) -> bool:
|
|
return (
|
|
time_seconds >= MIN_VALID_PLAYER_PARTICIPATION_SECONDS
|
|
and participation_ratio >= MIN_VALID_PLAYER_PARTICIPATION_RATIO
|
|
)
|
|
|
|
|
|
def _build_outcome_score(*, team_outcome: str, allied_score: int | None, axis_score: int | None) -> float:
|
|
if allied_score is None or axis_score is None:
|
|
return 50.0 if team_outcome == "draw" else 65.0 if team_outcome == "win" else 35.0
|
|
total_score = max(1, allied_score + axis_score)
|
|
margin_ratio = abs(allied_score - axis_score) / total_score
|
|
if team_outcome == "draw":
|
|
return 50.0
|
|
if team_outcome == "win":
|
|
return round(min(100.0, 68.0 + (margin_ratio * 32.0)), 3)
|
|
return round(max(0.0, 32.0 - (margin_ratio * 32.0)), 3)
|
|
|
|
|
|
def _resolve_opponent_average_rating(
|
|
*,
|
|
stable_player_key: str,
|
|
team_side: str,
|
|
players: list[dict[str, object]],
|
|
rating_before_by_player: dict[str, float],
|
|
) -> float:
|
|
normalized_team_side = str(team_side or "").strip().lower()
|
|
opponent_ratings = [
|
|
rating_before_by_player.get(str(player["stable_player_key"]), DEFAULT_BASE_MMR)
|
|
for player in players
|
|
if str(player["stable_player_key"]) != stable_player_key
|
|
and _is_same_team(str(player.get("team_side") or ""), normalized_team_side) is False
|
|
]
|
|
if not opponent_ratings:
|
|
return DEFAULT_BASE_MMR
|
|
return round(sum(opponent_ratings) / len(opponent_ratings), 3)
|
|
|
|
|
|
def _build_strength_of_schedule_match(
|
|
*,
|
|
stable_player_key: str,
|
|
team_side: str,
|
|
players: list[dict[str, object]],
|
|
rating_before_by_player: dict[str, float],
|
|
quality_factor: float,
|
|
) -> float:
|
|
opponent_average = _resolve_opponent_average_rating(
|
|
stable_player_key=stable_player_key,
|
|
team_side=team_side,
|
|
players=players,
|
|
rating_before_by_player=rating_before_by_player,
|
|
)
|
|
mmr_pressure = 50.0 + ((opponent_average - DEFAULT_BASE_MMR) / 8.0)
|
|
quality_pressure = quality_factor * 35.0
|
|
return round(min(100.0, max(0.0, mmr_pressure + quality_pressure)), 3)
|
|
|
|
|
|
def _build_expected_result(*, player_rating: float, opponent_average_rating: float) -> float:
|
|
exponent = (opponent_average_rating - player_rating) / 400.0
|
|
return round(1.0 / (1.0 + (10.0**exponent)), 4)
|
|
|
|
|
|
def _build_monthly_eligibility_reason(
|
|
*,
|
|
valid_match_count: int,
|
|
total_time_seconds: int,
|
|
avg_participation_ratio: float,
|
|
) -> str | None:
|
|
if valid_match_count < MONTHLY_MIN_VALID_MATCHES:
|
|
return "minimum-valid-matches-not-met"
|
|
if total_time_seconds < MONTHLY_MIN_TIME_SECONDS:
|
|
return "minimum-playtime-not-met"
|
|
if avg_participation_ratio < MONTHLY_MIN_AVG_PARTICIPATION_RATIO:
|
|
return "minimum-participation-ratio-not-met"
|
|
return None
|
|
|
|
|
|
def _classify_quality_bucket(quality_factor: float) -> str:
|
|
if quality_factor >= 0.8:
|
|
return QUALITY_BUCKET_HIGH
|
|
if quality_factor >= 0.55:
|
|
return QUALITY_BUCKET_MEDIUM
|
|
return QUALITY_BUCKET_LOW
|
|
|
|
|
|
def _resolve_team_outcome(*, team_side: str, allied_score: int | None, axis_score: int | None) -> str:
|
|
if allied_score is None or axis_score is None or allied_score == axis_score:
|
|
return "draw"
|
|
normalized = team_side.strip().lower()
|
|
allied_won = allied_score > axis_score
|
|
if normalized.startswith("all"):
|
|
return "win" if allied_won else "loss"
|
|
if normalized.startswith("ax"):
|
|
return "win" if not allied_won else "loss"
|
|
return "draw"
|
|
|
|
|
|
def _is_same_team(team_side: str, normalized_team_side: str) -> bool:
|
|
candidate = team_side.strip().lower()
|
|
if normalized_team_side.startswith("all"):
|
|
return candidate.startswith("all")
|
|
if normalized_team_side.startswith("ax"):
|
|
return candidate.startswith("ax")
|
|
return candidate == normalized_team_side
|
|
|
|
|
|
def _resolve_role_bucket(player: dict[str, object]) -> str:
|
|
axes = {
|
|
ROLE_BUCKET_SUPPORT: _safe_int(player.get("support")),
|
|
ROLE_BUCKET_OFFENSE: _safe_int(player.get("offense")),
|
|
ROLE_BUCKET_DEFENSE: _safe_int(player.get("defense")),
|
|
ROLE_BUCKET_COMBAT: _safe_int(player.get("combat")),
|
|
}
|
|
top_bucket, top_value = max(axes.items(), key=lambda item: item[1])
|
|
sorted_values = sorted(axes.values(), reverse=True)
|
|
if top_value <= 0 or (len(sorted_values) >= 2 and sorted_values[0] == sorted_values[1]):
|
|
return ROLE_BUCKET_GENERALIST
|
|
return top_bucket
|
|
|
|
|
|
def _build_consistency_score(rows: list[dict[str, object]]) -> float:
|
|
if len(rows) <= 1:
|
|
return 100.0 if rows else 0.0
|
|
values = [float(row["match_score"]) for row in rows]
|
|
average = sum(values) / len(values)
|
|
if average <= 0:
|
|
return 0.0
|
|
return round(100.0 * (1.0 - min(1.0, pstdev(values) / max(average, 1.0))), 3)
|
|
|
|
|
|
def _build_activity_score(rows: list[dict[str, object]], total_time_seconds: int) -> float:
|
|
match_component = min(1.0, len(rows) / MONTHLY_ACTIVITY_TARGET_MATCHES)
|
|
hour_component = min(1.0, (total_time_seconds / 3600.0) / MONTHLY_ACTIVITY_TARGET_HOURS)
|
|
return round(((0.6 * match_component) + (0.4 * hour_component)) * 100.0, 3)
|
|
|
|
|
|
def _normalize_scope_key(server_id: str | None) -> str:
|
|
normalized = str(server_id or SCOPE_ALL_SERVERS).strip()
|
|
return normalized or SCOPE_ALL_SERVERS
|
|
|
|
|
|
def _parse_optional_timestamp(value: object) -> datetime | None:
|
|
if not value:
|
|
return None
|
|
try:
|
|
parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
except ValueError:
|
|
return None
|
|
if parsed.tzinfo is None:
|
|
parsed = parsed.replace(tzinfo=timezone.utc)
|
|
return parsed.astimezone(timezone.utc)
|
|
|
|
|
|
def _safe_int(value: object) -> int:
|
|
try:
|
|
return int(value or 0)
|
|
except (TypeError, ValueError):
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|