ML backtest methodology¶
The ML model that feeds PigPro's 30%-weighted ml_projection component is a per-position GradientBoostingRegressor. This page explains the exact training setup, what baselines we compare against, and the two generations of results (before and after adding rookie features).
The training frame¶
For every (player, season) pair in a training season, we join:
flowchart LR
A[nflverse_weekly_stats
season X] -->|actual total PPR| LABEL[y = season PPR]
B[nflverse_weekly_stats
season X-1] -->|aggregate per player| PRIOR[prior-season features]
C[nflverse_snap_counts
season X-1] --> PRIOR
D[nflverse_player_ids] -->|age, draft slot| ROOKIE[rookie features]
E[nflverse_combine] --> ROOKIE
PRIOR --> X[X features]
ROOKIE --> X
X --> MODEL[GBR per position]
MODEL -->|predict| YHAT[ลท season PPR]
LABEL --- YHAT
Source: ml_pipeline/draft_value_trainer.py โ _build_player_season_frame(). The SQL joins are documented there.
Features (27 total)¶
Prior-season performance (from weekly_stats aggregates)
prior_games,prior_total_ppr,prior_ppgprior_carries,prior_rush_yards,prior_rush_tdsprior_targets,prior_receptions,prior_rec_yards,prior_rec_tdsprior_pass_attempts,prior_pass_yards,prior_pass_tdsprior_avg_target_share,prior_avg_air_yards_share
Prior-season usage (from snap_counts)
prior_avg_off_snap_pct,prior_total_off_snaps,prior_snap_games
Player profile
age
Rookie signals (the 2024 gap fix โ see below)
is_rookie(draft_year == season)years_into_leaguenfl_draft_round,nfl_draft_ovrcombine_forty,combine_weight,combine_vertical,combine_broad_jump
Label¶
actual_total_ppr = sum of fantasy_points_ppr across weeks 1-18 of the target season, regular season only.
Per-position modeling¶
One GradientBoostingRegressor per position (QB, RB, WR, TE). Hyperparams: n_estimators=200, max_depth=4, learning_rate=0.05, random_state=42. No hyperparameter search โ these are known-reasonable defaults that haven't been tuned further.
The per-position split matters because the feature signal differs wildly โ a prior_rush_yards=0 tells you a lot about RB future but nothing about WR future.
The temporal split¶
Train: 2022 + 2023 seasons (each season as features-of-prior + label-of-current). ~1,100 training examples.
Test: 2024 season (features from 2023, label from 2024). 559 players with complete prior-year data.
Why 2024 as holdout: it's the last completed NFL season nfl_data_py published. The 2025 NFL season was played but upstream hasn't pushed the parquet yet. When that publishes, the backlog task retrains on 2022-2024 and validates on 2025.
Validation metric: MAE (mean absolute PPR-point error), Rยฒ (variance explained), Pearson correlation, and position-specific top-20 hit rate.
The baselines we beat (or try to)¶
No model is good in a vacuum โ we only care about whether it's meaningfully better than:
Persistence: "assume the player scores the same as last year." Literally ลท = prior_total_ppr. It's dumb. It's hard to beat.
Position median: predict the median total PPR for the player's position across the training seasons. A pure no-signal baseline โ whatever our model does, it should crush this.
We also report top-20 hit rate: of the 20 players the model ranks highest at each position, how many actually ended up in the top 20 by actual PPR?
Generation 1 โ pre-rookie-features (v1)¶
Initial trained model with only prior-season + age features.
| Metric | Model | Persistence | Position median | Winner |
|---|---|---|---|---|
| MAE (PPR) | 53.3 | 48.2 | 67.3 | persistence |
| Rยฒ | 0.336 | 0.336 | โ0.086 | tied |
| Correlation | 0.62 | 0.67 | 0.19 | persistence |
Per-position MAE:
| Position | Model | Persistence | Winner |
|---|---|---|---|
| QB | 80.8 | 74.4 | persistence |
| RB | 51.7 | 47.3 | persistence |
| WR | 53.8 | 48.4 | persistence |
| TE | 36.2 | 31.4 | persistence |
Persistence won every position. That stung.
Top-5 biggest misses (actual vs predicted):
| Player | Pos | Actual | Predicted | ฮ |
|---|---|---|---|---|
| Jayden Daniels | QB | 355.8 | 42.9 | +313 |
| Bo Nix | QB | 317.2 | 50.2 | +267 |
| Brian Thomas | WR | 284.0 | 18.6 | +265 |
| Malik Nabers | WR | 273.6 | 18.6 | +255 |
| Brock Bowers | TE | 262.7 | 15.9 | +247 |
All five were 2024 rookies. They had zero prior-season NFL stats in our feature set, so the model predicted near-zero and they produced like first-round picks.
The pattern was obvious: fix the rookie blind spot, the model wins.
Generation 2 โ with rookie features¶
Added 4 features from existing (but unused-by-the-model) nflverse tables:
is_rookiebinary flagnfl_draft_round(1-7, 99 for UDFAs)nfl_draft_ovr(1-262)combine_forty,combine_weight,combine_vertical,combine_broad_jump
Retrained on the same 2022+2023 data, retested on 2024.
| Metric | Gen 1 | Gen 2 | ฮ |
|---|---|---|---|
| MAE (PPR) | 53.3 | 49.5 | โ 3.8 |
| Rยฒ | 0.336 | 0.420 | โ +0.08 |
| Correlation | 0.62 | 0.68 | โ matches persistence |
Per-position MAE:
| Position | Gen 1 | Gen 2 | Persistence | Winner (Gen 2 vs persistence) |
|---|---|---|---|---|
| QB | 80.8 | 74.7 | 74.4 | essentially tied |
| RB | 51.7 | 50.8 | 47.3 | persistence |
| WR | 53.8 | 48.2 | 48.4 | model |
| TE | 36.2 | 34.1 | 31.4 | persistence |
Top-20 hit rate (predicted top-20 โฉ actual top-20):
| Position | Gen 1 | Gen 2 | Persistence |
|---|---|---|---|
| QB | 60% | 65% | 60% |
| RB | 45% | 55% | 60% |
| WR | 50% | 50% | 40% โ model wins |
| TE | 55% | 55% | 65% |
Per-position CV-Rยฒ on training data:
| Position | N examples | Gen 1 | Gen 2 |
|---|---|---|---|
| QB | 243 | 0.23 | 0.29 |
| RB | 418 | 0.28 | 0.36 |
| WR | 665 | 0.51 | 0.57 |
| TE | 353 | 0.40 | 0.48 |
Feature importance ranking: nfl_draft_ovr landed in the top 5 features for all four positions. The signal was real.
New top-5 misses after the fix:
- Joe Burrow โ injury recovery year
- Sam Howell โ depth-chart churn
- Aaron Rodgers โ retirement U-turn
- Jake Browning โ backup who started unexpectedly
- Bucky Irving โ late-round rookie RB, limited college snaps (still surfaces at PigPro 8.00 now)
None of the old 5 rookie misses remain on the list. The rookie-feature fix closed that specific gap. The remaining misses are a different class of error โ context shifts we don't model yet (injury recovery, depth-chart churn, retirements).
Generation 3 โ v1.1 Hybrid (current shipping)¶
Promoted April 2026 (PR #47/#48). Adds Usage / Opportunity as an 8th component while keeping all seven v1 signals intact. Key insight from the failed v1.5 experiment that preceded it: dropping ADP Value entirely (replacing it with an ADPโvsโML gap signal) regressed Spearman by โ0.054. v1's market-consensus signal is too strong to throw away. v1.1 is "v1 + Usage overlay."
What's new¶
- Usage component at 8% โ
target_share ร 0.55 + snap_pct ร 0.45for WR/TE;rush_share ร 0.45 + target_share ร 0.25 + snap_pct ร 0.30for RB; QB special case folded into ML Projection. - Weights rebalanced from
30/25/20/10/10/2.5/2.5(sum 1.0) to27.6/23/18.4/9.2/9.2/8.0/2.3/2.3(sum 1.0). - QB override: when
position == 'QB', the 8% Usage weight folds into ML Projection (so QB ML weight = 35.6%). Necessary because the Usage formula couldn't differentiate pocket QBs from rushing QBs by enough percentile range. - Rookie-proxy logic for Usage on players with <12 career NFL games AND
draft_year >= season - 2. Usesnflverse_player_ids.draft_ovrto set Usage from draft capital (pick 1 โ 9.96, UDFA โ 2).
v1 vs v1.1 against 2025 actuals¶
flowchart LR
A[pigpro_baseline
version=v1] -->|persist via PR #43| HARNESS[pigpro_compare
back-test against 2025 PPR]
B[pigpro_baseline
version=v1.1] --> HARNESS
HARNESS --> RESULTS[per-position Spearman + top-N precision]
| Metric | v1 | v1.1 | ฮ |
|---|---|---|---|
| Overall Spearman r | 0.753 | 0.772 | +0.019 |
| QB Spearman r | 0.71 | 0.73 | +0.02 |
| RB Spearman r | 0.83 | 0.84 | +0.01 |
| WR Spearman r | 0.79 | 0.81 | +0.02 |
| TE Spearman r | 0.83 | 0.86 | +0.03 |
| QB top-12 precision | 50% | 50% | hold (after QB override) |
| RB top-24 precision | 75% | 75% | hold |
| WR top-30 precision | 67% | 70% | +3 |
| TE top-12 precision | 58% | 67% | +9 |
n = 533 players matched between pigpro_baseline and nflverse_weekly_stats 2025 totals.
Variant testing โ why v1.1-B (92/8) won¶
The promotion PR tested four blend ratios of v1-composite ร Usage. All four beat v1 on overall Spearman; the differentiator was RB top-24:
| Variant | Spearman | RB top-24 | TE top-12 |
|---|---|---|---|
| v1.1-A (95/5) | 0.766 | 75% | 67% |
| v1.1-B (92/8) | 0.772 | 75% | 67% |
| v1.1-C (90/10) | 0.775 | 71% (โ4) | 67% |
| v1.1-D (85/15) | 0.780 | 71% (โ4) | 67% |
C and D had marginally higher rank correlation but cost 4 points of RB top-24 precision โ RBs are the foundation of every fantasy draft, so we picked the variant that held them. Goldilocks: B.
Notable lifts under v1.1¶
The biggest legitimate score increases vs v1 (after the rookie-gate fix to exclude retired QBs from the proxy path):
- JJ McCarthy (rookie QB1) โ v1 1.85 โ v1.1 2.47
- Travis Hunter (#2 overall pick WR) โ 2.67 โ 3.25
- Ascending TE tier (the gap v1 missed): TJ Hockenson, Cole Kmet, Pat Freiermuth, Tyler Conklin, Cade Otton โ all up 0.4โ0.5 PigPro
- High-snap WRs v1 underweighted: Amon-Ra St Brown, Chris Godwin, Michael Pittman
The drops list (Bryce Ford-Wheaton, Kyle Monangai, Blake Corum) is entirely deep-bench players v1 was overrating. No star drops to a bust tier.
How does Gen 2 compare to persistence overall?¶
Honest answer: tied on variance explained, slightly behind on MAE, but wins where it matters.
- The model wins: Rยฒ (0.42 vs 0.34), correlation (matches 0.68), WR MAE and top-20 hit rate.
- The persistence baseline still edges out on overall MAE (48.2 vs 49.5) because it has a flatter floor on the long tail of low-scoring veterans.
- What the model uniquely does well: surfacing rookies. Persistence literally can't predict a rookie because it reads from prior_total_ppr=0 โ 0.
We ship the model because:
- Draft strategy benefits from rookie signal; persistence cannot give you that.
- WR is the deepest position; winning WR MAE + top-20 hit rate compounds weekly.
- The model output is one input among seven in PigPro, not the whole score. 30% weight means imperfect projection still helps the composite if it's directionally right.
Running the backtest yourself¶
The whole thing is reproducible end-to-end:
# In a backend pod or locally with DB_PASSWORD set
python -m ml_pipeline.backtest --persist
This trains on 2022+2023, scores 2024, writes a row to ml_backtests, and prints the per-position table. Persisted rows show up at /api/ml/backtest and on the /draft-analytics page.
Source files:
ml_pipeline/backtest.pyโ the harness itself.ml_pipeline/draft_value_trainer.pyโ the model definition + training.ml_pipeline/draft_value_features.pyโ SQL + feature engineering.ml_pipeline/value_scorer.pyโ how the ML prediction is normalized to PigPro's 0-10 scale.
What's next (Tier 2)¶
- 2025 outcome-based weight refit โ once the 2025 NFL season closes and
nflverse_weekly_statshas full 2025 totals, runpigpro_v1_1_hybrid.fit_weights(2025)with the constrained-SLSQP fitter against actual rank correlation. First chance to learn weights from data instead of intuition. - QB Usage reformulation โ current QB override is a workaround. A reformulated formula incorporating
passing_attempts_share,designed_run_rate, andred_zone_designed_toucheswould let QBs participate in Usage scoring without penalising pocket passers. - Empirical ADP from mocks โ mock-draft corpus (~19K decisions in
draft_picks_v2) can replace the recommender's Gaussianprob_availableestimate with measured per-player survival curves. Direct accuracy upgrade for the Draft Room. - Public-source ADP ensemble โ Sleeper + FFC are persisting nightly to
adp_records(April 2026). Phase 4 of that work: median-across-sources ensemble feeding ADP Value Gap so we stop deriving ADP from a single source. - Context-shift features for ML โ injury_return flag, team_change flag, QB_change flag. Would catch Burrow-type misses.
- XGBoost / LightGBM โ more capacity with better handling of heterogeneous features. GBR is a reasonable baseline; there's low-hanging accuracy beyond it.