Merge pull request 'Fix SLG formula in extracted card rating models' (#22) from feature/fullcard-migration into main
Reviewed-on: #22
This commit is contained in:
commit
a4e56f6062
@ -1,358 +1,24 @@
|
||||
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)
|
||||
from batters.models import BattingCardRatingsModel
|
||||
from batters.card_builder import build_batter_full_cards
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def stealing(chances: int, sb2s: int, cs2s: int, sb3s: int, cs3s: int, season_pct: float):
|
||||
def stealing(
|
||||
chances: int, sb2s: int, cs2s: int, sb3s: int, cs3s: int, season_pct: float
|
||||
):
|
||||
if chances == 0 or sb2s + cs2s == 0:
|
||||
return 0, 0, False, 0
|
||||
|
||||
total_attempts = sb2s + cs2s + sb3s + cs3s
|
||||
attempt_pct = total_attempts / chances
|
||||
|
||||
if attempt_pct >= .08:
|
||||
if attempt_pct >= 0.08:
|
||||
st_auto = True
|
||||
else:
|
||||
st_auto = False
|
||||
@ -399,7 +65,7 @@ def stealing_line(steal_data: dict):
|
||||
jump_chances = round(sd[3] * 36)
|
||||
|
||||
if jump_chances == 0:
|
||||
good_jump = '-'
|
||||
good_jump = "-"
|
||||
elif jump_chances <= 6:
|
||||
if jump_chances == 6:
|
||||
good_jump = 7
|
||||
@ -414,76 +80,76 @@ def stealing_line(steal_data: dict):
|
||||
elif jump_chances == 1:
|
||||
good_jump = 2
|
||||
elif jump_chances == 7:
|
||||
good_jump = '4,5'
|
||||
good_jump = "4,5"
|
||||
elif jump_chances == 8:
|
||||
good_jump = '4,6'
|
||||
good_jump = "4,6"
|
||||
elif jump_chances == 9:
|
||||
good_jump = '3-5'
|
||||
good_jump = "3-5"
|
||||
elif jump_chances == 10:
|
||||
good_jump = '2-5'
|
||||
good_jump = "2-5"
|
||||
elif jump_chances == 11:
|
||||
good_jump = '6,7'
|
||||
good_jump = "6,7"
|
||||
elif jump_chances == 12:
|
||||
good_jump = '4-6'
|
||||
good_jump = "4-6"
|
||||
elif jump_chances == 13:
|
||||
good_jump = '2,4-6'
|
||||
good_jump = "2,4-6"
|
||||
elif jump_chances == 14:
|
||||
good_jump = '3-6'
|
||||
good_jump = "3-6"
|
||||
elif jump_chances == 15:
|
||||
good_jump = '2-6'
|
||||
good_jump = "2-6"
|
||||
elif jump_chances == 16:
|
||||
good_jump = '2,5-6'
|
||||
good_jump = "2,5-6"
|
||||
elif jump_chances == 17:
|
||||
good_jump = '3,5-6'
|
||||
good_jump = "3,5-6"
|
||||
elif jump_chances == 18:
|
||||
good_jump = '4-6'
|
||||
good_jump = "4-6"
|
||||
elif jump_chances == 19:
|
||||
good_jump = '2,4-7'
|
||||
good_jump = "2,4-7"
|
||||
elif jump_chances == 20:
|
||||
good_jump = '3-7'
|
||||
good_jump = "3-7"
|
||||
elif jump_chances == 21:
|
||||
good_jump = '2-7'
|
||||
good_jump = "2-7"
|
||||
elif jump_chances == 22:
|
||||
good_jump = '2-7,12'
|
||||
good_jump = "2-7,12"
|
||||
elif jump_chances == 23:
|
||||
good_jump = '2-7,11'
|
||||
good_jump = "2-7,11"
|
||||
elif jump_chances == 24:
|
||||
good_jump = '2,4-8'
|
||||
good_jump = "2,4-8"
|
||||
elif jump_chances == 25:
|
||||
good_jump = '3-8'
|
||||
good_jump = "3-8"
|
||||
elif jump_chances == 26:
|
||||
good_jump = '2-8'
|
||||
good_jump = "2-8"
|
||||
elif jump_chances == 27:
|
||||
good_jump = '2-8,12'
|
||||
good_jump = "2-8,12"
|
||||
elif jump_chances == 28:
|
||||
good_jump = '2-8,11'
|
||||
good_jump = "2-8,11"
|
||||
elif jump_chances == 29:
|
||||
good_jump = '3-9'
|
||||
good_jump = "3-9"
|
||||
elif jump_chances == 30:
|
||||
good_jump = '2-9'
|
||||
good_jump = "2-9"
|
||||
elif jump_chances == 31:
|
||||
good_jump = '2-9,12'
|
||||
good_jump = "2-9,12"
|
||||
elif jump_chances == 32:
|
||||
good_jump = '2-9,11'
|
||||
good_jump = "2-9,11"
|
||||
elif jump_chances == 33:
|
||||
good_jump = '2-10'
|
||||
good_jump = "2-10"
|
||||
elif jump_chances == 34:
|
||||
good_jump = '3-11'
|
||||
good_jump = "3-11"
|
||||
elif jump_chances == 35:
|
||||
good_jump = '2-11'
|
||||
good_jump = "2-11"
|
||||
else:
|
||||
good_jump = '2-12'
|
||||
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 == '':
|
||||
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}')
|
||||
logger.error(f"calcs_batter running - {e}")
|
||||
xb_pct = 20
|
||||
|
||||
return max(min(round(6 + (10 * xb_pct)), 17), 8)
|
||||
@ -491,26 +157,36 @@ def running(extra_base_pct: str):
|
||||
|
||||
def bunting(num_bunts: int, season_pct: float):
|
||||
if num_bunts > max(round(10 * season_pct), 4):
|
||||
return 'A'
|
||||
return "A"
|
||||
elif num_bunts > max(round(5 * season_pct), 2):
|
||||
return 'B'
|
||||
return "B"
|
||||
elif num_bunts > 1:
|
||||
return 'C'
|
||||
return "C"
|
||||
else:
|
||||
return 'D'
|
||||
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 >= .35:
|
||||
return 'A'
|
||||
elif babip >= .3:
|
||||
return 'B'
|
||||
elif babip >= .25:
|
||||
return 'C'
|
||||
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'
|
||||
return "D"
|
||||
|
||||
|
||||
def get_batter_ratings(df_data) -> List[dict]:
|
||||
@ -518,108 +194,154 @@ def get_batter_ratings(df_data) -> List[dict]:
|
||||
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']
|
||||
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']
|
||||
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.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']))
|
||||
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}'
|
||||
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}'
|
||||
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'])
|
||||
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()}')
|
||||
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'])
|
||||
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 ""}')
|
||||
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'])
|
||||
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)}')
|
||||
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']
|
||||
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']
|
||||
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')
|
||||
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')
|
||||
logger.error(f"Have surplus of {diff} chances")
|
||||
if x.strikeout + 1 > diff:
|
||||
logger.error(f'Subtracting {diff} strikeouts to close gap')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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')
|
||||
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}')
|
||||
logger.error(f"total chances for {df_data.name} come to {vl_total_chances}")
|
||||
else:
|
||||
logger.debug(f'total chances: {vl_total_chances}')
|
||||
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}')
|
||||
logger.error(f"total chances for {df_data.name} come to {vr_total_chances}")
|
||||
else:
|
||||
logger.debug(f'total chances: {vr_total_chances}')
|
||||
logger.debug(f"total chances: {vr_total_chances}")
|
||||
|
||||
return [vl.custom_to_dict(), vr.custom_to_dict()]
|
||||
vl_dict = vl.custom_to_dict()
|
||||
vr_dict = vr.custom_to_dict()
|
||||
|
||||
try:
|
||||
offense_col = int(df_data["offense_col"]) if "offense_col" in df_data else 1
|
||||
player_id = (
|
||||
int(df_data["player_id"])
|
||||
if "player_id" in df_data
|
||||
else abs(hash(df_data["key_bbref"])) % 10000
|
||||
)
|
||||
vl_card, vr_card = build_batter_full_cards(
|
||||
vl, vr, offense_col, player_id, df_data["bat_hand"]
|
||||
)
|
||||
vl_dict.update(vl_card.card_output())
|
||||
vr_dict.update(vr_card.card_output())
|
||||
except Exception as e:
|
||||
logger.warning(f"Card layout builder failed for {df_data.name}: {e}")
|
||||
|
||||
return [vl_dict, vr_dict]
|
||||
|
||||
802
batters/card_builder.py
Normal file
802
batters/card_builder.py
Normal file
@ -0,0 +1,802 @@
|
||||
"""
|
||||
Batter card-building algorithm, ported from database/app/card_creation.py (~lines 1357-2226).
|
||||
|
||||
Converts BattingCardRatingsModel instances (vL and vR) into FullBattingCard objects
|
||||
that represent the physical card layout.
|
||||
"""
|
||||
import copy
|
||||
import math
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
from card_layout import FullBattingCard, PLAY_RESULTS, PlayResult, EXACT_CHANCES, get_chances
|
||||
from batters.models import BattingCardRatingsModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_batter_full_cards(
|
||||
ratings_vl: BattingCardRatingsModel,
|
||||
ratings_vr: BattingCardRatingsModel,
|
||||
offense_col: int,
|
||||
player_id: int,
|
||||
hand: str, # player's batting hand: 'R', 'L', or 'S'
|
||||
) -> tuple:
|
||||
"""Build vL and vR FullBattingCard objects from pre-calculated ratings.
|
||||
|
||||
Returns (vl_card, vr_card).
|
||||
"""
|
||||
player_binary = player_id % 2
|
||||
|
||||
vl = FullBattingCard(offense_col=offense_col, alt_direction=player_binary)
|
||||
vr = FullBattingCard(offense_col=offense_col, alt_direction=player_binary)
|
||||
|
||||
def assign_bchances(this_card, play, chances, secondary_play=None):
|
||||
r_data = this_card.add_result(play, chances, secondary_play)
|
||||
if r_data:
|
||||
return float(r_data[0]), float(r_data[1])
|
||||
else:
|
||||
for x in EXACT_CHANCES:
|
||||
if x < math.floor(chances):
|
||||
r_data = this_card.add_result(play, Decimal(math.floor(chances)), secondary_play)
|
||||
if r_data:
|
||||
return float(r_data[0]), float(r_data[1])
|
||||
break
|
||||
if x < chances:
|
||||
r_data = this_card.add_result(play, x, secondary_play)
|
||||
if r_data:
|
||||
return float(r_data[0]), float(r_data[1])
|
||||
return 0, 0
|
||||
|
||||
def get_pullside_of(vs_hand):
|
||||
if hand == 'L':
|
||||
return 'rf'
|
||||
elif hand == 'R':
|
||||
return 'lf'
|
||||
elif vs_hand == 'L':
|
||||
return 'lf'
|
||||
else:
|
||||
return 'rf'
|
||||
|
||||
def get_preferred_mif(ratings):
|
||||
if hand == 'L' and ratings.slap_rate > .24:
|
||||
return 'ss'
|
||||
elif hand == 'L' or (hand == 'R' and ratings.slap_rate > .24):
|
||||
return '2b'
|
||||
else:
|
||||
return 'ss'
|
||||
|
||||
for card, data, vs_hand in [
|
||||
(vl, copy.deepcopy(ratings_vl), 'L'),
|
||||
(vr, copy.deepcopy(ratings_vr), 'R'),
|
||||
]:
|
||||
logger.info(f'\n\nBeginning v{vs_hand}')
|
||||
|
||||
new_ratings = BattingCardRatingsModel(
|
||||
battingcard_id=data.battingcard_id,
|
||||
bat_hand=data.bat_hand,
|
||||
vs_hand=vs_hand,
|
||||
hard_rate=data.hard_rate,
|
||||
med_rate=data.med_rate,
|
||||
soft_rate=data.soft_rate,
|
||||
pull_rate=data.pull_rate,
|
||||
center_rate=data.center_rate,
|
||||
slap_rate=data.slap_rate,
|
||||
)
|
||||
pull_of = get_pullside_of(vs_hand)
|
||||
pref_mif = get_preferred_mif(data)
|
||||
|
||||
# BP Homerun
|
||||
res_chances = data.bp_homerun
|
||||
while res_chances > 0:
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_bchances(card, PLAY_RESULTS['bp-hr'], ch)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.bp_homerun += r_val[0]
|
||||
|
||||
# HBP
|
||||
retries = 0
|
||||
res_chances = data.hbp
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
break
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_bchances(card, PlayResult(full_name='HBP', short_name='HBP'), ch)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.hbp += r_val[0]
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
# Homerun
|
||||
retries = 0
|
||||
res_chances = data.homerun
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.double_pull > 0:
|
||||
data.double_pull += res_chances
|
||||
elif data.double_two > 0:
|
||||
data.double_two += res_chances
|
||||
elif data.triple > 0:
|
||||
data.triple += res_chances
|
||||
elif data.single_two > 0:
|
||||
data.single_two += res_chances
|
||||
elif data.single_center > 0:
|
||||
data.single_center += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.double_pull > (data.flyout_rf_b + data.flyout_lf_b) and data.double_pull > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS[f'do-{pull_of}']
|
||||
elif data.flyout_lf_b > data.flyout_rf_b and data.flyout_lf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['fly-lf']
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['fly-rf']
|
||||
elif data.double_pull > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS[f'do-{pull_of}']
|
||||
elif data.double_three > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['do***']
|
||||
elif data.double_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['do**']
|
||||
elif data.triple > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['tr']
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_bchances(card, PLAY_RESULTS['hr'], ch, secondary)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.homerun += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if secondary.short_name[:4] == 'DO (':
|
||||
data.double_pull -= r_val[1]
|
||||
new_ratings.double_pull += r_val[1]
|
||||
elif 'lf' in secondary.short_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif 'rf' in secondary.short_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
elif '***' in secondary.short_name:
|
||||
data.double_three -= r_val[1]
|
||||
new_ratings.double_three += r_val[1]
|
||||
elif '**' in secondary.short_name:
|
||||
data.double_two -= r_val[1]
|
||||
new_ratings.double_two += r_val[1]
|
||||
elif 'TR' in secondary.short_name:
|
||||
data.triple -= r_val[1]
|
||||
new_ratings.triple += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
# Triple
|
||||
retries = 0
|
||||
res_chances = data.triple
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.double_pull > 0:
|
||||
data.double_pull += res_chances
|
||||
elif data.double_two > 0:
|
||||
data.double_two += res_chances
|
||||
elif data.single_two > 0:
|
||||
data.single_two += res_chances
|
||||
elif data.single_center > 0:
|
||||
data.single_center += res_chances
|
||||
elif data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.single_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['si**']
|
||||
elif data.flyout_lf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['fly-lf']
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['fly-rf']
|
||||
elif data.double_pull > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS[f'do-{pull_of}']
|
||||
elif data.double_three > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['do***']
|
||||
elif data.double_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['do**']
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_bchances(card, PLAY_RESULTS['tr'], ch, secondary)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.triple += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if 'DO (' in secondary.short_name:
|
||||
data.double_pull -= r_val[1]
|
||||
new_ratings.double_pull += r_val[1]
|
||||
elif 'lf' in secondary.short_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif 'rf' in secondary.short_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
elif '***' in secondary.short_name:
|
||||
data.double_three -= r_val[1]
|
||||
new_ratings.double_three += r_val[1]
|
||||
elif 'SI' in secondary.short_name:
|
||||
data.single_two -= r_val[1]
|
||||
new_ratings.single_two += r_val[1]
|
||||
elif '**' in secondary.short_name:
|
||||
data.double_two -= r_val[1]
|
||||
new_ratings.double_two += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
# Double***
|
||||
retries = 0
|
||||
res_chances = data.double_three
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.double_pull > 0:
|
||||
data.double_pull += res_chances
|
||||
elif data.double_two > 0:
|
||||
data.double_two += res_chances
|
||||
elif data.single_two > 0:
|
||||
data.single_two += res_chances
|
||||
elif data.single_center > 0:
|
||||
data.single_center += res_chances
|
||||
elif data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.single_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['si**']
|
||||
elif data.flyout_lf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['fly-lf']
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['fly-rf']
|
||||
elif data.double_pull > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS[f'do-{pull_of}']
|
||||
elif data.double_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['do**']
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_bchances(card, PLAY_RESULTS['do***'], ch, secondary)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.double_three += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if 'DO (' in secondary.short_name:
|
||||
data.double_pull -= r_val[1]
|
||||
new_ratings.double_pull += r_val[1]
|
||||
elif 'lf' in secondary.short_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif 'rf' in secondary.short_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
elif 'SI' in secondary.short_name:
|
||||
data.single_two -= r_val[1]
|
||||
new_ratings.single_two += r_val[1]
|
||||
elif '**' in secondary.short_name:
|
||||
data.double_two -= r_val[1]
|
||||
new_ratings.double_two += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
# Double pull-side
|
||||
retries = 0
|
||||
res_chances = data.double_pull
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.double_two > 0:
|
||||
data.double_two += res_chances
|
||||
elif data.single_two > 0:
|
||||
data.single_two += res_chances
|
||||
elif data.single_center > 0:
|
||||
data.single_center += res_chances
|
||||
elif data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.flyout_lf_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'fly (lf) B', short_name=f'fly B')
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'fly (rf) B', short_name=f'fly b')
|
||||
elif data.single_one > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['si*']
|
||||
elif data.single_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['si**']
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_bchances(card, PLAY_RESULTS[f'do-{pull_of}'], ch, secondary)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.double_pull += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if 'lf' in secondary.full_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif 'rf' in secondary.full_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
elif '***' in secondary.short_name:
|
||||
data.double_three -= r_val[1]
|
||||
new_ratings.double_three += r_val[1]
|
||||
elif 'SI' in secondary.short_name:
|
||||
data.single_two -= r_val[1]
|
||||
new_ratings.single_two += r_val[1]
|
||||
elif '**' in secondary.short_name:
|
||||
data.double_two -= r_val[1]
|
||||
new_ratings.double_two += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
# Double**
|
||||
retries = 0
|
||||
res_chances = data.double_two
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.single_two > 0:
|
||||
data.single_two += res_chances
|
||||
elif data.single_center > 0:
|
||||
data.single_center += res_chances
|
||||
elif data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
elif data.walk > 0:
|
||||
data.walk += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.single_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['si**']
|
||||
elif data.single_center > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['si-cf']
|
||||
elif data.flyout_lf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['fly-lf']
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS['fly-rf']
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_bchances(card, PLAY_RESULTS['do**'], ch, secondary)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.double_two += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if 'lf' in secondary.full_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif 'rf' in secondary.full_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
elif 'SI' in secondary.short_name:
|
||||
data.single_two -= r_val[1]
|
||||
new_ratings.single_two += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
# Single**
|
||||
retries = 0
|
||||
res_chances = data.single_two
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.single_center > 0:
|
||||
data.single_center += res_chances
|
||||
elif data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
elif data.walk > 0:
|
||||
data.walk += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.groundout_a > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A')
|
||||
elif data.groundout_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B')
|
||||
elif data.groundout_c > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'gb ({pref_mif}) C', short_name=f'gb ({pref_mif}) C')
|
||||
elif data.lineout > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})')
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_bchances(card, PLAY_RESULTS['si**'], ch, secondary)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.single_two += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if 'C' in secondary.short_name:
|
||||
data.groundout_c -= r_val[1]
|
||||
new_ratings.groundout_c += r_val[1]
|
||||
elif 'B' in secondary.short_name:
|
||||
data.groundout_b -= r_val[1]
|
||||
new_ratings.groundout_b += r_val[1]
|
||||
elif 'A' in secondary.short_name:
|
||||
data.groundout_a -= r_val[1]
|
||||
new_ratings.groundout_a += r_val[1]
|
||||
elif 'lo' in secondary.short_name:
|
||||
data.lineout -= r_val[1]
|
||||
new_ratings.lineout += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
# Single (cf)
|
||||
retries = 0
|
||||
res_chances = data.single_center
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
elif data.walk > 0:
|
||||
data.walk += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.flyout_bq > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'fly B?', short_name=f'fly B?')
|
||||
elif data.flyout_lf_b > max(1 - ch, 0) and data.flyout_lf_b > data.flyout_rf_b:
|
||||
secondary = PlayResult(full_name=f'fly (LF) B', short_name=f'fly B')
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'fly (RF) B', short_name=f'fly B')
|
||||
elif data.flyout_lf_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'fly (LF) B', short_name=f'fly B')
|
||||
elif data.lineout > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})')
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_bchances(card, PLAY_RESULTS['si-cf'], ch, secondary)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.single_center += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if '?' in secondary.short_name:
|
||||
data.flyout_bq -= r_val[1]
|
||||
new_ratings.flyout_bq += r_val[1]
|
||||
elif 'LF' in secondary.full_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif 'RF' in secondary.full_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
elif 'lo' in secondary.short_name:
|
||||
data.lineout -= r_val[1]
|
||||
new_ratings.lineout += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
# Single*
|
||||
retries = 0
|
||||
res_chances = data.single_one
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.walk > 0:
|
||||
data.walk += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.groundout_c > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'gb ({pref_mif}) C', short_name=f'gb ({pref_mif}) C')
|
||||
elif data.groundout_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'gb ({pref_mif}) B', short_name=f'gb ({pref_mif}) B')
|
||||
elif data.groundout_a > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'gb ({pref_mif}) A', short_name=f'gb ({pref_mif}) A')
|
||||
elif data.lineout > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'lo ({pref_mif})', short_name=f'lo ({pref_mif})')
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_bchances(card, PLAY_RESULTS['si*'], ch, secondary)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.single_one += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if 'C' in secondary.short_name:
|
||||
data.groundout_c -= r_val[1]
|
||||
new_ratings.groundout_c += r_val[1]
|
||||
elif 'B' in secondary.short_name:
|
||||
data.groundout_b -= r_val[1]
|
||||
new_ratings.groundout_b += r_val[1]
|
||||
elif 'A' in secondary.short_name:
|
||||
data.groundout_a -= r_val[1]
|
||||
new_ratings.groundout_a += r_val[1]
|
||||
elif 'lo' in secondary.short_name:
|
||||
data.lineout -= r_val[1]
|
||||
new_ratings.lineout += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
# Walk
|
||||
retries = 0
|
||||
res_chances = data.walk
|
||||
while res_chances >= 1:
|
||||
if res_chances < 1 or retries > 0:
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.strikeout > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name=f'strikeout', short_name=f'so')
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_bchances(card, PLAY_RESULTS['walk'], ch, secondary)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.walk += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
data.strikeout -= r_val[1]
|
||||
new_ratings.strikeout += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
# BP Single
|
||||
retries = 0
|
||||
res_chances = data.bp_single
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_bchances(card, PLAY_RESULTS['bp-si'], ch)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.bp_single += r_val[0]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
# Special lomax result
|
||||
r_val = assign_bchances(
|
||||
card, PlayResult(full_name=f'lo ({pref_mif}) max', short_name=f'lo ({pref_mif}) max'), Decimal(1))
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
data.lineout -= r_val[0]
|
||||
new_ratings.lineout += r_val[0]
|
||||
|
||||
# Popout
|
||||
retries = 0
|
||||
res_chances = data.popout
|
||||
while res_chances >= 1:
|
||||
if res_chances < 1 or retries > 0:
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
this_if = '2b' if pref_mif == 'ss' else 'ss'
|
||||
r_val = assign_bchances(
|
||||
card,
|
||||
PlayResult(full_name=f'popout ({this_if})', short_name=f'popout ({this_if})'),
|
||||
Decimal(math.floor(ch))
|
||||
)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
|
||||
if r_val[0] == 0:
|
||||
data.lineout += res_chances
|
||||
break
|
||||
else:
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.popout += r_val[0]
|
||||
|
||||
# Flyout A
|
||||
retries = 0
|
||||
res_chances = data.flyout_a
|
||||
while res_chances >= 1:
|
||||
if res_chances < 1 or retries > 0:
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_bchances(
|
||||
card, PlayResult(full_name=f'fly (cf) A', short_name=f'fly (cf) A'), Decimal(math.floor(ch)))
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
|
||||
if r_val[0] == 0:
|
||||
data.strikeout += res_chances if data.strikeout > 2 else 0
|
||||
break
|
||||
else:
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.flyout_a += r_val[0]
|
||||
|
||||
# Flyout LF B
|
||||
retries = 0
|
||||
res_chances = data.flyout_lf_b
|
||||
while res_chances >= 1:
|
||||
if res_chances < 1 or retries > 0:
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_bchances(
|
||||
card, PlayResult(full_name=f'fly (lf) B', short_name=f'fly (lf) B'), Decimal(math.floor(ch)))
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
|
||||
if r_val[0] == 0:
|
||||
data.strikeout += res_chances if data.strikeout > 2 else 0
|
||||
break
|
||||
else:
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.flyout_lf_b += r_val[0]
|
||||
|
||||
# Flyout RF B
|
||||
retries = 0
|
||||
res_chances = data.flyout_rf_b
|
||||
while res_chances >= 1:
|
||||
if res_chances < 1 or retries > 0:
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_bchances(
|
||||
card, PlayResult(full_name=f'fly (rf) B', short_name=f'fly (rf) B'), Decimal(math.floor(ch)))
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
|
||||
if r_val[0] == 0:
|
||||
data.strikeout += res_chances if data.strikeout > 2 else 0
|
||||
break
|
||||
else:
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.flyout_rf_b += r_val[0]
|
||||
|
||||
# Groundout A
|
||||
count_gb = 0
|
||||
|
||||
def get_gb_if():
|
||||
if count_gb % 4 == 1:
|
||||
return pref_mif
|
||||
elif count_gb % 4 == 2:
|
||||
return '2b' if pref_mif == 'ss' else 'ss'
|
||||
elif count_gb % 4 == 3:
|
||||
return '1b' if pref_mif == '2b' else 'p'
|
||||
else:
|
||||
return '3b' if pref_mif == 'ss' else 'p'
|
||||
|
||||
retries = 0
|
||||
res_chances = data.groundout_a
|
||||
while res_chances >= 1:
|
||||
if res_chances < 1 or retries > 0:
|
||||
break
|
||||
|
||||
count_gb += 1
|
||||
this_if = get_gb_if()
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_bchances(
|
||||
card, PlayResult(full_name=f'gb ({this_if}) A', short_name=f'gb ({this_if}) A'),
|
||||
Decimal(math.floor(ch)))
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
|
||||
if r_val[0] == 0:
|
||||
data.groundout_b += res_chances
|
||||
break
|
||||
else:
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.groundout_a += r_val[0]
|
||||
|
||||
# Groundout B
|
||||
retries = 0
|
||||
res_chances = data.groundout_b
|
||||
while res_chances >= 1:
|
||||
if res_chances < 1 or retries > 0:
|
||||
break
|
||||
|
||||
count_gb += 1
|
||||
this_if = get_gb_if()
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_bchances(
|
||||
card, PlayResult(full_name=f'gb ({this_if}) B', short_name=f'gb ({this_if}) B'),
|
||||
Decimal(math.floor(ch)))
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
|
||||
if r_val[0] == 0:
|
||||
data.groundout_c += res_chances
|
||||
break
|
||||
else:
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.groundout_b += r_val[0]
|
||||
|
||||
# Groundout C
|
||||
retries = 0
|
||||
res_chances = data.groundout_c
|
||||
while res_chances >= 1:
|
||||
if res_chances < 1 or retries > 0:
|
||||
break
|
||||
|
||||
count_gb += 1
|
||||
this_if = get_gb_if()
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_bchances(
|
||||
card, PlayResult(full_name=f'gb ({this_if}) C', short_name=f'gb ({this_if}) C'),
|
||||
Decimal(math.floor(ch)))
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
|
||||
if r_val[0] == 0:
|
||||
data.strikeout += res_chances
|
||||
break
|
||||
else:
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.groundout_c += r_val[0]
|
||||
|
||||
# Lineout
|
||||
retries = 0
|
||||
res_chances = data.lineout
|
||||
while res_chances >= 1:
|
||||
if res_chances < 1 or retries > 0:
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
this_if = '3b' if pref_mif == 'ss' else '1b'
|
||||
r_val = assign_bchances(
|
||||
card,
|
||||
PlayResult(full_name=f'lineout ({this_if})', short_name=f'lineout ({this_if})'),
|
||||
Decimal(math.floor(ch))
|
||||
)
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
|
||||
if r_val[0] == 0:
|
||||
break
|
||||
else:
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.lineout += r_val[0]
|
||||
|
||||
# Strikeout
|
||||
retries = 0
|
||||
res_chances = data.strikeout
|
||||
while res_chances >= 1:
|
||||
if res_chances < 1 or retries > 0:
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_bchances(
|
||||
card, PlayResult(full_name=f'strikeout', short_name=f'strikeout'), Decimal(math.floor(ch)))
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
|
||||
if r_val[0] == 0:
|
||||
break
|
||||
else:
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.strikeout += r_val[0]
|
||||
|
||||
# Filler loop — fill any remaining empty card slots
|
||||
plays = sorted(
|
||||
[(data.strikeout, 'so'), (data.lineout, 'lo'), (data.groundout_c, 'gb'), (data.popout, 'po')],
|
||||
key=lambda z: z[0],
|
||||
reverse=True
|
||||
)
|
||||
count_filler = -1
|
||||
while not card.is_complete():
|
||||
count_filler += 1
|
||||
this_play = plays[count_filler % 4]
|
||||
if this_play[1] == 'so':
|
||||
play_res = PlayResult(full_name=f'strikeout', short_name=f'strikeout')
|
||||
elif this_play[1] == 'lo':
|
||||
this_if = '3b' if pref_mif == 'ss' else '1b'
|
||||
play_res = PlayResult(full_name=f'lineout ({this_if})', short_name=f'lineout ({this_if})')
|
||||
elif this_play[1] == 'gb':
|
||||
count_gb += 1
|
||||
this_if = get_gb_if()
|
||||
play_res = PlayResult(full_name=f'gb ({this_if}) C', short_name=f'gb ({this_if}) C')
|
||||
else:
|
||||
play_res = PlayResult(full_name=f'popout (c)', short_name=f'popout (c)')
|
||||
|
||||
logger.debug(f'Send Card Fill\n{play_res}')
|
||||
r_raw = card.card_fill(play_res)
|
||||
r_val = (float(r_raw[0]), float(r_raw[1]))
|
||||
logger.debug(f'Returned batting chances: {r_val[0]} / {r_val[1]}\n')
|
||||
|
||||
if this_play[1] == 'so':
|
||||
new_ratings.strikeout += r_val[0]
|
||||
elif this_play[1] == 'lo':
|
||||
new_ratings.lineout += r_val[0]
|
||||
elif this_play[1] == 'gb':
|
||||
new_ratings.groundout_c += r_val[0]
|
||||
else:
|
||||
new_ratings.popout += r_val[0]
|
||||
|
||||
new_ratings.calculate_rate_stats()
|
||||
|
||||
return vl, vr
|
||||
309
batters/models.py
Normal file
309
batters/models.py
Normal file
@ -0,0 +1,309 @@
|
||||
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.bp_homerun * 2 + self.triple * 3 + self.double_three * 2 +
|
||||
self.double_two * 2 + self.double_pull * 2 + self.single_two + self.single_one +
|
||||
self.single_center + 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
|
||||
}
|
||||
1015
card_layout.py
Normal file
1015
card_layout.py
Normal file
File diff suppressed because it is too large
Load Diff
89
docs/FULLCARD_MIGRATION_STATUS.md
Normal file
89
docs/FULLCARD_MIGRATION_STATUS.md
Normal file
@ -0,0 +1,89 @@
|
||||
# FullCard Migration Status
|
||||
|
||||
**Branch:** `feature/fullcard-migration` (5 commits ahead of main)
|
||||
**Last Updated:** 2026-02-26
|
||||
|
||||
## What This Branch Does
|
||||
|
||||
Moves card-building logic (fitting continuous chances to discrete 2d6×d20 card mechanics) from the database to Python. Previously, Python sent raw continuous values and the database fitted them — meaning what you sent ≠ what got stored. Now Python builds the complete discrete card structure before POSTing.
|
||||
|
||||
### New Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `card_layout.py` | Core card models: PlayResult, CardResult, CardColumn, FullCard, FullBattingCard, FullPitchingCard. Ported from database's `card_creation.py`. Uses `col_*` key names. |
|
||||
| `batters/card_builder.py` | `build_batter_full_cards()` — takes vL/vR ratings, returns two FullBattingCard objects |
|
||||
| `pitchers/card_builder.py` | `build_pitcher_full_cards()` — same for pitchers |
|
||||
| `batters/models.py` | Extracted `BattingCardRatingsModel` from `calcs_batter.py` |
|
||||
| `pitchers/models.py` | Extracted `PitchingCardRatingsModel` from `calcs_pitcher.py` |
|
||||
| `offense_col_resolver.py` | Maps player→offense_col for retrosheet pipeline (fixed 883 silent KeyErrors) |
|
||||
| `tests/test_rate_stats_formulas.py` | Tests for extracted rating model formulas |
|
||||
|
||||
### Integration Path
|
||||
|
||||
```
|
||||
retrosheet_data.py → calcs_batter.py → build_batter_full_cards() → card_output() → vl_dict.update()
|
||||
→ calcs_pitcher.py → build_pitcher_full_cards() → card_output() → vr_dict.update()
|
||||
```
|
||||
|
||||
Card builders are called inside a `try/except` in `calcs_batter.py:339` and `calcs_pitcher.py:134`. On failure, logs a warning and the card still posts without col_* layout data (backwards compatible).
|
||||
|
||||
## Commits on Branch
|
||||
|
||||
1. `a72abc0` — Add FullCard/CardColumn/CardResult models and card builder pipeline
|
||||
2. `39c652e` — Extract BattingCardRatingsModel and PitchingCardRatingsModel into models.py files
|
||||
3. `2bf3a6c` — Fix SLG formula drift in extracted rating models
|
||||
4. `32cadb1` — Fix two bugs in pitcher card builder dispatch logic
|
||||
5. `db38225` — Add offense_col resolver for retrosheet pipeline to fix 883 silent KeyErrors
|
||||
|
||||
## What's Left Before Merge
|
||||
|
||||
### 1. Database Migration (BLOCKING if you want col_* data persisted)
|
||||
|
||||
The database repo (`paper-dynasty-database`) has a parallel `feature/fullcard-migration` branch with:
|
||||
- 9 new nullable TextFields on `BattingCardRatings` and `PitchingCardRatings` tables
|
||||
- Pydantic model updates in `routers_v2/battingcardratings.py` and `pitchingcardratings.py`
|
||||
- Migration SQL documented but **intentionally not run**
|
||||
|
||||
Without this migration, the col_* fields in `card_output()` are computed but silently ignored by the API. The card-creation side works either way — it's a no-op until the DB accepts those fields.
|
||||
|
||||
### 2. `live_series_update.py` Not Integrated
|
||||
|
||||
This file has its own inline card generation — does NOT use `calcs_batter`/`calcs_pitcher`. Only the retrosheet pipeline benefits from the new card builders. Live series integration is a separate effort.
|
||||
|
||||
### 3. No Tests for Core New Code
|
||||
|
||||
`card_layout.py` (1015 lines), `batters/card_builder.py` (802 lines), `pitchers/card_builder.py` (776 lines) have zero test coverage. Priority targets:
|
||||
- `get_chances()` — maps continuous values to discrete EXACT_CHANCES
|
||||
- `CardColumn.add_result()` / `FullCard.card_fill()` — the filling algorithm
|
||||
- `card_output()` — serialization to col_* dict format
|
||||
- End-to-end: feed known ratings → assert expected card structure
|
||||
|
||||
### 4. Test Failures (Pre-existing, Not From This Branch)
|
||||
|
||||
- `test_wh_singles` — `wh_singles(12, .45)` returns `8.0` vs expected `Decimal('7.95')`. Fails on main too. The test file has a 1-line diff on this branch (import change).
|
||||
- 11 failures in `test_automated_data_fetcher.py` — mock setup issues, no diff on this branch.
|
||||
|
||||
## Key Bugs Fixed During Development
|
||||
|
||||
1. **Float/Decimal mismatch** — card_layout uses Decimal internally, models use float. Fix: wrap `card_fill()` outputs with `float()` in `assign_bchances()`/`assign_pchances()`.
|
||||
2. **PitchingCardRatingsModel xcheck defaults** — Non-zero defaults (xcheck_ss=7.0, etc.) corrupted accumulation. Fix: explicitly zero all xcheck fields in `new_ratings` constructor.
|
||||
3. **Pitcher dispatch logic** — Two bugs in how pitcher card builder routed plays to columns.
|
||||
4. **offense_col KeyError** — Retrosheet pipeline had no offense_col resolver, causing 883 silent failures.
|
||||
|
||||
## Architecture Reference
|
||||
|
||||
Full design doc: `docs/architecture/CARD_BUILDER_REDESIGN.md`
|
||||
|
||||
Migration phases:
|
||||
- **Phase 1 (Extract & Validate)** — Done
|
||||
- **Phase 2 (Python Adoption)** — In progress (retrosheet pipeline wired up)
|
||||
- **Phase 3 (Database Simplification)** — Pending DB migration + removing fitting logic from DB
|
||||
- **Phase 4 (Enhancements)** — Future (contracts/card personalities, preview endpoint)
|
||||
|
||||
## Decision Points for Next Session
|
||||
|
||||
1. **Merge as-is?** Branch is safe to merge — col_* fields are computed but harmlessly ignored until DB migrates. Card generation behavior is unchanged.
|
||||
2. **Add tests first?** Recommended but not strictly required since card builders are behind try/except.
|
||||
3. **Run DB migration?** Enables end-to-end persistence. Requires deploying database branch too.
|
||||
4. **Wire up live series?** Separate PR recommended — different pipeline, different concerns.
|
||||
102
offense_col_resolver.py
Normal file
102
offense_col_resolver.py
Normal file
@ -0,0 +1,102 @@
|
||||
"""Resolve offense_col for players in the retrosheet pipeline.
|
||||
|
||||
Three-tier resolution:
|
||||
1. Cache hit → stored value from data-input/offense_col_cache.csv
|
||||
2. API pre-fetch → bulk-fetch all MlbPlayers, merge new entries into cache
|
||||
3. Hash fallback → deterministic hash(player_name) % 3 + 1
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
import pandas as pd
|
||||
|
||||
from db_calls import db_get
|
||||
from exceptions import logger
|
||||
|
||||
CACHE_PATH = "data-input/offense_col_cache.csv"
|
||||
|
||||
|
||||
def hash_offense_col(player_name: str) -> int:
|
||||
"""Deterministic offense_col from player name. Returns 1, 2, or 3."""
|
||||
normalized = player_name.strip().lower()
|
||||
digest = hashlib.md5(normalized.encode()).hexdigest()
|
||||
return int(digest, 16) % 3 + 1
|
||||
|
||||
|
||||
def load_cache(path: str = CACHE_PATH) -> dict[str, int]:
|
||||
"""Load {key_bbref: offense_col} from CSV cache."""
|
||||
if not os.path.exists(path):
|
||||
return {}
|
||||
df = pd.read_csv(path, dtype={"key_bbref": str, "offense_col": int})
|
||||
return dict(zip(df["key_bbref"], df["offense_col"]))
|
||||
|
||||
|
||||
def save_cache(cache: dict[str, tuple[str, int]], path: str = CACHE_PATH):
|
||||
"""Write cache to CSV. cache values are (player_name, offense_col)."""
|
||||
rows = sorted(
|
||||
[
|
||||
{"key_bbref": k, "player_name": v[0], "offense_col": v[1]}
|
||||
for k, v in cache.items()
|
||||
],
|
||||
key=lambda r: r["key_bbref"],
|
||||
)
|
||||
pd.DataFrame(rows).to_csv(path, index=False)
|
||||
|
||||
|
||||
async def resolve_offense_cols(
|
||||
df: pd.DataFrame, api_available: bool = True
|
||||
) -> pd.DataFrame:
|
||||
"""Add offense_col column to a stats DataFrame.
|
||||
|
||||
Args:
|
||||
df: DataFrame with key_bbref, use_name, last_name columns.
|
||||
api_available: If True, fetch from API to refresh cache.
|
||||
|
||||
Returns:
|
||||
df with offense_col column added.
|
||||
"""
|
||||
cache = load_cache()
|
||||
full_cache: dict[str, tuple[str, int]] = {}
|
||||
|
||||
# Seed full_cache from existing file cache
|
||||
for bbref, oc in cache.items():
|
||||
full_cache[bbref] = ("", oc)
|
||||
|
||||
# Refresh from API if available
|
||||
if api_available:
|
||||
try:
|
||||
result = await db_get("mlbplayers")
|
||||
if result and "players" in result:
|
||||
api_count = 0
|
||||
for p in result["players"]:
|
||||
bbref = p.get("key_bbref")
|
||||
oc = p.get("offense_col")
|
||||
name = f'{p.get("first_name", "")} {p.get("last_name", "")}'.strip()
|
||||
if bbref and oc:
|
||||
full_cache[bbref] = (name, int(oc))
|
||||
api_count += 1
|
||||
logger.info(
|
||||
f"offense_col_resolver: loaded {api_count} entries from API"
|
||||
)
|
||||
save_cache(full_cache)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"offense_col_resolver: API fetch failed, using cache only: {e}"
|
||||
)
|
||||
|
||||
# Build lookup from full_cache
|
||||
lookup = {k: v[1] for k, v in full_cache.items()}
|
||||
|
||||
# Resolve for each row
|
||||
def resolve_row(row):
|
||||
bbref = row.get("key_bbref", "")
|
||||
if bbref in lookup:
|
||||
return lookup[bbref]
|
||||
name = f'{row.get("use_name", "")} {row.get("last_name", "")}'.strip()
|
||||
oc = hash_offense_col(name)
|
||||
logger.debug(f"offense_col_resolver: hash fallback for {name} ({bbref}) → {oc}")
|
||||
return oc
|
||||
|
||||
df["offense_col"] = df.apply(resolve_row, axis=1)
|
||||
return df
|
||||
@ -1,440 +1,205 @@
|
||||
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'])
|
||||
ob_vr = float(108 * (df_data['BB_vR'] + df_data['HBP_vR']) / df_data['TBF_vR'])
|
||||
ob_vl = float(108 * (df_data["BB_vL"] + df_data["HBP_vL"]) / df_data["TBF_vL"])
|
||||
ob_vr = float(108 * (df_data["BB_vR"] + df_data["HBP_vR"]) / df_data["TBF_vR"])
|
||||
|
||||
vl = PitchingCardRatingsModel(
|
||||
pitchingcard_id=df_data.pitchingcard_id,
|
||||
pit_hand=df_data.pitch_hand,
|
||||
vs_hand='L',
|
||||
all_hits=sanitize_chance_output((df_data['AVG_vL'] - 0.05) * 108), # Subtracting chances from BP results
|
||||
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(min(ob_vl, 0.8)),
|
||||
hard_rate=df_data['Hard%_vL'],
|
||||
med_rate=df_data['Med%_vL'],
|
||||
soft_rate=df_data['Soft%_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.pitch_hand,
|
||||
vs_hand='R',
|
||||
all_hits=sanitize_chance_output((df_data['AVG_vR'] - 0.05) * 108), # Subtracting chances from BP results
|
||||
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(min(ob_vr, 0.8)),
|
||||
hard_rate=df_data['Hard%_vR'],
|
||||
med_rate=df_data['Med%_vR'],
|
||||
soft_rate=df_data['Soft%_vR']
|
||||
hard_rate=df_data["Hard%_vR"],
|
||||
med_rate=df_data["Med%_vR"],
|
||||
soft_rate=df_data["Soft%_vR"],
|
||||
)
|
||||
vl.all_outs = mround(108 - vl.all_hits - vl.all_other_ob, base=0.5)
|
||||
vr.all_outs = mround(108 - vr.all_hits - vr.all_other_ob, base=0.5)
|
||||
|
||||
logger.info(
|
||||
f'vL - All Hits: {vl.all_hits} / Other OB: {vl.all_other_ob} / All Outs: {vl.all_outs} '
|
||||
f'/ Total: {vl.total_chances()}'
|
||||
f"vL - All Hits: {vl.all_hits} / Other OB: {vl.all_other_ob} / All Outs: {vl.all_outs} "
|
||||
f"/ Total: {vl.total_chances()}"
|
||||
)
|
||||
logger.info(
|
||||
f'vR - All Hits: {vr.all_hits} / Other OB: {vr.all_other_ob} / All Outs: {vr.all_outs} '
|
||||
f'/ Total: {vr.total_chances()}'
|
||||
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'])
|
||||
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"],
|
||||
)
|
||||
|
||||
logger.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}')
|
||||
logger.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}')
|
||||
logger.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}"
|
||||
)
|
||||
logger.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'])
|
||||
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"]
|
||||
)
|
||||
|
||||
logger.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}')
|
||||
logger.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}')
|
||||
logger.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}"
|
||||
)
|
||||
logger.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'])
|
||||
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.info(f'vL: All other OB: {vl.all_other_ob} / HBP: {vl.hbp} / BB: {vl.walk} / '
|
||||
f'Total Chances: {vl.total_chances()}')
|
||||
logger.info(f'vR: All other OB: {vr.all_other_ob} / HBP: {vr.hbp} / BB: {vr.walk} / '
|
||||
f'Total Chances: {vr.total_chances()}')
|
||||
logger.info(
|
||||
f"vL: All other OB: {vl.all_other_ob} / HBP: {vl.hbp} / BB: {vl.walk} / "
|
||||
f"Total Chances: {vl.total_chances()}"
|
||||
)
|
||||
logger.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'])
|
||||
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'])
|
||||
df_data["SO_vR"],
|
||||
df_data["TBF_vR"] - df_data["BB_vR"] - df_data["IBB_vR"] - df_data["HBP_vR"],
|
||||
df_data["H_vR"],
|
||||
)
|
||||
|
||||
logger.info(f'vL: All Outs: {vl.all_outs} / Ks: {vl.strikeout} / Current Outs: {vl.total_outs()}')
|
||||
logger.info(f'vR: All Outs: {vr.all_outs} / Ks: {vr.strikeout} / Current Outs: {vr.total_outs()}')
|
||||
logger.info(
|
||||
f"vL: All Outs: {vl.all_outs} / Ks: {vl.strikeout} / Current Outs: {vl.total_outs()}"
|
||||
)
|
||||
logger.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'])
|
||||
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"])
|
||||
|
||||
logger.info(f'vL: Total chances: {vl.total_chances()}')
|
||||
logger.info(f'vR: Total chances: {vr.total_chances()}')
|
||||
logger.info(f"vL: Total chances: {vl.total_chances()}")
|
||||
logger.info(f"vR: Total chances: {vr.total_chances()}")
|
||||
|
||||
return [vl.custom_to_dict(), vr.custom_to_dict()]
|
||||
vl_dict = vl.custom_to_dict()
|
||||
vr_dict = vr.custom_to_dict()
|
||||
|
||||
try:
|
||||
offense_col = int(df_data["offense_col"]) if "offense_col" in df_data else 1
|
||||
player_id = (
|
||||
int(df_data["player_id"])
|
||||
if "player_id" in df_data
|
||||
else abs(hash(df_data["key_bbref"])) % 10000
|
||||
)
|
||||
vl_card, vr_card = build_pitcher_full_cards(
|
||||
vl, vr, offense_col, player_id, df_data["pitch_hand"]
|
||||
)
|
||||
vl_dict.update(vl_card.card_output())
|
||||
vr_dict.update(vr_card.card_output())
|
||||
except Exception as e:
|
||||
logger.warning(f"Card layout builder failed for {df_data.name}: {e}")
|
||||
|
||||
return [vl_dict, vr_dict]
|
||||
|
||||
|
||||
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']:
|
||||
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'
|
||||
if pct > 0.2:
|
||||
return "high"
|
||||
elif pct < 0.1:
|
||||
return "low"
|
||||
else:
|
||||
return 'avg'
|
||||
return "avg"
|
||||
|
||||
|
||||
def med_rate(pct):
|
||||
if pct > .65:
|
||||
return 'high'
|
||||
elif pct < .4:
|
||||
return 'low'
|
||||
if pct > 0.65:
|
||||
return "high"
|
||||
elif pct < 0.4:
|
||||
return "low"
|
||||
else:
|
||||
return 'avg'
|
||||
return "avg"
|
||||
|
||||
|
||||
def hard_rate(pct):
|
||||
if pct > .4:
|
||||
return 'high'
|
||||
elif pct < .2:
|
||||
return 'low'
|
||||
if pct > 0.4:
|
||||
return "high"
|
||||
elif pct < 0.2:
|
||||
return "low"
|
||||
else:
|
||||
return 'avg'
|
||||
return "avg"
|
||||
|
||||
|
||||
def hr_per_fb_rate(pct):
|
||||
if pct > .18:
|
||||
return 'high'
|
||||
elif pct < .08:
|
||||
return 'low'
|
||||
if pct > 0.18:
|
||||
return "high"
|
||||
elif pct < 0.08:
|
||||
return "low"
|
||||
else:
|
||||
return 'avg'
|
||||
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]))
|
||||
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]))
|
||||
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)
|
||||
|
||||
@ -447,12 +212,12 @@ def bp_singles(singles_vl, singles_vr):
|
||||
|
||||
|
||||
def wh_singles(rem_si_vl, rem_si_vr, hard_rate_vl, hard_rate_vr):
|
||||
if hard_rate_vl == 'low':
|
||||
if hard_rate_vl == "low":
|
||||
whs_vl = 0
|
||||
else:
|
||||
whs_vl = rem_si_vl / 2
|
||||
|
||||
if hard_rate_vr == 'low':
|
||||
if hard_rate_vr == "low":
|
||||
whs_vr = 0
|
||||
else:
|
||||
whs_vr = rem_si_vr / 2
|
||||
@ -461,12 +226,12 @@ def wh_singles(rem_si_vl, rem_si_vr, hard_rate_vl, hard_rate_vr):
|
||||
|
||||
|
||||
def one_singles(rem_si_vl, rem_si_vr, soft_rate_vl, soft_rate_vr):
|
||||
if soft_rate_vl == 'high':
|
||||
if soft_rate_vl == "high":
|
||||
oss_vl = rem_si_vl
|
||||
else:
|
||||
oss_vl = 0
|
||||
|
||||
if soft_rate_vr == 'high':
|
||||
if soft_rate_vr == "high":
|
||||
oss_vr = rem_si_vr
|
||||
else:
|
||||
oss_vr = 0
|
||||
@ -475,19 +240,19 @@ def one_singles(rem_si_vl, rem_si_vr, soft_rate_vl, soft_rate_vr):
|
||||
|
||||
|
||||
def bp_homerun(hr_vl, hr_vr, hr_rate_vl, hr_rate_vr):
|
||||
if hr_rate_vl == 'low':
|
||||
if hr_rate_vl == "low":
|
||||
bphr_vl = hr_vl
|
||||
elif hr_rate_vl == 'avg':
|
||||
bphr_vl = hr_vl * .75
|
||||
elif hr_rate_vl == "avg":
|
||||
bphr_vl = hr_vl * 0.75
|
||||
else:
|
||||
bphr_vl = hr_vl * .4
|
||||
bphr_vl = hr_vl * 0.4
|
||||
|
||||
if hr_rate_vr == 'low':
|
||||
if hr_rate_vr == "low":
|
||||
bphr_vr = hr_vr
|
||||
elif hr_rate_vr == 'avg':
|
||||
bphr_vr = hr_vr * .75
|
||||
elif hr_rate_vr == "avg":
|
||||
bphr_vr = hr_vr * 0.75
|
||||
else:
|
||||
bphr_vr = hr_vr * .4
|
||||
bphr_vr = hr_vr * 0.4
|
||||
|
||||
return mround(bphr_vl), mround(bphr_vr)
|
||||
|
||||
@ -500,8 +265,8 @@ def triples(all_xbh_vl, all_xbh_vr, triple_rate_vl, triple_rate_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
|
||||
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)
|
||||
|
||||
@ -523,21 +288,21 @@ def hbps(all_ob, this_hbp_rate):
|
||||
|
||||
|
||||
def xchecks(pos, all_chances=True):
|
||||
if pos.lower() == 'p':
|
||||
if pos.lower() == "p":
|
||||
return 1 if all_chances else 0
|
||||
elif pos.lower() == 'c':
|
||||
elif pos.lower() == "c":
|
||||
return 3 if all_chances else 2
|
||||
elif pos.lower() == '1b':
|
||||
elif pos.lower() == "1b":
|
||||
return 2 if all_chances else 1
|
||||
elif pos.lower() == '2b':
|
||||
elif pos.lower() == "2b":
|
||||
return 6 if all_chances else 5
|
||||
elif pos.lower() == '3b':
|
||||
elif pos.lower() == "3b":
|
||||
return 3 if all_chances else 2
|
||||
elif pos.lower() == 'ss':
|
||||
elif pos.lower() == "ss":
|
||||
return 7 if all_chances else 6
|
||||
elif pos.lower() == 'lf':
|
||||
elif pos.lower() == "lf":
|
||||
return 2 if all_chances else 1
|
||||
elif pos.lower() == 'cf':
|
||||
elif pos.lower() == "cf":
|
||||
return 3 if all_chances else 2
|
||||
else:
|
||||
return 2 if all_chances else 1
|
||||
@ -553,7 +318,7 @@ def oppo_fly(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:
|
||||
elif dp_rate > 0.6:
|
||||
return all_gb
|
||||
else:
|
||||
return mround(all_gb * (dp_rate * 1.5))
|
||||
@ -563,20 +328,22 @@ def balks(total_balks: int, innings: float, season_pct):
|
||||
try:
|
||||
total_balks = int(total_balks)
|
||||
except ValueError:
|
||||
logger.error(f'Could not read balks: {total_balks} / setting to 0')
|
||||
logger.error(f"Could not read balks: {total_balks} / setting to 0")
|
||||
total_balks = 0
|
||||
|
||||
|
||||
try:
|
||||
innings = float(innings)
|
||||
except ValueError:
|
||||
logger.error(f'Could not read innings: {innings} / setting to 0')
|
||||
logger.error(f"Could not read innings: {innings} / setting to 0")
|
||||
innings = 0
|
||||
|
||||
if innings == 0:
|
||||
return 0
|
||||
|
||||
numerator = (total_balks * 290 * season_pct)
|
||||
logger.info(f'total_balks: {total_balks} / season_pct {season_pct} / innings: {innings} / numerator: {numerator}')
|
||||
|
||||
numerator = total_balks * 290 * season_pct
|
||||
logger.info(
|
||||
f"total_balks: {total_balks} / season_pct {season_pct} / innings: {innings} / numerator: {numerator}"
|
||||
)
|
||||
|
||||
return min(round(numerator / innings), 20)
|
||||
|
||||
@ -592,19 +359,19 @@ def closer_rating(gf: int, saves: int, games: int):
|
||||
if gf == 0 or games == 0 or saves == 0:
|
||||
return None
|
||||
|
||||
if gf / games >= .875:
|
||||
if gf / games >= 0.875:
|
||||
return 6
|
||||
elif gf / games >= .8:
|
||||
elif gf / games >= 0.8:
|
||||
return 5
|
||||
elif gf / games >= .7:
|
||||
elif gf / games >= 0.7:
|
||||
return 4
|
||||
elif gf / games >= .55:
|
||||
elif gf / games >= 0.55:
|
||||
return 3
|
||||
elif gf / games >= .4:
|
||||
elif gf / games >= 0.4:
|
||||
return 2
|
||||
elif gf / games >= .25:
|
||||
elif gf / games >= 0.25:
|
||||
return 1
|
||||
elif gf / games >= .1:
|
||||
elif gf / games >= 0.1:
|
||||
return 0
|
||||
else:
|
||||
return None
|
||||
|
||||
776
pitchers/card_builder.py
Normal file
776
pitchers/card_builder.py
Normal file
@ -0,0 +1,776 @@
|
||||
import copy
|
||||
import math
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
from card_layout import (
|
||||
FullPitchingCard,
|
||||
PLAY_RESULTS,
|
||||
PlayResult,
|
||||
EXACT_CHANCES,
|
||||
get_chances,
|
||||
)
|
||||
from pitchers.models import PitchingCardRatingsModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def build_pitcher_full_cards(
|
||||
ratings_vl: PitchingCardRatingsModel,
|
||||
ratings_vr: PitchingCardRatingsModel,
|
||||
offense_col: int,
|
||||
player_id: int,
|
||||
hand: str,
|
||||
) -> tuple:
|
||||
"""Build vL and vR FullPitchingCard objects from pre-calculated ratings.
|
||||
|
||||
Returns (vl_card, vr_card).
|
||||
"""
|
||||
player_binary = player_id % 2
|
||||
|
||||
vl = FullPitchingCard(offense_col=offense_col, alt_direction=player_binary)
|
||||
vr = FullPitchingCard(offense_col=offense_col, alt_direction=player_binary)
|
||||
|
||||
def assign_pchances(this_card, play, chances, secondary_play=None):
|
||||
r_data = this_card.add_result(play, chances, secondary_play)
|
||||
if r_data:
|
||||
return float(r_data[0]), float(r_data[1])
|
||||
else:
|
||||
for x in EXACT_CHANCES + [Decimal("0.95")]:
|
||||
if x < math.floor(chances - Decimal("0.05")):
|
||||
r_data = this_card.add_result(
|
||||
play, Decimal(math.floor(chances)), secondary_play
|
||||
)
|
||||
if r_data:
|
||||
return float(r_data[0]), float(r_data[1])
|
||||
break
|
||||
if x < chances and secondary_play is not None:
|
||||
r_data = this_card.add_result(play, x, secondary_play)
|
||||
if r_data:
|
||||
return float(r_data[0]), float(r_data[1])
|
||||
return 0, 0
|
||||
|
||||
def get_preferred_mif(ratings):
|
||||
if hand == "L" and ratings.vs_hand == "L":
|
||||
return "ss"
|
||||
elif hand == "L" or (hand == "R" and ratings.vs_hand == "R"):
|
||||
return "2b"
|
||||
else:
|
||||
return "ss"
|
||||
|
||||
for card, data, vs_hand in [
|
||||
(vl, copy.deepcopy(ratings_vl), "L"),
|
||||
(vr, copy.deepcopy(ratings_vr), "R"),
|
||||
]:
|
||||
new_ratings = PitchingCardRatingsModel(
|
||||
pitchingcard_id=data.pitchingcard_id,
|
||||
pit_hand=data.pit_hand,
|
||||
vs_hand=vs_hand,
|
||||
hard_rate=data.hard_rate,
|
||||
med_rate=data.med_rate,
|
||||
soft_rate=data.soft_rate,
|
||||
xcheck_p=0.0,
|
||||
xcheck_c=0.0,
|
||||
xcheck_1b=0.0,
|
||||
xcheck_2b=0.0,
|
||||
xcheck_3b=0.0,
|
||||
xcheck_ss=0.0,
|
||||
xcheck_lf=0.0,
|
||||
xcheck_cf=0.0,
|
||||
xcheck_rf=0.0,
|
||||
)
|
||||
|
||||
res_chances = data.bp_homerun
|
||||
while res_chances > 0:
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["bp-hr"], ch)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.bp_homerun += r_val[0]
|
||||
|
||||
res_chances = data.hbp
|
||||
while res_chances > 0:
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card, PlayResult(full_name="HBP", short_name="HBP"), ch
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.hbp += r_val[0]
|
||||
if r_val[0] == 0:
|
||||
break
|
||||
|
||||
res_chances = data.xcheck_p
|
||||
while res_chances > 0:
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card, PlayResult(full_name="GB (p) X", short_name="GB (p) X"), ch
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.xcheck_p += r_val[0]
|
||||
|
||||
res_chances = data.xcheck_c
|
||||
while res_chances > 0:
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card, PlayResult(full_name="CATCH X", short_name="CATCH X"), ch
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.xcheck_c += r_val[0]
|
||||
|
||||
res_chances = data.xcheck_1b
|
||||
while res_chances > 0:
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card, PlayResult(full_name="GB (1b) X", short_name="GB (1b) X"), ch
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.xcheck_1b += r_val[0]
|
||||
|
||||
res_chances = data.xcheck_3b
|
||||
while res_chances > 0:
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card, PlayResult(full_name="GB (3b) X", short_name="GB (3b) X"), ch
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.xcheck_3b += r_val[0]
|
||||
|
||||
res_chances = data.xcheck_rf
|
||||
while res_chances > 0:
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card, PlayResult(full_name="FLY (rf) X", short_name="FLY (rf) X"), ch
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.xcheck_rf += r_val[0]
|
||||
|
||||
res_chances = data.xcheck_lf
|
||||
while res_chances > 0:
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card, PlayResult(full_name="FLY (lf) X", short_name="FLY (lf) X"), ch
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.xcheck_lf += r_val[0]
|
||||
|
||||
res_chances = data.xcheck_2b
|
||||
while res_chances > 0:
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card, PlayResult(full_name="GB (2b) X", short_name="GB (2b) X"), ch
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.xcheck_2b += r_val[0]
|
||||
|
||||
res_chances = data.xcheck_cf
|
||||
while res_chances > 0:
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card, PlayResult(full_name="FLY (cf) X", short_name="FLY (cf) X"), ch
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.xcheck_cf += r_val[0]
|
||||
|
||||
res_chances = data.xcheck_ss
|
||||
while res_chances > 0:
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card, PlayResult(full_name="GB (ss) X", short_name="GB (ss) X"), ch
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.xcheck_ss += r_val[0]
|
||||
|
||||
res_chances = data.walk
|
||||
while res_chances >= 1:
|
||||
ch = get_chances(res_chances)
|
||||
if data.strikeout > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name="strikeout", short_name="so")
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["walk"], ch, secondary)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.walk += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
data.strikeout -= r_val[1]
|
||||
new_ratings.strikeout += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
break
|
||||
|
||||
res_chances = data.homerun
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.double_cf > 0:
|
||||
data.double_cf += res_chances
|
||||
elif data.double_two > 0:
|
||||
data.double_two += res_chances
|
||||
elif data.triple > 0:
|
||||
data.triple += res_chances
|
||||
elif data.single_two > 0:
|
||||
data.single_two += res_chances
|
||||
elif data.single_center > 0:
|
||||
data.single_center += res_chances
|
||||
elif data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.double_cf > (
|
||||
data.flyout_rf_b + data.flyout_lf_b
|
||||
) and data.double_cf > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["do-cf"]
|
||||
elif data.flyout_cf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-cf"]
|
||||
elif data.flyout_lf_b > data.flyout_rf_b and data.flyout_lf_b > max(
|
||||
1 - ch, 0
|
||||
):
|
||||
secondary = PLAY_RESULTS["fly-lf"]
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-rf"]
|
||||
elif data.double_cf > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["do-cf"]
|
||||
elif data.double_three > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["do***"]
|
||||
elif data.double_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["do**"]
|
||||
elif data.triple > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["tr"]
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["hr"], ch, secondary)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.homerun += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if "DO (" in secondary.short_name:
|
||||
data.double_cf -= r_val[1]
|
||||
new_ratings.double_cf += r_val[1]
|
||||
elif "lf" in secondary.short_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif "cf" in secondary.short_name:
|
||||
data.flyout_cf_b -= r_val[1]
|
||||
new_ratings.flyout_cf_b += r_val[1]
|
||||
elif "rf" in secondary.short_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
elif "***" in secondary.short_name:
|
||||
data.double_three -= r_val[1]
|
||||
new_ratings.double_three += r_val[1]
|
||||
elif "**" in secondary.short_name:
|
||||
data.double_two -= r_val[1]
|
||||
new_ratings.double_two += r_val[1]
|
||||
elif "TR" in secondary.short_name:
|
||||
data.triple -= r_val[1]
|
||||
new_ratings.triple += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.triple
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.double_cf > 0:
|
||||
data.double_cf += res_chances
|
||||
elif data.double_two > 0:
|
||||
data.double_two += res_chances
|
||||
elif data.single_two > 0:
|
||||
data.single_two += res_chances
|
||||
elif data.single_center > 0:
|
||||
data.single_center += res_chances
|
||||
elif data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.single_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["si**"]
|
||||
elif data.flyout_cf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-cf"]
|
||||
elif data.flyout_lf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-lf"]
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-rf"]
|
||||
elif data.double_cf > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["do-cf"]
|
||||
elif data.double_three > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["do***"]
|
||||
elif data.double_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["do**"]
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["tr"], ch, secondary)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.triple += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if "DO (" in secondary.short_name:
|
||||
data.double_cf -= r_val[1]
|
||||
new_ratings.double_cf += r_val[1]
|
||||
elif "lf" in secondary.short_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif "cf" in secondary.short_name:
|
||||
data.flyout_cf_b -= r_val[1]
|
||||
new_ratings.flyout_cf_b += r_val[1]
|
||||
elif "rf" in secondary.short_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
elif "***" in secondary.short_name:
|
||||
data.double_three -= r_val[1]
|
||||
new_ratings.double_three += r_val[1]
|
||||
elif "SI" in secondary.short_name:
|
||||
data.single_two -= r_val[1]
|
||||
new_ratings.single_two += r_val[1]
|
||||
elif "**" in secondary.short_name:
|
||||
data.double_two -= r_val[1]
|
||||
new_ratings.double_two += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.double_three
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.double_cf > 0:
|
||||
data.double_cf += res_chances
|
||||
elif data.double_two > 0:
|
||||
data.double_two += res_chances
|
||||
elif data.single_two > 0:
|
||||
data.single_two += res_chances
|
||||
elif data.single_center > 0:
|
||||
data.single_center += res_chances
|
||||
elif data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.single_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["si**"]
|
||||
elif data.flyout_cf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-cf"]
|
||||
elif data.flyout_lf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-lf"]
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-rf"]
|
||||
elif data.double_cf > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["do-cf"]
|
||||
elif data.double_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["do**"]
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["do***"], ch, secondary)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.double_three += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if "DO (" in secondary.short_name:
|
||||
data.double_cf -= r_val[1]
|
||||
new_ratings.double_cf += r_val[1]
|
||||
elif "lf" in secondary.short_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif "cf" in secondary.short_name:
|
||||
data.flyout_cf_b -= r_val[1]
|
||||
new_ratings.flyout_cf_b += r_val[1]
|
||||
elif "rf" in secondary.short_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
elif "SI" in secondary.short_name:
|
||||
data.single_two -= r_val[1]
|
||||
new_ratings.single_two += r_val[1]
|
||||
elif "**" in secondary.short_name:
|
||||
data.double_two -= r_val[1]
|
||||
new_ratings.double_two += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.double_cf
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.double_two > 0:
|
||||
data.double_two += res_chances
|
||||
elif data.single_two > 0:
|
||||
data.single_two += res_chances
|
||||
elif data.single_center > 0:
|
||||
data.single_center += res_chances
|
||||
elif data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.flyout_cf_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name="fly (cf) B", short_name="fly B")
|
||||
elif data.flyout_lf_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name="fly (lf) B", short_name="fly B")
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name="fly (rf) B", short_name="fly b")
|
||||
elif data.single_one > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["si*"]
|
||||
elif data.single_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["si**"]
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["do-cf"], ch, secondary)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.double_cf += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if "lf" in secondary.full_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif "rf" in secondary.full_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
elif "cf" in secondary.full_name:
|
||||
data.flyout_cf_b -= r_val[1]
|
||||
new_ratings.flyout_cf_b += r_val[1]
|
||||
elif "***" in secondary.short_name:
|
||||
data.double_three -= r_val[1]
|
||||
new_ratings.double_three += r_val[1]
|
||||
elif "SI" in secondary.short_name:
|
||||
data.single_two -= r_val[1]
|
||||
new_ratings.single_two += r_val[1]
|
||||
elif "**" in secondary.short_name:
|
||||
data.double_two -= r_val[1]
|
||||
new_ratings.double_two += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.double_two
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.single_two > 0:
|
||||
data.single_two += res_chances
|
||||
elif data.single_center > 0:
|
||||
data.single_center += res_chances
|
||||
elif data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
elif data.walk > 0:
|
||||
data.walk += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.single_two > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["si**"]
|
||||
elif data.single_center > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["si-cf"]
|
||||
elif data.flyout_cf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-cf"]
|
||||
elif data.flyout_lf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-lf"]
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-rf"]
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["do**"], ch, secondary)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.double_two += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if "lf" in secondary.full_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif "rf" in secondary.full_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
elif "cf" in secondary.full_name:
|
||||
data.flyout_cf_b -= r_val[1]
|
||||
new_ratings.flyout_cf_b += r_val[1]
|
||||
elif "SI" in secondary.short_name:
|
||||
data.single_two -= r_val[1]
|
||||
new_ratings.single_two += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.single_two
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.single_center > 0:
|
||||
data.single_center += res_chances
|
||||
elif data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
elif data.walk > 0:
|
||||
data.walk += res_chances
|
||||
break
|
||||
|
||||
pref_mif = get_preferred_mif(new_ratings)
|
||||
ch = get_chances(res_chances)
|
||||
if data.groundout_a > max(1 - ch, 0):
|
||||
temp_mif = get_preferred_mif(new_ratings)
|
||||
pref_mif = "ss" if temp_mif == "2b" else "2b"
|
||||
secondary = PlayResult(
|
||||
full_name=f"gb ({pref_mif}) A", short_name=f"gb ({pref_mif}) A"
|
||||
)
|
||||
elif data.groundout_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(
|
||||
full_name=f"gb ({pref_mif}) B", short_name=f"gb ({pref_mif}) B"
|
||||
)
|
||||
elif data.flyout_cf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-cf"]
|
||||
elif data.flyout_lf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-lf"]
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PLAY_RESULTS["fly-rf"]
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["si**"], ch, secondary)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.single_two += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if "lf" in secondary.full_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif "rf" in secondary.full_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
elif "cf" in secondary.full_name:
|
||||
data.flyout_cf_b -= r_val[1]
|
||||
new_ratings.flyout_cf_b += r_val[1]
|
||||
elif "B" in secondary.short_name:
|
||||
data.groundout_b -= r_val[1]
|
||||
new_ratings.groundout_b += r_val[1]
|
||||
elif "A" in secondary.short_name:
|
||||
data.groundout_a -= r_val[1]
|
||||
new_ratings.groundout_a += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.single_center
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.single_one > 0:
|
||||
data.single_one += res_chances
|
||||
elif data.walk > 0:
|
||||
data.walk += res_chances
|
||||
break
|
||||
|
||||
ch = get_chances(res_chances)
|
||||
if data.flyout_cf_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name="fly (cf) B", short_name="fly B")
|
||||
elif (
|
||||
data.flyout_lf_b > max(1 - ch, 0)
|
||||
and data.flyout_lf_b > data.flyout_rf_b
|
||||
):
|
||||
secondary = PlayResult(full_name="fly (lf) B", short_name="fly B")
|
||||
elif data.flyout_rf_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name="fly (rf) B", short_name="fly B")
|
||||
elif data.flyout_lf_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(full_name="fly (lf) B", short_name="fly B")
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["si-cf"], ch, secondary)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.single_center += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if "CF" in secondary.short_name:
|
||||
data.flyout_cf_b -= r_val[1]
|
||||
new_ratings.flyout_cf_b += r_val[1]
|
||||
elif "LF" in secondary.full_name:
|
||||
data.flyout_lf_b -= r_val[1]
|
||||
new_ratings.flyout_lf_b += r_val[1]
|
||||
elif "RF" in secondary.full_name:
|
||||
data.flyout_rf_b -= r_val[1]
|
||||
new_ratings.flyout_rf_b += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.single_one
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if res_chances < 1 or retries > 0:
|
||||
if data.walk > 0:
|
||||
data.walk += res_chances
|
||||
break
|
||||
|
||||
pref_mif = get_preferred_mif(new_ratings)
|
||||
ch = get_chances(res_chances)
|
||||
if data.groundout_b > max(1 - ch, 0):
|
||||
secondary = PlayResult(
|
||||
full_name=f"gb ({pref_mif}) B", short_name=f"gb ({pref_mif}) B"
|
||||
)
|
||||
elif data.groundout_a > max(1 - ch, 0):
|
||||
temp_mif = get_preferred_mif(new_ratings)
|
||||
pref_mif = "ss" if temp_mif == "2b" else "2b"
|
||||
secondary = PlayResult(
|
||||
full_name=f"gb ({pref_mif}) A", short_name=f"gb ({pref_mif}) A"
|
||||
)
|
||||
else:
|
||||
secondary = None
|
||||
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["si*"], ch, secondary)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.single_one += r_val[0]
|
||||
if r_val[1] > 0:
|
||||
if "B" in secondary.short_name:
|
||||
data.groundout_b -= r_val[1]
|
||||
new_ratings.groundout_b += r_val[1]
|
||||
elif "A" in secondary.short_name:
|
||||
data.groundout_a -= r_val[1]
|
||||
new_ratings.groundout_a += r_val[1]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.bp_single
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if retries > 0:
|
||||
break
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["bp-si"], ch)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.bp_single += r_val[0]
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.strikeout
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if retries > 0:
|
||||
break
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card, PlayResult(full_name="strikeout", short_name="so"), ch
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.strikeout += r_val[0]
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.flyout_cf_b
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if retries > 0:
|
||||
break
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["fly-cf"], ch)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.flyout_cf_b += r_val[0]
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.flyout_lf_b
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if retries > 0:
|
||||
break
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["fly-lf"], ch)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.flyout_lf_b += r_val[0]
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.flyout_rf_b
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if retries > 0:
|
||||
break
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(card, PLAY_RESULTS["fly-rf"], ch)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.flyout_rf_b += r_val[0]
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.groundout_a
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if retries > 0:
|
||||
break
|
||||
|
||||
temp_mif = get_preferred_mif(new_ratings)
|
||||
pref_mif = "ss" if temp_mif == "2b" else "2b"
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card,
|
||||
PlayResult(
|
||||
full_name=f"gb ({pref_mif}) A", short_name=f"gb ({pref_mif}) A"
|
||||
),
|
||||
ch,
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.groundout_a += r_val[0]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
res_chances = data.groundout_b
|
||||
retries = 0
|
||||
while res_chances > 0:
|
||||
if retries > 0:
|
||||
break
|
||||
|
||||
pref_mif = get_preferred_mif(new_ratings)
|
||||
ch = get_chances(res_chances)
|
||||
r_val = assign_pchances(
|
||||
card,
|
||||
PlayResult(
|
||||
full_name=f"gb ({pref_mif}) B", short_name=f"gb ({pref_mif}) B"
|
||||
),
|
||||
ch,
|
||||
)
|
||||
res_chances -= r_val[0]
|
||||
new_ratings.groundout_b += r_val[0]
|
||||
|
||||
if r_val[0] == 0:
|
||||
retries += 1
|
||||
|
||||
plays = sorted(
|
||||
[
|
||||
(data.strikeout, "so"),
|
||||
(data.groundout_a, "gb"),
|
||||
(data.flyout_lf_b, "lf"),
|
||||
(data.flyout_rf_b, "rf"),
|
||||
],
|
||||
key=lambda z: z[0],
|
||||
reverse=True,
|
||||
)
|
||||
count_filler = -1
|
||||
pref_mif = get_preferred_mif(new_ratings)
|
||||
while not card.is_complete():
|
||||
count_filler += 1
|
||||
this_play = plays[count_filler % 4]
|
||||
if this_play[1] == "so":
|
||||
play_res = PlayResult(full_name="strikeout", short_name="strikeout")
|
||||
elif this_play[1] == "gb":
|
||||
this_if = "3b" if pref_mif == "ss" else "1b"
|
||||
play_res = PlayResult(
|
||||
full_name=f"gb ({this_if}) A", short_name=f"gb ({this_if}) A"
|
||||
)
|
||||
elif this_play[1] == "lf":
|
||||
play_res = PLAY_RESULTS["fly-lf"]
|
||||
else:
|
||||
play_res = PLAY_RESULTS["fly-rf"]
|
||||
|
||||
r_raw = card.card_fill(play_res)
|
||||
r_val = (float(r_raw[0]), float(r_raw[1]))
|
||||
|
||||
if this_play[1] == "so":
|
||||
new_ratings.strikeout += r_val[0]
|
||||
elif this_play[1] == "gb":
|
||||
new_ratings.groundout_a += r_val[0]
|
||||
elif this_play[1] == "lf":
|
||||
new_ratings.flyout_lf_b += r_val[0]
|
||||
else:
|
||||
new_ratings.flyout_rf_b += r_val[0]
|
||||
|
||||
card.add_fatigue()
|
||||
new_ratings.calculate_rate_stats()
|
||||
|
||||
return vl, vr
|
||||
300
pitchers/models.py
Normal file
300
pitchers/models.py
Normal file
@ -0,0 +1,300 @@
|
||||
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.bp_homerun * 2 + self.triple * 3 + self.double_three * 2 +
|
||||
self.double_two * 2 + self.double_cf * 2 + self.single_two + self.single_one +
|
||||
self.single_center + 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')
|
||||
2540
retrosheet_data.py
2540
retrosheet_data.py
File diff suppressed because it is too large
Load Diff
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
90
tests/test_rate_stats_formulas.py
Normal file
90
tests/test_rate_stats_formulas.py
Normal file
@ -0,0 +1,90 @@
|
||||
from batters.models import BattingCardRatingsModel
|
||||
from pitchers.models import PitchingCardRatingsModel
|
||||
from creation_helpers import mround
|
||||
|
||||
|
||||
def test_batting_model_slg_formula_matches_canonical_weights():
|
||||
ratings = BattingCardRatingsModel(
|
||||
battingcard_id=1,
|
||||
bat_hand='R',
|
||||
vs_hand='R',
|
||||
hard_rate=0.3,
|
||||
med_rate=0.3,
|
||||
soft_rate=0.3,
|
||||
pull_rate=0.3,
|
||||
center_rate=0.3,
|
||||
slap_rate=0.3,
|
||||
homerun=1,
|
||||
bp_homerun=2,
|
||||
triple=3,
|
||||
double_three=4,
|
||||
double_two=5,
|
||||
double_pull=6,
|
||||
single_two=7,
|
||||
single_one=8,
|
||||
single_center=9,
|
||||
bp_single=10,
|
||||
)
|
||||
|
||||
ratings.calculate_rate_stats()
|
||||
|
||||
expected = mround(
|
||||
(
|
||||
ratings.homerun * 4
|
||||
+ ratings.bp_homerun * 2
|
||||
+ ratings.triple * 3
|
||||
+ ratings.double_three * 2
|
||||
+ ratings.double_two * 2
|
||||
+ ratings.double_pull * 2
|
||||
+ ratings.single_two
|
||||
+ ratings.single_one
|
||||
+ ratings.single_center
|
||||
+ ratings.bp_single / 2
|
||||
) / 108,
|
||||
prec=5,
|
||||
base=0.00001,
|
||||
)
|
||||
|
||||
assert ratings.slg == expected
|
||||
|
||||
|
||||
def test_pitching_model_slg_formula_matches_canonical_weights():
|
||||
ratings = PitchingCardRatingsModel(
|
||||
pitchingcard_id=1,
|
||||
pit_hand='R',
|
||||
vs_hand='R',
|
||||
hard_rate=0.3,
|
||||
med_rate=0.3,
|
||||
soft_rate=0.3,
|
||||
homerun=1,
|
||||
bp_homerun=2,
|
||||
triple=3,
|
||||
double_three=4,
|
||||
double_two=5,
|
||||
double_cf=6,
|
||||
single_two=7,
|
||||
single_one=8,
|
||||
single_center=9,
|
||||
bp_single=10,
|
||||
)
|
||||
|
||||
ratings.calculate_rate_stats()
|
||||
|
||||
expected = mround(
|
||||
(
|
||||
ratings.homerun * 4
|
||||
+ ratings.bp_homerun * 2
|
||||
+ ratings.triple * 3
|
||||
+ ratings.double_three * 2
|
||||
+ ratings.double_two * 2
|
||||
+ ratings.double_cf * 2
|
||||
+ ratings.single_two
|
||||
+ ratings.single_one
|
||||
+ ratings.single_center
|
||||
+ ratings.bp_single / 2
|
||||
) / 108,
|
||||
prec=5,
|
||||
base=0.00001,
|
||||
)
|
||||
|
||||
assert ratings.slg == expected
|
||||
Loading…
Reference in New Issue
Block a user