NBA Odds Predictor Elo ratings, scraper, and matchup predictor

System guide

Inner-Workings

How the app actually works under the hood: the Elo formula behind the leaderboard, the feature set and model pairing behind the predictor, and the two scrapers that feed both. Numbers and code paths first, marketing language last.

1. Elo leaderboard

Each team starts at 1500. After every completed game the winner takes points from the loser. The size of the swap depends on how surprising the result was: a heavy favorite winning by 2 barely moves the needle; a 20-point underdog blowout reshapes the ratings.

The expected-win term folds in home court as a fixed additive advantage, so the model doesn't treat a home win by a 1480 team over a 1520 team the same way it treats that matchup on a neutral floor.

Margin of victory multiplies the base K-factor update, which is the whole point of using Elo here instead of a win/loss counter: a 30-point win carries more signal than a 2-point win, and the formula should reflect that.

P ( home win ) = 1 1 + 10 rhome raway + h s Δ = K m ( y p )

P is expected home-win probability, r are the two ratings, h is the home-court advantage in rating points, and s is the scale factor (typically 400). The update Δ multiplies the base K by the margin multiplier m and the actual-vs-expected gap y − p.

  • Ratings update game by game, in chronological order, replaying the whole season each refresh.
  • Home court is baked into the expected-win formula, not applied as a post-hoc bump.
  • Margin of victory scales the K-factor, so a blowout moves ratings more than a one-point win regardless of who was favored.
  • Ratings regress toward 1500 between seasons, so a single bad year doesn't permanently sink a team in the model.

2. Home-win predictor

The predictor trains on every completed game in the window before the target date, then builds one matchup row for the home and away teams you pick. Two models train on the same engineered feature set and run side by side: a logistic regression (linear, readable, coefficients you can inspect) and a random forest (nonlinear, captures interactions the linear model can't).

Keeping both visible isn't indecision. Linear and tree models weigh the same inputs differently on a given matchup, and when they disagree the context table tells you why: rest, injury load, and strength gap all show up as raw numbers beneath the winner call.

  • Season win percentage tracks overall team quality over the full year so far.
  • Team strength blends win rate with average scoring margin, which is a better quality signal than either alone.
  • Recent form is a rolling window over the last few games, capturing whether a team is hot or cold right now.
  • Rest days count the gap since each team's last game, since a team on a back-to-back plays differently than one on three days' rest.
  • Injury features count out, doubtful, questionable, and probable players per team.
  • Injury impact goes a step further, weighting status severity and estimated absence length so a star out for a week reads differently than a role player out for a day.
The benchmark for the current training window: home-court baseline accuracy 54.6%, logistic 64.8%, random forest 64.5%. The linear model edges the tree model by about one point on held-out 2025 games.

3. Game scraper

The Basketball-Reference scraper walks every month page in each requested season, so the raw CSV gets full regular-season coverage rather than just October and November. It parses the box-score tables directly and writes one row per game with date, season, away team, away points, home team, and home points.

  • The season-range input fans out across multiple years in one run, so you can pull 2016 through 2025 in a single submit.
  • Raw rows are normalized into the consistent game dataset both the Elo model and the predictor consume, with team names mapped to NBA abbreviations and win/margin columns stamped on.
  • For the current season, the scraper stops at the latest completed game so a scheduled result never leaks into the ratings as a played one.
  • Re-running a season overwrites that season's slice of the raw CSV, so refreshing mid-season doesn't duplicate old games.

4. Injury scraper

Official injury reports come from the NBA's feed, pulled through the nbainjuries library. Three modes cover the cases the predictor cares about: grab the single latest report, walk a date range day by day, or pull a whole season range keyed off the scraped game calendar.

  • Latest mode fetches the most recent valid official report and writes it to a CSV.
  • Date-range mode walks each date in the window and saves whatever report exists for that date, skipping days with nothing.
  • Season-range mode uses the scraped game calendar, so injury dates line up with the games the predictor already knows about.
  • Missing report dates either raise a clear error or are skipped with a note, depending on whether the gap is expected for that date.
  • Backend CSVs are cached after the first pull, so later predictor refreshes reuse them and finish much faster than the initial run.