Move each ratings model class (and, for batters, the helper functions it depends on) into a dedicated models.py so that calcs_*.py can import from card_builder.py at module level without circular imports. - batters/models.py: BattingCardRatingsModel + bp_singles, wh_singles, one_singles, bp_homeruns, triples, two_doubles, hit_by_pitch, strikeouts, flyout_a, flyout_bq, flyout_b, groundball_a, groundball_c - pitchers/models.py: PitchingCardRatingsModel (no helper deps needed) - batters/calcs_batter.py: imports model + build_batter_full_cards at top - pitchers/calcs_pitcher.py: imports model + build_pitcher_full_cards at top - batters/card_builder.py: imports from batters.models - pitchers/card_builder.py: imports from pitchers.models - tests/test_batter_calcs.py: import bp_singles, wh_singles from batters.models Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
301 lines
10 KiB
Python
301 lines
10 KiB
Python
import random
|
|
|
|
from creation_helpers import mround, sanitize_chance_output
|
|
from typing import List
|
|
from decimal import Decimal
|
|
from exceptions import logger
|
|
|
|
from batters.models import BattingCardRatingsModel
|
|
from batters.card_builder import build_batter_full_cards
|
|
|
|
def stealing(chances: int, sb2s: int, cs2s: int, sb3s: int, cs3s: int, season_pct: float):
|
|
if chances == 0 or sb2s + cs2s == 0:
|
|
return 0, 0, False, 0
|
|
|
|
total_attempts = sb2s + cs2s + sb3s + cs3s
|
|
attempt_pct = total_attempts / chances
|
|
|
|
if attempt_pct >= .08:
|
|
st_auto = True
|
|
else:
|
|
st_auto = False
|
|
|
|
# chance_odds = [x / 36 for x in range(1, 36)]
|
|
if attempt_pct * 1.5 >= 1.0:
|
|
st_jump = 1.0
|
|
else:
|
|
st_jump = 0
|
|
for x in range(1, 37):
|
|
if attempt_pct * 1.5 <= x / 36:
|
|
st_jump = x / 36
|
|
break
|
|
|
|
st_high = mround(20 * (sb2s / (sb2s + cs2s + cs2s)))
|
|
if st_high <= 10:
|
|
st_auto = False
|
|
|
|
if sb3s + cs3s < max((3 * season_pct), 1):
|
|
st_low = 3
|
|
else:
|
|
st_low = mround(16 * ((sb2s + sb3s) / (sb2s + sb3s + cs2s * 2 + cs3s * 2)))
|
|
if not st_auto:
|
|
st_low = min(st_low, 10)
|
|
|
|
if st_low >= st_high - 3:
|
|
if st_high == 0:
|
|
st_low = 0
|
|
st_jump = 0
|
|
elif st_high <= 3:
|
|
st_high = 4
|
|
st_low = 1
|
|
else:
|
|
st_low = st_high - 3
|
|
|
|
# if ((st_high - 7) > st_low) and st_high > 7:
|
|
# st_low = st_high - 7
|
|
|
|
return round(st_low), round(st_high), st_auto, st_jump
|
|
|
|
|
|
def stealing_line(steal_data: dict):
|
|
sd = steal_data
|
|
jump_chances = round(sd[3] * 36)
|
|
|
|
if jump_chances == 0:
|
|
good_jump = '-'
|
|
elif jump_chances <= 6:
|
|
if jump_chances == 6:
|
|
good_jump = 7
|
|
elif jump_chances == 5:
|
|
good_jump = 6
|
|
elif jump_chances == 4:
|
|
good_jump = 5
|
|
elif jump_chances == 3:
|
|
good_jump = 4
|
|
elif jump_chances == 2:
|
|
good_jump = 3
|
|
elif jump_chances == 1:
|
|
good_jump = 2
|
|
elif jump_chances == 7:
|
|
good_jump = '4,5'
|
|
elif jump_chances == 8:
|
|
good_jump = '4,6'
|
|
elif jump_chances == 9:
|
|
good_jump = '3-5'
|
|
elif jump_chances == 10:
|
|
good_jump = '2-5'
|
|
elif jump_chances == 11:
|
|
good_jump = '6,7'
|
|
elif jump_chances == 12:
|
|
good_jump = '4-6'
|
|
elif jump_chances == 13:
|
|
good_jump = '2,4-6'
|
|
elif jump_chances == 14:
|
|
good_jump = '3-6'
|
|
elif jump_chances == 15:
|
|
good_jump = '2-6'
|
|
elif jump_chances == 16:
|
|
good_jump = '2,5-6'
|
|
elif jump_chances == 17:
|
|
good_jump = '3,5-6'
|
|
elif jump_chances == 18:
|
|
good_jump = '4-6'
|
|
elif jump_chances == 19:
|
|
good_jump = '2,4-7'
|
|
elif jump_chances == 20:
|
|
good_jump = '3-7'
|
|
elif jump_chances == 21:
|
|
good_jump = '2-7'
|
|
elif jump_chances == 22:
|
|
good_jump = '2-7,12'
|
|
elif jump_chances == 23:
|
|
good_jump = '2-7,11'
|
|
elif jump_chances == 24:
|
|
good_jump = '2,4-8'
|
|
elif jump_chances == 25:
|
|
good_jump = '3-8'
|
|
elif jump_chances == 26:
|
|
good_jump = '2-8'
|
|
elif jump_chances == 27:
|
|
good_jump = '2-8,12'
|
|
elif jump_chances == 28:
|
|
good_jump = '2-8,11'
|
|
elif jump_chances == 29:
|
|
good_jump = '3-9'
|
|
elif jump_chances == 30:
|
|
good_jump = '2-9'
|
|
elif jump_chances == 31:
|
|
good_jump = '2-9,12'
|
|
elif jump_chances == 32:
|
|
good_jump = '2-9,11'
|
|
elif jump_chances == 33:
|
|
good_jump = '2-10'
|
|
elif jump_chances == 34:
|
|
good_jump = '3-11'
|
|
elif jump_chances == 35:
|
|
good_jump = '2-11'
|
|
else:
|
|
good_jump = '2-12'
|
|
|
|
return f'{"*" if sd[2] else ""}{good_jump}/- ({sd[1] if sd[1] else "-"}-{sd[0] if sd[0] else "-"})'
|
|
|
|
|
|
def running(extra_base_pct: str):
|
|
if extra_base_pct == '':
|
|
return 8
|
|
try:
|
|
xb_pct = float(extra_base_pct.strip("%")) / 80
|
|
except Exception as e:
|
|
logger.error(f'calcs_batter running - {e}')
|
|
xb_pct = 20
|
|
|
|
return max(min(round(6 + (10 * xb_pct)), 17), 8)
|
|
|
|
|
|
def bunting(num_bunts: int, season_pct: float):
|
|
if num_bunts > max(round(10 * season_pct), 4):
|
|
return 'A'
|
|
elif num_bunts > max(round(5 * season_pct), 2):
|
|
return 'B'
|
|
elif num_bunts > 1:
|
|
return 'C'
|
|
else:
|
|
return 'D'
|
|
|
|
|
|
|
|
def hit_and_run(ab_vl: int, ab_vr: int, hits_vl: int, hits_vr: int, hr_vl: int, hr_vr: int, so_vl: int, so_vr: int):
|
|
babip = (hits_vr + hits_vl - hr_vl - hr_vr) / max(ab_vl + ab_vr - so_vl - so_vr - hr_vl - hr_vl, 1)
|
|
if babip >= .35:
|
|
return 'A'
|
|
elif babip >= .3:
|
|
return 'B'
|
|
elif babip >= .25:
|
|
return 'C'
|
|
else:
|
|
return 'D'
|
|
|
|
|
|
def get_batter_ratings(df_data) -> List[dict]:
|
|
# Consider a sliding offense_mod based on OPS; floor of 1x and ceiling of 1.5x ?
|
|
offense_mod = 1.2
|
|
vl = BattingCardRatingsModel(
|
|
battingcard_id=df_data.battingcard_id,
|
|
bat_hand=df_data['bat_hand'],
|
|
vs_hand='L',
|
|
all_hits=sanitize_chance_output(108 * offense_mod * df_data['AVG_vL']),
|
|
all_other_ob=sanitize_chance_output(108 * offense_mod *
|
|
((df_data['BB_vL'] + df_data['HBP_vL']) / df_data['PA_vL'])),
|
|
hard_rate=df_data['Hard%_vL'],
|
|
med_rate=df_data['Med%_vL'],
|
|
soft_rate=df_data['Soft%_vL'],
|
|
pull_rate=df_data['Pull%_vL'],
|
|
center_rate=df_data['Cent%_vL'],
|
|
slap_rate=df_data['Oppo%_vL']
|
|
)
|
|
vr = BattingCardRatingsModel(
|
|
battingcard_id=df_data.battingcard_id,
|
|
bat_hand=df_data['bat_hand'],
|
|
vs_hand='R',
|
|
all_hits=sanitize_chance_output(108 * offense_mod * df_data['AVG_vR']),
|
|
all_other_ob=sanitize_chance_output(108 * offense_mod *
|
|
((df_data['BB_vR'] + df_data['HBP_vR']) / df_data['PA_vR'])),
|
|
hard_rate=df_data['Hard%_vR'],
|
|
med_rate=df_data['Med%_vR'],
|
|
soft_rate=df_data['Soft%_vR'],
|
|
pull_rate=df_data['Pull%_vR'],
|
|
center_rate=df_data['Cent%_vR'],
|
|
slap_rate=df_data['Oppo%_vR']
|
|
)
|
|
vl.all_outs = mround(108 - vl.all_hits - vl.all_other_ob) #.quantize(Decimal("0.05"))
|
|
vr.all_outs = mround(108 - vr.all_hits - vr.all_other_ob) #.quantize(Decimal("0.05"))
|
|
|
|
vl.calculate_singles(df_data['1B_vL'], df_data['H_vL'], mround(df_data['IFH%_vL']))
|
|
vr.calculate_singles(df_data['1B_vR'], df_data['H_vR'], mround(df_data['IFH%_vR']))
|
|
|
|
logger.debug(
|
|
f'vL - All Hits: {vl.all_hits} / Other OB: {vl.all_other_ob} / All Outs: {vl.all_outs} '
|
|
f'/ Total: {vl.all_hits + vl.all_other_ob + vl.all_outs}'
|
|
)
|
|
logger.debug(
|
|
f'vR - All Hits: {vr.all_hits} / Other OB: {vr.all_other_ob} / All Outs: {vr.all_outs} '
|
|
f'/ Total: {vr.all_hits + vr.all_other_ob + vr.all_outs}'
|
|
)
|
|
|
|
vl.calculate_xbh(df_data['3B_vL'], df_data['2B_vL'], df_data['HR_vL'], df_data['HR/FB_vL'])
|
|
vr.calculate_xbh(df_data['3B_vR'], df_data['2B_vR'], df_data['HR_vR'], df_data['HR/FB_vR'])
|
|
|
|
logger.debug(f'all_hits: {vl.all_hits} / sum of hits: {vl.total_chances()}')
|
|
logger.debug(f'all_hits: {vr.all_hits} / sum of hits: {vr.total_chances()}')
|
|
|
|
vl.calculate_other_ob(df_data['BB_vL'], df_data['HBP_vL'])
|
|
vr.calculate_other_ob(df_data['BB_vR'], df_data['HBP_vR'])
|
|
|
|
logger.debug(f'all on base: {vl.hbp + vl.walk + vl.total_hits()} / all chances: {vl.total_chances()}'
|
|
f'{"*******ERROR ABOVE*******" if vl.hbp + vl.walk + vl.total_hits() != vl.total_chances() else ""}')
|
|
logger.debug(f'all on base: {vr.hbp + vr.walk + vr.total_hits()} / all chances: {vr.total_chances()}'
|
|
f'{"*******ERROR ABOVE*******" if vr.hbp + vr.walk + vr.total_hits() != vr.total_chances() else ""}')
|
|
|
|
vl.calculate_strikeouts(df_data['SO_vL'], df_data['AB_vL'], df_data['H_vL'])
|
|
vr.calculate_strikeouts(df_data['SO_vR'], df_data['AB_vR'], df_data['H_vR'])
|
|
|
|
logger.debug(f'K rate vL: {round(vl.strikeout / vl.all_outs, 2)} / '
|
|
f'K rate vR: {round(vr.strikeout / vr.all_outs, 2)}')
|
|
|
|
vl.calculate_other_outs(
|
|
df_data['FB%_vL'], df_data['LD%_vL'], df_data['GB%_vL'], df_data['GDP_vL'], df_data['AB_vL']
|
|
)
|
|
vr.calculate_other_outs(
|
|
df_data['FB%_vR'], df_data['LD%_vR'], df_data['GB%_vR'], df_data['GDP_vR'], df_data['AB_vR']
|
|
)
|
|
|
|
# Correct total chance errors
|
|
for x in [vl, vr]:
|
|
if x.total_chances() < 108:
|
|
diff = mround(108) - x.total_chances()
|
|
logger.error(f'Adding {diff} strikeouts to close gap')
|
|
x.strikeout += diff
|
|
elif x.total_chances() > 108:
|
|
diff = x.total_chances() - mround(108)
|
|
logger.error(f'Have surplus of {diff} chances')
|
|
if x.strikeout + 1 > diff:
|
|
logger.error(f'Subtracting {diff} strikeouts to close gap')
|
|
x.strikeout -= diff
|
|
elif x.lineout + 1 > diff:
|
|
logger.error(f'Subtracting {diff} lineouts to close gap')
|
|
x.lineout -= diff
|
|
elif x.groundout_a + 1 > diff:
|
|
logger.error(f'Subtracting {diff} gbA to close gap')
|
|
x.groundout_a -= diff
|
|
elif x.groundout_b + 1 > diff:
|
|
logger.error(f'Subtracting {diff} gbB to close gap')
|
|
x.groundout_b -= diff
|
|
elif x.groundout_c + 1 > diff:
|
|
logger.error(f'Subtracting {diff} gbC to close gap')
|
|
x.groundout_c -= diff
|
|
|
|
vl_total_chances = vl.total_chances()
|
|
vr_total_chances = vr.total_chances()
|
|
if vl_total_chances != 108:
|
|
logger.error(f'total chances for {df_data.name} come to {vl_total_chances}')
|
|
else:
|
|
logger.debug(f'total chances: {vl_total_chances}')
|
|
if vr_total_chances != 108:
|
|
logger.error(f'total chances for {df_data.name} come to {vr_total_chances}')
|
|
else:
|
|
logger.debug(f'total chances: {vr_total_chances}')
|
|
|
|
vl_dict = vl.custom_to_dict()
|
|
vr_dict = vr.custom_to_dict()
|
|
|
|
try:
|
|
vl_card, vr_card = build_batter_full_cards(
|
|
vl, vr, int(df_data['offense_col']), int(df_data['player_id']), df_data['bat_hand']
|
|
)
|
|
vl_dict.update(vl_card.card_output())
|
|
vr_dict.update(vr_card.card_output())
|
|
except Exception as e:
|
|
logger.warning(f'Card layout builder failed for {df_data.name}: {e}')
|
|
|
|
return [vl_dict, vr_dict]
|