Seven decisions that separate this from a dressed-up Elo. None came free: each was chosen by measuring, and several made things worse before they made them better.
Schedule and play-by-play behind these numbers, retrieved 2026-10-14 — not when this page was built.
NFL margins are not continuous. They pile up hard on 3 and 7, because that is how scoring works. Turning “expected margin 2.8” into a probability with a normal makes large and systematic errors at exactly the lines where the money is.
The density factors as P(margin = k) ∝ w(k) · N(k; pred, σ), where w(k) is the ratio of each margin’s observed frequency to its kernel-smoothed version. It comes out ~1.9 at k=3, ~1.5 at k=7 and ~0.55 at k=2 and k=5, with nobody telling it to. That it shows up unasked is the check that it measures something real, and it is where correct push probabilities come from.
The production model does not predict the margin with the line as just another feature. It predicts margin − line: where the market is wrong. The target has near-zero mean, so regularization defaults toward “the market is right” and only departs from it on evidence. That is the difference between a model that respects the market and one that fights it over noise.
Raw EPA measures outcome, not quality: an offense at 0.15 EPA/play may be good, or may have played the league’s three worst defenses. The adjustment is iterative and online, never looking forward, shrunk by games played (week 1 is not entitled to strong opinions) and partially carried across seasons.
No team rating captures that the starter changed. qb_vs_offense measures the gap between the announced QB’s rating and the offense’s recent level: it catches backups and injuries without paying for an injury feed. In the NFL that is worth 2 to 7 points of spread.
Home-field advantage fell from ~2.7 points in the mid-2000s to ~1.5 in 2020–22, and has climbed back. Fixing it at a constant is a systematic half-point error across entire seasons, so it is estimated recursively from the residuals of home games.
Coordinates for every venue in the data, including international sites (Wembley, Tottenham, Azteca, Munich, São Paulo, Madrid, Melbourne). Haversine distance, a signed time-zone shift — flying east costs more than flying west — and altitude relative to a team’s own home: Denver’s edge is not playing high, it is playing higher than the opponent.
There is no random cross-validation in this project. Shuffling 2015 and 2023 games into the same fold leaks the future through team ratings and massively overstates performance. To predict season S only earlier seasons are used: model, distribution, calibration and ensemble weights are all refit at every step.
During development the ensemble weights were fit on component predictions in sample: the classic stacking leak. It cost 0.6 points of MAE and made the combined model worse than any of its parts — which is the clearest alarm bell there is.
Fixed with temporal cross-fitting: the predictions used to fit the weights are generated in disjoint time blocks on an expanding window. Never in sample, and never with the future.
The betting module uses fractional Kelly (0.25) plus an explicit 50% shrink of the estimated edge, a hard 2% cap of bankroll per bet, and a 1.5% minimum edge threshold. Full Kelly on estimated probabilities produces 60–80% drawdowns: it is not a defensible option.
De-vigging uses Shin’s method, not proportional normalization. Longshot odds overstate their true probability, and on lopsided moneylines the difference between the two methods is 1–2 percentage points — exactly the size of the edge being looked for.
Strict walk-forward: to predict season S only earlier seasons are used. Everything is refit at every step — model, margin distribution, calibration and ensemble weights.
| Metric | Model | Market (close) | Model without the line |
|---|---|---|---|
| Brier | 0.2130 | 0.2122 | 0.2185 |
| Log-loss | 0.6141 | — | — |
| Calibration error (ECE) | 0.0160 | — | — |
| Margin MAE | 10.03 | 9.97 | 10.27 |
| Total MAE | 10.51 | 10.51 | — |
| Straight-up accuracy | 66.3% | — | — |
The margin model fits a residual on top of the closing line: the prediction is the line plus a correction. So the obvious objection is the right one — if it was trained to resemble the market, a one-point disagreement sits well inside its own error. The third column is that objection measured, on the same 3,908 out-of-sample games: a variant that never sees the line at all.
It is worse than the line on its own by 0.0063 of Brier and 0.30 points of MAE. That is the honest ranking: market first, model-with-market second, model-without-market third. The line is the strongest thing on this page and the model does not beat it.
The calibration is fitted against realised outcomes, not against lines — the target is whether the home side actually won, and the margin distribution comes from margins that actually happened. That is why the probabilities above can be read as probabilities. What it does not buy is an edge: with a margin MAE of 10.03 points, a one-point disagreement is a tenth of the typical error, and the ATS record shows accuracy does not rise with the size of the disagreement.
Until 3 September 2026 the model fitted a residual on top of the closing total the same way it does for the spread. Over the full walk-forward — 3,829 games — it was worse than the line on its own: MAE 10.574 against 10.510, a paired difference of +0.064 ± 0.019 (t = +3.42), worse in 12 of 14 seasons.
The direction was no better. When the model disagreed with the total by more than a point it picked the right side 47.8% of the time; at two points, 47.5%. Break-even at −110 is 52.4%. A component that is worse than its own input is not kept “just in case”: the total this model publishes is the line, and there is no over/under lean anywhere on the site.
Reproduce it with python scripts/totals_vs_line.py --from 2012.
The two columns sit side by side on purpose. Reporting the model’s Brier without the market’s next to it says nothing: the question is not “is it good?” but “is it better than what already exists for free?”
1893-1916-99 (49.7%), IC 95% [48.1%, 51.3%]. Breakeven at -110 is 52.4%. Not significant.
An against-the-spread percentage without a confidence interval means nothing. Over a few hundred bets the standard error is several percentage points, and presenting the bare number is the most common way of selling noise as edge.
Careful with the ECE column here. It runs between 0.04 and 0.10 each season while the overall ECE above is 0.0160 — far better than any of its parts, which sounds like a trick. It is not: ECE is biased upward on small samples. With 267 games split across ten bins that is about 27 per bin, and sampling noise alone moves the observed frequency by some 9 points. That is mostly what those numbers measure. Pooled across 3,908 games the noise drops and what is left is real miscalibration.
| Season | Games | Brier | Market Brier | Margin MAE | Market MAE | ECE | Accuracy |
|---|---|---|---|---|---|---|---|
| 2012 | 267 | 0.2150 | 0.2107 | 10.83 | 10.72 | 0.0540 | 64.8% |
| 2013 | 267 | 0.2110 | 0.2057 | 10.01 | 9.81 | 0.0985 | 70.8% |
| 2014 | 267 | 0.2083 | 0.2090 | 11.26 | 11.28 | 0.0729 | 67.0% |
| 2015 | 267 | 0.2247 | 0.2274 | 10.06 | 10.12 | 0.0291 | 63.3% |
| 2016 | 267 | 0.2167 | 0.2184 | 9.04 | 9.04 | 0.0555 | 62.9% |
| 2017 | 267 | 0.2051 | 0.2042 | 10.16 | 10.08 | 0.0452 | 69.3% |
| 2018 | 267 | 0.2106 | 0.2122 | 9.87 | 9.87 | 0.0532 | 67.8% |
| 2019 | 267 | 0.2155 | 0.2135 | 10.35 | 10.18 | 0.0943 | 65.2% |
| 2020 | 269 | 0.2041 | 0.2044 | 9.81 | 9.79 | 0.0876 | 67.3% |
| 2021 | 285 | 0.2211 | 0.2187 | 10.72 | 10.67 | 0.0716 | 62.8% |
| 2022 | 284 | 0.2095 | 0.2096 | 8.89 | 8.78 | 0.0517 | 67.6% |
| 2023 | 285 | 0.2193 | 0.2173 | 10.07 | 9.98 | 0.0622 | 66.3% |
| 2024 | 285 | 0.2024 | 0.2039 | 9.75 | 9.70 | 0.0679 | 69.5% |
| 2025 | 285 | 0.2144 | 0.2112 | 9.77 | 9.67 | 0.0376 | 64.9% |
| 2026 | 79 | 0.2255 | 0.2297 | 9.80 | 9.78 | 0.0623 | 62.0% |
Predicted probability against observed frequency. If the two diverge the model is lying even when its Brier looks good — and to price a bet, the probability matters more than the ranking.
| Probability range | Predicted | Observed | Games |
|---|---|---|---|
| 0–10% | 8.9% | 0.0% | 6 |
| 10–20% | 16.1% | 17.6% | 68 |
| 20–30% | 26.1% | 23.9% | 276 |
| 30–40% | 35.8% | 35.3% | 621 |
| 40–50% | 44.2% | 45.3% | 530 |
| 50–60% | 55.7% | 57.1% | 847 |
| 60–70% | 64.8% | 65.6% | 796 |
| 70–80% | 74.2% | 77.5% | 520 |
| 80–90% | 84.2% | 89.1% | 220 |
| 90–100% | 92.2% | 91.7% | 24 |