korb 0.3.1__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. {korb-0.3.1 → korb-0.3.2}/CHANGELOG.md +21 -0
  2. {korb-0.3.1 → korb-0.3.2}/PKG-INFO +29 -10
  3. {korb-0.3.1 → korb-0.3.2}/README.md +28 -9
  4. {korb-0.3.1 → korb-0.3.2}/docs/coding-standard.md +5 -5
  5. {korb-0.3.1 → korb-0.3.2}/korb/__init__.py +1 -1
  6. {korb-0.3.1 → korb-0.3.2}/korb/__main__.py +41 -12
  7. korb-0.3.2/korb/backtest.py +150 -0
  8. korb-0.3.2/korb/model.py +420 -0
  9. korb-0.3.2/korb/predict.py +186 -0
  10. {korb-0.3.1 → korb-0.3.2}/pyproject.toml +5 -0
  11. korb-0.3.2/tests/test_backtest.py +111 -0
  12. {korb-0.3.1 → korb-0.3.2}/tests/test_cli.py +131 -0
  13. {korb-0.3.1 → korb-0.3.2}/tests/test_core.py +13 -0
  14. korb-0.3.2/tests/test_model.py +211 -0
  15. korb-0.3.2/tests/test_predict.py +124 -0
  16. {korb-0.3.1 → korb-0.3.2}/tests/test_schedule.py +21 -0
  17. {korb-0.3.1 → korb-0.3.2}/tests/test_standings.py +12 -0
  18. {korb-0.3.1 → korb-0.3.2}/tests/test_team.py +18 -0
  19. korb-0.3.1/korb/predict.py +0 -445
  20. korb-0.3.1/tests/test_predict.py +0 -285
  21. {korb-0.3.1 → korb-0.3.2}/.github/workflows/release.yml +0 -0
  22. {korb-0.3.1 → korb-0.3.2}/.github/workflows/test.yml +0 -0
  23. {korb-0.3.1 → korb-0.3.2}/.gitignore +0 -0
  24. {korb-0.3.1 → korb-0.3.2}/LICENSE +0 -0
  25. {korb-0.3.1 → korb-0.3.2}/files/.gitkeep +0 -0
  26. {korb-0.3.1 → korb-0.3.2}/korb/core.py +0 -0
  27. {korb-0.3.1 → korb-0.3.2}/korb/download.py +0 -0
  28. {korb-0.3.1 → korb-0.3.2}/korb/ergebnisse.py +0 -0
  29. {korb-0.3.1 → korb-0.3.2}/korb/py.typed +0 -0
  30. {korb-0.3.1 → korb-0.3.2}/korb/schedule.py +0 -0
  31. {korb-0.3.1 → korb-0.3.2}/korb/standings.py +0 -0
  32. {korb-0.3.1 → korb-0.3.2}/korb/team.py +0 -0
  33. {korb-0.3.1 → korb-0.3.2}/tests/__init__.py +0 -0
  34. {korb-0.3.1 → korb-0.3.2}/tests/conftest.py +0 -0
  35. {korb-0.3.1 → korb-0.3.2}/tests/fixtures/ergebnisse_minimal.html +0 -0
  36. {korb-0.3.1 → korb-0.3.2}/tests/fixtures/ergebnisse_nested.html +0 -0
  37. {korb-0.3.1 → korb-0.3.2}/tests/fixtures/spielplan_finalized.html +0 -0
  38. {korb-0.3.1 → korb-0.3.2}/tests/fixtures/spielplan_minimal.html +0 -0
  39. {korb-0.3.1 → korb-0.3.2}/tests/test_download.py +0 -0
  40. {korb-0.3.1 → korb-0.3.2}/tests/test_ergebnisse.py +0 -0
  41. {korb-0.3.1 → korb-0.3.2}/uv.lock +0 -0
