167996d99a
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
108 lines
4.4 KiB
JavaScript
108 lines
4.4 KiB
JavaScript
/**
|
|
* MLB park factors (Session 15).
|
|
*
|
|
* Source: FanGraphs 2024-25 park factor data (three-year weighted
|
|
* average — FanGraphs methodology). Numbers indexed to 100 = league
|
|
* average. Pulled from https://www.fangraphs.com/guts.aspx (Park
|
|
* Factors tab) 2025-04. Update annually; the rolling-three-year
|
|
* window means values move slowly.
|
|
*
|
|
* Fields per park:
|
|
* hr — home run factor (Coors ~128 means HRs are 28% more likely;
|
|
* Oracle/SF ~85 means 15% less likely)
|
|
* h — overall hit factor
|
|
* r — runs factor
|
|
*
|
|
* The feature extractor reads this via `getParkFactor(homeTeam)`
|
|
* during the MLB branch and merges hr/h/r as `park_hr`, `park_h`,
|
|
* `park_r` so the grading engine + reasoning builder can read them
|
|
* without re-doing the lookup.
|
|
*
|
|
* Indexing convention: home-team abbreviation. The road team plays
|
|
* at the home team's park, so a Yankees-at-Angels game uses LAA's
|
|
* park factors regardless of whose batter you're grading.
|
|
*/
|
|
|
|
const PARK_FACTORS = Object.freeze({
|
|
// AL East
|
|
BAL: { hr: 109, h: 100, r: 102 }, // Camden Yards — left field wall changes leveled off post-2022
|
|
BOS: { hr: 100, h: 108, r: 106 }, // Fenway — Monster boosts singles/doubles, suppresses HR
|
|
NYY: { hr: 114, h: 101, r: 104 }, // Yankee Stadium — short porch in right
|
|
TB: { hr: 93, h: 96, r: 94 }, // Tropicana — neutral-to-pitcher dome
|
|
TOR: { hr: 106, h: 101, r: 102 }, // Rogers Centre — slight HR boost
|
|
|
|
// AL Central
|
|
CHW: { hr: 109, h: 102, r: 105 }, // Guaranteed Rate Field
|
|
CLE: { hr: 96, h: 99, r: 98 }, // Progressive Field — slight pitcher park
|
|
DET: { hr: 90, h: 100, r: 97 }, // Comerica — deep alleys, HR-suppressed
|
|
KC: { hr: 93, h: 103, r: 101 }, // Kauffman — XBH-friendly, HR-neutral
|
|
MIN: { hr: 105, h: 100, r: 101 }, // Target Field
|
|
|
|
// AL West
|
|
HOU: { hr: 100, h: 101, r: 101 }, // Minute Maid — Crawford boxes ~neutral, dome
|
|
LAA: { hr: 97, h: 98, r: 98 }, // Angel Stadium
|
|
OAK: { hr: 91, h: 95, r: 92 }, // Coliseum — extreme pitcher park
|
|
SEA: { hr: 96, h: 98, r: 96 }, // T-Mobile Park — marine air
|
|
TEX: { hr: 104, h: 102, r: 103 }, // Globe Life — climate-controlled since 2020
|
|
|
|
// NL East
|
|
ATL: { hr: 103, h: 100, r: 101 }, // Truist Park
|
|
MIA: { hr: 89, h: 97, r: 94 }, // loanDepot park — deep alleys
|
|
NYM: { hr: 95, h: 99, r: 97 }, // Citi Field — slight pitcher
|
|
PHI: { hr: 109, h: 100, r: 102 }, // Citizens Bank Park — bandbox right field
|
|
WSH: { hr: 100, h: 100, r: 100 }, // Nationals Park — true neutral
|
|
|
|
// NL Central
|
|
CHC: { hr: 106, h: 100, r: 102 }, // Wrigley — wind-driven swings; multi-year average
|
|
CIN: { hr: 113, h: 102, r: 105 }, // Great American — top-3 HR park most years
|
|
MIL: { hr: 104, h: 100, r: 101 }, // American Family Field
|
|
PIT: { hr: 96, h: 101, r: 99 }, // PNC Park — XBH-favorable, HR-suppressed
|
|
STL: { hr: 93, h: 98, r: 96 }, // Busch — neutral-to-pitcher
|
|
|
|
// NL West
|
|
ARI: { hr: 100, h: 98, r: 98 }, // Chase Field — humidor since 2018
|
|
COL: { hr: 128, h: 112, r: 120 }, // Coors Field — altitude. Single biggest park effect.
|
|
LAD: { hr: 102, h: 99, r: 99 }, // Dodger Stadium
|
|
SD: { hr: 95, h: 97, r: 95 }, // Petco — marine air pitcher park
|
|
SF: { hr: 85, h: 96, r: 92 }, // Oracle Park — Bay winds suppress HRs heavily
|
|
});
|
|
|
|
const NEUTRAL = Object.freeze({ hr: 100, h: 100, r: 100 });
|
|
|
|
function normalizeTeamCode(team) {
|
|
if (!team) return null;
|
|
return String(team).trim().toUpperCase();
|
|
}
|
|
|
|
/**
|
|
* getParkFactor — lookup by home-team code. Returns null for unknown
|
|
* teams so the feature extractor can drop the signal entirely rather
|
|
* than falsely report "neutral park" on a misspelled abbreviation.
|
|
*
|
|
* @param {string} homeTeam — three-letter team code (BAL, NYY, COL, ...)
|
|
* @returns {{hr:number, h:number, r:number}|null}
|
|
*/
|
|
function getParkFactor(homeTeam) {
|
|
const code = normalizeTeamCode(homeTeam);
|
|
if (!code) return null;
|
|
return PARK_FACTORS[code] || null;
|
|
}
|
|
|
|
/**
|
|
* Convenience — when you want a guaranteed object (e.g. arithmetic
|
|
* downstream that can't handle null). Falls back to league-neutral.
|
|
* Prefer getParkFactor + explicit-null branching in code; this is
|
|
* for tests and downstream services that prefer 100s over null.
|
|
*/
|
|
function getParkFactorOrNeutral(homeTeam) {
|
|
return getParkFactor(homeTeam) || NEUTRAL;
|
|
}
|
|
|
|
module.exports = {
|
|
PARK_FACTORS,
|
|
NEUTRAL,
|
|
getParkFactor,
|
|
getParkFactorOrNeutral,
|
|
__internals: { normalizeTeamCode },
|
|
};
|