import decimal import logging import math import pydantic from creation_helpers import mround from typing import List, Literal from decimal import Decimal class BattingCardRatingsModel(pydantic.BaseModel): battingcard_id: int vs_hand: Literal['R', 'L'] all_hits: Decimal = Decimal(0.0) other_ob: Decimal = Decimal(0.0) all_outs: Decimal = Decimal(0.0) rem_singles: Decimal = Decimal(0.0) rem_xbh: Decimal = Decimal(0.0) rem_hr: Decimal = Decimal(0.0) rem_doubles: Decimal = Decimal(0.0) hard_rate: Decimal med_rate: Decimal soft_rate: Decimal homerun: Decimal = Decimal(0.0) bp_homerun: Decimal = Decimal(0.0) triple: Decimal = Decimal(0.0) double_three: Decimal = Decimal(0.0) double_two: Decimal = Decimal(0.0) double_pull: Decimal = Decimal(0.0) single_two: Decimal = Decimal(0.0) single_one: Decimal = Decimal(0.0) single_center: Decimal = Decimal(0.0) bp_single: Decimal = Decimal(0.0) hbp: Decimal = Decimal(0.0) walk: Decimal = Decimal(0.0) strikeout: Decimal = Decimal(0.0) lineout: Decimal = Decimal(0.0) popout: Decimal = Decimal(0.0) rem_flyballs: Decimal = Decimal(0.0) flyout_a: Decimal = Decimal(0.0) flyout_bq: Decimal = Decimal(0.0) flyout_lf_b: Decimal = Decimal(0.0) flyout_rf_b: Decimal = Decimal(0.0) rem_groundballs: Decimal = Decimal(0.0) groundout_a: Decimal = Decimal(0.0) groundout_b: Decimal = Decimal(0.0) groundout_c: Decimal = Decimal(0.0) avg: Decimal = 0.0 obp: Decimal = 0.0 slg: Decimal = 0.0 def total_chances(self): return sum([ self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_pull, self.single_two, self.single_one, self.single_center, self.bp_single, self.hbp, self.walk, self.strikeout, self.lineout, self.popout, self.flyout_a, self.flyout_bq, self.flyout_lf_b, self.flyout_rf_b, self.groundout_a, self.groundout_b, self.groundout_c ]) def rem_hits(self): return (self.all_hits - sum([ self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_pull, self.single_two, self.single_one, self.single_center, self.bp_single ])) def rem_outs(self): return (self.all_outs - sum([ self.strikeout, self.lineout, self.popout, self.flyout_a, self.flyout_bq, self.flyout_lf_b, self.flyout_rf_b, self.groundout_a, self.groundout_b, self.groundout_c ])) def rem_other_ob(self): return self.other_ob - self.hbp - self.walk def calculate_singles(self, szn_singles, szn_hits, ifh_rate: Decimal): tot = sanitize_chance_output(self.all_hits * Decimal((szn_singles * .8) / szn_hits)) logging.debug(f'tot: {tot}') self.rem_singles = tot self.bp_single = bp_singles(self.rem_singles) self.rem_singles -= self.bp_single self.single_two = wh_singles(self.rem_singles, self.hard_rate) self.rem_singles -= self.single_two self.single_one = one_singles(self.rem_singles, ifh_rate) self.rem_singles -= self.single_one self.single_center = sanitize_chance_output(self.rem_singles) self.rem_singles -= self.single_center self.rem_xbh = self.all_hits - self.bp_single - self.single_two - self.single_one - self.single_center def calculate_xbh(self, szn_triples, szn_doubles, szn_hr, hr_per_fb: Decimal): self.triple = triples(self.rem_xbh, szn_triples, szn_doubles + szn_hr) self.rem_xbh -= self.triple tot_doubles = sanitize_chance_output(self.rem_xbh * Decimal(szn_doubles / max(szn_hr + szn_doubles, 1))) self.double_two = two_doubles(tot_doubles, self.soft_rate) self.double_pull = sanitize_chance_output(tot_doubles - self.double_two) self.rem_xbh -= Decimal(self.double_two + self.double_pull) if self.rem_xbh > Decimal(0): self.bp_homerun = bp_homeruns(self.rem_xbh, hr_per_fb) self.homerun = sanitize_chance_output(self.rem_xbh - self.bp_homerun, min_chances=0.5) self.rem_xbh -= Decimal(self.bp_homerun + self.homerun) if szn_triples > 0 and self.rem_xbh > 0: self.triple = sanitize_chance_output(self.rem_xbh, min_chances=0.5) if self.rem_xbh > 0: logging.error(f'Adding {self.rem_xbh} results to all outs') print(self) self.all_outs += self.rem_xbh # def total_chances(chance_data): # sum_chances = 0 # for key in chance_data: # if key not in ['id', 'player_id', 'cardset_id', 'vs_hand', 'is_prep']: # sum_chances += chance_data[key] # # return mround(sum_chances) def sanitize_chance_output(total_chances, min_chances=1.0, rounding=0.05): # r_val = mround(total_chances) if total_chances >= min_chances else 0 r_val = Decimal(total_chances) if total_chances >= min_chances else Decimal(0) logging.debug(f'r_val: {r_val}') return Decimal(float(round(total_chances / Decimal(rounding)) * Decimal(rounding))).quantize(Decimal("0.05")) # return r_val.quantize(Decimal(rounding)) def total_singles(all_hits, szn_singles, szn_hits): return sanitize_chance_output(all_hits * ((szn_singles * .8) / szn_hits)) def bp_singles(all_singles): if all_singles < 6: return Decimal(0) else: return Decimal(5) def wh_singles(rem_singles, hard_rate): if rem_singles == 0 or hard_rate < .2: return 0 elif hard_rate > .4: return sanitize_chance_output(rem_singles * Decimal(.666), min_chances=2) else: return sanitize_chance_output(rem_singles * Decimal(.333), min_chances=2) def one_singles(rem_singles, ifh_rate, force_rem=False): if force_rem: return mround(rem_singles) elif rem_singles == 0 or ifh_rate < .05: return Decimal(0) else: return sanitize_chance_output(rem_singles * ifh_rate * Decimal(3), min_chances=2) def all_homeruns(rem_hits, all_hits, hrs, hits, singles): if rem_hits == 0 or all_hits == 0 or hrs == 0 or hits - singles == 0: return 0 else: return mround(min(rem_hits, all_hits * ((hrs * 1.15) / hits))) def nd_homeruns(all_hr, hr_rate): if all_hr == 0 or hr_rate == 0: return Decimal(0) elif hr_rate > .2: return sanitize_chance_output(all_hr * .6) else: return sanitize_chance_output(all_hr * .25) def bp_homeruns(all_hr, hr_rate): if all_hr == 0 or hr_rate == 0: return Decimal(0) elif hr_rate > .2: return sanitize_chance_output(all_hr * Decimal(.4), rounding=1.0) else: return sanitize_chance_output(all_hr * Decimal(.75), rounding=1.0) def triples(all_xbh, tr_count, do_count): if all_xbh == Decimal(0) or tr_count == Decimal(0): return Decimal(0) else: return sanitize_chance_output(all_xbh * Decimal(tr_count / (tr_count + do_count)), min_chances=1) def two_doubles(all_doubles, soft_rate): if all_doubles == 0 or soft_rate == 0: return Decimal(0) elif soft_rate > .2: return sanitize_chance_output(all_doubles / 2) else: return sanitize_chance_output(all_doubles / 4) def hit_by_pitch(other_ob, hbps, walks): if hbps == 0 or other_ob * (hbps / (hbps + walks)) < 1: return 0 else: return mround(other_ob * (hbps / (hbps + walks)), base=1.0) def strikeouts(all_outs, k_rate): if all_outs == 0 or k_rate == 0: return 0 else: return mround(all_outs * k_rate) def flyout_a(all_flyouts, hard_rate): if all_flyouts == 0 or hard_rate < .4: return 0 else: return 1 def flyout_bq(rem_flyouts, soft_rate): if rem_flyouts == 0 or soft_rate < .1: return 0 else: return mround(rem_flyouts * soft_rate * 3) def flyout_b(rem_flyouts, pull_rate, cent_rate): if rem_flyouts == 0 or pull_rate == 0: return 0 else: return mround(rem_flyouts * (pull_rate + cent_rate / 2)) def popouts(rem_outs, iffb_rate): if rem_outs == 0 or iffb_rate * rem_outs < 1: return 0 else: return mround(rem_outs * iffb_rate) def groundball_a(all_groundouts, gidps, abs): if all_groundouts == 0 or gidps == 0: return 0 else: return mround((min(gidps ** 2.5, abs) / abs) * all_groundouts) def groundball_c(rem_groundouts, med_rate): if rem_groundouts == 0 or med_rate < .4: return 0 elif med_rate > .6: return mround(rem_groundouts) else: return mround(rem_groundouts * med_rate) 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)] 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 xb_pct = float(extra_base_pct.strip("%")) / 100 return round(8 + (10 * xb_pct)) 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) / (ab_vl + ab_vr - so_vl - so_vr - hr_vl - hr_vl) 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[BattingCardRatingsModel]: offense_mod = 1.2 vl = BattingCardRatingsModel( battingcard_id=df_data.key_fangraphs, vs_hand='L', all_hits=mround(108 * offense_mod * df_data['AVG_vL']), other_ob=mround(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'] ) vr = BattingCardRatingsModel( battingcard_id=df_data.key_fangraphs, vs_hand='R', all_hits=mround(108 * offense_mod * df_data['AVG_vR']), other_ob=mround(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'] ) vl.all_outs = Decimal(108 - vl.all_hits - vl.other_ob).quantize(Decimal("0.05")) vr.all_outs = Decimal(108 - vr.all_hits - vr.other_ob).quantize(Decimal("0.05")) vl.calculate_singles(df_data['1B_vL'], df_data['H_vL'], Decimal(df_data['IFH%_vL'])) vr.calculate_singles(df_data['1B_vR'], df_data['H_vR'], Decimal(df_data['IFH%_vR'])) logging.debug( f'vL - All Hits: {vl.all_hits} / Other OB: {vl.other_ob} / All Outs: {vl.all_outs} ' f'/ Total: {vl.all_hits + vl.other_ob + vl.all_outs}' ) logging.debug( f'vR - All Hits: {vr.all_hits} / Other OB: {vr.other_ob} / All Outs: {vr.all_outs} ' f'/ Total: {vr.all_hits + vr.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']) logging.info(f'all_hits: {vl.all_hits} / sum of hits: {Decimal(vl.bp_single + vl.single_one + vl.single_two + vl.single_center + vl.double_two + vl.double_pull + vl.double_three + vl.triple + vl.homerun + vl.bp_homerun)}') logging.info(f'all_hits: {vr.all_hits} / sum of hits: {Decimal(vr.bp_single + vr.single_one + vr.single_two + vr.single_center + vr.double_two + vr.double_pull + vr.double_three + vr.triple + vr.homerun + vr.bp_homerun)}') return [vl, vr]