← Back to the league page

The Cool Guys · technical notes · last updated Oct 23, 2026

A model for 9-category head-to-head fantasy basketball

Abstract

The chances on the league page come from a model with six parts: player projections that combine stat forecasts with draft-market information, updating from this season's games, a model of who actually plays, adjustments for each game's opponent and missing teammates, a weekly engine that turns player totals into category and matchup chances, and a simulation of the rest of the season and the playoffs. Every part is checked the same way: its rules are fixed in advance, its numbers are estimated from one past NBA season, and it is judged on other seasons it never saw. Each part is included only because it made the predictions more accurate on all of them. This paper describes each part, the math behind it, and the evidence for it.

−37%
error from updating with this season's stats
−50%
error predicting games for injured players
−25%
error from using draft-market information
−5%
error in weekly matchup chances from the weekly engine

1. The problem

Each week two teams compare totals in nine categories: points, rebounds, assists, steals, blocks, three-pointers, turnovers (fewer is better), field-goal percentage and free-throw percentage. Winning at least 5 of the 9 wins the week. The top 8 teams by category record reach a three-round playoff in March, and the top three are paid $150, $90 and $60.

To give a chance for any of this, the model needs to know how much each player produces per game, how many games each player will actually play and start, how uncertain both are, and how those uncertainties add up across a roster, a week and a season.

2. Model overview

Figure 1 shows how the parts connect. Each step feeds the next; the output of step 5 is the weekly chance shown on the league page, and step 6 repeats steps 4–5 for every remaining week to produce season-long numbers.

1
Player projectionsPer-game stats for every player from ESPN's forecast and the player's history, adjusted with average draft position (§4.1).
↓
2
In-season updatingEach player's rates move toward what they have actually done this season, faster for stable stats (§4.2).
↓
3
AvailabilityHow often each player plays, how long current absences last, and who starts under the 10-player daily limit (§4.3).
↓
4
Game contextEach game adjusted for the opponent and for teammates who are out (§4.4).
↓
5
Weekly matchup engineTeam totals with uncertainty, nine category chances, then one matchup chance (§4.5).
↓
6
Season simulationThe real remaining schedule and playoff bracket simulated thousands of times (§4.6).
Figure 1. Structure of the model.

3. Methodology

3.1 Validation design

Every part of the model is held to the same standard.

  1. Rules fixed in advance. What will be measured, on which data, and what counts as a pass is written down before any results are computed. Results can't change the test.
  2. Estimated on one season, judged on others. Any numbers a part needs are estimated from one past season (usually 2024-25). The part is then judged on seasons that played no role in building it (usually 2025-26 and 2023-24). For the weekly engine, those seasons are replayed day by day with random 13-player rosters, and the model's chances are compared with what really happened.
  3. Better everywhere. A part is included only if it is more accurate on every test season, not better on one and worse on another. If it fails, it stays out.

3.2 Measuring accuracy

For any chance the model gives, the main score is the Brier score: the average squared gap between the chance and the result.

\[ \text{error} = \frac{1}{N}\sum_{i=1}^{N} (p_i - y_i)^2 \]

\(p_i\) is the chance given for matchup \(i\) (between 0 and 1), \(y_i\) is 1 if that team won and 0 if it lost, and \(N\) is the number of matchups. Saying 0.8 for a team that wins costs \((0.8-1)^2 = 0.04\); saying 0.8 for a team that loses costs \(0.64\). Always answering 0.5 scores 0.25, so lower than 0.25 means better than guessing.

Two further checks apply. Calibration: predictions are grouped into ranges (0–10%, 10–20%, …), and in each range with at least 200 predictions the average predicted chance must be within 4 percentage points of how often it happened. Not luck: for small improvements, the matchups are resampled 2,000 times at random (with repeats allowed) and the difference in error is recomputed each time; the middle 95% of those differences must sit entirely below zero. When several ideas are tested at once, the bar rises to the middle 99%, so that the chance of any one passing by luck stays around 5%.

Player projections are scored with a related measure, the CRPS, which rewards forecasts that are both close to the truth and honest about how uncertain they are. Games-played predictions are scored by squared error.

4. Components

4.1 Player projections and draft-market information

Per-game projections start from ESPN's forecast combined with the player's previous seasons. On their own these miss things that drafters know: an injury that isn't fully healed, a new role on a new team, a coach's plans. That knowledge shows up in ADP (average draft position, where a player typically goes in drafts). The model moves each player's projected value toward the value implied by their ADP:

