Moftah
Fit and validate World Cup forecast model
b38dafd
Raw
History Blame Contribute Delete
1.57 kB
from __future__ import annotations
import math
from underdog_lab.domain import Forecast, Outcome, UserForecast
def probabilities(forecast: Forecast | UserForecast) -> dict[Outcome, float]:
return {
"home": forecast.p_home,
"draw": forecast.p_draw,
"away": forecast.p_away,
}
def log_loss(forecast: Forecast | UserForecast, observed: Outcome) -> float:
probability = min(1.0 - 1e-15, max(1e-15, probabilities(forecast)[observed]))
return -math.log(probability)
def brier_score(forecast: Forecast | UserForecast, observed: Outcome) -> float:
probs = probabilities(forecast)
return sum(
(probability - (1.0 if outcome == observed else 0.0)) ** 2
for outcome, probability in probs.items()
)
# Conventional ordering for the Rank Probability Score: away win, draw, home
# win. RPS treats outcomes as ordered categories (a forecast that confuses
# "away win" for "draw" is penalised less than one that confuses it for
# "home win"), which is standard practice for 1X2 football forecasts.
_RPS_ORDER: tuple[Outcome, ...] = ("away", "draw", "home")
def rank_probability_score(forecast: Forecast | UserForecast, observed: Outcome) -> float:
probs = probabilities(forecast)
cumulative_forecast = 0.0
cumulative_observed = 0.0
total = 0.0
for outcome in _RPS_ORDER:
cumulative_forecast += probs[outcome]
cumulative_observed += 1.0 if outcome == observed else 0.0
total += (cumulative_forecast - cumulative_observed) ** 2
return total / (len(_RPS_ORDER) - 1)