New to these reports? Start here
  • Dotted-underlined words have a plain-English definition — hover or tap them. Every term is also on the glossary page.
  • "Null" means we found nothing, not that something broke. Most reports here are negative results, on purpose — knowing an idea doesn't work is the point.
  • Two questions get asked separately. First, is the effect real? Second, is it already priced into the betting odds? An effect can be completely real and still useless to bet on.
  • A "calibration" row is a self-check. It runs the same method on something already known to be true. If that fails, the whole report is unreliable — so it's reported alongside the findings.
  • If a confidence interval includes zero, the real effect might be nothing at all, so no claim gets made.

NFL Prop Usage Model — Design Doc (Step 1: data + walk-forward harness)

Status: data layer + fold generator only. No model is fit yet. Deterministic seed 20260714. House rules apply: judge downstream models vs devigged closing lines, walk-forwardwalk-forwardEvaluating week by week using only what was knowable before each week, mimicking how the model would actually have been used at the time. only, cluster bootstraps by game_id / player_week_id, honest negative results are valid.

Motivation

Production fantasy projections ([redacted]:: _calculate_weighted_baseline) collapse each stat to a single exponentially-decayed trailing mean (half-life ~4.6 games, optional faster volume decay). That conflates three things that move independently:

  1. Volume (usage): targets, carries, pass attempts — driven by role/depth chart.
  2. Efficiency: yards per target/carry, catch rate, TD rate — noisier, regresses hard.
  3. Game environment: how many plays and how pass/run-tilted this specific game projects to be, driven by the point spread and total.

This harness produces the features to model each stat as E[stat] = volume x efficiency, with volume and the game-script tilt conditioned on the pre-game game environment (implied team total + spread). Modeling volume and efficiency separately lets each layer regress to its own prior at its own rate — the single-mean baseline cannot.

Layers and features (all strictly-prior unless noted)

Layer 1 — Volume / usage (trail3_*, trail8_* over targets, carries, attempts, receptions, plus trailing target_share, air_yards_share). Short (3g) window captures recent role change; long (8g) window captures the stable rate.

Layer 2 — Efficiency (eff_ypc, eff_ypt, eff_catch_rate, eff_rush_td_rate, eff_rec_td_rate, eff_ypa, eff_pass_td_rate). Each is a trailing 8-game sum-num / sum-den ratio, shrunk toward the position-level median ratio with a pseudo-count k=20 denominator units so small samples do not explode. prior_games carries the confidence.

Layer 3 — Game environment (implied_team_total, implied_opp_total, team_spread, game_total, is_home). Derived from consensus (median-across-books) spread + total: implied_team_total = game_total/2 - team_spread/2. These are pre-game market lines, legitimately known before kickoff — the one contemporaneous (not trailing) signal, and not leakage. has_environment flags availability.

Layer 3b — Team pace / pass-run tendency (trail5_team_plays, trail5_team_pass_epa_play, trail5_team_rush_epa_play, trail5_team_off_epa_play). Trailing 5-game team means from nfl_team_game_epa, an expected-pace prior. Trailing only — the current game's realized plays never enter.

Targets (y): y_targets, y_carries, y_receptions, y_receiving_yards, y_rushing_yards, y_attempts, y_passing_yards, y_{rush,rec,pass}_tds = the current-week actuals that downstream models predict.

Data sources actually used

Layer Source Coverage
Player weekly logs cache/nflverse_data/player_stats_{2019..2025}.csv (local; no network) 2019-2025, all weeks incl. POST
Spreads/totals [redacted] (historical_spreads, historical_totals, 24 books) 2020-2024
2025 late odds [redacted] 43 games, Dec-2025→Feb-2026 (playoffs)
Team pace [redacted]::nfl_team_game_epa 2019-2025
Priors only viz_aggregates.db::nfl_player_season_agg prior-season target_share/air_yards

The 2019-2024 CSVs are the legacy nfl_data_py schema (recent_team, interceptions); 2025 is the new nflreadpy 0.1.5 schema (team, passing_interceptions, plus defensive/kicking rows). _load_player_weeks() harmonizes both to one canonical column set and filters to skill positions (QB/RB/WR/TE/FB).

Leakage guards enforced

  1. Trailing = shift-then-roll. Every trail* / eff_* feature shifts within player_id (or team) by 1 before rolling, so a row never sees its own week. Verified: trail3_targets at each row equals the mean of the prior ≤3 weeks, and prior_games==0 rows have NaN trailing.
  2. Efficiency priors are aggregates of prior sums, shrunk by position median — not the current outcome.
  3. Game environment is pre-game market lines only (spread/total set before kickoff), never a final score.
  4. Team pace is trailing (5g) — the current game's realized play count is excluded.
  5. Season-agg priors are prior-season only — end-of-season aggregates never leak into mid-season weeks.
  6. Walk-forward folds (walk_forward_folds): for each (season, week) test point, training is every row with season*100+week strictly less. Verified mid-fold: max_train_t=202222 < test_t=202301. 88 environment-gated folds, first test season 2021.
  7. Cluster keys preserved (game_id, player_week_id) so downstream bootstraps cluster by game/player-week, never iid over correlated rows.

Coverage (as built)

  • 39,385 player-weeks, 1,338 players, 2,688 games, seasons 2019-2025.
  • Game environment present on 24,711 rows (62.7%); by season: 2019=0, 2020=4867, 2021=5119, 2022=4831, 2023=5000, 2024=4894, 2025=0.
  • Team-pace prior present on 39,077 rows.

Data gaps (honest)

  • 2019 has no game environment — the spread/total cache starts 2020. 2019 player-weeks are usable for the trailing usage/efficiency layers but must be excluded from any environment-conditioned eval.
  • 2025 game environment is effectively absent (0 joined rows). odds_history.db is the only 2025 odds source and its game_schedule.week is NULLnull resultA test that found nothing. "Null" is the starting assumption that there is no real effect; a "null result" means the data gave us no reason to abandon that assumption. It does not mean the data was missing or the test failed to run.; those 43 late-season/playoff games cannot be week-joined to player logs, so the environment-gated harness stops at 2024. Trailing/efficiency features still cover 2025. Backfilling 2025 weekly spreads/totals is the top follow-up before this layer can be evaluated on 2025.
  • Snaps not available in these logs — volume is proxied by attempts/targets/ carries, not snap share. snap_share_projection in production comes from a separate source; wiring true snap counts is a follow-up.
  • Efficiency eff_ypt min is slightly negative (lateral/loss weeks shrunk toward a small prior); acceptable for step 1, revisit clipping when modeling.

Files

  • build_dataset.py — loader + harmonizer + trailing/efficiency/environment assembly + walk_forward_folds() generator + coverage report. Run: python3 build_dataset.py → writes dataset.parquet and prints coverage + a leakage check.
  • team_names.py — full-name→nflverse-abbr mapping (relocations folded).
  • dataset.parquet — built table (39,385 rows).

On this page

Terms in this report

Related

NFL Hierarchical Usage Model — Evaluation vs Baselines

Source

backtests/nfl_prop_usage/DESIGN.md
updated 2026-07-14 21:35