NBA Odds Predictor Elo ratings, scraper, and matchup predictor

NBA scraper and rating dashboard

NBA Odds Predictor

Scrape Basketball-Reference, run an Elo model over the results, and get a live leaderboard plus a matchup predictor in the browser.

The app does one thing end to end: pull games, clean them, update ratings, and show you the current state of the model. There's no database to stand up and no opaque scoring layer. Every file the app writes lives under data/, and the leaderboard is just the sorted output of the most recent Elo pass over a CSV.

On the prediction side, a second model trains over the same normalized games, folds in official injury reports, and compares logistic regression against a random forest on identical features. The Inner-Workings page walks through the math and the feature set in detail.

Games tracked 1321
Teams modeled 30
Seasons covered 1
Latest season 2025
Latest season games 1321
Last updated 2026-07-10

Scraping

The scraper walks every month page of each requested season, so the raw CSV gets full regular-season coverage instead of just the opening month. It parses Basketball-Reference's box-score tables directly, not a third-party feed.

  • Only games with a final score are written. Postponed, suspended, or not-yet-played rows are dropped at parse time.
  • For the current season, the import stops at the most recent completed game, so a scheduled result never gets fed to the model as a played one.
  • Each scrape writes one CSV per season range under data/raw/, and a copy is downloadable straight from the Scrape page.
  • Season-range input lets you pull 2016 through 2025 in a single run; the scraper fans out across years and merges the results.

Cleaning

Raw Basketball-Reference rows don't all use the same team labels, and they don't carry the win/margin signal the model needs. The cleaner closes that gap before any rating is touched.

  • Team strings like "Boston Celtics" or "Golden State Warriors" collapse to NBA three-letter abbreviations (BOS, GSW, DEN) using a lookup table covering every team the scraper has seen since 2016.
  • Each row gets a home_win flag and a margin integer (home pts minus away pts), which is what the Elo updater and the predictor both key off of.
  • Processed games land in data/processed/ as one normalized CSV per season range, so the leaderboard can be rebuilt without re-scraping.
  • Malformed rows, missing scores, and unparseable dates are filtered out and counted, not silently absorbed.

Elo updates

Ratings update one game at a time, in the order the games were played. The model is a standard margin-of-victory Elo with home court folded into the expected-win formula.

  • Every team starts at 1500 at the start of the first season in the dataset, and is carried forward season to season with a mild regression toward that prior.
  • Home court shows up as a fixed additive term in the expected-win formula, so a home underdog can still be favored in the model.
  • Margin of victory multiplies the base K-factor update: a 30-point blowout moves ratings meaningfully more than a one-point win, while a narrow favorite win barely shifts anything.
  • Refresh the leaderboard from the latest scrape whenever you want; the updater reads the processed CSV and replays the full game history into the current ratings.

Outputs

The final state of the model surfaces as a leaderboard and, separately, as a per-matchup prediction. Both read from the same processed dataset, so they're always consistent with each other.

  • The leaderboard reflects the live current-season ratings, or the most recent completed season if the current one hasn't tipped off yet.
  • The predictor takes a home/away matchup and returns a winner with a home-win probability from both the logistic and random forest models side by side.
  • A reset button wipes the raw, processed, and results files in one go, useful when a scrape goes sideways or you want to rebuild the model from scratch.
  • Every CSV the app produces is also available for direct download, so you can pull the data out and work on it elsewhere.

Why 1500

1500 is the rating every team gets before the model has seen a single game. It's a shared starting point, not a points-scored stat, and it's the baseline every team's rating is measured against.

The number itself is conventional in Elo systems (most chess implementations use it too), which makes the math readable: a 100-point gap between two teams maps to an expected win probability you can compute in your head.

The model also regresses ratings slightly toward 1500 between seasons, so a team that fell to 1400 over a bad year doesn't carry that full deficit into opening night of the next.

  • Differences are measured relative to that shared baseline, so "rating 1620" means "120 points above the average starter."
  • A rating meaningfully above 1500 means the team has beaten its prior; below 1500 means it has lost ground.
  • The scale is symmetric: a 30-point favorite is a 30-point favorite whether the matchup is 1650 vs 1620 or 1320 vs 1290.

If every team starts at the same number, the ratings only describe what the games have actually shown. There's no preseason polling or seed bias baked in.

Latest snapshot

A quick read on what the model is currently sitting on.

The "live season" is whichever season has the most recent completed games in the dataset. If you scrape 2022-2025 in June 2025, the live season is 2024-25.