\[ v = v_{\text{proj}} + \beta\,\big(v_{\text{market}} - v_{\text{proj}}\big) \]

\(v_{\text{proj}}\) is the value from the stat projection and \(v_{\text{market}}\) is the value of the player the projections rank at that ADP spot. \(\beta\) is how much to trust the market. Because the market is right more often in some seasons than others, \(\beta\) is not a single fixed number: each simulated season draws its own \(\beta\) from a range estimated across past seasons. The adjustment applies only to players with a real ADP; others keep their projection.

EvidencePlayer value predictions with market information had 25% less error on a season held out from every fitting step (7.02 → 5.29). Projection error (CRPS) is 7% lower than projections without market information, averaged over three seasons each left out of its own fit (1.564 → 1.457).

4.2 Updating with this season's games

As the season goes on, each player's expected per-game stats blend the projection with what they've actually done:

\[ \text{rate} = \frac{K \cdot \text{projection} + n \cdot \text{season average}}{K + n} \]

\(n\) is games played this season. \(K\) is the number of games at which real stats and the projection get equal weight, estimated per stat from past seasons: 9.5 games for points, 13.6 for rebounds, about 24 for steals and blocks. Steals and blocks are noisier from game to game, so real stats need longer to become trustworthy. The model's uncertainty about each rate also shrinks as \(n\) grows.

EvidenceFor rest-of-season points, the blend had 37% less error than the projection alone and 30% less than this season's average alone, on a season not used to estimate \(K\).

Traded players. After a trade, a player's role usually changes, so games with the old team say less about the future. The model finds trades from the NBA schedule (the games a player played stop matching one team's schedule and start matching another's) and counts each pre-trade game at a fraction \(w\) of a normal game:

\[ \text{rate} = \frac{K \cdot \text{projection} + w \sum_{\text{before}} x_g + \sum_{\text{after}} x_g}{K + w\, n_{\text{before}} + n_{\text{after}}}, \qquad w = 0.2 \]

\(x_g\) is the stat in game \(g\), and \(n_{\text{before}}\), \(n_{\text{after}}\) count games before and after the trade. \(w = 0.2\) was estimated from 2024-25. For players who were never traded nothing changes.

EvidencePer-game error for traded players fell on both test seasons and on 2021-22, with the 99% range entirely below zero. On 2024-25's 36 traded players, the points projection ten games after the trade was closer to what they actually did for 25 of them. Ignoring pre-trade games completely also helped, but not by enough to pass.

4.3 Availability: who plays and who starts

Share of games played. Each player's expected share of games blends the preseason estimate with this season's record, using the same formula as §4.2 with \(K = 34\) team games.

Current absences. For a player who is out (listed out by ESPN, or has missed 2 or more games in a row), the model uses how long absences actually last. Across 3,329 absences in three seasons, \(S(m)\) is the share that lasted at least \(m\) games (Figure 2):

\[ P(\text{still out at the } j\text{-th next game} \mid \text{out for } m \text{ games so far}) = \frac{S(m+j)}{S(m)} \]

With \(S(4) = 0.259\) and \(S(5) = 0.218\), a player who has missed 4 games misses the next one with chance \(0.218 / 0.259 = 84\%\). Absences that have already lasted a while tend to keep going.

Figure 2. How long absences last. Share of absences lasting at least this many games (3,329 absences over three seasons). About half end after one game; about one in four lasts 4 or more.
Show data

The next few games. A healthy player who just played is more likely to play the next game than their season average suggests, and this fades over the following games:

\[ P(\text{plays the } j\text{-th next game}) = \pi + (1-\pi)(1-\rho)^{j} \]

\(\pi\) is the player's usual share of games played and \(\rho = 0.361\), estimated from 2024-25.

Starting. Only 10 players can start per day. On days with more than 10 available players, the model simulates which 10 a manager would start, by position eligibility and value, and counts only those games.

Figure 3. Predicting the remaining games of players who are currently out (squared error, lower is better; 722 cases over three seasons). The model's availability component (bottom bar) against simpler alternatives.

EvidenceFor players currently out, the availability component halves the error of the preseason estimate (0.148 → 0.074) and beats it in every one of three test seasons; for all players it cuts error by 25%.

4.4 Game context

Opponent and pace. Each game is scaled by what that game's opponent has allowed this season, compared with what the same players usually produce:

\[ \mu_{\text{game}} = \mu \cdot f_o, \qquad f_o = 1 + \frac{n_o}{n_o + K}\left(\frac{A_o}{E_o} - 1\right) \]

