tb-v1: model total_bases as a compound outcome (challenger)
Current ladder (proj_p_over_line) and champion p_win are BYTE-IDENTICAL. tb-v1 writes alongside them, on total_bases props only. STEP 0 -- components confirmed on real data, not assumed. statsapi has no singles field, but hits - doubles - triples - homeRuns reproduces stored totalBases EXACTLY on a real 10-game log. So the decomposition is exact, not an approximation. THE MODEL. Each component gets its own per-game Poisson rate; TB is their weighted sum, and the PMF is built by exact convolution rather than simulated (TB support is small). It inherits the SAME combined multiplier proj-v1.1 computes, so the two models differ only in STRUCTURE. Why this is the fix: with identical mean TB of 1.0, a pure-HR hitter and a pure-singles hitter get P(TB>=4) of 0.221 vs 0.019 -- a 12x difference an NB on TB alone cannot express, because it treats one home run as four events. A test asserts that separation, and asserts P(TB>=4) for a pure-HR hitter equals P(at least one HR) exactly. INDEPENDENCE IS AN APPROXIMATION AND IS LABELLED AS ONE: a plate appearance that becomes a double cannot also become a single, so the components are weakly negatively correlated and independent Poissons slightly overstate the tail. Closer to the truth than what it replaces; not a solved problem. HONEST-ABSENT throughout: fewer than 3 usable games, or no derivable component, returns null and the prop keeps the current ladder value. An inconsistent row (hits < extra-base hits) is SKIPPED rather than clamped to zero -- clamping would invent a plausible line out of a broken one. I HIT THE Number(null)===0 TRAP IN MY OWN CODE and a test caught it: a null rate passed a naive finite check and was treated as a measured zero, which is the difference between "this player never triples" and "we do not know his triple rate". Both tbPmf and tbMean now reject null/''/boolean strictly. Holdout committed: TB ROWS ONLY (49 of 437 settled -- averaging into other stats would hide the effect) and DIRECTION-ALIGNED, since the unaligned comparison is the artifact that accounted for 41% of the ladder's apparent loss. If tb-v1 does NOT improve, the family-mismatch hypothesis is wrong and the mean/similarity branch reopens -- recorded in the query header. Migration applied: proj_tb_p_over + proj_tb_meta, NULL-meaningful. Gates: 4,104 tests / 329 suites green; next build exit 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QJs13VsyiSKYQP6rj3NNmc
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
-- tb-compound-holdout.sql — TOTAL BASES ONLY, direction-aligned.
|
||||
--
|
||||
-- TWO guards this query exists to enforce:
|
||||
-- 1. TB ROWS ONLY. Averaging into other stats would hide the effect, since
|
||||
-- total_bases is 49 of 437 settled rows.
|
||||
-- 2. DIRECTION-ALIGNED. p_win is P(GRADED SIDE); proj_p_over_line and
|
||||
-- proj_tb_p_over are P(OVER). 31.4% of rows are under-graded, and comparing
|
||||
-- raw P(over) against an under-side outcome measures the model BACKWARDS —
|
||||
-- that artifact alone accounted for 41% of the ladder's apparent loss.
|
||||
--
|
||||
-- Promote tb-v1 ONLY if it materially improves TB resolution toward/past the
|
||||
-- champion. If it does NOT, the family-mismatch hypothesis is WRONG and the
|
||||
-- mean-weakness / similarity branch REOPENS. Record which.
|
||||
|
||||
with tb as (
|
||||
select
|
||||
game_date, id, lower(side) side, (outcome='hit')::int won,
|
||||
p_win::numeric champ,
|
||||
case when lower(side)='under' then 1 - proj_p_over_line::numeric
|
||||
else proj_p_over_line::numeric end ladder_al,
|
||||
case when lower(side)='under' then 1 - proj_tb_p_over::numeric
|
||||
else proj_tb_p_over::numeric end tbv1_al
|
||||
from public.ledger_entries
|
||||
where sport='mlb' and user_id is null
|
||||
and stat = 'total_bases'
|
||||
and outcome in ('hit','miss')
|
||||
and p_win is not null
|
||||
and proj_p_over_line is not null
|
||||
and proj_tb_p_over is not null -- matched rows: all three present
|
||||
)
|
||||
select
|
||||
count(*) n,
|
||||
count(*) filter (where side='under') under_rows,
|
||||
round(avg(won::numeric),3) base_rate,
|
||||
round(corr(champ, won::numeric)::numeric,4) res_champion,
|
||||
round(corr(ladder_al, won::numeric)::numeric,4) res_ladder_v11,
|
||||
round(corr(tbv1_al, won::numeric)::numeric,4) res_tb_v1,
|
||||
round(stddev(ladder_al)::numeric,4) sd_ladder,
|
||||
round(stddev(tbv1_al)::numeric,4) sd_tb_v1,
|
||||
round(avg(ladder_al)::numeric,4) mean_ladder,
|
||||
round(avg(tbv1_al)::numeric,4) mean_tb_v1
|
||||
from tb;
|
||||
Reference in New Issue
Block a user