paper-dynasty-card-creation/calcs_pitcher.py
2023-10-03 12:06:11 -05:00

568 lines
21 KiB
Python

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