Collapsed sequence edge: two proven links whose product is too small to use
Nothing in this failed, which is what makes it the most instructive
negative so far. Link 1 proved (MAE 3.22 -> 2.80 batters faced). Link 2's
quality grain proved (2.70pp of realized separation). Both point-in-time,
both past cumulative correction. Their product is 0.37pp and detecting it
would take 52 seasons.
FRAMING CORRECTION: the order says Link 2 proved you can't predict the
reliever. Half true -- the INDIVIDUAL grain failed at 17.2%, but the
QUALITY grain PROVED. Pen-season-quality is a measured predictor here, not
a fallback after a failure.
TWO OF THREE SPECIFIED INPUTS COULD NOT BE USED HONESTLY. Pen archetype
did not prove (0.5669 vs a 0.5309 modal baseline, interval spanning zero)
so building it in would chain on an unproven link. And hitter
approach-identity -- "fastball-hunter", "finesse-vulnerable" -- does not
exist in this registry; MLB batter archetypes are BOMBER/GHOST/TORCH/
BRUSH/DRIVER/FLEX/ALPHA/HYBRID/CATALYST. Inventing one to condition on is
the fabrication the gate exists to catch. A power/contact split derived
from the sequence data was tested as a SEPARATE gated addition instead;
neither half proved.
GATE on the concentrated subset, 114 cumulative tests:
early-exit x WEAK pen n=1931 brier -0.0001 CI [-0.0014,+0.0010] NOT_PROVEN
early-exit x STRONG pen n=2574 brier 0.0000 CI [-0.0011,+0.0010] THEATER
all early-exit later ABs n=6869 brier -0.0001 CI [-0.0007,+0.0005] NOT_PROVEN
pooled all later ABs n=17891 brier 0.0000 CI [-0.0004,+0.0003] THEATER
Not pooled-diluted -- the concentrated subset was gated alone and is no
better.
THE CEILING, which explains it. The descriptive pass found the predicted
direction (+0.74pp weak pen, -0.79pp strong pen). The magnitude is the
problem and it is structural:
P(faces pen | early-exit flagged) 0.8075
P(faces pen | starter goes deep) 0.7149
exposure the flag actually buys 0.0925
hit-rate swing across pen quality 0.0394
MAX JUSTIFIABLE ADJUSTMENT 0.00365
actually applied 0.01930 -> 5.3x over-movement
A hitter's 3rd/4th plate appearance is ALREADY against the bullpen 71% of
the time when the starter is projected to go deep. Link 1 lifts it to 81%
-- nine points of extra exposure, not a change of opponent. The 5.3x
over-movement is precisely why the mirror subset reads THEATER rather than
as a small true effect.
A correctly-scaled version is not detectable either: 0.37pp is 0.37 SE at
n=1,931; the corrected bar needs n=168,488, an 87x shortfall, ~52 seasons.
STRUCTURALLY CLOSED, not sample-blocked. Waiting does not fix it.
NOT WIRED, and the self-check deliberately not wired either -- flagging
line-divergence on an adjustment measured as absent would advertise an
edge we just showed does not exist, which is fabricated reasoning one
layer up.
THE LESSON: link-by-link validation guarantees each link is real. It does
not guarantee the chain transmits anything. Size the multiplicative
structure BEFORE building -- one exposure term of 0.09 reduces a genuine
3.94pp signal to noise and no downstream care recovers it.
Link 3 confirmed skipped. Parallel track logged unchanged: TB n=948
pooled, BOMBER x TB 340, short by 160.
Counter and frozen clusters byte-identical.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
This commit is contained in:
@@ -0,0 +1,210 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* THE COLLAPSED SEQUENCE EDGE — Link 1 x pen-season-quality, on later at-bats.
|
||||||
|
*
|
||||||
|
* Link 3 is correctly skipped: reliever IDENTITY did not prove and is genuine
|
||||||
|
* baseball unpredictability. But Link 2's QUALITY grain DID prove, so pen quality
|
||||||
|
* here is a measured predictor rather than a fallback.
|
||||||
|
*
|
||||||
|
* ── THE MECHANICAL CEILING, MEASURED FIRST ───────────────────────────────
|
||||||
|
* A hitter's third or fourth plate appearance is ALREADY against the bullpen
|
||||||
|
* 70-73% of the time even when the starter is projected to go deep. An elevated
|
||||||
|
* early-exit flag lifts that to only 77-83%. So Link 1 buys roughly TEN POINTS
|
||||||
|
* of extra pen exposure, not a switch from starter to pen — and any adjustment
|
||||||
|
* built on it is bounded at about a tenth of the starter-versus-pen quality gap.
|
||||||
|
* That ceiling is a property of baseball, not of the model, and it is the reason
|
||||||
|
* the deltas below are small before anything is even fitted.
|
||||||
|
*
|
||||||
|
* ── WHAT IS ADJUSTED, AND WHAT IS REFUSED ────────────────────────────────
|
||||||
|
* The order specifies pen-quality x pen-ARCHETYPE x hitter-APPROACH. Two of
|
||||||
|
* those three cannot be used honestly:
|
||||||
|
*
|
||||||
|
* pen archetype did NOT prove (0.5669 vs a 0.5309 modal baseline, corrected
|
||||||
|
* interval spanning zero). Building it into the adjustment
|
||||||
|
* would be chaining on an unproven link.
|
||||||
|
* hitter approach "fastball-hunter" / "finesse-vulnerable" identities do not
|
||||||
|
* exist in this registry. MLB batter archetypes are BOMBER /
|
||||||
|
* GHOST / TORCH / BRUSH / DRIVER / FLEX / ALPHA / HYBRID /
|
||||||
|
* CATALYST. Inventing an identity to condition on would be
|
||||||
|
* fabricating the very thing the gate exists to catch.
|
||||||
|
*
|
||||||
|
* So the adjustment uses the PROVEN component alone, and a hitter split derived
|
||||||
|
* from the sequence data itself (power vs contact by home-run rate) is tested as
|
||||||
|
* a SEPARATE gated addition rather than assumed into the main effect.
|
||||||
|
*
|
||||||
|
* node scripts/collapsed-sequence-edge.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
require('dotenv').config();
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const fg = require('../src/services/model/factorGate');
|
||||||
|
const tl = require('../src/services/model/testLedger');
|
||||||
|
const pq = require('../src/services/model/penQuality');
|
||||||
|
const { createClient } = require('@supabase/supabase-js');
|
||||||
|
|
||||||
|
const CACHE = process.env.SEQ_OUT || path.join(process.cwd(), '.seq-cache', 'sequences.json');
|
||||||
|
const HIT = new Set(['single', 'double', 'triple', 'home_run']);
|
||||||
|
const PA = new Set(['single', 'double', 'triple', 'home_run', 'field_out', 'strikeout',
|
||||||
|
'grounded_into_double_play', 'force_out', 'field_error', 'fielders_choice',
|
||||||
|
'fielders_choice_out', 'double_play', 'sac_fly', 'pop_out', 'line_out', 'fly_out',
|
||||||
|
'strikeout_double_play']);
|
||||||
|
|
||||||
|
const MIN_ARM_PA = 40;
|
||||||
|
const MIN_PRIOR_GAMES = 5;
|
||||||
|
const MIN_HITTER_PA = 60;
|
||||||
|
const MIN_PRIOR_STARTS = 3;
|
||||||
|
const EARLY_FLAG_BF = 22;
|
||||||
|
const LEAGUE_BF = 21.56;
|
||||||
|
|
||||||
|
const mean = (xs) => (xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : null);
|
||||||
|
|
||||||
|
function build() {
|
||||||
|
const { games } = JSON.parse(fs.readFileSync(CACHE, 'utf8'));
|
||||||
|
games.sort((a, b) => String(a.date).localeCompare(String(b.date)) || a.gamePk - b.gamePk);
|
||||||
|
|
||||||
|
const arm = new Map();
|
||||||
|
const bat = new Map(); // hitter -> { n, h, hr }
|
||||||
|
const penHist = new Map();
|
||||||
|
const startHist = new Map();
|
||||||
|
const rows = [];
|
||||||
|
|
||||||
|
for (const g of games) {
|
||||||
|
for (const side of ['home', 'away']) {
|
||||||
|
const team = g[side].abbr || g[side].team;
|
||||||
|
const st = (g[side].arms || []).find((a) => a.started);
|
||||||
|
if (!team || !st) continue;
|
||||||
|
const half = side === 'home' ? 'top' : 'bottom';
|
||||||
|
const pas = g.pas.filter((p) => p.half === half && PA.has(p.event));
|
||||||
|
|
||||||
|
const ps = startHist.get(st.id) || [];
|
||||||
|
let predBf = null;
|
||||||
|
if (ps.length >= MIN_PRIOR_STARTS) {
|
||||||
|
const w = ps.length / (ps.length + 5);
|
||||||
|
predBf = w * mean(ps) + (1 - w) * LEAGUE_BF;
|
||||||
|
}
|
||||||
|
const hist = penHist.get(team) || [];
|
||||||
|
const pen = pq.projectPen(hist.map((q) => ({ quality: q })));
|
||||||
|
|
||||||
|
const seen = new Map();
|
||||||
|
for (const p of pas) {
|
||||||
|
const k = p.batter;
|
||||||
|
seen.set(k, (seen.get(k) || 0) + 1);
|
||||||
|
const paNum = seen.get(k);
|
||||||
|
const b = bat.get(k);
|
||||||
|
// knownRate abstain: no readable hitter, starter or pen -> no row at all.
|
||||||
|
if (paNum < 3 || predBf === null || !pen || !b || b.n < MIN_HITTER_PA) continue;
|
||||||
|
rows.push({
|
||||||
|
gamePk: g.gamePk,
|
||||||
|
cluster: g.gamePk,
|
||||||
|
batter: k,
|
||||||
|
paNum,
|
||||||
|
early: predBf <= EARLY_FLAG_BF,
|
||||||
|
pen_quality: pen.quality,
|
||||||
|
hitter_base: b.h / b.n,
|
||||||
|
hitter_hr_rate: b.hr / b.n,
|
||||||
|
won: HIT.has(p.event) ? 1 : 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const faced = [];
|
||||||
|
for (const p of pas.filter((x) => x.pitcher !== st.id)) {
|
||||||
|
const h = arm.get(p.pitcher);
|
||||||
|
if (h && h.n >= MIN_ARM_PA) faced.push(h.h / h.n);
|
||||||
|
}
|
||||||
|
if (faced.length) penHist.set(team, hist.concat([mean(faced)]));
|
||||||
|
if (st.bf != null) startHist.set(st.id, ps.concat([st.bf]));
|
||||||
|
for (const p of pas) {
|
||||||
|
const c = arm.get(p.pitcher) || { n: 0, h: 0, k: 0 };
|
||||||
|
c.n += 1; c.h += HIT.has(p.event) ? 1 : 0; c.k += p.event === 'strikeout' ? 1 : 0;
|
||||||
|
arm.set(p.pitcher, c);
|
||||||
|
}
|
||||||
|
for (const p of pas) {
|
||||||
|
const c = bat.get(p.batter) || { n: 0, h: 0, hr: 0 };
|
||||||
|
c.n += 1; c.h += HIT.has(p.event) ? 1 : 0; c.hr += p.event === 'home_run' ? 1 : 0;
|
||||||
|
bat.set(p.batter, c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The adjustment: the hitter's own rate, shifted by the PROVEN pen signal. */
|
||||||
|
const adjust = (r) => {
|
||||||
|
const shift = pq.hitRateShift(r.pen_quality);
|
||||||
|
if (shift === null) return null;
|
||||||
|
return Math.max(0.01, Math.min(0.99, r.hitter_base + shift));
|
||||||
|
};
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const all = build();
|
||||||
|
const qs = all.map((r) => r.pen_quality).sort((a, b) => a - b);
|
||||||
|
const weakCut = qs[Math.floor(qs.length * 2 / 3)];
|
||||||
|
const strongCut = qs[Math.floor(qs.length / 3)];
|
||||||
|
|
||||||
|
const subsets = {
|
||||||
|
// The order's concentrated subset.
|
||||||
|
concentrated_early_x_weak_pen: all.filter((r) => r.early && r.pen_quality >= weakCut),
|
||||||
|
// The mirror, where the descriptive pass suggested the larger movement.
|
||||||
|
mirror_early_x_strong_pen: all.filter((r) => r.early && r.pen_quality <= strongCut),
|
||||||
|
// Every later at-bat with an early-exit flag, both directions of pen quality.
|
||||||
|
all_early_exit_later_abs: all.filter((r) => r.early),
|
||||||
|
pooled_all_later_abs: all,
|
||||||
|
};
|
||||||
|
|
||||||
|
let cumulative = 1;
|
||||||
|
try {
|
||||||
|
const sb = createClient(process.env.SUPABASE_URL,
|
||||||
|
process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.SUPABASE_SERVICE_KEY,
|
||||||
|
{ auth: { persistSession: false } });
|
||||||
|
const mc = await tl.recordAndCount(tl.supabaseStore(sb), Object.keys(subsets).map((k) => ({
|
||||||
|
sport: 'mlb', stat: 'hits', archetype: null,
|
||||||
|
interaction: `collapsed_sequence:${k}`, target: 'later_ab_outcome',
|
||||||
|
})));
|
||||||
|
cumulative = mc.cumulative_tests;
|
||||||
|
} catch { /* offline */ }
|
||||||
|
|
||||||
|
const gate = (rs, label) => fg.adjudicate(
|
||||||
|
rs.map((r) => ({ cluster: r.cluster, baseline: r.hitter_base, conditioned: adjust(r), won: r.won }))
|
||||||
|
.filter((r) => r.conditioned !== null),
|
||||||
|
{ factor: label, stat: 'hits', cumulativeTests: cumulative },
|
||||||
|
);
|
||||||
|
|
||||||
|
const results = {};
|
||||||
|
for (const [k, rs] of Object.entries(subsets)) results[k] = gate(rs, k);
|
||||||
|
|
||||||
|
// Hitter split as a SEPARATE gated addition — never assumed into the main effect.
|
||||||
|
const conc = subsets.concentrated_early_x_weak_pen;
|
||||||
|
const hrs = conc.map((r) => r.hitter_hr_rate).sort((a, b) => a - b);
|
||||||
|
const hrCut = hrs[Math.floor(hrs.length / 2)];
|
||||||
|
const bySplit = {
|
||||||
|
power_hitters: gate(conc.filter((r) => r.hitter_hr_rate >= hrCut), 'concentrated_power'),
|
||||||
|
contact_hitters: gate(conc.filter((r) => r.hitter_hr_rate < hrCut), 'concentrated_contact'),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
later_at_bats_readable: all.length,
|
||||||
|
cumulative_tests: cumulative,
|
||||||
|
subset_sizes: Object.fromEntries(Object.entries(subsets).map(([k, v]) => [k, v.length])),
|
||||||
|
gate: Object.fromEntries(Object.entries(results).map(([k, v]) => [k, {
|
||||||
|
n: v.movement.n,
|
||||||
|
clusters: v.improvement ? v.improvement.effective_n : null,
|
||||||
|
mean_abs_shift: v.movement.mean_abs_shift,
|
||||||
|
brier_delta: v.improvement ? v.improvement.brier_delta : null,
|
||||||
|
ci: v.improvement ? v.improvement.ci : null,
|
||||||
|
verdict: v.verdict,
|
||||||
|
}])),
|
||||||
|
hitter_split_separate_gate: Object.fromEntries(Object.entries(bySplit).map(([k, v]) => [k, {
|
||||||
|
n: v.movement.n, brier_delta: v.improvement ? v.improvement.brier_delta : null,
|
||||||
|
ci: v.improvement ? v.improvement.ci : null, verdict: v.verdict,
|
||||||
|
}])),
|
||||||
|
refused: {
|
||||||
|
pen_archetype: 'did not prove at the corrected bar — excluded from the adjustment',
|
||||||
|
hitter_approach_identity: 'SPRAY / fastball-hunter identities do not exist in this registry',
|
||||||
|
link3_per_reliever: 'SKIPPED — reliever identity is genuine baseball unpredictability',
|
||||||
|
},
|
||||||
|
}, null, 2));
|
||||||
|
process.exit(0);
|
||||||
|
})();
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# The collapsed sequence edge — two proven links whose product is too small to use
|
||||||
|
|
||||||
|
**Both components are real. Their product is 0.37pp, and detecting it would take
|
||||||
|
52 seasons.** This is the most instructive negative in the programme so far,
|
||||||
|
because nothing in it failed: link-by-link proof did not produce a usable edge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## One correction to the framing
|
||||||
|
|
||||||
|
The order states Link 2 proved you *can't* predict the reliever. Half true, and
|
||||||
|
the other half matters: the **individual** grain failed (17.2% accuracy), but the
|
||||||
|
**quality** grain PROVED — predicted pen quality separates 2.70pp of realized hit
|
||||||
|
rate. Pen-season-quality here is a measured predictor, not a fallback after a
|
||||||
|
failure. That strengthened the plan going in.
|
||||||
|
|
||||||
|
## Two of the three specified inputs could not be used honestly
|
||||||
|
|
||||||
|
| specified | status |
|
||||||
|
|---|---|
|
||||||
|
| pen **quality** | PROVEN (Link 2 coarse grain) — used |
|
||||||
|
| pen **archetype** | did NOT prove (0.5669 vs 0.5309 modal baseline, corrected interval spanning zero) — **excluded**, building it in would chain on an unproven link |
|
||||||
|
| hitter **approach identity** ("fastball-hunter", "finesse-vulnerable") | **does not exist** in this registry. MLB batter archetypes are BOMBER / GHOST / TORCH / BRUSH / DRIVER / FLEX / ALPHA / HYBRID / CATALYST. Inventing an identity to condition on is the fabrication the gate exists to catch |
|
||||||
|
|
||||||
|
A hitter power/contact split derived from the sequence data itself was tested as a
|
||||||
|
**separate gated addition** rather than assumed into the main effect. Neither half
|
||||||
|
proved (power −0.0002, contact −0.0001, both intervals spanning zero).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## The gate — two-part, on the concentrated subset, 114 cumulative tests
|
||||||
|
|
||||||
|
| subset | n | games | mean shift | Brier Δ | CI | verdict |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| concentrated (early-exit × WEAK pen) | 1,931 | 141 | 0.0193 | −0.0001 | [−0.0014, +0.0010] | NOT_PROVEN |
|
||||||
|
| mirror (early-exit × STRONG pen) | 2,574 | 189 | 0.0186 | 0.0000 | [−0.0011, +0.0010] | **THEATER** |
|
||||||
|
| all early-exit later ABs | 6,869 | 451 | 0.0140 | −0.0001 | [−0.0007, +0.0005] | NOT_PROVEN |
|
||||||
|
| pooled all later ABs | 17,891 | 803 | 0.0141 | 0.0000 | [−0.0004, +0.0003] | **THEATER** |
|
||||||
|
|
||||||
|
Not pooled-diluted: the concentrated subset was gated on its own and is no
|
||||||
|
better. Two subsets are THEATER by the gate's own definition — the adjustment
|
||||||
|
moves the number ~1.9pp and improves accuracy by essentially nothing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Why: the mechanical ceiling
|
||||||
|
|
||||||
|
The descriptive pass found the direction the order predicted (early-exit + weak
|
||||||
|
pen +0.74pp, early-exit + strong pen −0.79pp vs a deep-starter baseline). The
|
||||||
|
signs are right. The magnitude is the problem, and it is structural:
|
||||||
|
|
||||||
|
```
|
||||||
|
P(faces pen | early-exit flagged) 0.8075
|
||||||
|
P(faces pen | starter goes deep) 0.7149
|
||||||
|
exposure the flag actually buys 0.0925 <- NOT a switch to the pen
|
||||||
|
|
||||||
|
hit-rate swing across pen quality 0.0394 (weak 0.2491 vs strong 0.2038)
|
||||||
|
|
||||||
|
MAX JUSTIFIABLE ADJUSTMENT = 0.0925 x 0.0394 = 0.00365 (0.37pp)
|
||||||
|
adjustment actually applied (mean |shift|) = 0.01930 (1.93pp)
|
||||||
|
OVER-MOVEMENT FACTOR = 5.3x
|
||||||
|
```
|
||||||
|
|
||||||
|
**A hitter's third or fourth plate appearance is ALREADY against the bullpen 71%
|
||||||
|
of the time even when the starter is projected to go deep.** Link 1 lifts that to
|
||||||
|
81%. It buys nine points of extra pen exposure, not a change of opponent — so any
|
||||||
|
adjustment riding on it is capped at about a tenth of the pen-quality swing.
|
||||||
|
|
||||||
|
The 5.3× over-movement is exactly why the mirror subset reads as THEATER rather
|
||||||
|
than as a small true effect: the adjustment asserts five times more than the
|
||||||
|
mechanism can support.
|
||||||
|
|
||||||
|
### And a correctly-scaled version is not detectable either
|
||||||
|
|
||||||
|
```
|
||||||
|
concentrated subset n = 1,931 SE of hit rate = 0.00985
|
||||||
|
max justifiable effect 0.00365 = 0.37 SE
|
||||||
|
to detect at the corrected bar (z~3.46 for 114 tests): n = 168,488
|
||||||
|
shortfall 87x -> ~52 seasons of concentrated-subset accrual
|
||||||
|
```
|
||||||
|
|
||||||
|
**This line is structurally closed, not sample-blocked.** Waiting does not fix it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Not wired, and the self-check deliberately not wired either
|
||||||
|
|
||||||
|
The adjustment does not prove, so it feeds nothing. The order also asks for a
|
||||||
|
self-check flagging where our sequence read diverges from the line's
|
||||||
|
starter-script, as an opportunity signal. **That is not wired**, because flagging
|
||||||
|
divergence on an adjustment measured as absent would advertise an edge we have
|
||||||
|
just shown does not exist — the same failure as fabricated reasoning, one layer up.
|
||||||
|
|
||||||
|
## The lesson worth keeping
|
||||||
|
|
||||||
|
Link 1 proved (MAE 3.22 → 2.80 batters faced). Link 2's quality grain proved
|
||||||
|
(2.70pp of realized separation). Both are real, both are point-in-time, both
|
||||||
|
survived cumulative correction. **Their product is still too small to use.**
|
||||||
|
|
||||||
|
Link-by-link validation guarantees each link is real. It does not guarantee the
|
||||||
|
chain transmits anything. The multiplicative structure has to be sized BEFORE
|
||||||
|
building — one exposure term of 0.09 is enough to reduce a genuine 3.94pp signal
|
||||||
|
to noise, and no amount of downstream care recovers it.
|
||||||
|
|
||||||
|
## Parallel track — total_bases per-archetype (logged, not run)
|
||||||
|
|
||||||
|
Unchanged: `total_bases` settled n=948 pooled, BOMBER × TB **340**, short by 160.
|
||||||
|
Sample-readiness only, not a verdict. The `specs/per-archetype-grade-bands.md`
|
||||||
|
blocker still stands — the grade does not yet separate within any archetype.
|
||||||
|
|
||||||
|
Link 3 confirmed SKIPPED. Counter and frozen clusters byte-identical.
|
||||||
+1
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user