From 39c652e55c3166efdfb493b42df144cf145ba1b5 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 25 Feb 2026 16:42:51 -0600 Subject: [PATCH] Extract BattingCardRatingsModel and PitchingCardRatingsModel into models.py files 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> --- batters/calcs_batter.py | 344 +------------------------------------ batters/card_builder.py | 2 +- batters/models.py | 308 +++++++++++++++++++++++++++++++++ pitchers/calcs_pitcher.py | 300 +------------------------------- pitchers/card_builder.py | 2 +- pitchers/models.py | 299 ++++++++++++++++++++++++++++++++ tests/test_batter_calcs.py | 2 +- 7 files changed, 616 insertions(+), 641 deletions(-) create mode 100644 batters/models.py create mode 100644 pitchers/models.py diff --git a/batters/calcs_batter.py b/batters/calcs_batter.py index 99029c7..4f287da 100644 --- a/batters/calcs_batter.py +++ b/batters/calcs_batter.py @@ -1,349 +1,12 @@ import random -import pydantic - from creation_helpers import mround, sanitize_chance_output -from typing import List, Literal +from typing import List 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 * .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, .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 * .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 < .2: - return 0 - elif hard_rate > .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 < .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 > .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 mround(0) - elif hr_rate > .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 > .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 < .4: - return mround(0) - else: - return mround(1.0) - - -def flyout_bq(rem_flyouts, soft_rate): - if rem_flyouts == 0 or soft_rate < .1: - return mround(0) - else: - return sanitize_chance_output(rem_flyouts * min(soft_rate * 3, mround(.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 < .4: - return mround(0) - elif med_rate > .6: - return sanitize_chance_output(rem_groundouts) - else: - return sanitize_chance_output(rem_groundouts * med_rate) - +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: @@ -626,7 +289,6 @@ def get_batter_ratings(df_data) -> List[dict]: vr_dict = vr.custom_to_dict() try: - from batters.card_builder import build_batter_full_cards vl_card, vr_card = build_batter_full_cards( vl, vr, int(df_data['offense_col']), int(df_data['player_id']), df_data['bat_hand'] ) diff --git a/batters/card_builder.py b/batters/card_builder.py index 82109f8..58a375b 100644 --- a/batters/card_builder.py +++ b/batters/card_builder.py @@ -10,7 +10,7 @@ import logging from decimal import Decimal from card_layout import FullBattingCard, PLAY_RESULTS, PlayResult, EXACT_CHANCES, get_chances -from batters.calcs_batter import BattingCardRatingsModel +from batters.models import BattingCardRatingsModel logger = logging.getLogger(__name__) diff --git a/batters/models.py b/batters/models.py new file mode 100644 index 0000000..d044704 --- /dev/null +++ b/batters/models.py @@ -0,0 +1,308 @@ +import random + +import pydantic + +from creation_helpers import mround, sanitize_chance_output +from typing import Literal +from decimal import Decimal +from exceptions import logger + + +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 < .2: + return 0 + elif hard_rate > .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 < .05: + return mround(0) + else: + return sanitize_chance_output(rem_singles * min(ifh_rate * mround(3), 0.75), min_chances=2) + + +def bp_homeruns(all_hr, hr_rate): + if all_hr == 0 or hr_rate == 0: + return mround(0) + elif hr_rate > .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 > .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 < .4: + return mround(0) + else: + return mround(1.0) + + +def flyout_bq(rem_flyouts, soft_rate): + if rem_flyouts == 0 or soft_rate < .1: + return mround(0) + else: + return sanitize_chance_output(rem_flyouts * min(soft_rate * 3, mround(.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 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 < .4: + return mround(0) + elif med_rate > .6: + return sanitize_chance_output(rem_groundouts) + else: + return sanitize_chance_output(rem_groundouts * med_rate) + + +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 * .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') + 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') + 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, .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 + } diff --git a/pitchers/calcs_pitcher.py b/pitchers/calcs_pitcher.py index 3256948..a30b32d 100644 --- a/pitchers/calcs_pitcher.py +++ b/pitchers/calcs_pitcher.py @@ -1,304 +1,11 @@ import math -import pydantic - from creation_helpers import mround, sanitize_chance_output -from typing import List, Literal +from typing import List from exceptions import logger - -class PitchingCardRatingsModel(pydantic.BaseModel): - pitchingcard_id: int - pit_hand: Literal['R', 'L'] - 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_cf: 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 - rem_flyballs: float = 0.0 - flyout_lf_b: float = 0.0 - flyout_cf_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 - xcheck_p: float = float(1.0) - xcheck_c: float = float(3.0) - xcheck_1b: float = float(2.0) - xcheck_2b: float = float(6.0) - xcheck_3b: float = float(3.0) - xcheck_ss: float = float(7.0) - xcheck_lf: float = float(2.0) - xcheck_cf: float = float(3.0) - xcheck_rf: float = float(2.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_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 mround(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 mround(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 mround(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 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 { - 'pitchingcard_id': self.pitchingcard_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_cf': self.double_cf, - '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': self.strikeout, - 'flyout_lf_b': self.flyout_lf_b, - 'flyout_cf_b': self.flyout_cf_b, - 'flyout_rf_b': self.flyout_rf_b, - 'groundout_a': self.groundout_a, - 'groundout_b': self.groundout_b, - 'xcheck_p': self.xcheck_p, - 'xcheck_c': self.xcheck_c, - 'xcheck_1b': self.xcheck_1b, - 'xcheck_2b': self.xcheck_2b, - 'xcheck_3b': self.xcheck_3b, - 'xcheck_ss': self.xcheck_ss, - 'xcheck_lf': self.xcheck_lf, - 'xcheck_cf': self.xcheck_cf, - 'xcheck_rf': self.xcheck_rf, - 'avg': self.avg, - 'obp': self.obp, - 'slg': self.slg - } - - def calculate_singles(self, szn_hits, szn_singles): - if szn_hits == 0: - return - - tot = sanitize_chance_output(self.all_hits * (szn_singles / szn_hits)) - logger.debug(f'total singles: {tot}') - self.rem_singles = tot - - self.bp_single = 5.0 if self.rem_singles >= 5 else 0.0 - self.rem_singles -= self.bp_single - - self.single_two = sanitize_chance_output(self.rem_singles / 2) if self.hard_rate >= 0.2 else 0.0 - self.rem_singles -= self.single_two - - self.single_one = sanitize_chance_output(self.rem_singles) if self.soft_rate >= .2 else 0.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 - logger.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 = mround(szn_homeruns / szn_xbh) - tr_rate = mround(szn_triples / szn_xbh) - do_rate = mround(szn_doubles / szn_xbh) - logger.info(f'hr%: {hr_rate:.2f} / tr%: {tr_rate:.2f} / do%: {do_rate:.2f}') - - raw_do_chances = sanitize_chance_output(self.rem_xbh * do_rate) - logger.info(f'raw do chances: {raw_do_chances}') - self.double_two = raw_do_chances if self.soft_rate > .2 else 0.0 - self.double_cf = mround(raw_do_chances - self.double_two) - self.rem_xbh -= mround(self.double_two + self.double_cf + self.double_three) - logger.info(f'Double**: {self.double_two} / Double(cf): {self.double_cf} / rem xbh: {self.rem_xbh}') - - self.triple = sanitize_chance_output(self.rem_xbh * tr_rate) - self.rem_xbh = mround(self.rem_xbh - self.triple) - logger.info(f'Triple: {self.triple} / rem xbh: {self.rem_xbh}') - - raw_hr_chances = self.rem_xbh - logger.info(f'raw hr chances: {raw_hr_chances}') - - if hr_per_fb_rate < .08: - self.bp_homerun = sanitize_chance_output(raw_hr_chances, min_chances=1.0, 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 * 0.4, min_chances=1.0, rounding=1.0) - self.homerun = self.rem_xbh - self.bp_homerun - else: - self.bp_homerun = sanitize_chance_output(raw_hr_chances * .75, min_chances=1.0, rounding=1.0) - self.homerun = mround(self.rem_xbh - self.bp_homerun) - logger.info(f'BP HR: {self.bp_homerun} / ND HR: {self.homerun}') - - self.rem_xbh -= (self.bp_homerun + self.homerun) - logger.info(f'excess xbh: {self.rem_xbh}') - - if self.rem_xbh > 0: - if self.triple > 1: - logger.info(f'Passing {self.rem_xbh} xbh to triple') - self.triple += self.rem_xbh - self.rem_xbh = 0.0 - elif self.double_cf > 1: - logger.info(f'Passing {self.rem_xbh} xbh to double(cf)') - self.double_cf += self.rem_xbh - self.rem_xbh = 0.0 - elif self.double_two > 1: - logger.info(f'Passing {self.rem_xbh} xbh to double**') - self.double_two += self.rem_xbh - self.rem_xbh = 0.0 - elif self.single_two > 1: - logger.info(f'Passing {self.rem_xbh} xbh to single**') - self.single_two += self.rem_xbh - self.rem_xbh = 0.0 - elif self.single_center > 1: - logger.info(f'Passing {self.rem_xbh} xbh to single(cf)') - self.single_center += self.rem_xbh - self.rem_xbh = 0.0 - elif self.single_one > 1: - logger.info(f'Passing {self.rem_xbh} xbh to single*') - self.single_one += self.rem_xbh - self.rem_xbh = 0.0 - else: - logger.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 - - this_hbp = sanitize_chance_output(self.all_other_ob * szn_hbp / (szn_walks + szn_hbp), rounding=1.0) - logger.info(f'hbp value candidate: {this_hbp} / all_other_ob: {self.all_other_ob}') - self.hbp = max(min(this_hbp, self.all_other_ob), 0) - self.walk = mround(self.all_other_ob - self.hbp) - logger.info(f'self.hbp: {self.hbp} / self.walk: {self.walk}') - - def calculate_strikouts(self, szn_strikeouts, szn_ab, szn_hits): - denom = max(szn_ab - szn_hits, 1) - raw_so = sanitize_chance_output(self.all_outs * (szn_strikeouts * 1.2) / denom) - sum_bb_so = self.walk + raw_so - excess = sum_bb_so - mround(math.floor(sum_bb_so)) - logger.info(f'raw_so: {raw_so} / sum_bb_so: {sum_bb_so} / excess: {excess}') - - self.strikeout = max(raw_so - excess - .05, 0.0) - if self.strikeout < 0: - logger.error(f'Strikeouts are less than zero :confusedpsyduck:') - - def calculate_other_outs(self, fb_pct, gb_pct, oppo_pct): - rem_outs = 108 - self.total_chances() - - all_fo = sanitize_chance_output(rem_outs * fb_pct) - if self.pit_hand == 'L': - self.flyout_lf_b = sanitize_chance_output(all_fo * oppo_pct) - else: - self.flyout_rf_b = sanitize_chance_output(all_fo * 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 = 108 - self.total_chances() - logger.info(f'Remaining outs: {rem_chances}') - - if self.strikeout > 1: - logger.info(f'Passing {rem_chances} outs to strikeouts') - self.strikeout += rem_chances - elif self.flyout_cf_b > 1: - logger.info(f'Passing {rem_chances} outs to fly(cf)') - self.flyout_cf_b += rem_chances - elif self.flyout_rf_b > 1: - logger.info(f'Passing {rem_chances} outs to fly(rf)') - self.flyout_rf_b += rem_chances - elif self.flyout_lf_b > 1: - logger.info(f'Passing {rem_chances} outs to fly(lf)') - self.flyout_lf_b += rem_chances - elif self.groundout_a > 1: - logger.info(f'Passing {rem_chances} outs to gbA') - self.groundout_a += rem_chances - elif self.single_one > 1: - logger.info(f'Passing {rem_chances} outs to single*') - self.single_one += rem_chances - elif self.single_center > 1: - logger.info(f'Passing {rem_chances} outs to single(cf)') - self.single_center += rem_chances - elif self.single_two > 1: - logger.info(f'Passing {rem_chances} outs to single**') - self.single_two += rem_chances - elif self.double_two > 1: - logger.info(f'Passing {rem_chances} outs to double**') - self.double_two += rem_chances - elif self.double_cf > 1: - logger.info(f'Passing {rem_chances} outs to double(cf)') - self.double_cf += rem_chances - elif self.triple > 1: - logger.info(f'Passing {rem_chances} outs to triple') - self.triple += rem_chances - elif self.homerun > 1: - logger.info(f'Passing {rem_chances} outs to homerun') - self.homerun += rem_chances - else: - raise ValueError(f'Could not complete card') - - +from pitchers.models import PitchingCardRatingsModel +from pitchers.card_builder import build_pitcher_full_cards def get_pitcher_ratings(df_data) -> List[dict]: # Calculate OB values with min cap (ensure scalar values for comparison) ob_vl = float(108 * (df_data['BB_vL'] + df_data['HBP_vL']) / df_data['TBF_vL']) @@ -380,7 +87,6 @@ def get_pitcher_ratings(df_data) -> List[dict]: vr_dict = vr.custom_to_dict() try: - from pitchers.card_builder import build_pitcher_full_cards vl_card, vr_card = build_pitcher_full_cards( vl, vr, int(df_data['offense_col']), int(df_data['player_id']), df_data['pitch_hand'] ) diff --git a/pitchers/card_builder.py b/pitchers/card_builder.py index 4470925..8aa5baa 100644 --- a/pitchers/card_builder.py +++ b/pitchers/card_builder.py @@ -4,7 +4,7 @@ import logging from decimal import Decimal from card_layout import FullPitchingCard, PLAY_RESULTS, PlayResult, EXACT_CHANCES, get_chances -from pitchers.calcs_pitcher import PitchingCardRatingsModel +from pitchers.models import PitchingCardRatingsModel logger = logging.getLogger(__name__) diff --git a/pitchers/models.py b/pitchers/models.py new file mode 100644 index 0000000..19c255e --- /dev/null +++ b/pitchers/models.py @@ -0,0 +1,299 @@ +import math + +import pydantic + +from creation_helpers import mround, sanitize_chance_output +from typing import Literal +from exceptions import logger + + +class PitchingCardRatingsModel(pydantic.BaseModel): + pitchingcard_id: int + pit_hand: Literal['R', 'L'] + 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_cf: 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 + rem_flyballs: float = 0.0 + flyout_lf_b: float = 0.0 + flyout_cf_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 + xcheck_p: float = float(1.0) + xcheck_c: float = float(3.0) + xcheck_1b: float = float(2.0) + xcheck_2b: float = float(6.0) + xcheck_3b: float = float(3.0) + xcheck_ss: float = float(7.0) + xcheck_lf: float = float(2.0) + xcheck_cf: float = float(3.0) + xcheck_rf: float = float(2.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_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 mround(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 mround(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 mround(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 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 { + 'pitchingcard_id': self.pitchingcard_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_cf': self.double_cf, + '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': self.strikeout, + 'flyout_lf_b': self.flyout_lf_b, + 'flyout_cf_b': self.flyout_cf_b, + 'flyout_rf_b': self.flyout_rf_b, + 'groundout_a': self.groundout_a, + 'groundout_b': self.groundout_b, + 'xcheck_p': self.xcheck_p, + 'xcheck_c': self.xcheck_c, + 'xcheck_1b': self.xcheck_1b, + 'xcheck_2b': self.xcheck_2b, + 'xcheck_3b': self.xcheck_3b, + 'xcheck_ss': self.xcheck_ss, + 'xcheck_lf': self.xcheck_lf, + 'xcheck_cf': self.xcheck_cf, + 'xcheck_rf': self.xcheck_rf, + 'avg': self.avg, + 'obp': self.obp, + 'slg': self.slg + } + + def calculate_singles(self, szn_hits, szn_singles): + if szn_hits == 0: + return + + tot = sanitize_chance_output(self.all_hits * (szn_singles / szn_hits)) + logger.debug(f'total singles: {tot}') + self.rem_singles = tot + + self.bp_single = 5.0 if self.rem_singles >= 5 else 0.0 + self.rem_singles -= self.bp_single + + self.single_two = sanitize_chance_output(self.rem_singles / 2) if self.hard_rate >= 0.2 else 0.0 + self.rem_singles -= self.single_two + + self.single_one = sanitize_chance_output(self.rem_singles) if self.soft_rate >= .2 else 0.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 + logger.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 = mround(szn_homeruns / szn_xbh) + tr_rate = mround(szn_triples / szn_xbh) + do_rate = mround(szn_doubles / szn_xbh) + logger.info(f'hr%: {hr_rate:.2f} / tr%: {tr_rate:.2f} / do%: {do_rate:.2f}') + + raw_do_chances = sanitize_chance_output(self.rem_xbh * do_rate) + logger.info(f'raw do chances: {raw_do_chances}') + self.double_two = raw_do_chances if self.soft_rate > .2 else 0.0 + self.double_cf = mround(raw_do_chances - self.double_two) + self.rem_xbh -= mround(self.double_two + self.double_cf + self.double_three) + logger.info(f'Double**: {self.double_two} / Double(cf): {self.double_cf} / rem xbh: {self.rem_xbh}') + + self.triple = sanitize_chance_output(self.rem_xbh * tr_rate) + self.rem_xbh = mround(self.rem_xbh - self.triple) + logger.info(f'Triple: {self.triple} / rem xbh: {self.rem_xbh}') + + raw_hr_chances = self.rem_xbh + logger.info(f'raw hr chances: {raw_hr_chances}') + + if hr_per_fb_rate < .08: + self.bp_homerun = sanitize_chance_output(raw_hr_chances, min_chances=1.0, 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 * 0.4, min_chances=1.0, rounding=1.0) + self.homerun = self.rem_xbh - self.bp_homerun + else: + self.bp_homerun = sanitize_chance_output(raw_hr_chances * .75, min_chances=1.0, rounding=1.0) + self.homerun = mround(self.rem_xbh - self.bp_homerun) + logger.info(f'BP HR: {self.bp_homerun} / ND HR: {self.homerun}') + + self.rem_xbh -= (self.bp_homerun + self.homerun) + logger.info(f'excess xbh: {self.rem_xbh}') + + if self.rem_xbh > 0: + if self.triple > 1: + logger.info(f'Passing {self.rem_xbh} xbh to triple') + self.triple += self.rem_xbh + self.rem_xbh = 0.0 + elif self.double_cf > 1: + logger.info(f'Passing {self.rem_xbh} xbh to double(cf)') + self.double_cf += self.rem_xbh + self.rem_xbh = 0.0 + elif self.double_two > 1: + logger.info(f'Passing {self.rem_xbh} xbh to double**') + self.double_two += self.rem_xbh + self.rem_xbh = 0.0 + elif self.single_two > 1: + logger.info(f'Passing {self.rem_xbh} xbh to single**') + self.single_two += self.rem_xbh + self.rem_xbh = 0.0 + elif self.single_center > 1: + logger.info(f'Passing {self.rem_xbh} xbh to single(cf)') + self.single_center += self.rem_xbh + self.rem_xbh = 0.0 + elif self.single_one > 1: + logger.info(f'Passing {self.rem_xbh} xbh to single*') + self.single_one += self.rem_xbh + self.rem_xbh = 0.0 + else: + logger.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 + + this_hbp = sanitize_chance_output(self.all_other_ob * szn_hbp / (szn_walks + szn_hbp), rounding=1.0) + logger.info(f'hbp value candidate: {this_hbp} / all_other_ob: {self.all_other_ob}') + self.hbp = max(min(this_hbp, self.all_other_ob), 0) + self.walk = mround(self.all_other_ob - self.hbp) + logger.info(f'self.hbp: {self.hbp} / self.walk: {self.walk}') + + def calculate_strikouts(self, szn_strikeouts, szn_ab, szn_hits): + denom = max(szn_ab - szn_hits, 1) + raw_so = sanitize_chance_output(self.all_outs * (szn_strikeouts * 1.2) / denom) + sum_bb_so = self.walk + raw_so + excess = sum_bb_so - mround(math.floor(sum_bb_so)) + logger.info(f'raw_so: {raw_so} / sum_bb_so: {sum_bb_so} / excess: {excess}') + + self.strikeout = max(raw_so - excess - .05, 0.0) + if self.strikeout < 0: + logger.error(f'Strikeouts are less than zero :confusedpsyduck:') + + def calculate_other_outs(self, fb_pct, gb_pct, oppo_pct): + rem_outs = 108 - self.total_chances() + + all_fo = sanitize_chance_output(rem_outs * fb_pct) + if self.pit_hand == 'L': + self.flyout_lf_b = sanitize_chance_output(all_fo * oppo_pct) + else: + self.flyout_rf_b = sanitize_chance_output(all_fo * 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 = 108 - self.total_chances() + logger.info(f'Remaining outs: {rem_chances}') + + if self.strikeout > 1: + logger.info(f'Passing {rem_chances} outs to strikeouts') + self.strikeout += rem_chances + elif self.flyout_cf_b > 1: + logger.info(f'Passing {rem_chances} outs to fly(cf)') + self.flyout_cf_b += rem_chances + elif self.flyout_rf_b > 1: + logger.info(f'Passing {rem_chances} outs to fly(rf)') + self.flyout_rf_b += rem_chances + elif self.flyout_lf_b > 1: + logger.info(f'Passing {rem_chances} outs to fly(lf)') + self.flyout_lf_b += rem_chances + elif self.groundout_a > 1: + logger.info(f'Passing {rem_chances} outs to gbA') + self.groundout_a += rem_chances + elif self.single_one > 1: + logger.info(f'Passing {rem_chances} outs to single*') + self.single_one += rem_chances + elif self.single_center > 1: + logger.info(f'Passing {rem_chances} outs to single(cf)') + self.single_center += rem_chances + elif self.single_two > 1: + logger.info(f'Passing {rem_chances} outs to single**') + self.single_two += rem_chances + elif self.double_two > 1: + logger.info(f'Passing {rem_chances} outs to double**') + self.double_two += rem_chances + elif self.double_cf > 1: + logger.info(f'Passing {rem_chances} outs to double(cf)') + self.double_cf += rem_chances + elif self.triple > 1: + logger.info(f'Passing {rem_chances} outs to triple') + self.triple += rem_chances + elif self.homerun > 1: + logger.info(f'Passing {rem_chances} outs to homerun') + self.homerun += rem_chances + else: + raise ValueError(f'Could not complete card') diff --git a/tests/test_batter_calcs.py b/tests/test_batter_calcs.py index ce346c6..b40046c 100644 --- a/tests/test_batter_calcs.py +++ b/tests/test_batter_calcs.py @@ -1,7 +1,7 @@ from decimal import ROUND_HALF_EVEN, Decimal import math -from batters.calcs_batter import bp_singles, wh_singles +from batters.models import bp_singles, wh_singles