NBA outcome prediction, measured honestly — as a research project, a betting project, and a data-analytics project in one repository.
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.
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.
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 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.
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.pybuilds 310 CSVs (10+ seasons) andsrc/DataProviders/nba_data.pyassembles 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_classremoval.
Start here: Models, features and retraining · the availability block · the injury-report pipeline · the feature search · reproducing every number.
| 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. |
| 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.
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 inModels/predate every fix in this fork and were trained on the leaked dataset. Retrain before using anything — see Models, features and retraining.
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 |
- 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. - 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%.
- 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).
- 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.
- Kelly sizing here is full Kelly with no fractional scaling or drawdown control.
- Legal: not financial advice; upstream data is not licensed for redistribution; neither this fork nor its upstream grants reuse rights.
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.