843c8c6d4b
Strikeouts are NOT proven -- n=57 against a bar of 500. But the finding that matters is not a correlation. THE CAP. Measured on the live slate via the refusal diagnostic: 1,244 unique gradeable props exist, the 500 cap graded about 334, and because dedupeProps takes first-row-wins in FEED ORDER, what survives is decided by feed position rather than value. Pitchers are 2.6% of a batter-dominated feed, so we were grading SIX strikeout props a slate against 32 available -- putting n>=500 three months away for every pitcher stat. Pitcher props were never being refused (graded 5, refused 0, suppressed 0); it was truncation. Raised 500 -> 1500 on measured cost: 721ms per prop at concurrency 5 is about 179 seconds for the full board, against a cron that runs five times a day and a fire-and-forget caller that never holds an HTTP response. statsapi is free and unlimited. Concurrency stays at 5 -- one variable at a time. This unblocks every n-blocked stat in the programme, not just pitchers. THE ENGINE. pitcherEngine.js is its own engine, not the batter engine pointed at pitchers: the batter model asks whether contact becomes a hit and reads contact quality, the pitcher model asks whether the plate appearance ends without contact at all and reads stuff. Archetypes are FLAME (whiff-led), SCALPEL (chase-led), SINKER (pitches to contact) and DEFAULT, and a test asserts the weight keys are not the batter engine's. The projection is K% by log5 against THIS lineup, times batters faced, through a binomial. An unclassifiable arm gets the balanced map, never a guessed archetype. THE MEASUREMENT, at n=57 and contaminated. Four solo features clear the 0.15 effect bar and fail only on sample: arm angle at -0.250 -- the largest correlation measured anywhere in this programme -- then whiff +0.213, k rate +0.206, chase +0.195. The batter cluster's best was 0.135. Head to head, pitch-v1 resolves 0.1285 against the counter's -0.0639, delta +0.192 with a CI spanning zero. That negative is the interesting number. The counter is ANTI-PREDICTIVE on strikeouts: counting a pitcher's recent Ks is worse than useless, because his recent totals track which lineups he drew and how long he was left in rather than his skill. It is the one stat where the incumbent has no defensible edge. A bug caught on the way. resolveTeam wants an abbreviation and the game log supplies full team names, so the roster join silently resolved nothing and the first run reported 0% lineup coverage -- the theorized stuff x lineup carrier was never being tested, not failing. Fixed; coverage is now 94.7%. The carrier still shows no incremental signal over whiff alone, and adding the lineup term lowered head-to-head resolution, which is recorded rather than dropped. Calibration was not reached: nothing passed the first bar. The batter model and the counter are byte-identical, verified by diff. 4,221 tests green (335 suites); web build exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
175 lines
7.9 KiB
Markdown
175 lines
7.9 KiB
Markdown
# THE PITCHER ENGINE — built, and strikeouts measured
|
||
|
||
**2026-08-03.** Challenger-only. Batter model and counter byte-identical
|
||
(verified by diff). **Strikeouts NOT proven — n=57 against a 500 bar.**
|
||
|
||
> **The result that matters is not a correlation. It is that the cap was
|
||
> throwing away the board.** 1,244 unique gradeable props exist per slate; the
|
||
> 500 cap graded ~334, and because selection is first-row-wins in FEED ORDER,
|
||
> pitchers — 2.6% of the feed — were getting **six props a slate**. At six a
|
||
> slate, n≥500 for any pitcher stat is three months away. Raised to 1500 on
|
||
> measured cost. That unblocks every n-blocked stat in the programme, not just
|
||
> this one.
|
||
>
|
||
> **And the counter is ANTI-PREDICTIVE on strikeouts: resolution −0.064.**
|
||
> Counting a pitcher's recent strikeouts is worse than useless, which is the
|
||
> strongest theoretical case for a skill model anywhere in this work.
|
||
|
||
---
|
||
|
||
## 0. Premise note
|
||
|
||
The order refers to "the SAME BAR the proven batter cluster cleared" and to the
|
||
batter cluster as frozen-proven. **There is no proven batter cluster** — the
|
||
proven set came back empty last session (total_bases inconclusive at parity,
|
||
hits a well-powered negative). The bar used here is therefore the gate itself
|
||
(n≥500, |r|≥0.15, p<0.05, Bonferroni) plus a head-to-head CI excluding zero —
|
||
which is the bar total_bases *failed*, not one it set.
|
||
|
||
Also: "Ks first — high-volume" did not hold in our ledger. Strikeouts had **58
|
||
settled rows** and only started being graded 2026-07-31. §1 is why.
|
||
|
||
## 1. Why pitcher volume was six a slate — and it was us, not supply
|
||
|
||
`GET /api/internal/diagnose-refusals` on the live slate:
|
||
|
||
```
|
||
rows in feed 10,224
|
||
unique gradeable props 1,244
|
||
graded after suppression ~70%
|
||
strikeouts: graded 5 · refused 0 · suppressed 0
|
||
```
|
||
|
||
**Pitcher props were not being refused.** Supply was fine too — 32 strikeout
|
||
props covering 33 pitchers, with full model-book coverage (DK, FD, BetMGM,
|
||
BetRivers). The loss was the **cap**: `dedupeProps` takes first-row-wins in feed
|
||
order and stops at the limit, so what survives is decided by feed position, not
|
||
value, and pitchers are a thin slice of a batter-dominated feed.
|
||
|
||
**Raised 500 → 1500** on measured cost, not taste: 721ms/prop at concurrency 5
|
||
→ ~179s for the full 1,244, against a cron that runs 5×/day and a
|
||
fire-and-forget caller that never holds an HTTP response. statsapi is free and
|
||
unlimited. Concurrency stays at 5 — one variable at a time.
|
||
|
||
Expected effect: pitcher strikeouts from ~6 to ~32 a slate, so n≥500 moves from
|
||
~3 months to **~2 weeks**.
|
||
|
||
## 2. The engine (STEP 1) — its own archetypes, its own inputs
|
||
|
||
`src/services/model/pitcherEngine.js`. Not the batter engine pointed at
|
||
pitchers: the batter model asks *does contact become a hit* and reads contact
|
||
quality; the pitcher model asks *does the plate appearance end without contact
|
||
at all* and reads stuff. A test asserts the two weight-key sets are **not
|
||
equal**.
|
||
|
||
| archetype | function | weights (whiff / k-rate / chase) |
|
||
|---|---|---|
|
||
| **FLAME** | power arm — Ks come from stuff | 0.65 / 0.30 / 0.05 |
|
||
| **SCALPEL** | finesse — Ks come from chase and sequencing | 0.30 / 0.30 / 0.40 |
|
||
| **SINKER** | ground-ball arm — pitches to contact by design | 0.35 / 0.50 / 0.15 |
|
||
| DEFAULT | unclassified — balanced, no invented lean | 0.45 / 0.40 / 0.15 |
|
||
|
||
Projection: `K% (log5: this arm vs THIS lineup) × batters faced` →
|
||
`Binomial(BF, k_rate)` mixed over a BF distribution. Workload is opportunity,
|
||
not skill, so role sets the BF centre. **Unclassifiable → DEFAULT map, never a
|
||
guessed archetype** — "we don't know his type" and "he's a power arm" are
|
||
different statements.
|
||
|
||
**Coverage on live data:** 776 pitchers — whiff 736, k_pct 736, pitch_mix 753,
|
||
arm_angle 734. Classified on the settled sample: 21 FLAME, 17 SINKER, 7 SCALPEL,
|
||
12 unclassified.
|
||
|
||
## 3. STEP 2 — both ways, at n=57
|
||
|
||
Bonferroni across 9 tests. **Every result is refused on sample size**; r and p
|
||
are shown because "not enough data yet" and "nothing here" need opposite
|
||
decisions.
|
||
|
||
### Solo (control)
|
||
|
||
| feature | r vs outcome | **r vs counter residual** | p |
|
||
|---|---|---|---|
|
||
| **pitcher_arm_angle** | −0.247 | **−0.250** | 0.061 |
|
||
| **pitcher_whiff_pct** | +0.168 | **+0.213** | 0.112 |
|
||
| **pitcher_k_pct** | +0.157 | **+0.206** | 0.125 |
|
||
| **pitcher_chase_pct** | +0.143 | **+0.195** | 0.146 |
|
||
| pitcher_gb_pct | +0.077 | +0.042 | 0.755 |
|
||
| opposing_lineup_k_rate | +0.052 | +0.027 | 0.846 |
|
||
|
||
**Four features exceed the |r| ≥ 0.15 effect bar and fail only on n.** For
|
||
contrast, the entire batter cluster's best was 0.135. This is the strongest
|
||
solo signal measured anywhere in the programme.
|
||
|
||
### Interactions (theory-first, each vs its own parts)
|
||
|
||
| interaction | n | raw r | best part | **incremental** | verdict |
|
||
|---|---|---|---|---|---|
|
||
| stuff × lineup K-rate | 54 | 0.175 | 0.198 | −0.102 | UNDERPOWERED |
|
||
| stuff × power archetype | 45 | 0.172 | 0.217 | −0.053 | UNDERPOWERED |
|
||
| chase × lineup K-rate | 54 | 0.169 | 0.194 | +0.047 | UNDERPOWERED |
|
||
|
||
**The theorized carrier does not yet show incremental signal** — its raw
|
||
correlation is entirely explained by the pitcher's own whiff rate. At n=54 that
|
||
is not a verdict, but it is not encouraging either.
|
||
|
||
A bug worth recording: the first run reported **0% lineup coverage** and would
|
||
have read as "the carrier fails". The game log supplies a full team *name* while
|
||
`resolveTeam` wants an *abbreviation*, so the roster join silently resolved
|
||
nothing — the carrier was never being tested, not failing. Fixed via
|
||
`NAME_TO_ABBR`; coverage is now **94.7%**.
|
||
|
||
## 4. Head-to-head vs the counter
|
||
|
||
```
|
||
n 57 · base rate 0.597
|
||
resolution pitch-v1 0.1285 counter −0.0639
|
||
brier pitch-v1 0.2412 counter 0.2703
|
||
delta +0.1924 CI95 [−0.0978, +0.5090] → INCONCLUSIVE
|
||
```
|
||
|
||
**NOT PROVEN.** The CI spans zero and n is a ninth of the bar.
|
||
|
||
But two things are worth stating precisely:
|
||
|
||
1. **The counter's resolution is NEGATIVE.** Recent strikeout counts are
|
||
*anti-predictive* of whether a pitcher clears his line — which makes sense,
|
||
because a pitcher's recent K totals are dominated by which lineups he drew and
|
||
how long he was left in, not by his skill. **This is the one stat where the
|
||
incumbent has no defensible edge**, and it is exactly where a skill model
|
||
should win.
|
||
2. **Adding the lineup term made it worse** (0.174 → 0.1285). At n=57 that is
|
||
noise, but it is reported rather than quietly dropped, and it is consistent
|
||
with the interaction result above.
|
||
|
||
## 5. STEP 3 — calibration
|
||
|
||
**Not reached.** Strikeouts did not pass BAR 1, so there is nothing to calibrate.
|
||
Calibrating a model that has not beaten counting would produce an "A" backed by
|
||
nothing — the failure mode BAR 2 exists to prevent.
|
||
|
||
## 6. Frozen surfaces verified
|
||
|
||
`git diff` against the prior commit is **empty** for
|
||
`skillProjection.js`, `probabilityEstimator.js` and `analyzeViaEngine1.js`. The
|
||
batter model and the counter are byte-identical.
|
||
|
||
## 7. Next
|
||
|
||
1. **Let the cap change accrue.** Pitcher strikeouts should reach n≥500 in ~2
|
||
weeks rather than ~3 months. This was the binding constraint and it is now
|
||
released.
|
||
2. **Re-run `scripts/pitcher-prove-k.js`** at n≥500, with a point-in-time window
|
||
from `statcast_history` (which starts producing usable comparisons
|
||
2026-08-04).
|
||
3. **arm_angle is the feature to watch** — the largest |r| in the programme
|
||
(−0.250), and a plausible mechanism (release height and approach angle drive
|
||
swing-and-miss). It is currently a CANDIDATE in name only; it deserves its own
|
||
registry entry and a stated mechanism before it is tested for real.
|
||
4. Other pitcher props (outs, earned runs, hits allowed) through the same engine
|
||
once strikeouts resolves — they are 47/17/10 settled rows today and will
|
||
benefit from the same cap change.
|
||
|
||
**Not recommended:** promoting on an inconclusive result, calibrating anything,
|
||
or reading the four above-threshold solo correlations as findings. They fail on
|
||
n, and n is the thing that just got fixed.
|