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
7.9 KiB
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:
- 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.
- 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
- 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.
- Re-run
scripts/pitcher-prove-k.jsat n≥500, with a point-in-time window fromstatcast_history(which starts producing usable comparisons 2026-08-04). - 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.
- 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.