NBA game prediction

Predicts every NBA game before tip-off with a PyTorch model built from each team's players, keeps the regression that ran last season beside it as a benchmark, and records every pick against the result and the bookmaker's price.

Pre-gamepredicted before the game and storedBacktestscored afterwards, on games the model was not trained on

Python, PyTorch, FastAPI, Docker, Fly.io, Cloudflare R2

Tonight

last night's results and the next games

Loading the games.

Season

2026-27, pre-game

Loading the season.

Teams

2026-27, the same switch as above

Loading the season.

How it works

for a closer look

  1. 15 players × 23 numbers

    Each player

    Up to 15 players per team, from the team's previous game; the running model also drops anyone the injury report lists as out. Each player is 23 numbers: last-10-game form (minutes, points, rebounds, assists, steals, blocks, turnovers, plus-minus), season averages, shooting and usage rates per 36 minutes, and how many games and days of rest are behind him.

  2. 23 → 32 → 16, per player

    One small network for every player

    The same two-layer network reads each player and turns his 23 numbers into 16 that describe what he adds to a team. It is the same weights for every player, 1,296 of them, so it learns what kind of player matters rather than who he is.

  3. →
    15 × 16 → 16

    A team is its players, weighted by minutes

    The 16-number player vectors are averaged, weighted by the minutes each player has averaged over his last 10 games. The order of the players does not matter, which is what the name Deep Sets refers to: the network sees a set, not a list.

  4. 54 → 32 → 1

    The matchup

    Home team, away team and their difference (48 numbers), plus each side's rest days, back-to-back flag and games into the season (6 more) go through a second small network to one score. It runs a second time with the teams swapped and takes the difference, so swapping home and away flips the answer exactly. A learned home-court term is added.

  5. →62%for example
    5 copies → 1 probability

    Five copies, averaged

    The whole network is trained five times from different random starting weights. Their five scores are averaged, divided by a temperature fitted so that a 70% means 70%, and turned into the home team's win probability. Averaging five copies removes most of the luck of any single training run.

Which model runs. The set: all five copies, every day. They were trained in PyTorch on 2020-21 to 2024-25, each stopped at the epoch where its 2025-26 log loss was lowest (between 9 and 16 epochs), and retrained on 16 October 2026. On the server the same weights run in NumPy, which keeps PyTorch out of the 1 GB container; the two agree to within 0.0000002.

Why the backtest is still clean. The 2025-26 backtest on this page comes from an earlier copy, trained only up to 2023-24 with 2024-25 for stopping, so it never saw a 2025-26 game. The copy running now has seen 2025-26, so it can only be judged on 2026-27.

Size. About 3,100 weights per copy, small on purpose: there are only about 6,000 games to learn from. A deeper network with game sequences scored slightly better, but within noise, so the simpler one runs.

Ask the model

the player model, now

Pick two teams and the player model on Fly.io answers with each team's latest roster, minus the players ESPN lists as out right now. Not stored and not part of the record.

The code

live from GitHub

Finnish leagues

a side project

The same idea for Finnish men's basketball: win chances for every Korisliiga, I divisioona and southern II divisioona game, from team ratings that update after each result.

Finnish predictions