Layer 2: multi-axis archetype classifier; the FLEX fallback is gone
A player is a blend across independent axes, not one label. Skubal is a STARTER and a strikeout arm and a ground-ball arm and a control arm — four true things at once, and single-label classification threw three of them away. AXIS INDEPENDENCE WAS MEASURED, NOT ASSUMED. Correlations over the live store (467 batters, 531 pitchers); anything |r| >= 0.70 is one underlying trait and was collapsed so we never show one trait as two archetypes. Batter k% ~ whiff% +0.89, hard-hit% ~ exit velo +0.88, chase% ~ swing% +0.87, chase% ~ bb% -0.72; pitcher k% ~ whiff% +0.76, gb% ~ fb% -0.73 — all collapsed. The survivors are genuinely orthogonal, and one result is worth stating: pitcher velocity correlates +0.14 with K%, +0.07 with whiff% and +0.07 with GB%. Velocity is NOT a proxy for missing bats — a hard thrower who misses no bats is a real distinct type, so CANNON earns its own axis rather than being folded into STRIKEOUT. Pitcher K% ~ GB% is -0.10, so PUNCHOUT and SINKER are independent, which is exactly the multi-axis thesis. Cut-lines are the measured p75 (distinctive) and p90 (elite), per role where the tails differ even when the medians agree: reliever GB% p90 is 54.1 against a starter's 48.9, both with a median of 42.5. THE FALLBACK IS DELETED. classify() used to return FLEX (mlb) / SHIELD (wnba) / CONNECTOR (nba) at weight 1.0 when nothing scored — "could not classify" rendered as a fully-confident classification of a real archetype, with descriptive education copy attached. 8 of 18 MLB players carried it, and FLEX could never be earned because its only scoring input had zero writers. Every sport now does what MMA already did: unclassified is absent. Induced on real players. Skubal: STARTER, throws L, WHIFF + SEAM + PINPOINT, all elite. Judge: BOMBER + GRINDER + WHIFF RISK — elite power, patient, strikes out, three true things. Kwan: SURGEON + SNIPER + SLASH with NO power claimed (0.4 barrel% is absent, not "low power"). Josh Bell, who used to classify as DRIVER: empty blend, "No standout profile — league-average across every measured axis." Alan Roden, who was FLEX at weight 1.0 on 21 PA: every axis absent, "Not enough plate appearances yet — no profile claimed." Per-axis honest-absence holds: a velo-less pitcher keeps every other axis, and NO DATA is distinguishable from LEAGUE-AVERAGE rather than collapsing into one shrug. The full vector is stored for Layer 3; only the top three distinctive traits surface. Three existing tests asserted the fallback and were updated to assert absence. One of them surfaced a real robustness gap: classify(sport, null) threw, because an explicit null does not trigger a default parameter and every scorer dereferences its argument. Guarded. Every baseball name is accounted for in docs/ARCHETYPE-AXES.md — built, alias, tier, or shelved with its unlock condition. Zero orphans; cross-sport names left for their sport. Tests 3601 passed / 293 suites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VCNgGSt5qvcLxaeQqa7Zpj
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
# MLB ARCHETYPE AXES — Layer 2
|
||||
|
||||
A player is a **blend across independent axes**, not one label. Skubal is a
|
||||
STARTER *and* a strikeout arm *and* a ground-ball arm *and* a control arm —
|
||||
four true things at once. Single-label classification is lossy, and its failure
|
||||
mode is the FLEX disease: when nothing matches, invent a bucket.
|
||||
|
||||
**There is no fallback.** Unremarkable on an axis → absent on that axis. A
|
||||
genuinely average player surfaces nothing, and says so.
|
||||
|
||||
## Axis independence — measured, not assumed
|
||||
|
||||
Correlations over the live store (467 batters PA≥50, 531 pitchers IP≥10).
|
||||
**|r| ≥ 0.70 = one underlying trait → collapsed**, so one trait is never shown
|
||||
as two archetypes.
|
||||
|
||||
| Pair | r | Decision |
|
||||
|---|---|---|
|
||||
| batter k% ~ whiff% | **+0.89** | collapsed → one SWING-AND-MISS axis |
|
||||
| batter hard-hit% ~ avg exit velo | **+0.88** | folded into POWER as intensity |
|
||||
| batter chase% ~ swing% | **+0.87** | collapsed → one AGGRESSION axis |
|
||||
| batter chase% ~ bb% | **−0.72** | same axis inverted (discipline) |
|
||||
| pitcher k% ~ whiff% | **+0.76** | collapsed → one STRIKEOUT axis |
|
||||
| pitcher gb% ~ fb% | **−0.73** | collapsed → one signed tilt axis |
|
||||
| batter barrel% ~ hard-hit% | +0.70 | at the line; barrel leads |
|
||||
| **pitcher velo ~ k%** | **+0.14** | **INDEPENDENT** — velo earns its own axis |
|
||||
| pitcher velo ~ whiff% | +0.07 | independent |
|
||||
| pitcher velo ~ gb% | +0.07 | independent |
|
||||
| **pitcher k% ~ gb%** | **−0.10** | **INDEPENDENT** — PUNCHOUT ⊥ SINKER |
|
||||
| batter barrel ~ launch | +0.23 | independent |
|
||||
| batter k% ~ chase% | +0.05 | independent |
|
||||
|
||||
The velocity result matters: **velo is not a proxy for missing bats.** A hard
|
||||
thrower who misses no bats is a real, distinct type.
|
||||
|
||||
## Cut-lines
|
||||
|
||||
`p75` = distinctive · `p90` = elite, from the measured distribution — **per
|
||||
role where the tails differ** even when medians agree (reliever GB% p90 = 54.1
|
||||
vs starter 48.9, both median 42.5). Sample floors: **PA ≥ 50 · IP ≥ 10**.
|
||||
|
||||
## Every baseball name accounted for
|
||||
|
||||
| Name | Status | Axis / reason |
|
||||
|---|---|---|
|
||||
| WORKHORSE | **TIER** | role=starter + high IP; the old hardcoded default is gone |
|
||||
| PUNCHOUT / WHIFF | **BUILT** | strikeout (p75 / p90) |
|
||||
| SINKER / SEAM | **BUILT** | ground_ball |
|
||||
| FLY BALL / ELEVATOR | **BUILT** | fly_ball |
|
||||
| SURGEON ARM / PINPOINT | **BUILT** | control (low BB%) |
|
||||
| NIBBLER / SCATTERGUN | **BUILT** | wild (high BB%) |
|
||||
| BAIT / TRAPDOOR | **BUILT** | chase |
|
||||
| HITTABLE / BATTING PRACTICE | **BUILT** | contact_allowed |
|
||||
| CANNON / HOWITZER | **BUILT** | velocity — unlocked by the 53%→99% velo fix |
|
||||
| SIDEARM / SUBMARINE | **BUILT** | slot (low arm angle) |
|
||||
| STARTER / RELIEVER / CLOSER / SETUP | **BUILT** | role, from real usage |
|
||||
| SLUGGER / BOMBER | **BUILT** | power (barrel%) |
|
||||
| TECHNICIAN / SURGEON | **BUILT** | contact (low K%) |
|
||||
| WHIFF RISK / WINDMILL | **BUILT** | swing_miss |
|
||||
| GRINDER / SNIPER | **BUILT** | patience |
|
||||
| FREE SWINGER / HACKER | **BUILT** | aggression |
|
||||
| TOPSPIN / LOFT | **BUILT** | launch |
|
||||
| SLASH / BARREL FINDER | **BUILT** | line_drive |
|
||||
| HAMMER | **ALIAS** | of chase/strikeout — best-pitch whiff r=0.50 with overall; not independent enough for its own axis |
|
||||
| BRUSH | **ALIAS** | of contact (TECHNICIAN) |
|
||||
| DRIVER | **ALIAS** | of power — RBI is lineup context, not a player trait |
|
||||
| **FLEX** | **RETIRED** | it was the fallback, never earned: `utility` had zero writers |
|
||||
| MIRROR | **SHELVED** | needs switch-hitter axis; `bats='S'` now joined — unlock when a platoon-split feed lands |
|
||||
| GHOST / BURNER / LEG | **SHELVED** | speed: SB is statsapi, not yet joined into the aggregate store |
|
||||
| CATALYST | **SHELVED** | table-setter: needs lineup slot (not ingested) |
|
||||
| HYBRID | **BUILT-ADJACENT** | two-way = both role profiles exist (position `TWP`) |
|
||||
| ALPHA | **ALIAS** | of strikeout+control combined; the blend expresses it |
|
||||
| FASTBREAK · ENFORCER · CHAINMOVER · CONDUCTOR · ARCHITECT · TORCH · LINK · ARTILLERY · BELL COW · etc. | **OTHER SPORT** | NBA/WNBA/NFL names — correctly not built here |
|
||||
|
||||
Zero orphans.
|
||||
|
||||
## Output shape
|
||||
|
||||
```js
|
||||
{
|
||||
role, roleDetail, roleLabel, sufficient, sample, bats, throws,
|
||||
vector: { axisKey: {tier,label,value,strength} | null }, // Layer 3 reads ALL
|
||||
blend: [ {label, axis, tier, value, strength} ], // top ≤3 to surface
|
||||
absent: [axisKey…], // NO DATA — distinct from unremarkable (vector null)
|
||||
note // honest copy when the blend is empty
|
||||
}
|
||||
```
|
||||
|
||||
`absent` vs a `null` vector entry is a real distinction: absent means we could
|
||||
not measure it, null means we measured it and he is ordinary.
|
||||
Reference in New Issue
Block a user