How We Score (per-game model)

An analytical model built for fun and discussion — not advice. Every number is a probability with real uncertainty, not a prediction.

How the Forecasts Work — Simulation Methodology

A technical explainer for readers who want to know how we arrive at the numbers on the league, tournament, and parity pages — championship odds, playoff-qualification probabilities, and projected final standings.

What this is, and what it isn't. This is an analytical model built for insight and conversation — "how might the season play out, and how sure are we?" It is not a betting tool or a decision-making system. Youth hockey is high-variance and our data is thin (about 34 games per team in a season), so every number here is a genuine probability with real uncertainty around it, not a prediction of what will happen. We've gone out of our way to make the numbers honest about how unsure they are; please read them in that spirit.


1. The question the simulation answers

Given the games played so far, we want the distribution of ways the rest of the season could go:

There is no formula for these. The only honest way to compute them is to play the remaining games thousands of times and count how often each thing happens. That's a Monte Carlo simulation.

2. The two kinds of uncertainty (this is the important part)

A good forecast has to account for two separate sources of randomness, and getting this right is the difference between an honest number and an overconfident one.

  1. Game randomness ("aleatoric"). Even if we knew exactly how good two teams were, hockey games are noisy — the better team doesn't always win. A 3.8-goals-per-team league has a lot of bounce.
  2. Rating uncertainty ("epistemic"). We don't actually know how good each team is. We estimate it from ~34 games, which is not many. The team that looks best on paper might genuinely be the best — or might just have had a friendly schedule and good luck.

A simulation that only models the first kind looks far more confident than it should. It effectively says "assume our power ratings are exactly correct, now roll the dice" — and the ratings are not exactly correct. We model both.

3. The pipeline, end to end

Step 1 — Rate the teams (Dixon-Coles)

We fit a Dixon-Coles model, the standard approach for goal-scoring sports. It gives every team two numbers:

estimated jointly from every game's final score so that each team's rating is adjusted for the strength of the opponents it actually played. Beating strong teams counts for more than beating weak ones. From any two teams' ratings the model produces the full probability of every possible scoreline — which is what we need to simulate a game.

The ratings are shrunk toward the league average: with only ~34 games of evidence, raw estimates over-react to small samples, so we pull them gently toward "average" by an amount tuned and validated on five years of data. (Full detail: docs/model-research/03-shrinkage.md.)

Step 2 — Capture rating uncertainty (the bootstrap)

To represent the fact that we don't know the ratings, we don't fit them just once. We refit them many times on bootstrap resamples of the season — each resample draws the same number of games at random, with replacement, so each refit sees a slightly different version of the season and produces a slightly different set of ratings. The spread across these refits is our uncertainty about each team's true strength.

(In a future version these refits become draws from a Bayesian posterior, which is the same idea done more formally; the bootstrap is the current implementation. See docs/model-research/12-future-directions.md.)

Step 3 — Play the rest of the season, many times

For each rating sample from Step 2, we run a share of the Monte Carlo trials. In every trial:

We tally who finished where. Because Step 3 runs across many different rating samples from Step 2, the final tallies fold together both kinds of uncertainty at once: game-to-game randomness and our uncertainty about the ratings.

Step 4 — Turn tallies into probabilities

A team's championship probability is simply the fraction of all trials in which it won. Advancement and finish-position probabilities are counted the same way.

4. How many simulations, and how precise?

We run 50,000 trials per forecast, split across the rating samples. That's enough that the counting error is about ±0.2 percentage points — far smaller than the uncertainty that actually matters (the rating uncertainty from Step 2). In other words, we deliberately spend the computational budget where it counts: on exploring different plausible team-strength scenarios, not on grinding the coin-flip noise down to a meaningless extra decimal.

5. A worked illustration

To show why Step 2 matters, here is the title race in one division (2017 Platinum), computed two ways — first the old way (one fixed set of ratings), then the honest way (rating uncertainty propagated):

"Best team" (Young Guns) title odds How spread out the race looks
Fixed ratings only 86.8% concentrated
Rating uncertainty included 72.6% noticeably wider

The favorite is still the favorite — but "73%" is the honest number, and the chasers' real chances roughly double. The same mechanism now drives every championship and advancement number on the site. (Reproduce: PYTHONPATH=. uv run python -m scripts.model_lab.sim_uncertainty.)

6. How to read the numbers

7. Assumptions and limitations (in plain terms)

8. Reproducibility and where the code lives

Every simulation stores the random seed it used, so any published number can be regenerated exactly. The implementation is open in the repository:

Deeper reading