@@ -4,6 +4,27 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [0.3.2] — 2026-10-25
8
+
9
+ ### Added
10
+
11
+ - `backtest` command with chronological winner accuracy, Brier score,
12
+ score MAE, and score RMSE.
13
+ - Per-game win probabilities in text and JSON predictions.
14
+ - Opponent-adjusted offensive and defensive ratings via weighted ridge
15
+ regression.
16
+ - Fitted home advantage with a 3% prior and fitted score volatility.
17
+
18
+ ### Changed
19
+
20
+ - Prediction ratings are now additive strength coefficients converted
21
+ to the multiplicative score model.
22
+ - Recent form uses opponent-adjusted residuals instead of raw scoring.
23
+ - Removed the hard three-game rating cutoff; ridge regularization
24
+ handles thin samples.
25
+ - Split the prediction code into `model.py`, `predict.py`, and
26
+ `backtest.py`.
27
+
7
28
  ## [0.3.1] — 2026-10-25
8
29
 
9
30
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: korb
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: A CLI toolkit for DBB basketball league analysis
5
5
  Project-URL: Homepage, https://github.com/malvavisc0/korb
6
6
  Project-URL: Repository, https://github.com/malvavisc0/korb
@@ -47,8 +47,10 @@ platform and provides standings, results, schedules, predictions, and more.
47
47
  - 🏀 **Team** — game-by-game results, sparklines, quality metrics
48
48
  - 📋 **Ergebnisse** — all completed game results with optional team filter
49
49
  - 📅 **Schedule** — with back-to-back detection, filtering, pending games
50
- - 🔮 **Predict** — efficiency-model-based final standings forecast
50
+ - 🔮 **Predict** — opponent-adjusted final standings forecast with
51
+ win probabilities
51
52
  - 🥇 **Top** — quick leaderboard with ASCII bar chart
53
+ - 🧪 **Backtest** — chronological holdout metrics for the prediction model
52
54
  - 📥 **Download** — fetch fresh HTML data directly from basketball-bund.net
53
55
  - 🔧 **`--json`** — machine-readable output for data commands (not `download`)
54
56
 
@@ -131,8 +133,19 @@ uv run korb --ligaid 12345 schedule --team "Hawks" --pending --b2b
131
133
 
132
134
  ```bash
133
135
  uv run korb --ligaid 12345 predict
136
+ uv run korb --ligaid 12345 --json predict
134
137
  ```
135
138
 
139
+ ### `backtest` — Evaluate the prediction model
140
+
141
+ ```bash
142
+ uv run korb --ligaid 12345 backtest
143
+ uv run korb --ligaid 12345 backtest --min-train 10
144
+ ```
145
+
146
+ Trains on each game's prior results only and reports winner accuracy,
147
+ Brier score, score MAE, and score RMSE.
148
+
136
149
  ### `top` — Quick leaderboard
137
150
 
