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:
- Volume (usage): targets, carries, pass attempts — driven by role/depth chart.
- Efficiency: yards per target/carry, catch rate, TD rate — noisier, regresses hard.
- 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
- Trailing = shift-then-roll. Every
trail*/eff_*feature shifts withinplayer_id(orteam) by 1 before rolling, so a row never sees its own week. Verified:trail3_targetsat each row equals the mean of the prior ≤3 weeks, andprior_games==0rows have NaN trailing. - Efficiency priors are aggregates of prior sums, shrunk by position median — not the current outcome.
- Game environment is pre-game market lines only (spread/total set before kickoff), never a final score.
- Team pace is trailing (5g) — the current game's realized play count is excluded.
- Season-agg priors are prior-season only — end-of-season aggregates never leak into mid-season weeks.
- Walk-forward folds (
walk_forward_folds): for each(season, week)test point, training is every row withseason*100+weekstrictly less. Verified mid-fold:max_train_t=202222 < test_t=202301. 88 environment-gated folds, first test season 2021. - 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.dbis the only 2025 odds source and itsgame_schedule.weekis 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_projectionin production comes from a separate source; wiring true snap counts is a follow-up. - Efficiency
eff_yptmin 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→ writesdataset.parquetand prints coverage + a leakage check.team_names.py— full-name→nflverse-abbr mapping (relocations folded).dataset.parquet— built table (39,385 rows).