Spaces:
Running
Running
| from __future__ import annotations | |
| import math | |
| from underdog_lab.domain import Forecast, Outcome | |
| OUTCOMES: tuple[Outcome, ...] = ("home", "draw", "away") | |
| def apply_vector_scaling(forecast: Forecast, parameters: list[float]) -> Forecast: | |
| if len(parameters) != 5: | |
| raise ValueError("vector scaling requires five parameters") | |
| scales = parameters[:3] | |
| biases = (parameters[3], parameters[4], 0.0) | |
| probabilities = (forecast.p_home, forecast.p_draw, forecast.p_away) | |
| logits = [ | |
| scale * math.log(max(probability, 1e-15)) + bias | |
| for scale, probability, bias in zip(scales, probabilities, biases) | |
| ] | |
| maximum = max(logits) | |
| raw = [math.exp(value - maximum) for value in logits] | |
| total = sum(raw) | |
| data = forecast.model_dump() | |
| data.update( | |
| { | |
| "p_home": raw[0] / total, | |
| "p_draw": raw[1] / total, | |
| "p_away": raw[2] / total, | |
| } | |
| ) | |
| return Forecast(**data) | |
| def fit_vector_scaling( | |
| rows: list[tuple[Forecast, Outcome]], | |
| *, | |
| regularization: float, | |
| iterations: int = 350, | |
| learning_rate: float = 0.03, | |
| ) -> list[float]: | |
| if not rows: | |
| raise ValueError("at least one forecast row is required") | |
| parameters = [1.0, 1.0, 1.0, 0.0, 0.0] | |
| first = [0.0] * 5 | |
| second = [0.0] * 5 | |
| beta1 = 0.9 | |
| beta2 = 0.999 | |
| for step in range(1, iterations + 1): | |
| gradient = [0.0] * 5 | |
| for forecast, outcome in rows: | |
| calibrated = apply_vector_scaling(forecast, parameters) | |
| probs = (calibrated.p_home, calibrated.p_draw, calibrated.p_away) | |
| logs = ( | |
| math.log(max(forecast.p_home, 1e-15)), | |
| math.log(max(forecast.p_draw, 1e-15)), | |
| math.log(max(forecast.p_away, 1e-15)), | |
| ) | |
| target = OUTCOMES.index(outcome) | |
| differences = [ | |
| probability - (1.0 if index == target else 0.0) | |
| for index, probability in enumerate(probs) | |
| ] | |
| for index in range(3): | |
| gradient[index] += differences[index] * logs[index] | |
| gradient[3] += differences[0] | |
| gradient[4] += differences[1] | |
| count = len(rows) | |
| for index in range(5): | |
| center = 1.0 if index < 3 else 0.0 | |
| gradient[index] = ( | |
| gradient[index] / count | |
| + regularization * (parameters[index] - center) | |
| ) | |
| first[index] = beta1 * first[index] + (1 - beta1) * gradient[index] | |
| second[index] = ( | |
| beta2 * second[index] + (1 - beta2) * gradient[index] ** 2 | |
| ) | |
| corrected_first = first[index] / (1 - beta1**step) | |
| corrected_second = second[index] / (1 - beta2**step) | |
| parameters[index] -= ( | |
| learning_rate | |
| * corrected_first | |
| / (corrected_second**0.5 + 1e-8) | |
| ) | |
| return parameters | |