\(A_o\) is the total of a stat that players have produced against opponent \(o\), \(E_o\) is what those players were expected to produce, and \(n_o\) is the number of games \(o\) has played. The weight \(n_o/(n_o+K)\) grows from 0 as the season goes on (Figure 4), so a few lopsided games barely count. \(K\) is 20 games for assists, steals and turnovers and 40 for the other stats. Field-goal percentage is adjusted the same way, as a change in makes per attempt; free-throw percentage is not adjusted.

Figure 4. Weight given to an opponent's record after \(n\) games. Early in the season the adjustment is small; by mid-season it carries close to half weight.

Teammates out. When a teammate who has been playing misses 2 or more games in a row, the players still available get a little more of the team's production:

\[ \mu_{\text{game}} = \mu\,(1 + \beta_s\, x), \qquad x = \frac{\text{minutes per game of absent teammates}}{240} \]

240 is the player-minutes in one game (5 players × 48 minutes). \(\beta_s\) is estimated per stat: 0.082 for points and 0.075 for assists. With a 30-minute player out, \(x = 0.125\), so a teammate's expected points rise by about 1%.

Figure 5. Effect of game context on matchup prediction error (negative is better), with 95% ranges from 2,000 resamples, on two test seasons. Both adjustments help on both seasons, alone and together.
Show data

EvidenceTogether the two adjustments lower matchup error by 0.2–0.4% on both test seasons, with every day of the week no worse. The effect is small because opponent records carry little weight early in the season.

4.5 Weekly matchup engine

For each team, the engine adds up every remaining game of every player (steps 2–4), keeping both the known results so far this week and the uncertainty in the rest. That gives an expected total \(\mu\) and a variance \(\sigma^2\) in each category. The chance team A wins a counting category is

\[ p_c = \Phi\!\left( \frac{\mu_A - \mu_B}{\kappa \sqrt{\sigma_A^2 + \sigma_B^2}} \right) \]

\(\Phi\) converts a gap measured in standard deviations into a probability: a gap of 0 gives 50%, a gap of one standard deviation gives 84%. \(\kappa = 1.041\) slightly widens the uncertainty so the chances aren't overconfident. For FG% and FT% the same formula is applied to each team's makes above league average per attempt.

The nine category chances then become one matchup chance. Categories are not independent: a strong week usually lifts points, threes and assists together, which makes lopsided results like 7–2 more common than independence would predict. The model adds this extra spread to the number of categories won:

\[ P(\text{win the week}) = 1 - \Phi\!\left( \frac{4.5 - \sum_c p_c}{\sqrt{(1+\psi)\sum_c p_c(1-p_c)}} \right) \]

\(\sum_c p_c\) is the expected number of categories won and \(\sum_c p_c(1-p_c)\) is how much that number would vary if categories were independent. \(\psi = 0.35\) adds 35% more variance, the amount estimated from 2024-25. Winning needs 5 of 9, so the cut-off is 4.5.

Figure 6. Matchup prediction error through the week. The full engine against a simpler one without short-term availability, the daily starting limit and category dependence, on two test seasons. Error falls as the week goes on because more of it is already known; the full engine is lower on every day of both seasons.
Show data

EvidenceAbout 5% less matchup error than the simpler engine on both test seasons (0.130 → 0.123 and 0.130 → 0.124), with calibration within 2.3 percentage points in every range. A stricter replay that gives the model only what was known on each day (projections plus games played so far) scores the current engine at 0.124 and 0.123, with category chances within 2 percentage points of reality in every range, so the result holds without any hindsight.

4.6 Season simulation and expected winnings

The rest of the season is simulated on the league's real schedule: the current week uses the weekly engine above, and each future week uses each team's projected weekly totals and their uncertainty. Uncertainty in a team's weekly total comes from three places:

\[ \sigma^2_{\text{team}} = \sum_i g_i\,\sigma_i^2 \;+\; \gamma \sum_i g_i\,\mu_i^2 \;+\; \rho_{tm}\!\!\sum_{\substack{i \neq j \\ \text{same NBA team}}}\!\! \sqrt{\gamma g_i}\,\mu_i \sqrt{\gamma g_j}\,\mu_j \]

\(g_i\) is player \(i\)'s expected games that week, \(\mu_i\) and \(\sigma_i^2\) their per-game average and game-to-game variance. The first term is game-to-game noise. The second is uncertainty in how many games they play (\(\gamma = 0.46\), measured). The third: teammates on the same NBA team share a schedule, so their games played rise and fall together (\(\rho_{tm} = 0.25\), measured at 0.245).

