Spaces:
Running
Running
| 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) | |