Koddenbrock's picture
Leaderboard: earned-vs-possible view, gate group bonuses, fix group grid re-render
90c95e5
Raw
History Blame Contribute Delete
10 kB
"""Scoring rules for the WC 2026 prediction game."""
from __future__ import annotations
RULES: dict[str, int] = {
# Group stage
"group_match_correct": 1, # correct W/D/L outcome
"group_winner": 3, # correct group winner (1st place)
"group_runner_up": 2, # correct group 2nd place
# Knockout — points for correctly predicting the advancing team
"r32_correct": 2,
"r16_correct": 3,
"qf_correct": 4,
"sf_correct": 5,
"finalist_correct": 8, # reaching the final
"champion_correct": 15,
# Bonus
"both_finalists_correct": 5, # both finalists right (order-independent)
"exact_final_result": 3, # correct final score outcome (W for which team)
}
# Stage name → scoring key
_STAGE_KEY: dict[str, str] = {
"ROUND_OF_32": "r32_correct",
"ROUND_OF_16": "r16_correct",
"QUARTER_FINALS": "qf_correct",
"SEMI_FINALS": "sf_correct",
"THIRD_PLACE": "sf_correct", # same value as sf
"FINAL": "finalist_correct",
}
def compute_score(predictions: list[dict], results: list[dict]) -> dict:
"""Compute score for a single model/player's predictions.
Args:
predictions: list of prediction dicts with keys game_id, predicted_result,
home_team, away_team, stage, group_id.
results: list of actual result dicts with keys game_id, actual_result
(same encoding: 'A Win', 'Draw', 'B Win'), home_team, away_team,
winner (team name of advancing team for KO rounds).
Returns:
dict with keys 'total', 'breakdown' (dict of category -> points).
"""
if not results:
return {"total": 0, "breakdown": {k: 0 for k in RULES}}
results_by_id: dict[str, dict] = {r["game_id"]: r for r in results}
pred_by_id: dict[str, dict] = {p["game_id"]: p for p in predictions}
breakdown: dict[str, int] = {k: 0 for k in RULES}
for game_id, pred in pred_by_id.items():
actual = results_by_id.get(game_id)
if actual is None:
continue
stage = str(pred.get("stage", "")).upper()
if stage == "GROUP_STAGE":
if pred.get("predicted_result") == actual.get("actual_result"):
breakdown["group_match_correct"] += RULES["group_match_correct"]
else:
# Knockout: check if predicted advancing team matches actual winner
key = _STAGE_KEY.get(stage)
if key:
pred_winner = _resolve_ko_winner(pred)
actual_winner = actual.get("winner", "")
if pred_winner and pred_winner == actual_winner:
breakdown[key] += RULES[key]
# Group standings bonuses
group_results = [r for r in results if str(r.get("stage", "")).upper() == "GROUP_STAGE"]
if group_results:
_score_group_standings(pred_by_id, results_by_id, group_results, breakdown)
# Final bonuses
final_results = [r for r in results if str(r.get("stage", "")).upper() == "FINAL"]
if final_results and final_results[0].get("game_id") in pred_by_id:
final_res = final_results[0]
final_pred = pred_by_id[final_res["game_id"]]
# Champion bonus
actual_champ = final_res.get("winner", "")
pred_champ = _resolve_ko_winner(final_pred)
if pred_champ == actual_champ:
breakdown["champion_correct"] += RULES["champion_correct"]
# Both finalists
actual_finalists = {final_res.get("home_team", ""), final_res.get("away_team", "")}
pred_finalists = {final_pred.get("home_team", ""), final_pred.get("away_team", "")}
if actual_finalists == pred_finalists:
breakdown["both_finalists_correct"] += RULES["both_finalists_correct"]
# Exact final result (which team wins)
if final_pred.get("predicted_result") == final_res.get("actual_result"):
breakdown["exact_final_result"] += RULES["exact_final_result"]
total = sum(breakdown.values())
return {"total": total, "breakdown": breakdown}
def _resolve_ko_winner(pred: dict) -> str:
"""Determine the predicted advancing team for a knockout match."""
result = pred.get("predicted_result", "")
if result == "A Win":
return str(pred.get("home_team", ""))
elif result == "B Win":
return str(pred.get("away_team", ""))
# Draw in KO → no clear winner prediction (could be penalties)
return ""
def _score_group_standings(
pred_by_id: dict[str, dict],
results_by_id: dict[str, dict],
group_results: list[dict],
breakdown: dict[str, int],
) -> None:
"""Award group winner/runner-up bonus points.
Only scores a group once *all* of its matches have been played, so partial
standings after 1-2 games never award a (premature) winner/runner-up bonus.
"""
# Expected matches per group (from the full prediction set) vs. played so far.
expected_per_group: dict[str, int] = {}
for pred in pred_by_id.values():
if str(pred.get("stage", "")).upper() != "GROUP_STAGE":
continue
gid = pred.get("group_id", "")
if gid:
expected_per_group[gid] = expected_per_group.get(gid, 0) + 1
played_per_group: dict[str, int] = {}
for r in group_results:
gid = r.get("group_id", "")
if gid:
played_per_group[gid] = played_per_group.get(gid, 0) + 1
complete_groups = {
gid
for gid, expected in expected_per_group.items()
if expected > 0 and played_per_group.get(gid, 0) >= expected
}
# Collect actual group standings
actual_groups: dict[str, dict[str, int]] = {}
for r in group_results:
gid = r.get("group_id", "")
if not gid:
continue
for team_key in ("home_team", "away_team"):
team = r.get(team_key, "")
if team:
actual_groups.setdefault(gid, {})[team] = actual_groups.get(gid, {}).get(team, 0)
actual_result = r.get("actual_result", "")
home = r.get("home_team", "")
away = r.get("away_team", "")
if actual_result == "A Win":
actual_groups[gid][home] = actual_groups[gid].get(home, 0) + 3
elif actual_result == "Draw":
actual_groups[gid][home] = actual_groups[gid].get(home, 0) + 1
actual_groups[gid][away] = actual_groups[gid].get(away, 0) + 1
elif actual_result == "B Win":
actual_groups[gid][away] = actual_groups[gid].get(away, 0) + 3
# Collect predicted group standings from predictions
pred_groups: dict[str, dict[str, int]] = {}
for game_id, pred in pred_by_id.items():
if str(pred.get("stage", "")).upper() != "GROUP_STAGE":
continue
gid = pred.get("group_id", "")
if not gid:
continue
pred_result = pred.get("predicted_result", "")
home = pred.get("home_team", "")
away = pred.get("away_team", "")
if home:
pred_groups.setdefault(gid, {})[home] = pred_groups.get(gid, {}).get(home, 0)
if away:
pred_groups.setdefault(gid, {})[away] = pred_groups.get(gid, {}).get(away, 0)
if pred_result == "A Win":
pred_groups[gid][home] = pred_groups[gid].get(home, 0) + 3
elif pred_result == "Draw":
pred_groups[gid][home] = pred_groups[gid].get(home, 0) + 1
pred_groups[gid][away] = pred_groups[gid].get(away, 0) + 1
elif pred_result == "B Win":
pred_groups[gid][away] = pred_groups[gid].get(away, 0) + 3
for gid in actual_groups:
if gid not in pred_groups or gid not in complete_groups:
continue
actual_sorted = sorted(actual_groups[gid], key=lambda t: -actual_groups[gid][t])
pred_sorted = sorted(pred_groups[gid], key=lambda t: -pred_groups[gid].get(t, 0))
if len(actual_sorted) >= 1 and len(pred_sorted) >= 1:
if actual_sorted[0] == pred_sorted[0]:
breakdown["group_winner"] += RULES["group_winner"]
if len(actual_sorted) >= 2 and len(pred_sorted) >= 2:
if actual_sorted[1] == pred_sorted[1]:
breakdown["group_runner_up"] += RULES["group_runner_up"]
def compute_all_scores(
all_predictions: dict[str, list[dict]],
results: list[dict],
) -> dict[str, dict]:
"""Compute scores for all models/players.
Args:
all_predictions: mapping of model_name → list of prediction dicts
results: list of actual result dicts
Returns:
mapping of model_name → score dict (total + breakdown)
"""
return {
model_name: compute_score(preds, results)
for model_name, preds in all_predictions.items()
}
def compute_max_possible(predictions_template: list[dict], results: list[dict]) -> dict:
"""Maximum score attainable given the matches played so far.
Builds an *oracle* from the full fixture template (so group-completeness gating
sees the true match count per group), overriding the predicted outcome of every
played match to the actual result / advancing team. The total is the ceiling any
model could have reached, counting group winner/runner-up bonuses only once a
group is complete.
"""
if not results:
return {"total": 0, "breakdown": {k: 0 for k in RULES}}
def _oracle_result(r: dict) -> str:
actual_result = r.get("actual_result", "")
if actual_result:
return actual_result
winner = r.get("winner", "")
if winner == r.get("home_team", ""):
return "A Win"
if winner == r.get("away_team", ""):
return "B Win"
return ""
override = {r.get("game_id"): _oracle_result(r) for r in results}
oracle: list[dict] = []
for fixture in predictions_template:
gid = fixture.get("game_id")
oracle.append({**fixture, "predicted_result": override.get(gid, fixture.get("predicted_result", ""))})
return compute_score(oracle, results)