138
151
  ```bash
@@ -152,18 +165,22 @@ uv run korb --ligaid 12345 top -n 5
152
165
 
153
166
  ## How Predictions Work
154
167
 
155
- The `predict` command estimates final standings using a **multiplicative efficiency model**:
168
+ The `predict` command fits opponent-adjusted ratings and converts them
169
+ into a **multiplicative efficiency model**:
156
170
 
157
171
  | Factor | How it works |
158
172
  |---|---|
159
- | **Offensive rating** | Team's scored points vs. league average (>1.0 = above avg) |
160
- | **Defensive rating** | Points allowed vs. league average (>1.0 = worse defense) |
173
+ | **Opponent adjustment** | Weighted ridge regression on point differentials |
174
+ | **Offensive rating** | Adjusted scoring strength relative to league average (>1.0 = above avg) |
175
+ | **Defensive rating** | Adjusted points allowed relative to league average (>1.0 = worse defense) |
161
176
  | **Recency weighting** | 60-day half-life — recent games count more |
162
- | **Recent form** | Last 5 games blended at 30% weight into ratings |
163
- | **Home advantage** | 3% scoring boost applied symmetrically |
177
+ | **Recent form** | Last 5 opponent-adjusted residuals blended at 30% weight |
178
+ | **Regularization** | Ridge shrinkage replaces the hard 3-game cutoff |
179
+ | **Home advantage** | Fitted from results with a 3% prior |
180
+ | **Win probability** | Normal margin model using fitted score volatility |
164
181
  | **B2B fatigue** | ≤36h between games → 5% offense/defense penalty |
165
- | **New teams** | <3 games → ratings blended toward league average |
166
- | **No draws** | Ties broken by home advantage (basketball has OT) |
182
+ | **No draws** | The higher win probability decides ties |
183
+ | **Backtest** | `backtest` evaluates the model chronologically |
167
184
 
168
185
  ## Data Notes
169
186
 
@@ -177,10 +194,12 @@ The `predict` command estimates final standings using a **multiplicative efficie
177
194
  korb/
178
195
  ├── __init__.py # Package marker & version
179
196
  ├── __main__.py # CLI entry point & command handlers
197
+ ├── backtest.py # Chronological model evaluation
180
198
  ├── core.py # Shared models, HTML parsing, utilities
181
199
  ├── download.py # HTTP downloader, headers, rate limiting
182
200
  ├── ergebnisse.py # Game results viewer & filter
183
- ├── predict.py # Multiplicative efficiency prediction model
201
+ ├── model.py # Opponent-adjusted ratings & score model
202
+ ├── predict.py # Final standings orchestration & output
184
203
  ├── schedule.py # HTML schedule parser & filters
185
204
  ├── standings.py # Standings calculator
186
205
  └── team.py # Team results viewer & metrics
@@ -21,8 +21,10 @@ platform and provides standings, results, schedules, predictions, and more.
21
21
  - 🏀 **Team** — game-by-game results, sparklines, quality metrics
22
22
  - 📋 **Ergebnisse** — all completed game results with optional team filter
23
23
  - 📅 **Schedule** — with back-to-back detection, filtering, pending games
24
- - 🔮 **Predict** — efficiency-model-based final standings forecast
24
+ - 🔮 **Predict** — opponent-adjusted final standings forecast with
25
+ win probabilities
25
26
  - 🥇 **Top** — quick leaderboard with ASCII bar chart
27
+ - 🧪 **Backtest** — chronological holdout metrics for the prediction model
26
28
  - 📥 **Download** — fetch fresh HTML data directly from basketball-bund.net
27
29
  - 🔧 **`--json`** — machine-readable output for data commands (not `download`)
28
30
 
@@ -105,8 +107,19 @@ uv run korb --ligaid 12345 schedule --team "Hawks" --pending --b2b
105
107
 
106
108
  ```bash
107
109
  uv run korb --ligaid 12345 predict
110
+ uv run korb --ligaid 12345 --json predict
108
111
  ```
109
112
 
113
+ ### `backtest` — Evaluate the prediction model
114
+
115
+ ```bash
116
+ uv run korb --ligaid 12345 backtest
117
+ uv run korb --ligaid 12345 backtest --min-train 10
118
+ ```
119
+
120
+ Trains on each game's prior results only and reports winner accuracy,
121
+ Brier score, score MAE, and score RMSE.
122
+
110
123
  ### `top` — Quick leaderboard
111
124
 
112
125
  ```bash
@@ -126,18 +139,22 @@ uv run korb --ligaid 12345 top -n 5
126
139
 
127
140
  ## How Predictions Work
128
141
 
129
- The `predict` command estimates final standings using a **multiplicative efficiency model**:
142
+ The `predict` command fits opponent-adjusted ratings and converts them
143
+ into a **multiplicative efficiency model**:
130
144
 
131
145
  | Factor | How it works |
132
146
  |---|---|
133
- | **Offensive rating** | Team's scored points vs. league average (>1.0 = above avg) |
134
- | **Defensive rating** | Points allowed vs. league average (>1.0 = worse defense) |
147
+ | **Opponent adjustment** | Weighted ridge regression on point differentials |
148
+ | **Offensive rating** | Adjusted scoring strength relative to league average (>1.0 = above avg) |
149
+ | **Defensive rating** | Adjusted points allowed relative to league average (>1.0 = worse defense) |
135
150
  | **Recency weighting** | 60-day half-life — recent games count more |
136
- | **Recent form** | Last 5 games blended at 30% weight into ratings |
137
- | **Home advantage** | 3% scoring boost applied symmetrically |
151
+ | **Recent form** | Last 5 opponent-adjusted residuals blended at 30% weight |
152
+ | **Regularization** | Ridge shrinkage replaces the hard 3-game cutoff |
153
+ | **Home advantage** | Fitted from results with a 3% prior |
154
+ | **Win probability** | Normal margin model using fitted score volatility |
138
155
  | **B2B fatigue** | ≤36h between games → 5% offense/defense penalty |
