d8bf7765db
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
221 lines
12 KiB
Markdown
221 lines
12 KiB
Markdown
# 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 number `0.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:
|
||
|
||
1. **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.
|
||
2. **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.
|
||
3. **Stop diluting** — the ladder and the environment axis add movement with no
|
||
information, and are measurably worse or flat.
|
||
4. **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`
|