import random import pydantic from creation_helpers import mround, sanitize_chance_output from typing import List, Literal from decimal import Decimal from exceptions import logger class BattingCardRatingsModel(pydantic.BaseModel): battingcard_id: int bat_hand: Literal["R", "L", "S"] vs_hand: Literal["R", "L"] all_hits: float = 0.0 all_other_ob: float = 0.0 all_outs: float = 0.0 rem_singles: float = 0.0 rem_xbh: float = 0.0 rem_hr: float = 0.0 rem_doubles: float = 0.0 hard_rate: float med_rate: float soft_rate: float pull_rate: float center_rate: float slap_rate: float homerun: float = 0.0 bp_homerun: float = 0.0 triple: float = 0.0 double_three: float = 0.0 double_two: float = 0.0 double_pull: float = 0.0 single_two: float = 0.0 single_one: float = 0.0 single_center: float = 0.0 bp_single: float = 0.0 hbp: float = 0.0 walk: float = 0.0 strikeout: float = 0.0 lineout: float = 0.0 popout: float = 0.0 rem_flyballs: float = 0.0 flyout_a: float = 0.0 flyout_bq: float = 0.0 flyout_lf_b: float = 0.0 flyout_rf_b: float = 0.0 rem_groundballs: float = 0.0 groundout_a: float = 0.0 groundout_b: float = 0.0 groundout_c: float = 0.0 avg: float = 0.0 obp: float = 0.0 slg: float = 0.0 def total_chances(self): return mround( 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 total_hits(self): return mround( 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_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 mround( 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.all_other_ob - self.hbp - self.walk def calculate_singles(self, szn_singles, szn_hits, ifh_rate: Decimal): tot = sanitize_chance_output( self.all_hits * mround((szn_singles * 0.8) / max(szn_hits, 1)) ) logger.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 * mround(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 -= mround(self.double_two + self.double_pull) if (self.rem_xbh > mround(0)) and szn_hr > 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 -= mround(self.bp_homerun + self.homerun) if szn_triples > 0 and self.rem_xbh > 0: logger.error(f"Adding {self.rem_xbh} results to triples") self.triple += sanitize_chance_output(self.rem_xbh, min_chances=0.5) elif self.rem_xbh > 0: logger.error(f"Adding {self.rem_xbh} results to all other ob") # print(self) self.all_other_ob += self.rem_xbh def calculate_other_ob(self, szn_bb, szn_hbp): self.hbp = hit_by_pitch(self.all_other_ob, szn_hbp, szn_bb) self.walk = sanitize_chance_output(self.all_other_ob - self.hbp) if self.walk + self.hbp < self.all_other_ob: rem = self.all_other_ob - self.walk - self.hbp logger.error(f"Adding {rem} chances to all_outs") # print(self) self.all_outs += mround(rem) def calculate_strikeouts(self, szn_so, szn_ab, szn_hits): self.strikeout = strikeouts(self.all_outs, (szn_so / max(szn_ab - szn_hits, 1))) def calculate_other_outs(self, fb_rate, ld_rate, gb_rate, szn_gidp, szn_ab): self.rem_flyballs = sanitize_chance_output(self.rem_outs() * mround(fb_rate)) self.flyout_a = flyout_a(self.rem_flyballs, self.hard_rate) self.rem_flyballs -= self.flyout_a self.flyout_bq = flyout_bq(self.rem_flyballs, self.soft_rate) self.rem_flyballs -= self.flyout_bq self.flyout_lf_b = flyout_b( self.rem_flyballs, pull_rate=self.pull_rate if self.bat_hand == "R" else self.slap_rate, cent_rate=self.center_rate, ) self.rem_flyballs -= self.flyout_lf_b self.flyout_rf_b = sanitize_chance_output(self.rem_flyballs) self.rem_flyballs -= self.flyout_rf_b if self.rem_flyballs > 0: logger.debug(f"Adding {self.rem_flyballs} chances to lineouts") tot_oneouts = sanitize_chance_output( self.rem_outs() * mround(ld_rate / max(ld_rate + gb_rate, 0.01)) ) self.lineout = sanitize_chance_output(mround(random.random()) * tot_oneouts) self.popout = sanitize_chance_output(tot_oneouts - self.lineout) self.groundout_a = groundball_a(self.rem_outs(), szn_gidp, szn_ab) self.groundout_c = groundball_c(self.rem_outs(), self.med_rate) self.groundout_b = self.rem_outs() def calculate_rate_stats(self): self.avg = mround(self.total_hits() / 108, prec=5, base=0.00001) self.obp = mround( (self.total_hits() + self.hbp + self.walk) / 108, prec=5, base=0.00001 ) self.slg = mround( ( self.homerun * 4 + self.triple * 3 + self.single_center + self.single_two + self.single_two + ( self.double_two + self.double_three + self.double_two + self.bp_homerun ) * 2 + self.bp_single / 2 ) / 108, prec=5, base=0.00001, ) def custom_to_dict(self): self.calculate_rate_stats() return { "battingcard_id": self.battingcard_id, "vs_hand": self.vs_hand, "homerun": self.homerun, "bp_homerun": self.bp_homerun, "triple": self.triple, "double_three": self.double_three, "double_two": self.double_two, "double_pull": self.double_pull, "single_two": self.single_two, "single_one": self.single_one, "single_center": self.single_center, "bp_single": self.bp_single, "hbp": self.hbp, "walk": self.walk, "strikeout": mround(self.strikeout), "lineout": self.lineout, "popout": self.popout, "flyout_a": self.flyout_a, "flyout_bq": self.flyout_bq, "flyout_lf_b": self.flyout_lf_b, "flyout_rf_b": self.flyout_rf_b, "groundout_a": self.groundout_a, "groundout_b": self.groundout_b, "groundout_c": self.groundout_c, "pull_rate": self.pull_rate, "center_rate": self.center_rate, "slap_rate": self.slap_rate, "avg": self.avg, "obp": self.obp, "slg": self.slg, } # 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 total_singles(all_hits, szn_singles, szn_hits): return sanitize_chance_output(all_hits * ((szn_singles * 0.8) / max(szn_hits, 1))) def bp_singles(all_singles): if all_singles < 6: return mround(0) else: return mround(5) def wh_singles(rem_singles, hard_rate): if rem_singles == 0 or hard_rate < 0.2: return 0 elif hard_rate > 0.4: return sanitize_chance_output(rem_singles * 2 / 3, min_chances=2) else: return sanitize_chance_output(rem_singles / 3, 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 < 0.05: return mround(0) else: return sanitize_chance_output( rem_singles * min(ifh_rate * mround(3), 0.75), 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) / max(hits, 1)))) def nd_homeruns(all_hr, hr_rate): if all_hr == 0 or hr_rate == 0: return mround(0) elif hr_rate > 0.2: return sanitize_chance_output(all_hr * 0.6) else: return sanitize_chance_output(all_hr * 0.25) def bp_homeruns(all_hr, hr_rate): if all_hr == 0 or hr_rate == 0: return mround(0) elif hr_rate > 0.2: return mround(all_hr * 0.4, base=1.0) else: return mround(all_hr * 0.8, base=1.0) def triples(all_xbh, tr_count, do_count): if all_xbh == mround(0) or tr_count == mround(0): return mround(0) else: return sanitize_chance_output( all_xbh * mround(tr_count / max(tr_count + do_count, 1)), min_chances=1 ) def two_doubles(all_doubles, soft_rate): if all_doubles == 0 or soft_rate == 0: return mround(0) elif soft_rate > 0.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 * mround(hbps / max(hbps + walks, 1)) < 1: return 0 else: return sanitize_chance_output( other_ob * mround(hbps / max(hbps + walks, 1)), rounding=1.0 ) def strikeouts(all_outs, k_rate): if all_outs == 0 or k_rate == 0: return mround(0) else: return sanitize_chance_output(all_outs * k_rate) def flyout_a(all_flyouts, hard_rate): if all_flyouts == 0 or hard_rate < 0.4: return mround(0) else: return mround(1.0) def flyout_bq(rem_flyouts, soft_rate): if rem_flyouts == 0 or soft_rate < 0.1: return mround(0) else: return sanitize_chance_output(rem_flyouts * min(soft_rate * 3, mround(0.75))) def flyout_b(rem_flyouts, pull_rate, cent_rate): if rem_flyouts == 0 or pull_rate == 0: return mround(0) else: return sanitize_chance_output(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 mround(0) else: return sanitize_chance_output( mround(min(gidps**2.5, abs) / max(abs, 1)) * all_groundouts ) def groundball_c(rem_groundouts, med_rate): if rem_groundouts == 0 or med_rate < 0.4: return mround(0) elif med_rate > 0.6: return sanitize_chance_output(rem_groundouts) else: return sanitize_chance_output(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 >= 0.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}") return 8 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 >= 0.35: return "A" elif babip >= 0.3: return "B" elif babip >= 0.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}") return [vl.custom_to_dict(), vr.custom_to_dict()]