Graded every week, misses included

NFL props: how our numbers have held up

Every prop on our props board gets checked against the box score after the game. We grade the last board we built before kickoff, on a rule we wrote down before grading a single prop. So far that is 4,351 props over 90 games, weeks 2 to 7.

Updated 26 October 2026. Box scores from nflverse.

50.9%of our leans landed (1,382 of 2,715)
53.7%for the side the market priced as more likely
4 of 5stats where the market line was closer than ours
90games graded

The short version: the market has been better than us.

  • On pass yards, rush yards, receiving yards and catches, the sportsbook line landed closer to what happened than our number did. On all of them the gap is bigger than chance would explain.
  • Our side of the line landed 50.9% of the time. That is not clearly better than a coin flip, and it trails the 53.7% you would have hit by taking whichever side the books priced as more likely.
  • Big disagreements did not help: when our number was 20% or more away from the line, our side landed 51.2% (585 of 1143).
  • On anytime touchdowns the books' prices scored better than ours, and we rated long shots too low (more below).

We publish this because it is the honest state of the board. A props number is only useful if you know how far to trust it, and right now the answer is: as a second opinion next to the line, not as a replacement for it. This page updates automatically every week, whatever it shows.

How close each number landed

Average miss in the stat's own units, ours against the consensus sportsbook line, on the same props. "Closer" counts the props where ours was nearer the final number.

StatCloser overallOur avg missLine's avg missOurs closer
Pass yardsMarket57.353.663 of 168
Rush yardsMarket19.517.8207 of 484
Receiving yardsMarket2221.1457 of 994
CatchesMarket1.661.61466 of 944
Pass TDsToo close to call0.910.9367 of 111

"Better" means the difference holds up when we resample whole games (95% interval). Pass TDs use an expected count against a median-style line, so read that row loosely.

How often our side landed

Our lean is over when our number is above the line, under when it is below. Pushes count as neither.

StatLandedRate20%+ gap only
Pass yards72 of 16842.9%3 of 10 (30.0%) small
Rush yards234 of 48548.2%112 of 233 (48.1%)
Receiving yards511 of 99451.4%246 of 476 (51.7%)
Catches504 of 95252.9%214 of 404 (53.0%)
Pass TDs61 of 11652.6%10 of 20 (50.0%) small
All stats1382 of 2,71550.9%585 of 1143 (51.2%)

Week by week

WeekGamesLeans landedRateLine closer on
Week 216222 of 46148.2%3 of 5
Week 316224 of 47347.4%4 of 5
Week 416273 of 48656.2%5 of 5
Week 515234 of 45551.4%3 of 5
Week 614223 of 43850.9%4 of 5
Week 713206 of 40251.2%4 of 4

Anytime touchdowns

For touchdowns we give a chance, not a line, so we grade the chance. Across 1,587 player-games the books' prices scored better than ours (Brier score 0.1407 against our 0.1454; lower is better). The table shows where: when we gave a player under a 10% chance, we said 3.8% on average and 7.0% of them scored. The books said 10.0%. We are too low on long shots.

Our chancePlayersWe saidBooks saidScored
under 10%6393.8%10.0%7.0%
10-20%37214.7%20.2%21.0%
20-35%31726.5%28.7%27.8%
35-50%18141.6%39.4%33.1%
50% and over7860.1%55.5%56.4%

The books' touchdown prices still include their margin, so their chances run a little high across the board.

Our biggest misses

And our biggest hits

The rule, written first

It is easy to grade yourself kindly once you have seen the results, so the rule was written and committed on 19 October 2026, before the grader existed. It fixes which board counts (the last one built before kickoff), which props are left out (inactive players, as sportsbooks void them), and every number above. Its fingerprint is 7dd3152732318965; any change to the rule would change it.

Coverage: week 1 boards carried no sportsbook lines, so grading starts in week 2. 369 props were void because the player did not record a stat. 2 finished games had no board before kickoff.

Read the full rule
# NFL props board: pre-registered grading rule

Written 2026-10-19, before the grader existed and before any NFL props row had been compared with a box score by this
tool. `tools/nfl_props_grade.py` prints this file's sha256 and the commit that added it into every report, so the rule a
report was graded under can be checked against what was written here first. Mirrors `docs/CFB_PROPS_PREREG.md`.

Unlike the CFB rule there is no pass/fail bar and no "not yet" verdict. The record is public proof of how the board has
held up, so it is published whatever it says, misses included. The sections below fix how it is computed.

## Question

How close are the ATO model's NFL stat projections (pass yards, pass TDs, rush yards, receiving yards, catches, anytime
TD) to what happened, compared with the market line the board showed next to them, and how often does the model's side of
the line (over or under) land?

## What is graded

- The board used for a game is the last archived build whose `generated` time is before that game's kickoff
  (`~/archives/nfl-props-snapshots/<season>-weekNN/props_*.json`). Later builds never replace it. Kickoff comes from the
  nflverse schedule (`gameday` and `gametime`, Eastern time, converted to UTC). A board belongs to a game when its
  `season` and `week` equal the game's and the row's team and opponent are the game's two teams (opponent codes LAR, WSH
  and JAC are mapped to nflverse's LA, WAS, JAX, the same mapping the board builder uses).
