import logging import pydantic from creation_helpers import mround, sanitize_chance_output from decimal import Decimal from typing import List, Literal def get_pitcher_ratings(df_data) -> List[dict]: vl = PitchingCardRatingsModel( pitchingcard_id=df_data.pitchingcard_id, pit_hand=df_data.hand, vs_hand='L', all_hits=sanitize_chance_output((df_data['AVG_vL'] - 0.05) * 108), # Subtracting chances from BP results all_other_ob=sanitize_chance_output(108 * (df_data['BB_vL'] + df_data['HBP_vL']) / df_data['TBF_vL']), hard_rate=df_data['Hard%_vL'], med_rate=df_data['Med%_vL'], soft_rate=df_data['Soft%_vL'] ) vr = PitchingCardRatingsModel( pitchingcard_id=df_data.pitchingcard_id, pit_hand=df_data.hand, vs_hand='R', all_hits=sanitize_chance_output((df_data['AVG_vR'] - 0.05) * 108), # Subtracting chances from BP results all_other_ob=sanitize_chance_output(108 * (df_data['BB_vR'] + df_data['HBP_vR']) / df_data['TBF_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.all_other_ob).quantize(Decimal("0.05")) vr.all_outs = Decimal(108 - vr.all_hits - vr.all_other_ob).quantize(Decimal("0.05")) logging.info( f'vL - All Hits: {vl.all_hits} / Other OB: {vl.all_other_ob} / All Outs: {vl.all_outs} ' f'/ Total: {vl.total_chances()}' ) logging.info( f'vR - All Hits: {vr.all_hits} / Other OB: {vr.all_other_ob} / All Outs: {vr.all_outs} ' f'/ Total: {vr.total_chances()}' ) vl.calculate_singles(df_data['H_vL'], df_data['H_vL'] - df_data['2B_vL'] - df_data['3B_vL'] - df_data['HR_vL']) vr.calculate_singles(df_data['H_vR'], df_data['H_vR'] - df_data['2B_vR'] - df_data['3B_vR'] - df_data['HR_vR']) logging.info(f'vL: All Hits: {vl.all_hits} / BP Singles: {vl.bp_single} / Single 2: {vl.single_two} / ' f'Single 1: {vl.single_one} / Single CF: {vl.single_center}') logging.info(f'vR: All Hits: {vr.all_hits} / BP Singles: {vr.bp_single} / Single 2: {vr.single_two} / ' f'Single 1: {vr.single_one} / Single CF: {vr.single_center}') vl.calculate_xbh(df_data['2B_vL'], df_data['3B_vL'], df_data['HR_vL'], df_data['HR/FB_vL']) vr.calculate_xbh(df_data['2B_vR'], df_data['3B_vR'], df_data['HR_vR'], df_data['HR/FB_vR']) logging.debug(f'vL: All XBH: {vl.all_hits - vl.single_one - vl.single_two - vl.single_center - vl.bp_single} / ' f'Double**: {vl.double_two} / Double(cf): {vl.double_cf} / Triple: {vl.triple} / ' f'BP HR: {vl.bp_homerun} / ND HR: {vl.homerun}') logging.debug(f'vR: All XBH: {vr.all_hits - vr.single_one - vr.single_two - vr.single_center - vr.bp_single} / ' f'Double**: {vr.double_two} / Double(cf): {vr.double_cf} / Triple: {vr.triple} / ' f'BP HR: {vr.bp_homerun} / ND HR: {vr.homerun}') vl.calculate_other_ob(df_data['BB_vL'], df_data['HBP_vL']) vr.calculate_other_ob(df_data['BB_vR'], df_data['HBP_vR']) logging.info(f'vL: All other OB: {vl.all_other_ob} / HBP: {vl.hbp} / BB: {vl.walk} / ' f'Total Chances: {vl.total_chances()}') logging.info(f'vR: All other OB: {vr.all_other_ob} / HBP: {vr.hbp} / BB: {vr.walk} / ' f'Total Chances: {vr.total_chances()}') vl.calculate_strikouts( df_data['SO_vL'], df_data['TBF_vL'] - df_data['BB_vL'] - df_data['IBB_vL'] - df_data['HBP_vL'], df_data['H_vL']) vr.calculate_strikouts( df_data['SO_vR'], df_data['TBF_vR'] - df_data['BB_vR'] - df_data['IBB_vR'] - df_data['HBP_vR'], df_data['H_vR']) logging.info(f'vL: All Outs: {vl.all_outs} / Ks: {vl.strikeout} / Current Outs: {vl.total_outs()}') logging.info(f'vR: All Outs: {vr.all_outs} / Ks: {vr.strikeout} / Current Outs: {vr.total_outs()}') vl.calculate_other_outs(df_data['FB%_vL'], df_data['GB%_vL'], df_data['Oppo%_vL']) vr.calculate_other_outs(df_data['FB%_vR'], df_data['GB%_vR'], df_data['Oppo%_vR']) logging.info(f'vL: Total chances: {vl.total_chances()}') logging.info(f'vR: Total chances: {vr.total_chances()}') return [vl.custom_to_dict(), vr.custom_to_dict()] class PitchingCardRatingsModel(pydantic.BaseModel): pitchingcard_id: int pit_hand: Literal['R', 'L'] vs_hand: Literal['R', 'L'] all_hits: Decimal = Decimal(0.0) all_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 # pull_rate: Decimal # center_rate: Decimal # slap_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_cf: 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) rem_flyballs: Decimal = Decimal(0.0) flyout_lf_b: Decimal = Decimal(0.0) flyout_cf_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) xcheck_p: Decimal = Decimal(1.0) xcheck_c: Decimal = Decimal(3.0) xcheck_1b: Decimal = Decimal(2.0) xcheck_2b: Decimal = Decimal(6.0) xcheck_3b: Decimal = Decimal(3.0) xcheck_ss: Decimal = Decimal(7.0) xcheck_lf: Decimal = Decimal(2.0) xcheck_cf: Decimal = Decimal(3.0) xcheck_rf: Decimal = Decimal(2.0) avg: Decimal = 0.0 obp: Decimal = 0.0 slg: Decimal = 0.0 def total_chances(self): return Decimal(sum([ self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_cf, self.single_two, self.single_one, self.single_center, self.bp_single, self.hbp, self.walk, self.strikeout, self.flyout_lf_b, self.flyout_cf_b, self.flyout_rf_b, self.groundout_a, self.groundout_b, self.xcheck_p, self.xcheck_c, self.xcheck_1b, self.xcheck_2b, self.xcheck_3b, self.xcheck_ss, self.xcheck_lf, self.xcheck_cf, self.xcheck_rf ])) def total_hits(self): return Decimal(sum([ self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_cf, self.single_two, self.single_one, self.single_center, self.bp_single ])) def total_ob(self): return Decimal(sum([ self.homerun, self.bp_homerun, self.triple, self.double_three, self.double_two, self.double_cf, self.single_two, self.single_one, self.single_center, self.bp_single, self.hbp, self.walk ])) def total_outs(self): return Decimal(sum([ self.strikeout, self.flyout_lf_b, self.flyout_cf_b, self.flyout_rf_b, self.groundout_a, self.groundout_b, self.xcheck_p, self.xcheck_c, self.xcheck_1b, self.xcheck_2b, self.xcheck_3b, self.xcheck_ss, self.xcheck_lf, self.xcheck_cf, self.xcheck_rf ])) def custom_to_dict(self): return { 'pitchingcard_id': self.pitchingcard_id, 'vs_hand': self.vs_hand, 'homerun': float(self.homerun), 'bp_homerun': float(self.bp_homerun), 'triple': float(self.triple), 'double_three': float(self.double_three), 'double_two': float(self.double_two), 'double_cf': float(self.double_cf), 'single_two': float(self.single_two), 'single_one': float(self.single_one), 'single_center': float(self.single_center), 'bp_single': float(self.bp_single), 'hbp': float(self.hbp), 'walk': float(self.walk), 'strikeout': float(self.strikeout), 'flyout_lf_b': float(self.flyout_lf_b), 'flyout_cf_b': float(self.flyout_cf_b), 'flyout_rf_b': float(self.flyout_rf_b), 'groundout_a': float(self.groundout_a), 'groundout_b': float(self.groundout_b), 'xcheck_p': float(self.xcheck_p), 'xcheck_c': float(self.xcheck_c), 'xcheck_1b': float(self.xcheck_1b), 'xcheck_2b': float(self.xcheck_2b), 'xcheck_3b': float(self.xcheck_3b), 'xcheck_ss': float(self.xcheck_ss), 'xcheck_lf': float(self.xcheck_lf), 'xcheck_cf': float(self.xcheck_cf), 'xcheck_rf': float(self.xcheck_rf) } def calculate_singles(self, szn_hits, szn_singles): if szn_hits == 0: return tot = sanitize_chance_output(self.all_hits * Decimal(szn_singles / szn_hits)) logging.debug(f'total singles: {tot}') self.rem_singles = tot self.bp_single = Decimal(5) if self.rem_singles >= 5 else Decimal(0) self.rem_singles -= self.bp_single self.single_two = sanitize_chance_output(self.rem_singles / 2) if self.hard_rate >= 0.2 else Decimal(0) self.rem_singles -= self.single_two self.single_one = sanitize_chance_output(self.rem_singles) if self.soft_rate >= .2 else Decimal(0) 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.single_center - self.single_one - self.single_two - self.bp_single logging.info(f'remaining singles: {self.rem_singles} / total xbh: {self.rem_xbh}') def calculate_xbh(self, szn_doubles, szn_triples, szn_homeruns, hr_per_fb_rate): szn_xbh = szn_doubles + szn_triples + szn_homeruns if szn_xbh == 0: return hr_rate = Decimal(szn_homeruns / szn_xbh) tr_rate = Decimal(szn_triples / szn_xbh) do_rate = Decimal(szn_doubles / szn_xbh) logging.info(f'hr%: {hr_rate:.2f} / tr%: {tr_rate:.2f} / do%: {do_rate:.2f}') raw_do_chances = sanitize_chance_output(Decimal(self.rem_xbh * do_rate)) logging.info(f'raw do chances: {raw_do_chances}') self.double_two = raw_do_chances if self.soft_rate > .2 else Decimal(0) self.double_cf = raw_do_chances - self.double_two self.rem_xbh -= (self.double_two + self.double_cf + self.double_three) logging.info(f'Double**: {self.double_two} / Double(cf): {self.double_cf} / rem xbh: {self.rem_xbh}') self.triple = sanitize_chance_output(Decimal(self.rem_xbh * tr_rate)) self.rem_xbh -= self.triple logging.info(f'Triple: {self.triple} / rem xbh: {self.rem_xbh}') raw_hr_chances = self.rem_xbh logging.info(f'raw hr chances: {raw_hr_chances}') if hr_per_fb_rate < .08: self.bp_homerun = sanitize_chance_output(raw_hr_chances, rounding=1.0) elif hr_per_fb_rate > .28: self.homerun = raw_hr_chances elif hr_per_fb_rate > .18: self.bp_homerun = sanitize_chance_output(raw_hr_chances * Decimal(0.4), rounding=1.0) self.homerun = self.rem_xbh - self.bp_homerun else: self.bp_homerun = sanitize_chance_output(raw_hr_chances * Decimal(.75), rounding=1.0) self.homerun = self.rem_xbh - self.bp_homerun logging.info(f'BP HR: {self.bp_homerun} / ND HR: {self.homerun}') self.rem_xbh -= (self.bp_homerun + self.homerun) logging.info(f'excess xbh: {self.rem_xbh}') if self.rem_xbh > 0: if self.triple > 1: logging.info(f'Passing {self.rem_xbh} xbh to triple') self.triple += self.rem_xbh self.rem_xbh = Decimal(0) elif self.double_cf > 1: logging.info(f'Passing {self.rem_xbh} xbh to double(cf)') self.double_cf += self.rem_xbh self.rem_xbh = Decimal(0) elif self.double_two > 1: logging.info(f'Passing {self.rem_xbh} xbh to double**') self.double_two += self.rem_xbh self.rem_xbh = Decimal(0) elif self.single_two > 1: logging.info(f'Passing {self.rem_xbh} xbh to single**') self.single_two += self.rem_xbh self.rem_xbh = Decimal(0) elif self.single_center > 1: logging.info(f'Passing {self.rem_xbh} xbh to single(cf)') self.single_center += self.rem_xbh self.rem_xbh = Decimal(0) elif self.single_one > 1: logging.info(f'Passing {self.rem_xbh} xbh to single*') self.single_one += self.rem_xbh self.rem_xbh = Decimal(0) else: logging.info(f'Passing {self.rem_xbh} xbh to other_ob') self.all_other_ob += self.rem_xbh def calculate_other_ob(self, szn_walks, szn_hbp): if szn_walks + szn_hbp == 0: return self.hbp = sanitize_chance_output(self.all_other_ob * Decimal(szn_hbp / (szn_walks + szn_hbp)), rounding=1.0) self.walk = self.all_other_ob - self.hbp def calculate_strikouts(self, szn_strikeouts, szn_ab, szn_hits): self.strikeout = sanitize_chance_output(self.all_outs * Decimal((szn_strikeouts * 1.2) / (szn_ab - szn_hits))) def calculate_other_outs(self, fb_pct, gb_pct, oppo_pct): rem_outs = Decimal(108) - self.total_chances() all_fo = sanitize_chance_output(rem_outs * Decimal(fb_pct)) if self.pit_hand == 'L': self.flyout_lf_b = sanitize_chance_output(all_fo * Decimal(oppo_pct)) else: self.flyout_rf_b = sanitize_chance_output(all_fo * Decimal(oppo_pct)) self.flyout_cf_b = all_fo - self.flyout_lf_b - self.flyout_rf_b rem_outs -= (self.flyout_lf_b + self.flyout_cf_b + self.flyout_rf_b) all_gb = rem_outs self.groundout_a = sanitize_chance_output(all_gb * self.soft_rate) self.groundout_b = sanitize_chance_output(all_gb - self.groundout_a) rem_chances = Decimal(108) - self.total_chances() logging.info(f'Remaining outs: {rem_chances}') if self.strikeout > 1: logging.info(f'Passing {rem_chances} outs to strikeouts') self.strikeout += rem_chances elif self.flyout_cf_b > 1: logging.info(f'Passing {rem_chances} outs to fly(cf)') self.flyout_cf_b += rem_chances elif self.flyout_rf_b > 1: logging.info(f'Passing {rem_chances} outs to fly(rf)') self.flyout_rf_b += rem_chances elif self.flyout_lf_b > 1: logging.info(f'Passing {rem_chances} outs to fly(lf)') self.flyout_lf_b += rem_chances elif self.groundout_a > 1: logging.info(f'Passing {rem_chances} outs to gbA') self.groundout_a += rem_chances elif self.single_one > 1: logging.info(f'Passing {rem_chances} outs to single*') self.single_one += rem_chances elif self.single_center > 1: logging.info(f'Passing {rem_chances} outs to single(cf)') self.single_center += rem_chances elif self.single_two > 1: logging.info(f'Passing {rem_chances} outs to single**') self.single_two += rem_chances elif self.double_two > 1: logging.info(f'Passing {rem_chances} outs to double**') self.double_two += rem_chances elif self.double_cf > 1: logging.info(f'Passing {rem_chances} outs to double(cf)') self.double_cf += rem_chances elif self.triple > 1: logging.info(f'Passing {rem_chances} outs to triple') self.triple += rem_chances elif self.homerun > 1: logging.info(f'Passing {rem_chances} outs to homerun') self.homerun += rem_chances else: raise ValueError(f'Could not complete card') 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 sum_chances def soft_rate(pct): if pct > .2: return 'high' elif pct < .1: return 'low' else: return 'avg' def med_rate(pct): if pct > .65: return 'high' elif pct < .4: return 'low' else: return 'avg' def hard_rate(pct): if pct > .4: return 'high' elif pct < .2: return 'low' else: return 'avg' def hr_per_fb_rate(pct): if pct > .18: return 'high' elif pct < .08: return 'low' else: return 'avg' def all_singles(row, hits_vl, hits_vr): if int(row[7]) == 0: tot_singles_vl = 0 else: tot_singles_vl = hits_vl * ((int(row[7]) - int(row[8]) - int(row[9]) - int(row[12])) / int(row[7])) if int(row[40]) == 0: tot_singles_vr = 0 else: tot_singles_vr = hits_vr * ((int(row[40]) - int(row[41]) - int(row[42]) - int(row[45])) / int(row[40])) return mround(tot_singles_vl), mround(tot_singles_vr) def bp_singles(singles_vl, singles_vr): bpsi_vl = 5 if singles_vl >= 5 else 0 bpsi_vr = 5 if singles_vr >= 5 else 0 return mround(bpsi_vl), mround(bpsi_vr) def wh_singles(rem_si_vl, rem_si_vr, hard_rate_vl, hard_rate_vr): if hard_rate_vl == 'low': whs_vl = 0 else: whs_vl = rem_si_vl / 2 if hard_rate_vr == 'low': whs_vr = 0 else: whs_vr = rem_si_vr / 2 return mround(whs_vl), mround(whs_vr) def one_singles(rem_si_vl, rem_si_vr, soft_rate_vl, soft_rate_vr): if soft_rate_vl == 'high': oss_vl = rem_si_vl else: oss_vl = 0 if soft_rate_vr == 'high': oss_vr = rem_si_vr else: oss_vr = 0 return mround(oss_vl), mround(oss_vr) def bp_homerun(hr_vl, hr_vr, hr_rate_vl, hr_rate_vr): if hr_rate_vl == 'low': bphr_vl = hr_vl elif hr_rate_vl == 'avg': bphr_vl = hr_vl * .75 else: bphr_vl = hr_vl * .4 if hr_rate_vr == 'low': bphr_vr = hr_vr elif hr_rate_vr == 'avg': bphr_vr = hr_vr * .75 else: bphr_vr = hr_vr * .4 return mround(bphr_vl), mround(bphr_vr) def triples(all_xbh_vl, all_xbh_vr, triple_rate_vl, triple_rate_vr): tr_vl = all_xbh_vl * triple_rate_vl if all_xbh_vl > 0 else 0 tr_vr = all_xbh_vr * triple_rate_vr if all_xbh_vr > 0 else 0 return mround(tr_vl), mround(tr_vr) def two_doubles(all_doubles_vl, all_doubles_vr, soft_rate_vl, soft_rate_vr): two_doubles_vl = all_doubles_vl if soft_rate_vl == 'high' else 0 two_doubles_vr = all_doubles_vr if soft_rate_vr == 'high' else 0 return mround(two_doubles_vl), mround(two_doubles_vr) def hbp_rate(hbp, bb): if hbp == 0: return 0 elif bb == 0: return 1 else: return hbp / bb def hbps(all_ob, this_hbp_rate): if all_ob == 0 or this_hbp_rate == 0: return 0 else: return mround(all_ob * this_hbp_rate) def xchecks(pos, all_chances=True): if pos.lower() == 'p': return 1 if all_chances else 0 elif pos.lower() == 'c': return 3 if all_chances else 2 elif pos.lower() == '1b': return 2 if all_chances else 1 elif pos.lower() == '2b': return 6 if all_chances else 5 elif pos.lower() == '3b': return 3 if all_chances else 2 elif pos.lower() == 'ss': return 7 if all_chances else 6 elif pos.lower() == 'lf': return 2 if all_chances else 1 elif pos.lower() == 'cf': return 3 if all_chances else 2 else: return 2 if all_chances else 1 def oppo_fly(all_fly, oppo_rate): if all_fly == 0 or oppo_rate == 0: return 0 else: return mround(all_fly * oppo_rate) def groundball_a(all_gb, dp_rate): if all_gb == 0 or dp_rate == 0: return 0 elif dp_rate > .6: return all_gb else: return mround(all_gb * (dp_rate * 1.5)) def balks(total_balks: int, innings: float, season_pct): if innings == 0: return 0 return min(round((total_balks * 290 * season_pct) / innings), 20) def wild_pitches(total_wps: int, innings: float, season_pct): if innings == 0: return 0 return min(round((total_wps * 200 * season_pct) / innings), 20) def closer_rating(gf: int, saves: int, games: int): if gf == 0 or games == 0 or saves == 0: return None if gf / games >= .875: return 6 elif gf / games >= .8: return 5 elif gf / games >= .7: return 4 elif gf / games >= .55: return 3 elif gf / games >= .4: return 2 elif gf / games >= .25: return 1 elif gf / games >= .1: return 0 else: return None