Build the two-part factor gate: one factor proves, and zero are theatre

The question was whether the hit grade reads tonight's game or just says he is
due. Answering it needed a gate that correlation cannot provide, because
correlation cannot separate the two ways a factor looks alive: it reads the
game, or it moves the number and reads nothing. The second is what a product
ships by accident -- arch-v1 moved 76% of rows by 2.5 points, changed
resolution by 0.0000, and was live for months, and no user could have told.

So a factor must now clear both conditions: move the prediction off the
player's own leave-one-out base rate, AND improve out-of-sample Brier. Brier
rather than correlation, because correlation asks whether the ordering improved
and this asks whether the NUMBER got closer to what happened -- and for a graded
probability the number is the product.

The correction applies to the interval itself, which turned out to matter more
than expected. A plain 95% CI is the right bar for one test; at fifty
cumulative tests roughly two or three intervals exclude zero by chance alone.
Widening to 1 - 0.05/tests, currently 99.9%, flipped both defence and platoon
out of "proves". A 95% interval would have shipped two unproven factors into
the grade, with reasoning text explaining them to users.

That forced a distinction I had initially collapsed. Defence and platoon have
FAVOURABLE point estimates whose corrected intervals merely span zero, and
calling that THEATER would repeat the error this codebase keeps correcting:
insufficient evidence is not evidence of absence. THEATER is now reserved for
its one real meaning -- moves the number, reads nothing -- and
NOT_PROVEN_AT_CORRECTED_BAR names a real candidate held to a bar that rises with
every hypothesis the programme tests.

Result on 741 settled hits rows: pitcher_contact_profile PROVES, improving
Brier by 0.0066 with a 99.9% interval of [-0.0114, -0.0016]. Defence (-0.0043)
and platoon (-0.0039) are not proven at the corrected bar. Park is
sample-blocked at n=405. Zero factors are theatre, which is the genuinely good
news: nothing decorative is being wired. Per-archetype every slot is
sample-blocked (BOMBER 252-294, GHOST 67-125).

Two spec gaps worth recording. The approach identities the order names -- SPRAY,
DAMAGE-DEALER, COUNT-WORKER -- do not exist in the registry; the MLB batter
archetypes are BOMBER, GHOST, TORCH, BRUSH, DRIVER, FLEX, ALPHA, HYBRID and
CATALYST. And parkFactors maps hits to run_base, so there is no hits-specific
park factor at all: a park that turns outs into hits without producing runs is
invisible to the input we have.

The grade rescale is NOT run. It was explicitly gated on the factor proving,
and one pooled factor worth 0.0066 of Brier is not a factor-informed
distribution -- rescaling on it would dress a base-rate model as a matchup
model, which is the exact thing this gate was built to prevent.

4,286 tests green (340 suites); web build exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
This commit is contained in:
Kev
2026-08-04 19:30:18 -04:00
parent 4d1803f6d7
commit a9ee55550b
5 changed files with 595 additions and 0 deletions
+34
View File
@@ -1655,6 +1655,40 @@ phased plan in the Session-57 conversation / BUILD-STATE Next section).
correlate with date, so a time-split trains on wins and certifies on losses —
the generator creating the exact leakage the split prevents. Interleave.
## The two-part factor gate (Session 92 — non-obvious)
- **`src/services/model/factorGate.js` asks a question correlation cannot.** A
factor must (a) MOVE the prediction off the player's base rate AND (b) improve
out-of-sample BRIER. Movement alone is **THEATER** — the grade LOOKS like it
read tonight's game while reading nothing, and neither a user nor a
correlation test can see it. arch-v1 was exactly this: moved 76% of rows by
2.5pts, changed resolution by 0.0000, live for months.
- **BRIER, not correlation.** Correlation asks whether the ORDERING improved;
this asks whether the NUMBER got closer to what happened. For a graded
probability the number IS the product, and a factor can improve ordering while
degrading the number.
- **Baseline = the player's LEAVE-ONE-OUT base rate** — literally the "he's due"
null. A factor earns its place only by beating that. Leave-one-out matters: a
row must never contribute to its own baseline.
- **CUMULATIVE CORRECTION APPLIES TO THE INTERVAL ITSELF.** A plain 95% CI is
right for ONE test; at 50 cumulative tests ~2-3 of them exclude zero by chance.
The bootstrap interval now widens to 1 0.05/tests (currently **99.9%**).
Applying it flipped defense and platoon from "proves" to not-proven — a 95% CI
would have shipped two unproven factors.
- **NOT_PROVEN_AT_CORRECTED_BAR ≠ THEATER, and conflating them is the same error
as "insufficient evidence = evidence of absence".** THEATER is reserved for
brier_delta >= 0 (moves, reads nothing). A favourable point estimate whose
corrected CI spans zero is a real candidate held to a rising bar.
- **RESULT for hits (n=741):** `pitcher_contact_profile` **PROVES**
(Brier 0.0066, CI [0.0114,0.0016] at 99.9%). `defense` (0.0043) and
`platoon` (0.0039) are NOT_PROVEN at the corrected bar; `park_hits` is
sample-blocked (n=405). **Zero theater.** Per-archetype all sample-blocked
(BOMBER 252294, GHOST 67125).
- **Two spec gaps found:** approach identities (SPRAY / DAMAGE-DEALER /
COUNT-WORKER) **do not exist** in the registry — MLB batter archetypes are
BOMBER/GHOST/TORCH/BRUSH/DRIVER/FLEX/ALPHA/HYBRID/CATALYST. And
`parkFactors.STAT_BASE` maps `hits → run_base`, so there is **no hits-specific
park factor**: a park that turns outs into hits without scoring is invisible.
## Active Skills
- vyndr-voice (all user-facing output)
- prop-analysis (grading methodology)