139
- | **New teams** | <3 games → ratings blended toward league average |
140
- | **No draws** | Ties broken by home advantage (basketball has OT) |
156
+ | **No draws** | The higher win probability decides ties |
157
+ | **Backtest** | `backtest` evaluates the model chronologically |
141
158
 
142
159
  ## Data Notes
143
160
 
@@ -151,10 +168,12 @@ The `predict` command estimates final standings using a **multiplicative efficie
151
168
  korb/
152
169
  ├── __init__.py # Package marker & version
153
170
  ├── __main__.py # CLI entry point & command handlers
171
+ ├── backtest.py # Chronological model evaluation
154
172
  ├── core.py # Shared models, HTML parsing, utilities
155
173
  ├── download.py # HTTP downloader, headers, rate limiting
156
174
  ├── ergebnisse.py # Game results viewer & filter
157
- ├── predict.py # Multiplicative efficiency prediction model
175
+ ├── model.py # Opponent-adjusted ratings & score model
176
+ ├── predict.py # Final standings orchestration & output
158
177
  ├── schedule.py # HTML schedule parser & filters
159
178
  ├── standings.py # Standings calculator
160
179
  └── team.py # Team results viewer & metrics
@@ -126,9 +126,9 @@ Write the simplest correct solution. Delete anything that isn't needed.
126
126
  `set_defaults(func=cmd_<name>)`. Global flags are `--results`,
127
127
  `--schedule`, `--json`, `--ligaid`, `--download`.
128
128
  - Feature modules sit next to the CLI (`standings.py`, `team.py`,
129
- `schedule.py`, `predict.py`, `ergebnisse.py`, `download.py`); shared
130
- models, HTML parsing, date/score helpers, and output headers live in
131
- `core.py`.
129
+ `schedule.py`, `model.py`, `predict.py`, `backtest.py`,
130
+ `ergebnisse.py`, `download.py`); shared models, HTML parsing,
131
+ date/score helpers, and output headers live in `core.py`.
132
132
  - HTML parsing extends the shared public `DBBRowParser`
133
133
  (`<td>`/`<tr>`/`<strike>` state machine). Parser changes need a
134
134
  fixture in `tests/fixtures/` and a test; cover cancelled rows
@@ -136,8 +136,8 @@ Write the simplest correct solution. Delete anything that isn't needed.
136
136
  - Parsers return `(data, LeagueInfo)`. Every data command's `--json`
137
137
  response is a wrapped object built with `_liga_meta(...)` plus a
138
138
  named data key: `standings`, `results` (team), `ergebnisse`,
139
- `schedule`, `predictions`, and `top`. `download` is a side-effect
140
- command and emits no JSON.
139
+ `schedule`, `predictions`, `backtest`, and `top`. `download` is a
140
+ side-effect command and emits no JSON.
141
141
  - Data paths resolve as `files/<ligaid>/ergebnisse.html` and
142
142
  `files/<ligaid>/spielplan.html`; explicit `--results` / `--schedule`
143
143
  win. Use `_resolve_results` / `_resolve_schedule`, not ad hoc paths.
@@ -3,4 +3,4 @@
3
3
  Target: DBB Version ≤12.01.1-f733da5 (legacy JSP platform).