Each simulation plays out every remaining week, seeds the top 8 by category record into the playoff bracket, and plays the three playoff rounds using each roster's real games in those weeks. NBA teams with poor records tend to rest players in March, so for the playoff weeks each player's expected games are scaled by their team's record from the previous season:

\[ \text{games}_{\text{March}} = \text{games} \times \min\!\Big(1.05,\; \max\!\big(0.5,\; 1 + \tfrac{a + b \cdot \text{win \%}}{0.777}\big)\Big) \]

\(a\) and \(b\) come from how much players' participation dropped in past March windows compared with the rest of the season (0.777 is the usual share of games played). This rule improved predictions of March games in all six seasons it was tested on, but the improvement has not yet been large enough to be statistically confirmed, so it is labelled unvalidated and will be re-tested on this season's March.

Repeating the simulation thousands of times gives each team's chance of each finish, and expected winnings:

\[ \text{expected winnings} = \$150\cdot P(\text{1st}) + \$90\cdot P(\text{2nd}) + \$60\cdot P(\text{3rd}) \]

EvidenceThe widening factor used for future weeks (\(\kappa = 1.18\)) lowered weekly prediction error on an unseen season (0.2106 → 0.2095), and teams given 75–90% then won about as often as predicted. Season-long results will be scored at the end of 2026-27.

5. Overall contribution of each component

Figure 7 collects the evidence above. Each bar compares the model with and without one component, on that component's own error measure, so bars show how much each part helps but are not directly comparable to each other.

Figure 7. Reduction in error from each component, on data not used to build it. Hover for the values before and after.
Show data

6. What the model leaves out, and why

Several plausible effects were tested and are not in the model, either because they made predictions worse or because the data showed no effect.

EffectFinding
Players in the same game, and nights when the whole league runs hot or coldExcludedPlayers facing each other do move together slightly (about 0.03 for points), but it made no difference to matchup chances.
Back-to-back gamesExcludedPlayers play the second night of a back-to-back about 95% as often as other games, and assists, steals and blocks dip slightly. Both adjustments helped every season, but too little to pass the 99% bar.
Scaling projections down early in the seasonExcludedThere is no early-season effect: players play about 3–8% fewer minutes than ESPN projects in every week of the season, which the projections already absorb.
Opponent strength in future weeksExcludedHelped at every horizon (1–4 weeks ahead) in every season, but narrowly missed the 99% bar.
Re-estimating the weekly engine's two settings on ten times more dataConfirmedThe current values (\(\kappa = 1.041\), \(\psi = 0.35\)) did slightly better on unseen seasons than the re-estimated ones.
Teammates taking each other's statsNo effectOnce games played are accounted for, teammates' stats don't rise and fall together; only their games played do, which §4.6 includes.
Letting the market weight be re-estimated for each season separatelyExcludedLess accurate overall than drawing it from a range estimated across seasons.
An outside projection system (DARKO) as an extra inputExcludedMore accurate for single stats, less accurate for overall player value in 2 of 3 seasons.
Separating ESPN's points-league ranking from its category rankingExcludedFailed when tested forward in time (earlier seasons predicting later ones).
Fitted weights for past seasons and an age curveExcludedNo improvement over standard weights.
Wider ranges for surprise seasonsExcludedMade typical ranges less accurate while barely helping extreme ones.
Correcting games-played projections toward the averageExcludedOver-corrected; overall error rose.
Modelling other managers' draft habitsNo effectChanged title chances by less than half a percentage point.
Simulating every single day of the seasonNo effectThe weekly engine already matches real weeks; the daily starting limit is handled directly (§4.3).
Assumed settings (injury replacement, learning speed, games per week)ConfirmedValues measured from real data were close enough to the assumed ones that no prediction changed.

7. Limitations

8. Open questions

  1. Day-to-day tags. Every player's ESPN status is being recorded; once about 300 games by day-to-day players exist, their chance of playing will be estimated and tested.
  2. Real lineups. Every team's daily lineup is now being recorded. Once there are a few weeks of it, the test is whether using the lineups managers actually set, rather than assuming each manager starts their best players, sharpens the odds.
  3. The March rest rule, re-tested on this season's playoff weeks.
  4. Full-season check: every weekly prediction of 2026-27 scored against what happened, plus a re-test of the outside projection system on this season.

← Back to the league page