- One row per player, stat and game. A row counts only if all of these hold:
  1. the player's `status` on that build is not OUT, DOUBTFUL, IR or D (the builder already drops these; the check is kept
     so the rule does not depend on it);
  2. it has the market in question (see Markets);
  3. the game is final in nflverse (the schedule has a score and the weekly player stats hold rows for that `game_id`);
  4. the player has a stats row for that game, matched on game, normalised name and team. Normalisation is
     `tools.nfl_props_history.norm_name`, the rule the board builder uses (lower case, suffixes Jr/Sr/II/III/IV dropped,
     letters only). A player with no row is a void prop (inactive, or no recorded stat in any category) and is left out of
     the primary numbers, as sportsbooks void those props.
- Sensitivity, reported next to the primary and never instead of it: players with no stats row counted as 0.
- Rows are counted and reported at every drop: no board before kickoff, game not final, status, no market, void.
- The reported actuals are nflverse weekly player stats (`stats_player_week_<season>.parquet`, CC-BY-4.0): `passing_yards`,
  `passing_tds`, `rushing_yards`, `receiving_yards`, `receptions`. Anytime TD is `rushing_tds + receiving_tds >= 1`, the
  same quantity the model's probability is built from. Sensitivity: also counting `special_teams_tds`.

## Markets (kept separate, never pooled)

- **Books**: the consensus line `bk_line` (the line most books hang, at least 2 books, `bk_books >= 2`) and its de-vigged
  over chance `bk_over`. Anytime TD: `bk_over` is the average Yes price across books converted to a chance, so it keeps the
  books' margin; this is stated wherever it is shown.
- **Underdog**: `ud_line` and `ud_over` where the board carried them (they were removed from the page on 2026-10-15, so
  the Underdog arm covers earlier weeks only).
- Kalshi and Novig fields are not graded: Kalshi's terms bar public display and Novig is display-only on the board.

A row can appear in both arms; each arm is analysed on its own rows.

## Comparisons (all with n)

1. **Accuracy, line stats** (pass yards, rush yards, receiving yards, catches, pass TDs). By stat: mean absolute error of
   ours and of the market line, bias (prediction minus actual) for each, the paired mean of (|ours - actual| minus
   |line - actual|) with a 95% bootstrap interval, and the share of rows where ours was closer. The bootstrap resamples
   whole GAMES (players in one game share script and weather, so rows are not independent), 2,000 resamples, seed
   20261019. `ours` is the board's `ours` field: the model's per-game expected stat line scaled to its adjusted projection;
   for pass TDs it is an expected count, the market line is a median-style line, and the report says so.
2. **Lean.** Over when ours is above the line, Under when below; equal means no lean. Pass TDs: Over when the model's
   chance of clearing the line (`ours_over`) is above the market's (`bk_over` or `ud_over`), the comparison the board
   itself shows for that stat. A lean hits when the actual lands on that side. A push (actual equals the line) is reported
   separately and is in neither hits nor misses. Reported overall and by stat for all leans, and for the large-gap bucket:
   **gap percent >= 20 in absolute value**, where gap percent = (ours - line) / max(line, floor) with floors 50 pass yards,
   15 rush or receiving yards, 2 catches, recomputed per market arm; for pass TDs the bucket is a gap of at least 20
   percentage points between the two chances. Each rate carries its Wilson 95% interval and the exact one-sided binomial
   p-value against 50%.
3. **Baselines for the lean.** The market's own lean (Over when its de-vigged over chance is above 50%) on the same rows
   where that chance exists; always Over.
4. **Anytime TD** is a probability and is not graded by MAE. Brier score and log loss (probabilities clipped to
   [0.001, 0.999]) of ours and of the market's implied chance, the paired mean of (Brier ours minus Brier market) with the
   same game-cluster bootstrap, the Brier of a constant equal to the sample's scoring rate (context), and a calibration
   table by ours-probability band (under 10%, 10-20%, 20-35%, 35-50%, 50% and over).
5. **By week and by stat** the same numbers are produced for each NFL week, so the page can show the record week by week.
6. **Largest misses and hits.** In the large-gap bucket on the Books arm: the ten leans that missed by the widest margin
   and the ten that hit by the widest, margin = |actual - line| on the lean side. For anytime TD: the five rows where the
   model's chance beat the market's by the most and the player did not score, and the five where it was lower by the most
   and he scored.

X = 20 is fixed here and is not tuned after seeing results.

## Display rule

A rate or interval whose n is under 30 is not shown on the public page; it says "n under 30, not shown" and the count.
The JSON keeps n for every number and marks a rate with `small: true`.

## Known limits, stated in advance

- The board's lines are only as fresh as its last sportsbook pull before the build; the board can show a line that moved
  later. That can only make the market look worse than its closing line.
- The model does not use the prop lines as an input. It is fitted on earlier games, so weeks 1-6 are out of sample for the
  weekly projections, but the role adjustment on `ours` is the board's own and was changed on 2026-10-10 and 2026-10-17;
  earlier boards were graded as they stood, not re-built.
- Weeks 1 and 2 have thinner coverage: the archive starts at 2026-09-13 15:48Z, so games before that have no board, and
  early builds carried no sportsbook lines on some rows.
- Rows within a game are correlated; intervals resample games, but six weeks is still a small sample.
- Lines are mostly x.5, so pushes are rare; whole-number Underdog lines can push and are counted as such.
- A void prop (player did not play) is left out; a player who played and recorded nothing may also lack a stats row and
  is treated as void in the primary numbers. The sensitivity run counts those as 0.

Lines and prices are what the books showed when the board was built, captured a median of 23 minutes before kickoff. Nothing here is betting advice.