2026-08-19 · Machine learning
Building the NBA Rebound Predictor
A walk through how the interactive rebound tool works, from a court doodle to a probability.
TODO: this is a starter draft — edit it into your own voice.
The idea
Give the model ten players standing on a half-court at the instant a shot goes up, and ask a simple question: who grabs the rebound? You place five offensive and five defensive players, mark the shooter, and hit run. The tool returns a probability for each player and animates them to where the model thinks they'll end up.
Two models, in sequence
There are two learning steps. First a movement model takes the pre-shot positions and samples where all ten players end up when the ball comes off the rim — rebounding is about movement, not just where you start. Then those positions, plus a stack of engineered features, go to a ranker that outputs a rebound probability per player.
The ranker scores the ten players together rather than one at a time. That sounds like a detail and isn't: exactly one of them gets the rebound, so it's a ranking problem over a group of ten, not ten independent yes/no questions. Training it under a grouped softmax means the probabilities come out as one distribution that already sums to one.
The movement model predicts a distribution over whole scenes rather than one position per player, which took me a while to appreciate as the right shape for the problem. A big man might crash the glass or might leak out in transition. Those are two futures. Predicting one number per player forces the model to answer "somewhere in between", which is a place he never actually stands. Sampling complete scenes lets it commit to one story at a time — so the demo draws a different plausible outcome each time you re-run the same play.
What the 2017 version did
The original was a small Keras network predicting one post-shot point per player, eight hand-built features, and a scikit-learn random forest scoring each player on his own. Because nothing told that forest only one player could win, its ten outputs didn't sum to anything in particular, and the app divided them by their total to make a distribution out of them. That worked, in the sense that it produced numbers.
Rebuilding it in 2026 replaced both stages, and the two changes that mattered most were the ones above: score the group jointly, and sample scenes instead of averaging them. The third was organisational — every feature definition moved into the modelling repo, so the web app no longer recomputes its own copy at request time. That duplication is the kind of thing that rots quietly, and by the time I came back to it, it had.
The features that matter
Twenty-seven features per player now, still trying to capture rebounding intuition:
- Distance and angle to the hoop — position relative to the rim, at
(41.75, 25)in court coordinates. - Shot context — how far out the shot was taken, each player's bearing relative to it, and an estimate of how long the ball is in the air.
- Traffic — distance to the nearest opponent, and how many bodies are inside six and ten feet.
- Box-outs — who is screening whom off the glass, counted in both directions.
- Relative standing — distance to the rim compared against the best-placed player, the nearest teammate, and the group average. Being twelve feet out means nothing until you know where the other nine are.
- Identity — offense or defense, shooter or not, and listed position.
Does it work?
It picks the actual rebounder 29.7% of the time on held-out shots, and has him in its top three 66.8% of the time. With ten players on the floor, guessing gets you 10%. I've made my peace with that ceiling: a tip-out or a carom off someone's shoulder isn't in the pre-shot positions, and no model is going to find it there.
The single most surprising thing I found rebuilding this: telling the model who's a guard and who's a centre is worth 2.8 points of top-1. Strip the positions out and the same model drops to 26.9%. That's why the demo makes you pick positions instead of just dropping dots — the earlier version defaulted everyone to "forward" and quietly gave up those points. Worth saying that 29.7% was measured on real lineups, though: if you sketch five centres, you're outside anything the training data has seen.
The front-end
The court is a D3.js sketch over an SVG half-court. Clicks drop players, a POST sends their coordinates to a small Flask service, and the response drives a color gradient from blue (unlikely) to red (most likely) with the top rebounder outlined.