Pre-game NFL forecasts -- win probability, point margin and total points -- built to be honest about what they know, and when they knew it.
NFL/Predict is an end-to-end machine-learning system. It ingests games, betting lines and weather, builds leakage-checked features, trains three walk-forward models, blends them with the betting market, and serves the results on a broadcast-style web dashboard. It is a personal research tool and a portfolio project, and it is strict about one thing above all: no prediction may use information that was not available at the time.
- Three forecasts for every game. Each team's chance of winning, the expected winning margin (set against the point spread) and the expected total points (set against the over/under). Each is shown beside the market's number for the same game, so disagreements stand out.
- A fixed information deadline. Every game's inputs lock at 6 PM Eastern on the day before kickoff. Anything known by then can be used; anything that arrives later cannot.
- A weekly bet list. Every game is evaluated for all three bet types. Bets that clear an expected-value threshold fixed before the season's first bet are ranked and sized in units, and every candidate that did not clear it is listed with its reason.
- Game pages. Model versus market, the inputs each model leans on most, a tale of the tape, and the venue and weather.
- A live season record. The Season page tracks the 2026 season as games are played. Past-season backtests are on the Track Record page, clearly labelled (see Results, honestly).
- Runs on its own. A scheduled job runs every day at 5 PM Eastern, an hour before the lock: it collects fresh data, rebuilds features, predicts, builds the bet list and refreshes the site.
- Exports. Any week or season as CSV or JSON.
flowchart TB
subgraph sources [Sources]
direction LR
A[nflverse<br/>games and play-by-play]
B[The Odds API<br/>betting lines]
C[Open-Meteo and NWS<br/>weather forecasts]
A ~~~ B ~~~ C
end
subgraph lake [Lakehouse and features]
direction LR
D[(Bronze<br/>raw snapshots)] --> E[(Silver<br/>schema-checked tables)] --> F[Feature builders<br/>and leakage gates] --> G[(Gold<br/>feature matrices)]
end
subgraph models [Models]
direction LR
H[Win probability<br/>calibrated logistic regression]
I[Margin<br/>XGBoost]
J[Total<br/>XGBoost]
H ~~~ I ~~~ J
end
subgraph serve [Blend and serve]
direction LR
K[Market blend<br/>latest pre-lock line] --> L[(Web cache<br/>DuckDB)] --> M[FastAPI + HTMX<br/>dashboard]
end
sources --> lake --> models --> serve
- Ingest. Games and play-by-play come from nflverse via
nflreadpy, betting lines from The Odds API, live weather forecasts from Open-Meteo, and historical day-before forecasts from archived National Weather Service bulletins. Historical games use the forecast as it stood at the lock, not the weather that actually happened. - Store. A bronze / silver / gold lakehouse in Parquet and DuckDB. Raw snapshots are kept append-only, and ingested rows are checked against Pydantic schemas: for games, weather, snap counts and injuries one bad row fails the whole batch, while the odds ingest drops a bad row with a warning.
- Build features. Elo team ratings running since 2002, recent form, rest, travel and schedule, the venue, and the weather forecast at the lock. Betting lines are not model inputs: the models never see the market.
- Train. Win probability comes from a calibrated logistic regression; the margin and the total each come from an XGBoost regressor. Training is walk-forward only -- a model is only ever tested on games played after everything it learned from -- with Optuna for hyperparameter search.
- Blend. Each model's number is blended with the latest market line from before the lock (in log-odds for win probability, in points for the margin and total), using one fixed weight per prediction tuned on past seasons. The weight can be zero: where the market alone did at least as well, the published number is the market's own, and the model's number is shown beside it.
- Serve. Everything the site shows is precomputed into a read-only DuckDB cache. The FastAPI + Jinja2 + HTMX app only reads that cache; no model code runs while a page is served.
- One lock rule. The information deadline is defined once, in
utils/game_lock.py: information timed exactly at the lock is admissible; one second later is not. - Leakage defences in layers. Every feature builder satisfies a
FeatureBuilderprotocol that requires an as-of time (features/protocol.py); an information-time gate checks every source's timestamps against each game's lock (features/provenance.py); aLeakageGatescans the combined matrix for post-game columns and checks Elo ordering (features/validation.py); and walk-forward splits refuse to run if training and test seasons overlap (models/temporal.py). The deployed models are tuned on season-ordered folds only (see Current limitations for the legacy trainers). - Pre-registered, tamper-evident evaluation. Decision rules -- the bet list's expected-value
threshold, the significance tests -- are committed before the results they govern exist. Tests
then use git history to check that the freezing commit really came first
(
tests/unit/test_preregistration_ancestry.py). - Reproducible inputs.
config/upstream_pin.jsonrecords exactly which nflverse snapshot was used, with a SHA-256 for every season file. - A hard wall between serving and modelling. The web app may not import model, feature or
rating code; an AST-walking test fails the build if it ever does
(
tests/api/test_import_guard.py). - Honesty enforced by tests. Every published readout is guarded by tests: results built on defective inputs must carry a dated "not evidence" label, and readouts may not use over-claiming words.
- No model here has shown that it beats the betting market.
- In September 2026, the inputs behind every earlier result were found to be defective -- for example, closing lines known only at kickoff had been used as model inputs, and missing weather had been filled with a flat placeholder. All three models were rebuilt from scratch on corrected inputs.
- Every earlier result -- the past-season backtests and the one-shot 2025 test -- stays in the record, unedited, and is labelled as built on inputs later found defective: not evidence.
- The only evidence that counts is the 2026 season, recorded live with every game locked at 6 PM Eastern the day before kickoff. The Season page shows it as it accumulates.
This is a personal research tool, not betting advice. A bet on the list cleared a threshold fixed before the season; that is not a forecast that it will win.
- No demonstrated edge. No model has shown that it beats the betting market; the 2026 season is the first clean test (see Results, honestly).
- One machine, on Eastern time. The scheduled run is a Windows Task Scheduler task that expects the machine to stay on Eastern time. Hosted deployment is out of scope for now.
- Single-worker server. The app shares one read-only DuckDB connection and an in-process
cache, so it must run as a single uvicorn worker (
--workers 1). - Legacy trainers still in the tree. The original
models/train_ats.pyandmodels/train_ou.pyremain because the prediction path imports their distribution converters, andmodels/train_wp.pysits beside them. They contain k-fold grid and random search, but the deployed models are trained bymodels/trainers/on season-ordered folds only. Moving the converters out is a deferred refactor.
| Area | Tools |
|---|---|
| Language and tooling | Python 3.13, uv, Ruff, Pyright, pytest, Hypothesis |
| Data | pandas, DuckDB, Parquet (pyarrow), Pydantic v2 |
| Modelling | scikit-learn, XGBoost, Optuna |
| Data sources | nflreadpy (nflverse), The Odds API, Open-Meteo, NWS forecast archive |
| Web | FastAPI, Jinja2, HTMX 2, Tailwind CSS v4, Plotly |
| Operations | Windows Task Scheduler, structlog, tenacity |
You need Python 3.12 or newer (developed on 3.13), uv, and a free
The Odds API key. Commands are shown for PowerShell on Windows, where
the project runs; on macOS or Linux use cp instead of Copy-Item.
git clone https://github.com/jackc625/nfl-predict.git
cd nfl-predict
uv sync
Copy-Item .env.example .env # then add your ODDS_API_KEYThe data and models are not committed. The full build runs in eight stages -- ingest, features,
train, promote, backtest, predict, cache and serve -- documented command by command in
docs/guides/PIPELINE.md; the first build pulls every season back to
2002. Once the cache exists, the site is one command:
uv run uvicorn api.main:app --port 8000 # then open http://localhost:8000The scheduled daily run is uv run python scripts/daily_lock_pipeline.py; see
docs/guides/AUTOMATION.md for what it does and how to tell a run
succeeded. Many tests read the locally built data, so run them after the first build.
api/ FastAPI app: pages, HTMX fragments, CSV/JSON exports, /health
audit/ independent Elo replay used to audit the ratings
backtest/ walk-forward backtests, betting simulation, pre-registered constants
conf/ settings and the season partition
config/ deploy-gate, pre-registration and data-correction configs; upstream data pins
data/ storage layer, schemas and quality gates (the data itself is not committed)
deployment/ Windows Task Scheduler task definition
docs/ guides, the records trail, and images
features/ feature builders, the information-time and leakage gates, normalisation
models/ trainers, calibration, market blending, the deploy gate, artifacts
pipeline/ the scheduled orchestrator: steps, health checks, alerts
ratings/ Elo ratings
scripts/ command-line entry points for every pipeline stage
tests/ unit, integration and API tests
utils/ the lock rule, team data, dates, probability and Kelly-sizing helpers
web/ Jinja2 templates, Tailwind CSS, fonts
Guides -- how the system works and how to run it
PIPELINE.md-- the canonical run sequence, stage by stageRUNBOOK.md-- operator runbook: setup, each operation, recoveryAUTOMATION.md-- what the scheduled daily run doesMETHODOLOGY.md-- modelling and feature-engineering deep diveSTATE-OF-SYSTEM.md-- what is trustworthy, fixed and deferred
Records -- the paper trail, in docs/records/: data and feature audits,
accuracy diagnoses, every model re-fit and its gate decision, signal screens, the 2025
profitability readout, and the corrections made under the day-before lock. Results reported there
from before the September 2026 fix carry a dated label saying they are not evidence.
Frozen records -- kept at the top level because their git history is the proof that each rule was fixed before its results existed:
PROFITABILITY-PREREGISTRATION.md-- the 2025 test, registered in advanceCOLD-START-PREREGISTRATION.md-- the original 2026 bet ruleCOLD-START-CORRECTION.mdandNEUTRAL-HFA-BET-RULE-CORRECTION.md-- the corrections that superseded itEV-CHAIN-CORRECTION.md-- the expected-value floor, re-derivedMOS-DECODE-COMPARISON.md-- the check behind the historical forecast decoding
Informational only, not betting advice. Not affiliated with or endorsed by the NFL or any team.
MIT -- Copyright (c) 2025-2026 Jack Cutrara





