CLAUDE: Add safe WPA lookup with fallback to prevent KeyError crashes
Implements defensive error handling for WPA (Win Probability Added) calculations when rare game states are missing from the static lookup table. The safe_wpa_lookup() function uses a three-tier fallback strategy: exact lookup, bases-empty fallback, and 0.0 default value. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
660c6ad904
commit
80e94498fa
@ -33,6 +33,49 @@ TO_BASE = {
|
||||
3: 'to third',
|
||||
4: 'home'
|
||||
}
|
||||
|
||||
|
||||
def safe_wpa_lookup(inning_half: str, inning_num: int, starting_outs: int, on_base_code: int, run_diff: int) -> float:
|
||||
"""
|
||||
Safely lookup win probability from WPA_DF with fallback logic for missing keys.
|
||||
|
||||
Fallback strategy:
|
||||
1. Try exact lookup
|
||||
2. Try with simplified on_base_code (reduce to bases empty state)
|
||||
3. Return 0.0 if no data available
|
||||
|
||||
Args:
|
||||
inning_half: 'top' or 'bot'
|
||||
inning_num: Inning number (1-9)
|
||||
starting_outs: Number of outs (0-2)
|
||||
on_base_code: Base occupation code (0-7)
|
||||
run_diff: Home run differential (-6 to 6)
|
||||
|
||||
Returns:
|
||||
float: Home team win expectancy (0.0 to 1.0)
|
||||
"""
|
||||
# Construct the primary key
|
||||
key = f'{inning_half}_{inning_num}_{starting_outs}_out_{on_base_code}_obc_{run_diff}_home_run_diff'
|
||||
|
||||
# Try exact lookup first
|
||||
try:
|
||||
return float(WPA_DF.loc[key, 'home_win_ex'])
|
||||
except KeyError:
|
||||
logger.warning(f'WPA key not found: {key}, attempting fallback')
|
||||
|
||||
# Fallback 1: Try with simplified on_base_code (bases empty = 0)
|
||||
if on_base_code != 0:
|
||||
fallback_key = f'{inning_half}_{inning_num}_{starting_outs}_out_0_obc_{run_diff}_home_run_diff'
|
||||
try:
|
||||
result = float(WPA_DF.loc[fallback_key, 'home_win_ex'])
|
||||
logger.info(f'WPA fallback successful using bases empty state: {fallback_key}')
|
||||
return result
|
||||
except KeyError:
|
||||
logger.warning(f'WPA fallback key not found: {fallback_key}')
|
||||
|
||||
# Fallback 2: Return 0.0 if no data available
|
||||
logger.warning(f'WPA using fallback value 0.0 for missing key: {key}')
|
||||
return 0.0
|
||||
AT_BASE = {
|
||||
2: 'at second',
|
||||
3: 'at third',
|
||||
@ -503,11 +546,11 @@ def get_wpa(this_play: Play, next_play: Play):
|
||||
new_win_ex = 1.0
|
||||
else:
|
||||
inning_num = 9 if next_play.inning_num > 9 else next_play.inning_num
|
||||
new_win_ex = WPA_DF.loc[f'{next_play.inning_half}_{inning_num}_{next_play.starting_outs}_out_{next_play.on_base_code}_obc_{new_rd}_home_run_diff'].home_win_ex
|
||||
new_win_ex = safe_wpa_lookup(next_play.inning_half, inning_num, next_play.starting_outs, next_play.on_base_code, new_rd)
|
||||
# print(f'new_win_ex = {new_win_ex}')
|
||||
|
||||
inning_num = 9 if this_play.inning_num > 9 else this_play.inning_num
|
||||
old_win_ex = WPA_DF.loc[f'{this_play.inning_half}_{inning_num}_{this_play.starting_outs}_out_{this_play.on_base_code}_obc_{old_rd}_home_run_diff'].home_win_ex
|
||||
old_win_ex = safe_wpa_lookup(this_play.inning_half, inning_num, this_play.starting_outs, this_play.on_base_code, old_rd)
|
||||
# print(f'old_win_ex = {old_win_ex}')
|
||||
|
||||
wpa = float(round(new_win_ex - old_win_ex, 3))
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import pytest
|
||||
from sqlmodel import Session, select, func
|
||||
|
||||
from command_logic.logic_gameplay import advance_runners, doubles, gb_result_1, get_obc, get_re24, get_wpa, complete_play, log_run_scored, strikeouts, steals, xchecks, walks, popouts, hit_by_pitch, homeruns, singles, triples, bunts, chaos
|
||||
from command_logic.logic_gameplay import advance_runners, doubles, gb_result_1, get_obc, get_re24, get_wpa, complete_play, log_run_scored, strikeouts, steals, xchecks, walks, popouts, hit_by_pitch, homeruns, singles, triples, bunts, chaos, safe_wpa_lookup
|
||||
from in_game.gameplay_models import Lineup, Play
|
||||
from tests.factory import session_fixture, Game
|
||||
|
||||
@ -83,6 +83,32 @@ def test_get_wpa(session: Session):
|
||||
assert get_wpa(this_play, next_play) == 0.153
|
||||
|
||||
|
||||
def test_safe_wpa_lookup():
|
||||
"""Test safe_wpa_lookup handles both valid and missing keys gracefully."""
|
||||
# Test 1: Valid key should return actual value
|
||||
valid_result = safe_wpa_lookup('bot', 1, 0, 0, 0)
|
||||
assert isinstance(valid_result, float)
|
||||
assert 0.0 <= valid_result <= 1.0
|
||||
assert valid_result == 0.589 # Known value from wpa_data.csv
|
||||
|
||||
# Test 2: Missing key with bases loaded should fallback to bases empty
|
||||
# This simulates the error case: top_1_0_out_6_obc_-5_home_run_diff doesn't exist
|
||||
fallback_result = safe_wpa_lookup('top', 1, 0, 6, -5)
|
||||
assert isinstance(fallback_result, float)
|
||||
assert 0.0 <= fallback_result <= 1.0
|
||||
# Should use fallback to bases empty state if available
|
||||
|
||||
# Test 3: If even bases empty fallback doesn't exist, should return 0.0
|
||||
# (This is an edge case that shouldn't happen with complete data)
|
||||
extreme_fallback = safe_wpa_lookup('top', 1, 0, 7, -10) # Invalid obc and extreme run diff
|
||||
assert extreme_fallback == 0.0
|
||||
|
||||
# Test 4: Another valid key
|
||||
valid_result_2 = safe_wpa_lookup('top', 9, 2, 3, 1)
|
||||
assert isinstance(valid_result_2, float)
|
||||
assert 0.0 <= valid_result_2 <= 1.0
|
||||
|
||||
|
||||
def test_complete_play(session: Session):
|
||||
game_1 = session.get(Game, 1)
|
||||
this_play = game_1.current_play_or_none(session)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user