← back to the record
# `opp_nine_k_prior` — killed by quant gate, live 2026-07-17 to 2026-07-19

> **Dates corrected 2026-07-20.** This document previously said the
> feature "had been live since 7/17" and was killed on 7/18, and
> described the revert as not yet deployed. All three were wrong, and
> the site's disclosure inherited the error.
>
> The feature was live in **production** from **2026-07-17 19:20 UTC**
> (commit `7506da3`, when it entered `FEATURES`) until **2026-07-19
> 16:11 UTC** — the mtime of `data/k_model.joblib`, when the droplet
> actually retrained without it. The revert was *committed* 2026-07-18
> 12:19 UTC (`babd80f`), but a commit is not a deployment: production
> served the withdrawn model for a further **28 hours** after it.
>
> Measured effect: **147 of 252 graded strikeout line-results (58%),
> across 51 starts.** Both ends of the window fall mid-slate — 7/17 is
> affected for 2 of its 30 starts, 7/19 for 20 of 31 — so no
> whole-days description of this era can be correct. The site computes
> the figure from these two timestamps at build time rather than
> restating it in prose.


**Verdict: DO NOT SHIP. Reverted from the model's feature list the same
day it was reviewed. It had been live since 7/17.**

## What the feature was

Mean strictly-prior strikeout rate (vs the starter's throwing hand) across
the nine batters the starter would face — the idea being that a lineup of
free swingers is a different strikeout matchup than a contact lineup, and
that a team-level rate blurs exactly that.

## Why it was killed

**1. The sign inverts on the serve path it actually uses.** Training used
`game_nines()` — the nine the pitcher *actually faced*, recovered from the
game's own play-by-play. Serving uses the posted lineup when available and
a projection when not. Rebuilding the feature from the projected nine:

| | Δ log-loss vs no feature |
|---|---|
| Trained on actual nine, served actual nine (the published claim) | −0.00343 |
| Trained on actual nine, served **projected** nine | **+0.00334** |

The projected nine overlaps the actual nine 75.6% of the time (6.8 of 9;
an exact match in only 2.7% of starts). On the fallback path the feature
made the model **worse than not having it at all** — and nothing in the
code distinguished the two paths at inference time.

**2. The gain was never distinguishable from zero.** Paired bootstrap
(B=20,000) over the 1,035 holdout starts:

| Metric | Δ | 95% CI | P(wrong sign) |
|---|---|---|---|
| Log-loss | −0.0035 | [−0.0146, +0.0078] | 0.27 |
| Brier 4.5 | −0.0010 | [−0.0028, +0.0009] | 0.16 |
| Brier 5.5 | −0.0008 | [−0.0027, +0.0011] | 0.21 |
| Brier 6.5 | −0.0012 | [−0.0029, +0.0006] | 0.09 |

Effect ÷ bootstrap SE = 0.60 (significance needs ~1.96). The CI is four
times wider than the effect. "All three line Briers improved" was true and
meaningless: the three lines are nested thresholds of one outcome, so they
are one piece of evidence, not three.

## What this changes about a public claim

The gate also corrected a claim on our own goal table, in the direction of
being *unfairly harsh on ourselves*. The K model's log-loss deficit versus
the persistence baseline is +0.0135, but that gap's 95% CI is
**[−0.0103, +0.0371]** — it crosses zero. The honest statement is
therefore:

> We cannot distinguish the K model from a shrunk-Poisson persistence
> baseline on log-loss.

not "persistence beats us." Goal K-1's wording is corrected accordingly.
Being wrong in the self-critical direction is still being wrong.

## The salvage path (not attempted yet)

Train the feature **from a projected nine** so training and serving see
the same construction, then re-measure. If it survives that, it ships. The
feature-computation functions are retained in `pipeline/features.py` for
exactly this; only the entry in `FEATURES` was removed.

## Deployment state

- Live from 7/17 until 7/18. Predictions published in that window were
  built with it; they are immutable and stay on the record as published,
  which is why this document exists rather than a silent rollback.
- The revert is staged on branch `k-feature-revert`. It **must** ship
  together with a droplet retrain: the deployed model artifact expects the
  feature, so code-without-retrain would fail on dimension mismatch.
- Measured impact of the revert on the holdout: log-loss 2.2552 → 2.2587,
  line Briers 0.2289 / 0.2130 / 0.1715. Both figures are inside the
  noise band described above; nothing measurable was lost.

Source: K-FEATURE-VERDICT.md — the original, unrendered, for anyone who wants to diff it.