Skip to content

About

NBA sports betting using machine learning

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

 
 

Latest commit

 

History

295 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Legit NBA Predictions

NBA outcome prediction, measured honestly — as a research project, a betting project, and a data-analytics project in one repository.

CI Python Data No betting edge claimed

Research · Betting · Data & engineering · Key findings · Quick start · Docs · Limitations

There is no demonstrated betting edge here, and we do not claim one. This is an audit and a set of prediction models, judged on how well they predict — not on whether they make money (they lose, and we measure that). A fork of kyleskom/NBA-Machine-Learning-Sports-Betting that corrects its defects and rebuilds the modelling layer from NBA's own data; the original project is by Kyle Skompinski and contributors, and this fork corrects rather than replaces.


One repository, three identities

The same project is genuinely read three ways, and each way has different evidence attached. Use the section that matches what you are here for.

🔬 The research project {#research}

Pre-registered experiments, an append-only ledger, and a high bar for calling anything real. Every candidate feature and model is measured against the same frame; the project's bar is |z| > 3 (Vandenbruaene et al. 2022, J Sports Econ 23(7)).

  • Two effects clear the bar, and both are about who plays: player-level expected minutes (availability) improves log loss by +0.00390 nats, |z| 4.78; the same-game injury multiplier on top of it clears a forward two-season holdout at +0.003579, |z| 3.85. The closing line already prices the availability block in.
  • Two headline "wins" were leaks and are documented as such: season-to-date stats that contained the game's own result (the famous 68.9%), and a per-player Elo block that summed over the lineup which actually played (|z| 5.18 → +0.00065 on a pre-game roster).
  • The simple model wins: an L2 logistic regression (0.6157 log loss) beats XGBoost, HistGradientBoosting, random forest, Gaussian NB, a five-seed neural net and margin/total models. Every run — including the nulls — is in results/experiments.jsonl.
  • The field agrees: ~45 public NBA repos top out at 65–69% accuracy; the external literature lands at roughly this project's log loss and loses betting too. Quotes and links in the research-notes page.

Start here: Results: Pipeline A · the October 2026 experiment log · reading the results honestly · the reusable leakage gate.

🎰 The betting project {#betting}

The same models, scored against the closing line and the posted price — the audit of an original betting pipeline, plus a rebuilt one that cannot claim an edge.

  • The market beats the model decisively: on 5,788 games with both a prediction and a closing line, market log loss 0.59766 vs model + availability 0.62034 — a gap of 0.02268 nats, interval entirely on the market's side (|z| 7.28).
  • Selective betting does not rescue it: bet wherever the model beats the posted price (both sides, no free parameters) → −8.23% over 4,828 bets; no margin/threshold cell reaches |z| 3.
  • The folk spots are priced as well: a 5-5 home team against a 2-8 road team wins 79.5% of the time and the close asks 82.7% — −4.12% on the moneyline, −5.28% on the spread at −110 over 965 games. The broader form gap covers 48.8% of the time, a real |z| 4.36 mispricing that is smaller than the vig.
  • Stacking filters does not create a price: an Elo gap ≥100 plus a 6-4/3-7 form mismatch returns −4.42% ATS (n=1,886, |z| 2.01) and −4.06% on the moneyline (|z| 3.32). The higher-Elo side is the favourite in 95.2% of those games, and the form gap correlates 0.736 with the Elo gap — the layers are not independent information.
  • The one real model edge is already in the price: availability adds +0.00390 nats to the model, then nothing after conditioning on the close (tight null at the market-anchored stack). The injury multiplier is measured as a model feature, not at the close.
  • A pre-registered 42-arm search finds no positive structural cell: best is away ATS at |line| 10.5-12.5, +3.52% on discovery (|z| 1.12) and +3.19% on the 2025-26 holdout (n=111). Backing the underdog loses −5.63% (|z| 6.45); the two sides of the spread sum to −8.94%, the hold. Bar was |z| > 3.24.
  • Best-of-books prices are not an edge either: at OddsPortal best prices (overround ~1%, backfilled to 2015-16), backing the underdog looks like +3.57% (CI entirely positive) — but on coherent snapshots it is −0.14% and the dog win rate lands exactly on the implied break-even. The +20.41% big-underdog cell clears the bar on discovery and fails the holdout. The closing moneyline is efficient; the remaining distance to profit is information, not price.
  • The model at best prices closes the information route: the availability model's bets at best-of-books prices return +6.77% on discovery (|z| 2.61, below the bar) and −0.21% on the holdout; on coherent rows every model arm pays the vig. The model backs the underdog 68.2% of the time — its probabilities are flatter than the market's, and coherent best-price longshots are exactly fair.
  • The original pipeline's headline was measured leakage: the audit found and fixed six defects, including a 68.9% accuracy and ROI inflation from target leakage, a leaked API key, and a totals model with no signal.

Start here: Results: Pipeline B · why odds are legitimate here · the totals target · the full audit · roadmap to a real forecasting engine.

🧮 The data & engineering project {#data-engineering}

A single reproducible feature layer over NBA's own data, and the tooling that keeps its numbers trustworthy.

  • 13,199 games, one shared design: scripts/download_nba_data.py builds 310 CSVs (10+ seasons) and src/DataProviders/nba_data.py assembles 26 causal feature columns used by every model. No model has a second loader.
  • Causality is tested, not asserted: deleting 5,199 future games changes no historical feature by more than 0.000e+00; a reusable leakage gate separates confirmed future-dated leaks from same-date dependencies.
  • Append-only evidence: the injury-report store holds 2,046 snapshots across 1,037 report dates / 237,436 designation events (official pre-tip injury reports, point-in-time semantics).
  • 1,556 tests, 0 failing, 104 subtests across 66 files; CI on Python 3.10 & 3.11 and both sides of the scikit-learn 1.7 multi_class removal.

Start here: Models, features and retraining · the availability block · the injury-report pipeline · the feature search · reproducing every number.


Key findings

The 68.9% headline was target leakage Season-to-date stats were scraped with the game's own result included. Rebuilt without the leak, accuracy is ~48% on the affected rows.
The market beats the model decisively Market log loss 0.59766 vs model + availability 0.62034 on 5,788 games. Gap 0.02268 nats, |z| 7.28 — a loss, not a tie.
Selective betting does not rescue it Bet-where-the-model-beats-the-price returns −8.23% over 4,828 bets; no margin or threshold cell reaches |z| 3.
Folk betting spots are already priced A 5-5 home team against a 2-8 road team wins 79.5% of the time; the close asks 82.7% → −4.12% on the moneyline, −5.28% on the spread at −110 over 965 games. The broad form gap covers 48.8%, a real |z| 4.36 mispricing that is smaller than the vig.
Stacking filters does not create a price An Elo gap ≥100 plus a 6-4/3-7 form mismatch returns −4.42% ATS (n=1,886, |z| 2.01) and −4.06% ML (|z| 3.32). The higher-Elo side is the favourite 95.2% of the time; the form gap correlates 0.736 with the Elo gap.
No positive structural cell survives multiplicity A pre-registered 42-arm scan of the close (bar |z| > 3.24) finds its best positive at +3.52% (away ATS, |line| 10.5-12.5, |z| 1.12). The largest signals are negative: underdog ATS −5.63% (|z| 6.45), heavy ML favourites −5.26% (|z| 6.21); the two spread sides sum to −8.94%, the hold.
Best-of-books prices close the "just shop better" route At OddsPortal best prices (overround ~1%, 2015-16..2025-26), ML underdogs look like +3.57% (CI entirely positive) — but on coherent snapshots it is −0.14%, with the dog win rate exactly at the implied break-even (32.28% vs 32.10%). The +20.41% big-underdog cell fails the holdout. Price improvement is real (+2 to +4 pts); it is not an edge.
The model at best prices closes the information route The availability model's bets at best-of-books prices: +6.77% discovery (|z| 2.61, below the bar), −0.21% holdout; coherent rows pay the vig. The model backs the underdog 68.2% of the time (flatter probabilities) and coherent best-price longshots are exactly fair.
Who plays is what works Player-level expected minutes (availability) improves log loss +0.00390 at |z| 4.78; the pre-tip injury multiplier on top of it clears a forward two-season holdout at +0.003579, |z| 3.85. Availability is priced out at the close; the multiplier is a model result, not a measured bet.
The biggest-looking win was a leak A per-player Elo block measured |z| 5.18 and was leaking the real lineup; on a pre-game roster it is +0.00065.
The simplest sensible model wins Logistic regression (0.6157) leads XGBoost, HistGradientBoosting, random forest, Gaussian NB, a five-seed neural network and margin/total models.
The close absorbs everything our model knows Price + model loses to price alone (0.00177 nats, |z| 3.15); availability beyond price is a tight null.
Do not use the committed model files They were trained on the leaked dataset. Retrain before using anything.

The two pipelines

A. NBA-data models (new) B. Original betting pipeline (fixed)
Purpose Predict outcomes; measure how good the predictions are Predict games against sportsbook odds, with Kelly sizing
Data NBA's own data via nba_api (310 CSVs, 13,199 games) Scraped team stats and sportsbook odds
Models Elo, logistic regression, margin/total, boosting, MLP XGBoost and neural networks
Judged on Log loss, Brier, AUC, calibration Accuracy, log loss, market baseline, ROI
Entry points scripts/download_nba_data.py, python -m src.Models.* main.py, src/Process-Data/, src/Train-Models/

Every feature for a game is computed only from games that already happened; the leakage gate tests it.


Quick start

Python 3.10 or 3.11 (the ceiling is deliberate: tensorflow==2.14.0 has no wheels for 3.12+).

pip install -r requirements.txt

python scripts/evaluate.py                 # reproduce every number; opens with a leakage gate
python scripts/download_nba_data.py        # resumable; core = 10+ seasons / 13,199 games
python -m src.Models.logreg_rolling        # the canonical model (Pipeline A)
python scripts/closing_line.py             # does any of this beat the closing line? (it doesn't)

⚠️ The model files committed in Models/ predate every fix in this fork and were trained on the leaked dataset. Retrain before using anything — see Models, features and retraining.


Docs

Research — the measurement discipline and what it found

doc what it covers
Results: Pipeline A Model comparison, feature blocks, the one thing that is not a null (and its injury add-on)
The October 2026 experiment campaign Every experiment from declared rationale to verdict, plus errata
Reading the results Why the ordering below the top row is the wrong thing to read
The reusable leakage gate One function, three probes, the three known leaks as fixtures
The player-Elo leak A |z| = 5.18 result that was target leakage
Two measurement bugs The ROI denominator and the data guard
Survey of ~45 public NBA repos The field's accuracy ceiling, and how honestly it is reported
The research links, assessed 38 external papers/posts triaged against the measured results

Betting — the market, the rules, and the audit of the original project

doc what it covers
Results: Pipeline B Closing-line comparison, betting rules, the repo survey
The market anchor Why odds are legitimate as an input only here; the close absorbs everything
The totals target Why OU-Cover keeps its push class; the closing price that would justify folding it
Form-mismatch spots The hot-home / cold-road rule, priced: the mismatch is real, the close already has it
Layered Elo + form Stacking an Elo filter with the form filter: the layer re-selects the favourite and loses the vig
The search loop A pre-registered 42-arm scan of the close, the multiplicity bar, and why the full-season 2025-26 holdout replaced the snapshot
Best-of-books moneylines The profit objective: OddsPortal best prices backfilled to 2015-16, and why the apparent underdog edge is a snapshot artifact
Exploratory follow-ups What 4th-win teams look like pre-game, and the Elo-parity underdog — the worst measured bet in the repo (−9.8%, z −7.5)
13 fan-style rules, backtested The user's form/schedule rules pre-registered and settled at best-of-books prices: no candidate; R8 and R10 fail the holdout
The 50-rule grid The fan rules expanded to a systematic 50-arm family: no candidate; the one |z|>3 result is a negative (heavy chalk ≤1.30 loses, z −3.1); fading tired teams loses everywhere
Lay bets Laying the favourite at its own price: the mirror of the favourite-overbet finding, coherent-surviving (z 3.1), +2% after 5% commission — exchange-only, re-expression not new information
The lay grid Laying dogs, pick'em sides, streak teams: no candidate; laying longshots ≥4.00 loses 14.9% (z −3.1) and commission asymmetry punishes every long lay
The audit and the fixes The six defects found in the original, and everything this fork changed
ROADMAP The levels between this pipeline and a real forecasting engine

Data & engineering — the feature layer, the evidence store, and the tooling

doc what it covers
Models, features and retraining Model modules, load_design, retraining commands, the feature-order contract
The availability block The mechanism that survived: expected minutes over the prior game's squad (the membership convention is load-bearing), plus the pre-tip injury multiplier
The injury-report pipeline Point-in-time availability as append-only evidence, the three pre-2020 grammars, and the backfill policy
Late scratches What a designation is worth by time to tip: Questionable is a coin flip, Available is not the safest state
The feature search Four pre-registered candidates and the fold-count artifact
Modelling exploration Nine further candidate blocks, all scored, all null
Usage Full commands for both pipelines and the Flask /games route
Verifying and testing Reproducing every number, the experiment-log CLI, the test suite
The feature-order contract How training and inference agree on column order
The test suite What each test file protects
/games Flask route The three upstream traps it absorbs

Limitations

  1. No demonstrated betting edge — this is a measured loss. Betting wherever the model beats the posted price returns −8.23% over 4,828 bets; no margin or threshold cell reaches |z| 3. Reproduce with python scripts/edge_betting.py.
  2. The model is worse than the market. On 5,788 games with both a prediction and a closing line: market 0.59766 vs model + availability 0.62034 (gap 0.02268, CI [0.01638, 0.02859], |z| 7.28). Accuracy is not profitability; this project's 0.6573 is the field's normal – and no published figure in the survey accounts for the vig that this one measures at +4.13%.
  3. Effect sizes here are small and the honest bar is high. Everything measured is of order 0.001–0.03 nats; the effects that clear |z| > 3 are availability and the pre-tip injury multiplier on top of it, and availability is priced in at the close (see Pipeline A).
  4. The committed model artifacts must be retrained. They were fitted on the leaked dataset; the neural artifact also carries a stale totals column order. The totals model should not be used.
  5. Kelly sizing here is full Kelly with no fractional scaling or drawdown control.
  6. Legal: not financial advice; upstream data is not licensed for redistribution; neither this fork nor its upstream grants reuse rights.

Credit and licence

Original project: kyleskom/NBA-Machine-Learning-Sports-Betting by Kyle Skompinski and contributors (github.com/kyleskom). This fork's audit, corrections, rebuilt modelling layer, measurements and documentation are ours; it corrects rather than replaces. Neither fork currently grants reuse rights (see Limitations). Contributing: see CONTRIBUTING.md — start with Models, features and retraining.

About

NBA sports betting using machine learning

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages