xSPN
Get statistical football predictions for major leagues and tournaments
AI summary · website content
NBA win projections, power ratings, spreads and daily player projections for 2026-27. Python, nba_api and scikit-learn, with backtests.
Sports & fitnessPython · HTMLGitHub @mcgahranjtOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗Live dashboard on Tableau Public, updated weekly during the season. Daily projections site, updated twice a day.
Preseason win projections for all 30 NBA teams, built from two models and a weighted average of both, then run through 10,000 simulated seasons to get win ranges and playoff odds:
Each model was backtested on the five most recent seasons. For every season, the model was trained only on the seasons before it, the same way it would have been used in real time. The combined model's 75/25 weighting was chosen using 2015-16 through 2020-21, so the test seasons played no part in picking it.
Mean absolute error, in wins per team:
| Season | Combined (75% B) | 50/50 average | Version B | Version A | Repeat last season | Every team wins 41 |
|---|---|---|---|---|---|---|
| 2021-22 | 7.1 | 7.1 | 7.8 | 7.0 | 8.2 | 9.7 |
| 2022-23 | 4.4 | 4.9 | 4.7 | 6.4 | 8.1 | 7.5 |
| 2023-24 | 7.8 | 7.7 | 8.0 | 7.9 | 8.5 | 11.2 |
| 2024-25 | 7.2 | 7.2 | 7.2 | 7.5 | 9.1 | 10.6 |
| 2025-26 | 8.2 | 8.4 | 8.2 | 9.1 | 10.4 | 11.6 |
| Average | 6.95 | 7.08 | 7.17 | 7.60 | 8.84 | 10.12 |
The combined model beat or tied Version B in all five seasons and beat Version A in four. Published projection systems and preseason betting lines usually land somewhere around 6 to 7 wins of error, so this is in the same general range, though I haven't measured the market directly yet (see Next steps).
The gain from combining is modest. A and B's errors had a correlation of 0.83, which means they mostly miss on the same teams. Averaging still helps where they disagree, and B gets most of the weight because it knows more about each roster.
2025-26 was the hardest season for every method, which fits a year with a lot of injuries and teams openly playing for draft position.
The idea is that point differential says more about a team than its record does, and it holds up better from one season to the next. So the model starts from last season's net rating (points scored minus points allowed per 100 possessions) and adjusts for how much of the roster changed.
| Feature | Description |
|---|---|
prev_net_rating | Last season's net rating |
returning_min_share | Share of last season's minutes played by players still on the roster |
net_x_returning | Net rating × returning share. Last year's rating should count for more when the roster stayed together |
roster_age | Average age of the upcoming roster, weighted by last season's minutes |
wins = -4.1 + 0.90 × prev_net_rating + 22.89 × returning_min_share
+ 0.41 × net_x_returning + 1.13 × roster_age
For a team bringing back about 70% of its minutes, each point of net rating is worth roughly 1.2 wins the next season. Within a single season a point is worth about 2.7 wins, so a little under half of a team's edge carries over and the rest fades toward average.
The age coefficient is positive, which looks backwards. It's almost certainly because contenders give minutes to proven veterans, so at the team level age acts as a proxy for quality. I also tested the share of minutes going to players 31 and older as a more direct aging measure. It did slightly worse (7.68 vs. 7.60) and still came out positive, so I kept average age and left real aging effects to Version B.
Blind spots: A knows how much of a roster left but not who replaced them, and it has no idea when an injured star is coming back.
Version B builds each team's rating from its individual players, using Box Plus/Minus (BPM) from Basketball-Reference as the player rating. BPM estimates a player's impact in points per 100 possessions.
wins = 45.5 + 2.45 × team_rating
A slope close to the real-world 2.7 wins per point suggests the team ratings are well calibrated. I also tested adding last season's team net rating to B. It barely helped (coefficient 0.39, and less than 0.1 wins of improvement), since the player ratings already capture most of what the team numbers know.
This is the beginning of the README. Read the rest at the original. Read the full README ↗