4
4
  """
5
5
 
6
- __version__ = "0.3.1"
6
+ __version__ = "0.3.2"
@@ -10,10 +10,17 @@ import sys
10
10
  from pathlib import Path
11
11
 
12
12
  from . import __version__
13
+ from .backtest import DEFAULT_MIN_TRAIN, backtest, print_backtest
13
14
  from .core import LeagueInfo, read_games
14
15
  from .download import discover_league_ids, download_league, wait_between_requests
15
16
  from .ergebnisse import filter_ergebnisse, print_ergebnisse
16
- from .predict import predict_standings, print_predicted_standings, print_predictions
17
+ from .predict import (
18
+ PredictionError,
19
+ format_prediction,
20
+ predict_standings,
21
+ print_predicted_standings,
22
+ print_predictions,
23
+ )
17
24
  from .schedule import b2b_flags, filter_schedule, parse_schedule, print_schedule
18
25
  from .standings import Standing, calculate_standings, print_table
19
26
  from .team import get_team_results, print_bars, print_metrics, print_results
@@ -174,7 +181,11 @@ def cmd_predict(args: argparse.Namespace) -> None:
174
181
  rp = _resolve_results(args, "prediction results")
175
182
  sp = _resolve_schedule(args, "prediction schedule")
176
183
 
177
- standings, preds, league_info = predict_standings(rp, sp)
184
+ try:
185
+ standings, preds, league_info = predict_standings(rp, sp)
186
+ except PredictionError as exc:
187
+ print(f"Error: {exc}", file=sys.stderr)
188
+ sys.exit(1)
178
189
  if not preds:
179
190
  print(
180
191
  "Error: Season is finalized (no pending games). "
@@ -186,16 +197,7 @@ def cmd_predict(args: argparse.Namespace) -> None:
186
197
  _json_out(
187
198
  {
188
199
  **_liga_meta(league_info, args.ligaid),
189
- "predictions": [
190
- {
191
- "home": g.home,
192
- "away": g.away,
193
- "winner": w,
194
- "home_score": hs,
195
- "away_score": aws,
196
- }
197
- for g, w, hs, aws in preds
198
- ],
200
+ "predictions": [format_prediction(pred) for pred in preds],
199
201
  "standings": [s.to_dict() for s in standings],
200
202
  }
201
203
  )
@@ -204,6 +206,21 @@ def cmd_predict(args: argparse.Namespace) -> None:
204
206
  print_predicted_standings(standings)
205
207
 
206
208
 
209
+ def cmd_backtest(args: argparse.Namespace) -> None:
210
+ """Handle 'backtest' subcommand."""
211
+ fp = _resolve_results(args, "backtest")
212
+ result, league_info = backtest(fp, min_train_games=args.min_train)
213
+ if args.json:
214
+ _json_out(
215
+ {
216
+ **_liga_meta(league_info, args.ligaid),
217
+ "backtest": result.to_dict(),
218
+ }
219
+ )
220
+ else:
221
+ print_backtest(result)
222
+
223
+
207
224
  def _blocks(value: int, scale: int) -> str:
208
225
  return "█" * max(0, value // scale)
209
226
 
@@ -404,6 +421,18 @@ def _build_parser() -> argparse.ArgumentParser:
404
421
  )
405
422
  p_pr.set_defaults(func=cmd_predict)
406
423
 
424
+ p_bt = subs.add_parser(
425
+ "backtest",
426
+ help="Backtest the prediction model on completed games",
427
+ )
428
+ p_bt.add_argument(
429
+ "--min-train",
430
+ type=_positive_int,
431
+ default=DEFAULT_MIN_TRAIN,
432
+ help=f"Minimum prior games before evaluating (default: {DEFAULT_MIN_TRAIN})",
433
+ )
434
+ p_bt.set_defaults(func=cmd_backtest)
435
+
407
436
  p_top = subs.add_parser(
408
437
  "top",
409
438
  help="Show top teams from standings",
@@ -0,0 +1,150 @@
1
+ """Chronological backtesting for the prediction model."""
2
+
3
+ from dataclasses import dataclass
4
+ from math import sqrt
5
+
6
+ from korb.core import Game, LeagueInfo, read_games
7
+ from korb.model import (
8
+ FATIGUE_THRESHOLD_H,
9
+ Winner,
10
+ fatigue,
11
+ fit_model,
12
+ game_times,
13
+ predict_game,
14
+ )
15
+
16
+ DEFAULT_MIN_TRAIN = 5
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class BacktestResult:
21
+ """Chronological holdout metrics for the prediction model."""
22
+
23
+ games: int
24
+ winner_games: int
25
+ winner_accuracy: float
26
+ brier: float
27
+ score_mae: float
28
+ score_rmse: float
29
+
30
+ def to_dict(self) -> dict[str, object]:
31
+ """Serializable metrics."""
32
+ return {
33
+ "games": self.games,
34
+ "winner_games": self.winner_games,
35
+ "winner_accuracy": round(self.winner_accuracy, 4),
36
+ "brier": round(self.brier, 4),
37
+ "score_mae": round(self.score_mae, 2),
38
+ "score_rmse": round(self.score_rmse, 2),
39
+ }
40
+
41
+
42
+ def _actual_home_win(game: Game) -> float | None:
43
+ if game.home_score > game.away_score:
44
+ return 1.0
45
+ if game.home_score < game.away_score:
46
+ return 0.0
47
+ return None
48
+
49
+
50
+ def _predict_historical(
51
+ game: Game, prior: list[Game]
52
+ ) -> tuple[Winner, int, int, float] | None:
53
+ model = fit_model(prior)
54
+ if game.home not in model.ratings or game.away not in model.ratings:
55
+ return None
56
+ times = game_times(prior)
57
+ timestamp = game.date.timestamp()
58
+ threshold_s = FATIGUE_THRESHOLD_H * 3600
59
+ return predict_game(
60
+ game.home,
61
+ game.away,
62
+ model.ratings,
63
+ model.league_avg,
64
+ home_fatigue=fatigue(game.home, timestamp, times, threshold_s),
65
+ away_fatigue=fatigue(game.away, timestamp, times, threshold_s),
66
+ home_advantage=model.home_advantage,
67
+ sigma=model.sigma,
68
+ )
69
+
70
+
71
+ def backtest(
72
+ results_path: str,
73
+ min_train_games: int = DEFAULT_MIN_TRAIN,
74
+ ) -> tuple[BacktestResult, LeagueInfo]:
75
+ """Evaluate the model chronologically on completed games."""
76
+ games, league_info = read_games(results_path)
77
+ games = sorted(
78
+ (game for game in games if not game.forfeit),
79
+ key=lambda game: game.date,
80
+ )
81
+
82
+ evaluated = 0
83
+ winner_games = 0
84
+ correct = 0
85
+ brier = 0.0
86
+ absolute_errors: list[float] = []
87
+ squared_errors: list[float] = []
88
+
89
+ for index, game in enumerate(games):
90
+ prior = games[:index]
91
+ if len(prior) < min_train_games:
92
+ continue
93
+ prediction = _predict_historical(game, prior)
94
+ if prediction is None:
95
+ continue
96
+ winner, home_score, away_score, home_probability = prediction
97
+
98
+ evaluated += 1
99
+ absolute_errors.extend(
100
+ [
101
+ abs(home_score - game.home_score),
102
+ abs(away_score - game.away_score),
103
+ ]
104
+ )
105
+ squared_errors.extend(
106
+ [
107
+ float((home_score - game.home_score) ** 2),
108
+ float((away_score - game.away_score) ** 2),
109
+ ]
110
+ )
111
+
112
+ actual_home = _actual_home_win(game)
113
+ if actual_home is not None:
114
+ winner_games += 1
115
+ correct += int((winner == "home") == (actual_home == 1.0))
116
+ brier += (home_probability - actual_home) ** 2
117
+
118
+ if not evaluated:
119
+ return BacktestResult(0, 0, 0.0, 0.0, 0.0, 0.0), league_info
120
+
121
+ return (
122
+ BacktestResult(
123
+ games=evaluated,
124
+ winner_games=winner_games,
125
+ winner_accuracy=correct / winner_games if winner_games else 0.0,
126
+ brier=brier / winner_games if winner_games else 0.0,
127
+ score_mae=sum(absolute_errors) / len(absolute_errors),
128
+ score_rmse=sqrt(sum(squared_errors) / len(squared_errors)),
129
+ ),
130
+ league_info,
131
+ )
132
+
133
+
134
+ def print_backtest(result: BacktestResult) -> None:
135
+ """Print backtest metrics."""
136
+ print("\n" + "=" * 70)
137
+ print(" Prediction Backtest")
138
+ print("=" * 70)
139
+
140
+ if result.games == 0:
141
+ print(" Not enough completed games to backtest.")
142
+ return
143
+
144
+ print(f" Games evaluated: {result.games}")
145
+ print(
146
+ f" Winner accuracy: {result.winner_accuracy:.1%} ({result.winner_games} games)"
147
+ )
148
+ print(f" Brier score: {result.brier:.4f}")
149
+ print(f" Score MAE: {result.score_mae:.2f}")
150
+ print(f" Score RMSE: {result.score_rmse:.2f}")