READ-ONLY. src/ and web/ untouched; 4,159 tests still green. WHAT THE CHAMPION IS. probabilityEstimator is five lines of arithmetic: the empirical frequency of (stat > THIS line) over the game log, blended 0.6/0.4 with the last-5 frequency, then +/-0.03 opponent, +/-0.015 home/away, a cv>0.40 pull toward 0.50, and a clamp to [0.10, 0.95]. It reads three features. featureCache retains a dozen more that p_win never touches. THE ABLATION IS EXACT, NOT A REFIT. Every adjustment is closed-form from stored features and the consistency step is linear, so each layer subtracts algebraically out of the stored p_win -- no re-estimation, no re-fetch, no lookahead possible. Per stat, paired bootstrap: removing ALL THREE adjustments changes resolution by NOTHING on every stat hits -0.0059 total_bases -0.0015 rbi +0.0106 runs +0.0130 walks +0.0008 and rbi's home/away is mildly HARMFUL (+0.0053, CI excludes zero). So ~100% of the champion's resolution is base+recency: how often this player has cleared this number lately. Everything else is decoration. A CORRECTION. Pooled, the champion resolves 0.46; per stat it is 0.196 (hits) to 0.499 (rbi). Pooling stats with different base rates inflates correlation, so 0.46 should not be quoted as the champion's resolution. Last session's paired differences remain valid; only the absolute level was inflated. THE BIGGEST LOSS IS NOT A MISSING FEATURE -- IT IS THE CLAMP. 358 of 1,741 settled rows (20.6%) sit on the boundary, so the model emits a constant there and cannot rank a fifth of the book at all. And that constant hides two opposite failures: 0.900 covers home_runs-under truly winning 99.5% (9.5pts under-confident) next to hits-under truly winning 51.9% (38.1pts over- confident). PROB_CEIL=0.95 makes the 99.5% case inexpressible. Global over-prediction is +3.5pts, +7.6 on total_bases. None of this needs new data. ONE REAL MISSING-WEIGHTING LEAD: opportunity_drift, residual corr +0.156 on hits and +0.145 on total_bases -- it REPEATS across independent stats, unlike the weather hits on TB which sit inside the expected false-positive count (70 tests at alpha .05 expects 3-4). And we already compute it: arch-v1's opportunity axis uses it and extracts nothing (delta +0.0001). Wrong implementation, not a missing feature -- opportunity must scale the rate, not nudge the probability. ARCHETYPE IS UNMEASURABLE, NOT REFUTED. Only 2 of 41 labels (BOMBER, GHOST) reach n>=40 settled rows and every mean residual straddles zero. That is "we have not measured it", and it does not license acting in either direction. Why every challenger has failed is now legible: the ladder and hits-v1 REPLACE the frequency question with a fitted distribution; the environment axis adds inputs the champion ignores. Asking the frequency question at the traded line is the thing that works. Flagged, not fixed: model_snapshots.outcome is NULL on all 22,032 rows -- the retention table built for exactly this replay was never settled, so labels had to be joined from ledger_entries. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
12 KiB
DECOMPOSING THE CHAMPION — where its edge actually comes from
Read-only diagnosis, 2026-08-03. Nothing built, nothing touched. Run on the repaired settled set (n=1,741 after the settlement outage fix), not the frozen pre-fix set.
VERDICT, one line: the champion's entire edge is a hit-rate counter, its three adjustment layers contribute nothing (two are mildly harmful), and the largest recoverable loss is not a missing feature — it is the
[0.10, 0.95]clamp, which pins 20.6% of settled props to a constant and hides outcomes ranging from 52% to 99.5% behind the same number0.900.
1. What the champion actually uses (STEP 1)
src/services/intelligence/probabilityEstimator.js is five lines of arithmetic:
base = empirical frequency of (stat > THIS line) over the game log
weighted = 0.6·base + 0.4·(same frequency over the last 5 games)
p = weighted + oppAdj(±0.03) + homeAdj(±0.015)
if cv > 0.40: p = 0.9·p + 0.05 (volatile → pull toward 0.50)
p_over = clamp(p, 0.10, 0.95)
p_win = side === 'under' ? 1 − p_over : p_over
It reads exactly three features: opp_rank_stat, home_away, and
l10_stddev/l20_avg (for cv). featureCache computes and retains a dozen more
— park_h/hr/r, weather_temp_f/wind_mph/precip, rest_days,
opportunity_drift, ab_per_game, recent_ab_per_game, l5/l10/l20_avg,
game_count_in_7d — and p_win reads none of them.
2. Per-stat ablation (STEP 2)
Exact and analytic, not a refit. Each adjustment is a closed-form function of
stored features and the consistency step is linear (f(x)=0.9x+0.05 ⟹
f(a+b)=f(a)+0.9b), so every layer is removed algebraically from the stored
p_win. Nothing re-estimated, nothing re-fetched, no lookahead possible.
Paired bootstrap, 3,000 resamples, deterministic seed.
A negative delta means removing the layer HURT — i.e. it carried signal.
| stat | n | resolution (full) | −opponent | −home/away | −consistency | −ALL THREE |
|---|---|---|---|---|---|---|
| hits | 578 | 0.1964 | −0.0049 | −0.0006 | −0.0004 | −0.0059 [−0.0168,+0.0056] |
| total_bases | 284 | 0.2370 | −0.0056 | +0.0042 | +0.0002 | −0.0015 [−0.0139,+0.0115] |
| rbi | 273 | 0.4986 | +0.0059 | +0.0053 [+0.0002,+0.0103] | +0.0005 | +0.0106 [−0.0003,+0.0220] |
| runs | 115 | 0.4062 | +0.0060 | +0.0087 | −0.0006 | +0.0130 [−0.0106,+0.0366] |
| walks | 66 | 0.4776 | −0.0035 | +0.0055 | +0.0018 | +0.0008 [−0.0268,+0.0281] |
Removing all three adjustments changes resolution by nothing on every stat, and on rbi/runs it IMPROVES it. Exactly one ablation anywhere has a CI excluding zero — rbi home/away, and its sign says removing it makes the model better.
So ~100% of the champion's resolution is base + recency: how often this
player has cleared THIS number lately. That is the whole model. Everything else
is decoration.
A correction to how we read last session's scoreboard
Pooled across stats the champion resolves 0.46; per stat it is 0.196 (hits) to 0.499 (rbi). Pooling stats with different base rates inflates correlation, because p_win varies across stats in the same direction as the true base rate. 0.46 is a pooling artifact and should not be quoted as the champion's resolution. The paired differences in the scoreboard remain valid (champion and challenger were pooled identically); only the absolute level was inflated.
3. Do the challengers have it, or dilute it? (STEP 3)
| challenger | uses the base-frequency signal? | verdict |
|---|---|---|
| proj-v1.1 ladder | No — it replaces it. Fits a rate + NB distribution instead of counting frequency at THIS line | DILUTING. Measured reliably worse (−0.0301, CI excludes 0). It discards the one thing that works in favour of a lossier route to the same question |
| hits-v1 | No — same substitution, binomial instead of NB | DILUTING. Refuted (−0.022, CI excludes 0) |
| arch-v1 · environment | Adds park/weather, which the champion ignores | DILUTING. n=871, −0.0028, CI includes 0 — movement without information |
| arch-v1 · opportunity | Uses opportunity_drift — the one feature with repeated residual signal |
HAS THE FEATURE, WRONG IMPLEMENTATION (see §4) |
| arch-v1 · matchup | — | STILL PENDING (rows settle after ET midnight) |
| contact-v1 | Statcast contact quality; not in the retained vector | No evidence either way (n=1,055, CI includes 0) |
4. The missing-feature test — one real lead, already in our hands
Correlation of each unused feature with the champion's residual (won − p_win), per stat, bootstrap CI.
Multiple-comparisons discipline first: 14 features × 5 stats = 70 tests at α=.05, so 3–4 CI-excludes-zero results are expected by chance. Six appeared. A single hit is noise. Only a feature that repeats across independent stats is evidence — and exactly one does:
| feature | hits | total_bases | walks |
|---|---|---|---|
opportunity_drift |
+0.156 [+0.007,+0.292] | +0.145 [+0.005,+0.278] | −0.261 [−0.463,−0.017] |
recent_ab_per_game (same quantity) |
+0.052 | +0.136 [+0.002,+0.272] | −0.157 |
opportunity_drift = recent at-bats ÷ season at-bats-per-game. It is the one
axis a frequency counter is structurally blind to: base knows how often he
cleared the number, not that he has moved from 8th in the order to leadoff, or
back from injury on a bench role. Sign flips on walks (n=47, and walks scale with
plate appearances differently) — so this is stat-specific, which is doctrine-
consistent, not a contradiction.
But we already compute it, retain it, and built an axis on it — and that axis
extracts nothing (opportunity axis: n=539, delta +0.0001, CI [−0.0091,+0.0090]).
So this is not "go get a new feature." It is "the feature has signal and our
implementation of it is wrong" — arch-v1 applies it as a small multiplicative
nudge to p_win, which is not how you use an opportunity term. Opportunity should
scale the rate, before the frequency question is asked.
Weather on total_bases (wind_mph −0.164, precip −0.154) appears on one stat
only and sits inside the expected false-positive count. Recorded as a
non-lead unless it repeats.
5. Archetype on trial (STEP 4 item) — NO EVIDENCE, and the test is underpowered
The champion reads no archetype feature at all, so archetype cannot be ablated out of it. The fair test is whether archetype explains what the champion gets wrong: if an archetype's rows are systematically mispriced, archetype carries prop signal we're missing.
| stat | archetype | n | mean residual | CI95 | mispriced? |
|---|---|---|---|---|---|
| hits | BOMBER | 195 | −0.038 | [−0.105, +0.030] | no |
| hits | GHOST | 85 | −0.076 | [−0.184, +0.033] | no |
| total_bases | BOMBER | 95 | −0.046 | [−0.141, +0.058] | no |
| total_bases | GHOST | 46 | −0.041 | [−0.187, +0.109] | no |
| rbi | BOMBER | 88 | −0.068 | [−0.155, +0.019] | no |
| runs | BOMBER | 41 | −0.012 | [−0.150, +0.121] | no |
Verdict: (a) no-signal is UNPROVEN and (b) wrong-implementation is UNPROVEN — the test cannot separate them yet. Only 2 of 41 archetypes (BOMBER, GHOST) reach n≥40 settled rows. That is not "archetypes don't work"; it is "we have not measured them." Distinguishing (a) from (b) needs archetype coverage across more than two labels. Do not act on archetype in either direction on this evidence.
Worth noting: every archetype's mean residual is negative, which is not an archetype effect — it is the global over-prediction in §6.
6. THE BIGGEST FINDING — the clamp, not a feature
358 of 1,741 settled props (20.6%) sit ON the clamp boundary (353 at the floor). Within that fifth of the book the model emits a constant, so it cannot rank those props at all — resolution there is zero by construction.
And the constant is hiding two opposite failures at once:
| stat · side | n | model says | actually wins | miscalibration |
|---|---|---|---|---|
| home_runs · under | 222 | 0.900 | 0.995 | −9.5 pts (badly UNDER-confident) |
| hits · under | 27 | 0.900 | 0.519 | +38.1 pts (a coin flip sold as 90%) |
| total_bases · under | 12 | 0.900 | 0.583 | +31.7 pts |
| rbi · over | 35 | 0.100 | 0.229 | −12.9 pts |
The same output 0.900 covers true probabilities from 52% to 99.5%. A
near-lock and a coin flip are indistinguishable in the product. PROB_CEIL = 0.95
also makes it impossible to express the 99.5% case honestly.
Overall calibration, all settled MLB rows:
| stat | n | mean p_win | actual | over-prediction |
|---|---|---|---|---|
| ALL POOLED | 1,741 | 0.585 | 0.550 | +3.5 pts |
| total_bases | 297 | 0.534 | 0.458 | +7.6 |
| hits | 589 | 0.606 | 0.562 | +4.4 |
| rbi | 320 | 0.399 | 0.356 | +4.3 |
| walks | 83 | 0.546 | 0.506 | +4.0 |
| runs | 124 | 0.596 | 0.605 | −0.9 (well calibrated) |
| home_runs | 228 | 0.899 | 0.996 | −9.6 |
Per the product doctrine, calibration is half the success criterion — "does 60% mean 60%?" Right now 58.5% means 55.0%, and on total_bases 53.4% means 45.8%. This is fixable with no new data at all.
7. VERDICT PER STAT (STEP 4)
| stat | n | resolution | verdict |
|---|---|---|---|
| hits | 578 | 0.196 | DILUTION + CALIBRATION. Adjustments contribute nothing; one real lead (opportunity_drift) that we already compute and implement wrongly; +4.4pt over-prediction |
| total_bases | 284 | 0.237 | DILUTION + CALIBRATION (worst). Same lead; +7.6pt over-prediction |
| rbi | 273 | 0.499 | DILUTION. Removing home/away improves it (CI excludes zero). Prune |
| runs | 115 | 0.406 | AT CEILING. Adjustments neutral-to-harmful, no residual leads. The model is good here |
| walks | 66 | 0.478 | AT CEILING (underpowered, n=66) |
| home_runs | 228 | n/a — entirely clamped | CALIBRATION. −9.6pts, structurally uncorrectable while PROB_CEIL=0.95 |
AT CEILING is a real result here, not a shrug: on runs and walks the champion already resolves ~0.41–0.48 and nothing we compute explains its residual.
8. What this says about the next order
The convergent evidence was read correctly — the problem is inputs, not shape. But the decomposition sharpens it, and the ranking is not what we assumed:
- The clamp + calibration (biggest, cheapest, no new data). 20.6% of the book pinned to a constant, a documented +3.5pt global over-prediction, and one number covering 52%–99.5%. This costs both halves of the success criterion.
- Opportunity, implemented properly — the one feature with repeated cross-stat residual signal. Not a new feature: a correct use of one we have. Scale the rate, don't nudge the probability.
- Stop diluting — the ladder and the environment axis add movement with no information, and are measurably worse or flat.
- Archetype: measure before judging. 2 of 41 labels have testable n.
Explicitly NOT recommended: another projection variant. That is the sixth thing, and this diagnosis is why it would fail — the champion's edge is asking the frequency question at the traded line, and every challenger so far has replaced that question rather than improved its inputs.
9. Incidental finding, flagged not fixed
model_snapshots.outcome is NULL on all 22,032 rows. The retention table
built expressly so "a different model can be replayed against the same
conditions" stores features but was never settled, so it cannot answer the
question it exists for. This diagnosis worked around it by joining outcomes from
ledger_entries on (player_key, stat, line, side, game_date). Settling retention
would make every future ablation a single-table query — and would make refusals
(which the ledger drops) measurable for the first time.
10. Reproduce
SUPABASE_URL=... node scripts/champion-ablation.js