← All findingssource · memory/project_minutes_lever_measured.md
Tested the "rotation minutes is the biggest lever" thesis properly (4 agents, real prod data). The physics is right; the edge is not there. Three do-not-builds, each with a measurement.
1. projected_minutes IS a better minutes number — but it does NOT transfer to props
SDS /wnba/lineups/{game_id} serves per-player role + projected_minutes (RotoWire proj), consumed by NOTHING. On 553 player-games it beats L5: overall MAE 3.97 vs 4.26; starters 3.52 vs 4.23 (16.7%, clustered CI [−1.03,−0.38]); the divergent subset |proj−L5|>5 (15.3% of rows) 4.39 vs 5.94 (26%). Divergence moves toward truth 80.8% of the time; OLS beta of (actual−L5) on (proj−L5) = 0.608. Leakage decisively ruled out (52 projected players DNP'd, 9 as 20+min starters; only 2/27 games had lineup roster == boxscore roster; RotoWire refresher writes only for non-final games).
BUT the scoping backtest on 1,633 real shipped props says it doesn't convert: ratio-rescaling the projection gives paired MAE −0.0505 (z=−1.75, below the 2σ bar at that n), and it is WRONG-SIGNED exactly where the lever is supposed to live — role-expansion rows (ratio>1.15, n=125) are +0.358 WORSE. Half the small aggregate gain isn't minutes at all: a constant ×1.02 with no minutes input gets −0.0135 (z=−1.84), consistent with the known UNDER-bias. Independently reproduces the prior sweep's "fitted minutes model has ~2% skill that does NOT transfer to prop MAE".
Prior-negative scope corrected: the −7.11% in project_brier_vs_vegas_closed used an internally-fitted minutes model (SDS lineups were empty then), so it never tested this source. What still binds is its diagnosis: the oracle edge is realized game script (blowouts, foul trouble, mid-game injury), unreachable pre-game at ANY MAE.
2. Redistribution is real, diffuse, and already priced — do NOT build a beneficiaries model
226 absence events / 4,554 player-games. Accounting closes (teammate deltas 21.86 min vs absent baseline 21.25, r=0.988). But the placebo is the story: in games with NO absence the biggest swinger still gains +10.79 min. Against that floor an absence buys the top beneficiary only +3.25 excess min (15% of freed minutes); effective absorbers 4.66 — a broad reshuffle, not next-man-up. Zero usage change (+0.08 overall, −0.04 for big gainers); points track minutes at 0.371/min = their own rate. No role expansion exists to model.
projected_minutesalready encodes the absence at team level: reallocates +12.42 min vs +11.29 actual. A separate model adds nothing.- The with/without approach — what BOTH existing implementations use — is worse than doing nothing: MAE 5.78 vs plain trailing-5 4.53; OLS beta 0.168.
- The stranded projections-engine artifact applied to 2026 absences made MAE 23.1% WORSE, sign agreement 44.5% (below a coin flip). It is not stale, it is harmful. Leave it undeployed; close the backlog item.
- SDS beneficiaries has a real SQL bug (LEFT JOIN collapses every baseline into
absent_id=NULL, sominutes_increaseis a raw average mislabelled as a delta) AND is prod-dead (NBA count 0, WNBA 501). Delete rather than repair. - Sizing kills it anyway: 0.63 role-shifted prop-liquid players per slate ≈ 2.6% of the board. Only top-4 minutes players get lines; just 27% of ≥5-min gainers are top-4.
- One survivor: same-position tilt (+1.635 min, CI [+0.745,+2.504]); BIG-out→BIG +4.67 vs BIG-out→GUARD +1.00.
3. No news-latency edge — and both standing claims were false
/wnba/injuries/status-history exists with occurred_at (30-min poller granularity, 22 days deep). The report is NOT final at 21:00Z — 18.3% of transitions land post-batch pre-tip (~4/slate). But lateness buys nothing: teammates of a scratch announced AFTER 21:00Z hit 53.9% (n=1,446) vs BEFORE 21:00Z 53.6% (n=817) — +0.3pp, z=+0.12; not significant clustered (z=+1.31) and it VANISHES for material scratches of ≥20-mpg players (50.6%, n=348). Market agrees: teammate lines move +0.016 units around a late Out and drift −0.065 from 21:00Z to close (against you — no CLV).
- "Lines don't exist at news time" — FALSE. Median first post 42.2h pre-tip; 93.3% posted >4h out; 85% of transitioning players with props already had a quote. The old "17/503 scorable" was a player-COVERAGE problem (only 98/355 transitions map to any prop), not line timing. Corrects project_news_speed_first_light.
- "WNBA props soft at release, +6.21% ROI" — FALSE. Blind over at the opening line = −1.3% ROI (51.7%, n=5,470) vs −3.4% at close. The opener is ~1.1pp better than the close but still inside the vig.
The one real defect found (see project_wnba_batch_misses_afternoon_games)
The 21:00Z batch is too LATE, not too early: verified independently, 8 of the 39 remaining games (21%) tip at or before 21:00Z and get no pre-tip board.
Provenance caveat on all lineup work
Every WNBA lineup row 7/28–8/06 carries updated_at inside a single 3-second burst (2026-08-06T18:19:19–22Z) — the "history" is one backfill, not point-in-time captures. Values are genuine projections (not laundered actuals), but nothing proves they were readable pre-tip on past dates. Snapshot /wnba/lineups/{game_id} nightly pre-tip before treating any such backtest as a replay. A clean pre-tip capture for 2026-08-06 is frozen in the session scratchpad.
Power correction for project_wnba_season_endgame: its "308 props for 0.10 MAE" came from a mild perturbation (paired sd 0.6263). The minutes rescale has sd 1.1679, so 0.10 needs ~1,070 props and the observed 0.0505 would need ~4,190. Carry the per-perturbation sd, not one number.
DO-NOT-BUILD #4 — threading pred_min into per-stat models (2026-08-25)
The reorder itself is a 4-line move (stat block :451-468, minutes :470-473,
line 469 blank, no shared state beyond series) — trivially safe and
completely pointless. Shipping it alone would be worse than useless: it would
look like promotions move props when they provably do not, and no test could
catch the difference.
And do NOT do the retrain that would make it bite. Already lost twice:
scripts/train_all_stats.py:7-9 trains control-only because Phase 5's A/B
proved pred_min redundant; the 2026-08-06 study found the minutes lever
wrong-signed by +0.358 MAE precisely on role-expansion rows (n=125) — the
only population this targets. Perturbing ~1,215 quantiles/slate for ~0.63
prop-liquid role-shifted players/night, needing ~4,190 props to resolve against
a 188-row board.
⚠️ scripts/historical_backtest.py CANNOT measure this class of change: it
bypasses score_matchup entirely (no lineups/minutes/NegBin/beneficiaries/
calibrator) while advertising calibration, hardcodes _quantile_v1, defaults
to NBA odds, and defaults --min-props 200 above a real 188-row WNBA slate.
Both feature caches are stale (WNBA ends 2025-10-10). Until a harness replays
score_matchup end-to-end, before/after on the scoring path is unfalsifiable.
SHIPPED INSTEAD (f01bca8): quarantine bets on fresh promotions. Because
is_starter moves minutes only, a promoted player gets starter minutes and
bench-priced props — measured live 08-25: Whitcomb +90% min / +0% pts,
Harrison +106% / +0%, Burke +12% / −43%. Bets withheld for
starter_rate_l5 <= 0.4 starters (PROMOTION_RATE_L5_MAX); projections and
observations retained, mirroring quarantine_combos. Verified: 10 promoted on
the board, 0 carrying bets, 320 bets served.
THE USER'S METHOD BEATS THE MODEL — conditional-on-minutes baselines (2026-08-25)
User asked how he'd sized the Caldwell over: average her points in games where she played 15+ minutes. That is a conditional-on-ROLE estimator, and it is decisively better than the model's blended rolling features.
Caldwell, computed EXCLUDING 8/24 (no hindsight), n=147 prior games:
| window | n | avg pts | over 4.5 | avg FGA |
|---|---|---|---|---|
| all games | 147 | 4.53 | 44.2% | 4.2 |
| minutes >= 15 | 62 | 7.81 | 77.4% | 6.6 |
| minutes >= 24 | 30 | 9.53 | 83.3% | 8.0 |
| minutes < 15 | 85 | 2.14 | 20.0% | 2.4 |
Model said 2.82. User's estimate 7.81. She scored 9.
⚠️ I WAS WRONG to call it shooting variance. Her FGA scales with minutes (2.4 → 6.6 → 8.0), so usage DOES follow role for her. On 8/24 she took 5 shots in 25.4 min — BELOW her own 8.0 average at 24+ minutes. She under-shot her starter profile and still cleared. Good bet, mediocre volume night.
IT GENERALISES — 9 of 9 promoted players on the 2026-08-25 board have a conditional average ABOVE the model's projection. Median ratio conditional/model = 1.36x, range 1.13x–2.07x, zero exceptions (Whitcomb 2.07x, Rivers 1.62x, Mack 1.64x, Kuier 1.13x).
WHY the model misses it: the points model reads 38 features, 6 of them minutes, ~30 of them scoring RATES (pts_l5, fga_l5, fga_ewm...). Those rates are computed over ALL recent games, blending bench and starter appearances, and the model cannot separate "few points because few minutes" from "few points because few touches". Perturbing only the minutes block on 60 real bench rows moves the points median just +22% (4.57 → 5.58 at 12 → 24 min) — nowhere near the 1.36x–2.07x the conditional split shows.
Three independent pieces of evidence now agree on the OVER for fresh promotions: this 9/9 projection gap, the Caldwell case, and the 2026-08-05 backtest's 19-12 on the over, +5.8u (n=31, thin).
⚠️ NOT yet proven profitable at market prices. Conditional > model is a projection comparison, not a settled-results test. Needs CLV/ROI forward measurement against actual lines before staking.
NEXT BUILD (highest-value open item): a conditional-on-minutes baseline for
role-changed players — condition on minutes >= 15, NOT on a starter-run
counter. Caldwell's minutes-conditioned sample was n=62; her
starter-run-conditioned sample was n=1. DH's existing detectRoleTrend
re-anchors only for ESTABLISHED promotions and deliberately does not on day one
(role_trend_run=1 flags without anchoring) — which is exactly the game that
matters.