TJStatsApps/2025_decision_value
0
1from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui2import datasets3from datasets import load_dataset4import pandas as pd5import numpy as np6import matplotlib.pyplot as plt7import seaborn as sns8import numpy as np9from scipy.stats import gaussian_kde10import matplotlib11from matplotlib.ticker import MaxNLocator12from matplotlib.gridspec import GridSpec13from scipy.stats import zscore14import math15import matplotlib16from adjustText import adjust_text17import matplotlib.ticker as mtick18from shinywidgets import output_widget, render_widget19import pandas as pd20from configure import base_url21import shinyswatch22 23from datetime import datetime, timedelta24year_input = 202425 26 27 28 29### Import Datasets30# dataset = load_dataset('nesticot/mlb_data', data_files=['mlb_pitch_data_2024.csv' ])31# dataset_train = dataset['train']32# df_2023_mlb = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True)33 34 35df_2023_mlb = pd.read_parquet(f"hf://datasets/TJStatsApps/mlb_data/data/mlb_pitch_data_2025.parquet")36 37# from api_scraper import MLB_Scrape38# mlb_stats = MLB_Scrape()39# schedule_spring = mlb_stats.get_schedule(year_input=2024,40# sport_id=1,41# start_date='2024-01-01',42# end_date='2024-12-31',43# final=False,44# regular=True,45# spring=False)46 47# schedule_spring = schedule_spring.drop_duplicates(subset=['game_id'])48 49# schedule_spring = schedule_spring[(schedule_spring['date']==(datetime.today() - timedelta(hours=8)).date())]50 51 52# data = mlb_stats.get_data(schedule_spring.game_id[:].values)53# df_2023_new = mlb_stats.get_data_df(data_list = data)54# df_2023 = pd.concat([df_2023_mlb,df_2023_new])55# df_2023 = df_2023.drop_duplicates(subset=['play_id'],keep='last')56# df_2023_mlb = pd.concat([df_2023_mlb,df_2023_new])57 58 59### Import Datasets60# dataset = load_dataset('nesticot/mlb_data', data_files=['aaa_pitch_data_2024.csv' ])61# dataset_train = dataset['train']62# df_2023_aaa = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True)63 64df_2023_aaa = pd.read_parquet(f"hf://datasets/TJStatsApps/mlb_data/data/aaa_pitch_data_2025.parquet")65 66 67df_2023_mlb['level'] = 'MLB'68df_2023_aaa['level'] = 'AAA'69 70df_2023 = pd.concat([df_2023_mlb,df_2023_aaa])71# df_2023 = pd.concat([df_2023_mlb])72 73#print(df_2023)74### Normalize Hit Locations75import joblib76swing_model = joblib.load('swing.joblib')77 78no_swing_model = joblib.load('no_swing.joblib')79 80# Now you can use the loaded model for prediction or any other task81 82 83batter_dict = df_2023.sort_values('batter_name').set_index('batter_id')['batter_name'].to_dict()84 85## Make Predictions86## Define Features and Target87features = ['px','pz','strikes','balls']88## Set up 2023 Data for Prediction of Run Expectancy89df_model_2023_no_swing = df_2023[df_2023.is_swing != 1].dropna(subset=features)90df_model_2023_swing = df_2023[df_2023.is_swing == 1].dropna(subset=features)91 92 93import xgboost as xgb94df_model_2023_no_swing['y_pred'] = no_swing_model.predict(xgb.DMatrix(df_model_2023_no_swing[features]))95df_model_2023_swing['y_pred'] = swing_model.predict(xgb.DMatrix(df_model_2023_swing[features]))96 97df_model_2023 = pd.concat([df_model_2023_no_swing,df_model_2023_swing])98import joblib99# # Dump the model to a file named 'model.joblib'100# model = joblib.load('xtb_model.joblib')101 102# ## Create a Dataset to calculate xRV/100 Pitches103# df_model_2023['pitcher_name'] = df_model_2023.pitcher.map(pitcher_dict)104# df_model_2023['player_team'] = df_model_2023.batter.map(team_player_dict)105df_model_2023_group = df_model_2023.groupby(['batter_id','batter_name','level']).agg(106 pitches = ('start_speed','count'),107 y_pred = ('y_pred','mean'),108 )109 110## Minimum 500 pitches faced111#min_pitches = 300112#df_model_2023_group = df_model_2023_group[df_model_2023_group.pitches >= min_pitches]113## Calculate 20-80 Scale114df_model_2023_group['decision_value'] = zscore(df_model_2023_group['y_pred'])115df_model_2023_group['decision_value'] = (50+df_model_2023_group['decision_value']*10)116 117## Create a Dataset to calculate xRV/100 for Pitches Taken118df_model_2023_group_no_swing = df_model_2023[df_model_2023.is_swing!=1].groupby(['batter_id','batter_name','level']).agg(119 pitches = ('start_speed','count'),120 y_pred = ('y_pred','mean')121 )122 123# Select Pitches with 500 total pitches124df_model_2023_group_no_swing = df_model_2023_group_no_swing[df_model_2023_group_no_swing.index.get_level_values(1).isin(df_model_2023_group.index.get_level_values(1))]125## Calculate 20-80 Scale126df_model_2023_group_no_swing['iz_awareness'] = zscore(df_model_2023_group_no_swing['y_pred'])127df_model_2023_group_no_swing['iz_awareness'] = (((50+df_model_2023_group_no_swing['iz_awareness']*10)))128 129## Create a Dataset for xRV/100 Pitches Swung At130df_model_2023_group_swing = df_model_2023[df_model_2023.is_swing==1].groupby(['batter_id','batter_name','level']).agg(131 pitches = ('start_speed','count'),132 y_pred = ('y_pred','mean')133 )134 135# Select Pitches with 500 total pitches136df_model_2023_group_swing = df_model_2023_group_swing[df_model_2023_group_swing.index.get_level_values(1).isin(df_model_2023_group.index.get_level_values(1))]137## Calculate 20-80 Scale138df_model_2023_group_swing['oz_awareness'] = zscore(df_model_2023_group_swing['y_pred'])139df_model_2023_group_swing['oz_awareness'] = (((50+df_model_2023_group_swing['oz_awareness']*10)))140 141## Create df for plotting142# Merge Datasets143df_model_2023_group_swing_plus_no = df_model_2023_group_swing.merge(df_model_2023_group_no_swing,left_index=True,right_index=True,suffixes=['_swing','_no_swing'])144df_model_2023_group_swing_plus_no['pitches'] = df_model_2023_group_swing_plus_no.pitches_swing + df_model_2023_group_swing_plus_no.pitches_no_swing145 146# Calculate xRV/100 Pitches147df_model_2023_group_swing_plus_no['y_pred'] = (df_model_2023_group_swing_plus_no.y_pred_swing*df_model_2023_group_swing_plus_no.pitches_swing + \148 df_model_2023_group_swing_plus_no.y_pred_no_swing*df_model_2023_group_swing_plus_no.pitches_no_swing) / \149 df_model_2023_group_swing_plus_no.pitches150 151df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no.merge(right=df_model_2023_group,152 left_index=True,153 right_index=True,154 suffixes=['','_y'])155 156df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no.reset_index()157team_dict = df_2023.groupby(['batter_name'])[['batter_id','batter_team']].tail().set_index('batter_id')['batter_team'].to_dict()158df_model_2023_group_swing_plus_no['team'] = df_model_2023_group_swing_plus_no['batter_id'].map(team_dict)159df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no.set_index(['batter_id','batter_name','level','team'])160 161df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no[df_model_2023_group_swing_plus_no['pitches']>=50]162df_model_2023_group_swing_plus_no_copy = df_model_2023_group_swing_plus_no.copy()163import matplotlib164 165colour_palette = ['#FFB000','#648FFF','#785EF0',166 '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']167 168cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],'#ffffff',colour_palette[0]])169cmap_hue2 = matplotlib.colors.LinearSegmentedColormap.from_list("",['#ffffff',colour_palette[0]])170 171 172from matplotlib.pyplot import text173import inflect174from scipy.stats import percentileofscore175p = inflect.engine()176 177 178 179 180def server(input,output,session):181 182 @output183 @render.plot(alt="hex_plot")184 @reactive.event(input.go, ignore_none=False)185 def scatter_plot():186 187 if input.batter_id() is "":188 fig = plt.figure(figsize=(12, 12))189 fig.text(s='Please Select a Batter',x=0.5,y=0.5)190 return191 print(df_model_2023_group_swing_plus_no_copy)192 print(input.level_list())193 df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no_copy[df_model_2023_group_swing_plus_no_copy.index.get_level_values(2) == input.level_list()]194 print('this one')195 print(df_model_2023_group_swing_plus_no)196 batter_select_id = int(input.batter_id())197 # batter_select_name = 'Edouard Julien'198 #max(1,int(input.pitch_min()))199 plot_min = max(50,int(input.pitch_min()))200 df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no[df_model_2023_group_swing_plus_no.pitches >= plot_min]201 ## Plot In-Zone vs Out-of-Zone Awareness202 sns.set_theme(style="whitegrid", palette="pastel")203 # fig, ax = plt.subplots(1,1,figsize=(12,12))204 fig = plt.figure(figsize=(12,12))205 gs = GridSpec(3, 3, height_ratios=[0.6,10,0.2], width_ratios=[0.25,0.50,0.25])206 207 axheader = fig.add_subplot(gs[0, :])208 #ax10 = fig.add_subplot(gs[1, 0])209 ax = fig.add_subplot(gs[1, :]) # Subplot at the top-right position210 #ax12 = fig.add_subplot(gs[1, 2])211 axfooter1 = fig.add_subplot(gs[-1, 0])212 axfooter2 = fig.add_subplot(gs[-1, 1])213 axfooter3 = fig.add_subplot(gs[-1, 2])214 215 cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],colour_palette[3],colour_palette[0]])216 norm = plt.Normalize(df_model_2023_group_swing_plus_no['y_pred'].min()*100, df_model_2023_group_swing_plus_no['y_pred'].max()*100)217 218 sns.scatterplot(219 x=df_model_2023_group_swing_plus_no['y_pred_swing']*100,220 y=df_model_2023_group_swing_plus_no['y_pred_no_swing']*100,221 hue=df_model_2023_group_swing_plus_no['y_pred']*100,222 size=df_model_2023_group_swing_plus_no['pitches_swing']/df_model_2023_group_swing_plus_no['pitches'],223 palette=cmap_hue,ax=ax)224 225 sm = plt.cm.ScalarMappable(cmap=cmap_hue, norm=norm)226 cbar = plt.colorbar(sm, cax=axfooter2, orientation='horizontal',shrink=1)227 cbar.set_label('Decision Value xRV/100 Pitches',fontsize=12)228 229 ax.axhline(y=df_model_2023_group_swing_plus_no['y_pred_no_swing'].mean()*100,color='gray',linewidth=3,linestyle='dotted',alpha=0.4)230 231 ax.axvline(x=df_model_2023_group_swing_plus_no['y_pred_swing'].mean()*100,color='gray',linewidth=3,linestyle='dotted',alpha=0.4)232 233 x_lim_min = (math.floor((df_model_2023_group_swing_plus_no['y_pred_swing'].min()*100*100)/5))*5/100234 x_lim_max = (math.ceil((df_model_2023_group_swing_plus_no['y_pred_swing'].max()*100*100)/5))*5/100235 236 y_lim_min = (math.floor((df_model_2023_group_swing_plus_no['y_pred_no_swing'].min()*100*100)/5))*5/100237 y_lim_max = (math.ceil((df_model_2023_group_swing_plus_no['y_pred_no_swing'].max()*100*100)/5))*5/100238 239 ax.set_xlim(x_lim_min,x_lim_max)240 ax.set_ylim(y_lim_min,y_lim_max)241 242 ax.tick_params(axis='both', which='major', labelsize=12)243 244 ax.set_xlabel('Out-of-Zone Awareness Value xRV/100 Swings',fontsize=16)245 ax.set_ylabel('In-Zone Awareness Value xRV/100 Takes',fontsize=16)246 ax.get_legend().remove()247 248 249 ts=[]250 251 252 # thresh = 0.5253 # thresh_2 = -0.9254 # for i in range(len(df_model_2023_group_swing_plus_no)):255 # if (df_model_2023_group_swing_plus_no['y_pred'].values[i]*100) >= thresh or \256 # (df_model_2023_group_swing_plus_no['y_pred'].values[i]*100) <= thresh_2 or \257 # (str(df_model_2023_group_swing_plus_no.index.get_level_values(0).values[i]) in (input.name_list())) :258 # ts.append(ax.text(x=df_model_2023_group_swing_plus_no['y_pred_swing'].values[i]*100,259 # y=df_model_2023_group_swing_plus_no['y_pred_no_swing'].values[i]*100,260 # s=df_model_2023_group_swing_plus_no.index.get_level_values(1).values[i],261 # fontsize=8))262 thresh = 0.5263 thresh_2 = -0.9264 for i in range(len(df_model_2023_group_swing_plus_no)):265 if (df_model_2023_group_swing_plus_no['y_pred_swing'].values[i]) >= df_model_2023_group_swing_plus_no['y_pred_swing'].quantile(0.98) or \266 (df_model_2023_group_swing_plus_no['y_pred_swing'].values[i]) <= df_model_2023_group_swing_plus_no['y_pred_swing'].quantile(0.02) or \267 (df_model_2023_group_swing_plus_no['y_pred_no_swing'].values[i]) >= df_model_2023_group_swing_plus_no['y_pred_no_swing'].quantile(0.98) or \268 (df_model_2023_group_swing_plus_no['y_pred_no_swing'].values[i]) <= df_model_2023_group_swing_plus_no['y_pred_no_swing'].quantile(0.02) or \269 (df_model_2023_group_swing_plus_no['y_pred'].values[i]) >= df_model_2023_group_swing_plus_no['y_pred'].quantile(0.98) or \270 (df_model_2023_group_swing_plus_no['y_pred'].values[i]) <= df_model_2023_group_swing_plus_no['y_pred'].quantile(0.02) or \271 (str(df_model_2023_group_swing_plus_no.index.get_level_values(0).values[i]) in (input.name_list())) :272 ts.append(ax.text(x=df_model_2023_group_swing_plus_no['y_pred_swing'].values[i]*100,273 y=df_model_2023_group_swing_plus_no['y_pred_no_swing'].values[i]*100,274 s=df_model_2023_group_swing_plus_no.index.get_level_values(1).values[i],275 fontsize=8))276 277 ax.text(x=x_lim_min+abs(x_lim_min)*0.02,y=y_lim_max-abs(y_lim_max-y_lim_min)*0.02,s=f'Min. {plot_min} Pitches',fontsize='10',fontstyle='oblique',va='top',278 bbox=dict(facecolor='white', edgecolor='black'))279 # ax.text(x=x_lim_min+abs(x_lim_min)*0.02,y=y_lim_max-abs(y_lim_max-y_lim_min)*0.06,s=f'Labels for Batters with\nDescion Value xRV/100 > {thresh:.2f}\nDescion Value xRV/100 < {thresh_2:.2f}',fontsize='10',fontstyle='oblique',va='top',280 # bbox=dict(facecolor='white', edgecolor='black'))281 ax.text(x=x_lim_min+abs(x_lim_min)*0.02,y=y_lim_max-abs(y_lim_max-y_lim_min)*0.06,s=f'Point Size Represents Swing%',fontsize='10',fontstyle='oblique',va='top',282 bbox=dict(facecolor='white', edgecolor='black'))283 284 adjust_text(ts,285 arrowprops=dict(arrowstyle="-", color=colour_palette[4], lw=1),ax=ax)286 287 axfooter1.axis('off')288 axfooter3.axis('off')289 axheader.axis('off')290 291 axheader.text(s=f'{input.level_list()} In-Zone vs Out-of-Zone Awareness Value',fontsize=24,x=0.5,y=0,va='top',ha='center')292 293 axfooter1.text(0.05, -0.5,"By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12)294 axfooter3.text(0.95, -0.5, "Data: MLB",ha='right', va='bottom',fontsize=12) 295 fig.subplots_adjust(left=0.01, right=0.99, top=0.975, bottom=0.025)296 297 @output298 @render.plot(alt="hex_plot")299 @reactive.event(input.go, ignore_none=False)300 def dv_plot():301 302 if input.batter_id() is "":303 fig = plt.figure(figsize=(12, 12))304 fig.text(s='Please Select a Batter',x=0.5,y=0.5)305 return306 307 player_select = int(input.batter_id())308 player_select_full = batter_dict[player_select]309 310 311 df_will = df_model_2023[df_model_2023.batter_id == player_select].sort_values(by=['game_date','start_time'])312 df_will = df_will[df_will['level']==input.level_list()]313 # df_will['y_pred'] = df_will['y_pred'] - df_will['y_pred'].mean()314 315 win = max(1,int(input.rolling_window()))316 sns.set_theme(style="whitegrid", palette="pastel")317 #fig, ax = plt.subplots(1, 1, figsize=(10, 10),dpi=300)318 319 from matplotlib.gridspec import GridSpec320 # fig,ax = plt.subplots(figsize=(12, 12),dpi=150)321 fig = plt.figure(figsize=(12,12))322 gs = GridSpec(3, 3, height_ratios=[0.3,10,0.2], width_ratios=[0.01,2,0.01])323 324 axheader = fig.add_subplot(gs[0, :])325 ax10 = fig.add_subplot(gs[1, 0])326 ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position327 ax12 = fig.add_subplot(gs[1, 2])328 axfooter1 = fig.add_subplot(gs[-1, :])329 330 axheader.axis('off')331 ax10.axis('off')332 ax12.axis('off')333 axfooter1.axis('off')334 335 336 sns.lineplot( x= range(win,len(df_will.y_pred.rolling(window=win).mean())+1),337 y= df_will.y_pred.rolling(window=win).mean().dropna()*100,338 color=colour_palette[0],linewidth=2,ax=ax,zorder=100)339 340 ax.hlines(y=df_will.y_pred.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[0],linestyle='--',341 label=f'{player_select_full} Average: {df_will.y_pred.mean()*100:.2} xRV/100 ({p.ordinal(int(np.around(percentileofscore(df_model_2023_group_swing_plus_no.y_pred,df_will.y_pred.mean(), kind="strict"))))} Percentile)')342 343 # ax.hlines(y=df_model_2023.y_pred.std()*100,xmin=win,xmax=len(df_will))344 345 # sns.scatterplot( x= [976],346 # y= df_will.y_pred.rolling(window=win).mean().min()*100,347 # color=colour_palette[0],linewidth=2,ax=ax,zorder=100,s=100,edgecolor=colour_palette[7])348 349 350 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[1],linestyle='-.',alpha=1,351 label = f'{input.level_list()} Average: {df_model_2023_group_swing_plus_no.y_pred.mean()*100:.2f} xRV/100')352 353 ax.legend()354 355 hard_hit_dates = [df_model_2023_group_swing_plus_no.y_pred.quantile(0.9)*100,356 df_model_2023_group_swing_plus_no.y_pred.quantile(0.75)*100,357 df_model_2023_group_swing_plus_no.y_pred.quantile(0.25)*100,358 df_model_2023_group_swing_plus_no.y_pred.quantile(0.1)*100]359 360 361 362 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred.quantile(0.9)*100,xmin=win,xmax=len(df_will),color=colour_palette[2],linestyle='dotted',alpha=0.5,zorder=1)363 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred.quantile(0.75)*100,xmin=win,xmax=len(df_will),color=colour_palette[3],linestyle='dotted',alpha=0.5,zorder=1)364 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred.quantile(0.25)*100,xmin=win,xmax=len(df_will),color=colour_palette[4],linestyle='dotted',alpha=0.5,zorder=1)365 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred.quantile(0.1)*100,xmin=win,xmax=len(df_will),color=colour_palette[5],linestyle='dotted',alpha=0.5,zorder=1)366 367 hard_hit_text = ['90th %','75th %','25th %','10th %']368 for i, x in enumerate(hard_hit_dates):369 ax.text(min(win+win/1000,win+win+5), x ,hard_hit_text[i], rotation=0,va='center', ha='left',370 bbox=dict(facecolor='white',alpha=0.7, edgecolor=colour_palette[2+i], pad=2),zorder=1100)371 372 # # Annotate with an arrow373 # ax.annotate('June 6, 2023\nSeason Worst Decision Value', xy=(976, df_will.y_pred.rolling(window=win).mean().min()*100-0.03),374 # xytext=(976 - 150, df_will.y_pred.rolling(window=win).mean().min()*100 - 0.2),375 # arrowprops=dict(facecolor=colour_palette[7], shrink=0.01),zorder=150,fontsize=10,376 # bbox=dict(facecolor='white', edgecolor='black'),va='top')377 378 ax.set_xlim(win,len(df_will))379 #ax.set_ylim(-1.5,1.5)380 ax.set_yticks([-1.5,-1,-0.5,0,0.5,1,1.5])381 ax.set_xlabel('Pitch')382 ax.set_ylabel('Expected Run Value Added per 100 Pitches (xRV/100)')383 384 axheader.text(s=f'{player_select_full} - {win} Pitch Rolling Swing Decision Expected Run Value Added\n{input.level_list()} - {year_input}',x=0.5,y=-0.8,ha='center',va='bottom',fontsize=14)385 axfooter1.text(.05, 0.2, "By: Thomas Nestico",ha='left', va='bottom',fontsize=12)386 axfooter1.text(0.95, 0.2, "Data: MLB",ha='right', va='bottom',fontsize=12)387 388 fig.subplots_adjust(left=0.01, right=0.99, top=0.98, bottom=0.02)389 #fig.set_facecolor(colour_palette[5])390 391 @output392 @render.plot(alt="hex_plot")393 @reactive.event(input.go, ignore_none=False)394 def iz_plot():395 396 if input.batter_id() is "":397 fig = plt.figure(figsize=(12, 12))398 fig.text(s='Please Select a Batter',x=0.5,y=0.5)399 return400 401 player_select = int(input.batter_id())402 player_select_full = batter_dict[player_select]403 404 405 df_will = df_model_2023[df_model_2023.batter_id == player_select].sort_values(by=['game_date','start_time'])406 df_will = df_will[df_will['level']==input.level_list()]407 df_will = df_will[df_will['is_swing'] != 1]408 409 win = max(1,int(input.rolling_window()))410 sns.set_theme(style="whitegrid", palette="pastel")411 #fig, ax = plt.subplots(1, 1, figsize=(10, 10),dpi=300)412 413 from matplotlib.gridspec import GridSpec414 # fig,ax = plt.subplots(figsize=(12, 12),dpi=150)415 fig = plt.figure(figsize=(12,12))416 gs = GridSpec(3, 3, height_ratios=[0.3,10,0.2], width_ratios=[0.01,2,0.01])417 418 axheader = fig.add_subplot(gs[0, :])419 ax10 = fig.add_subplot(gs[1, 0])420 ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position421 ax12 = fig.add_subplot(gs[1, 2])422 axfooter1 = fig.add_subplot(gs[-1, :])423 424 axheader.axis('off')425 ax10.axis('off')426 ax12.axis('off')427 axfooter1.axis('off')428 429 430 sns.lineplot( x= range(win,len(df_will.y_pred.rolling(window=win).mean())+1),431 y= df_will.y_pred.rolling(window=win).mean().dropna()*100,432 color=colour_palette[0],linewidth=2,ax=ax,zorder=100)433 434 ax.hlines(y=df_will.y_pred.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[0],linestyle='--',435 label=f'{player_select_full} Average: {df_will.y_pred.mean()*100:.2} xRV/100 ({p.ordinal(int(np.around(percentileofscore(df_model_2023_group_swing_plus_no.y_pred_no_swing,df_will.y_pred.mean(), kind="strict"))))} Percentile)')436 437 # ax.hlines(y=df_model_2023.y_pred_no_swing.std()*100,xmin=win,xmax=len(df_will))438 439 # sns.scatterplot( x= [976],440 # y= df_will.y_pred.rolling(window=win).mean().min()*100,441 # color=colour_palette[0],linewidth=2,ax=ax,zorder=100,s=100,edgecolor=colour_palette[7])442 443 444 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_no_swing.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[1],linestyle='-.',alpha=1,445 label = f'{input.level_list()} Average: {df_model_2023_group_swing_plus_no.y_pred_no_swing.mean()*100:.2} xRV/100')446 447 ax.legend()448 449 hard_hit_dates = [df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.9)*100,450 df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.75)*100,451 df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.25)*100,452 df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.1)*100]453 454 455 456 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.9)*100,xmin=win,xmax=len(df_will),color=colour_palette[2],linestyle='dotted',alpha=0.5,zorder=1)457 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.75)*100,xmin=win,xmax=len(df_will),color=colour_palette[3],linestyle='dotted',alpha=0.5,zorder=1)458 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.25)*100,xmin=win,xmax=len(df_will),color=colour_palette[4],linestyle='dotted',alpha=0.5,zorder=1)459 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.1)*100,xmin=win,xmax=len(df_will),color=colour_palette[5],linestyle='dotted',alpha=0.5,zorder=1)460 461 hard_hit_text = ['90th %','75th %','25th %','10th %']462 for i, x in enumerate(hard_hit_dates):463 ax.text(min(win+win/1000,win+win+5), x ,hard_hit_text[i], rotation=0,va='center', ha='left',464 bbox=dict(facecolor='white',alpha=0.7, edgecolor=colour_palette[2+i], pad=2),zorder=111)465 466 # # Annotate with an arrow467 # ax.annotate('June 6, 2023\nSeason Worst Decision Value', xy=(976, df_will.y_pred.rolling(window=win).mean().min()*100-0.03),468 # xytext=(976 - 150, df_will.y_pred.rolling(window=win).mean().min()*100 - 0.2),469 # arrowprops=dict(facecolor=colour_palette[7], shrink=0.01),zorder=150,fontsize=10,470 # bbox=dict(facecolor='white', edgecolor='black'),va='top')471 472 ax.set_xlim(win,len(df_will))473 ax.set_yticks([1.0,1.5,2.0,2.5,3.0])474 # ax.set_ylim(1,3)475 476 ax.set_xlabel('Takes')477 ax.set_ylabel('Expected Run Value Added per 100 Pitches (xRV/100)')478 479 axheader.text(s=f'{player_select_full} - {win} Pitch Rolling In-Zone Awareness Expected Run Value Added\n{input.level_list()} - {year_input}',x=0.5,y=-0.8,ha='center',va='bottom',fontsize=14)480 axfooter1.text(.05, 0.2, "By: Thomas Nestico",ha='left', va='bottom',fontsize=12)481 axfooter1.text(0.95, 0.2, "Data: MLB",ha='right', va='bottom',fontsize=12)482 483 fig.subplots_adjust(left=0.01, right=0.99, top=0.98, bottom=0.02)484 485 @output486 @render.plot(alt="hex_plot")487 @reactive.event(input.go, ignore_none=False)488 def oz_plot():489 if input.batter_id() is "":490 fig = plt.figure(figsize=(12, 12))491 fig.text(s='Please Select a Batter',x=0.5,y=0.5)492 return493 494 player_select = int(input.batter_id())495 player_select_full = batter_dict[player_select]496 497 498 499 df_will = df_model_2023[df_model_2023.batter_id == player_select].sort_values(by=['game_date','start_time'])500 df_will = df_will[df_will['level']==input.level_list()]501 df_will = df_will[df_will['is_swing'] == 1]502 503 win = max(1,int(input.rolling_window()))504 sns.set_theme(style="whitegrid", palette="pastel")505 #fig, ax = plt.subplots(1, 1, figsize=(10, 10),dpi=300)506 507 from matplotlib.gridspec import GridSpec508 # fig,ax = plt.subplots(figsize=(12, 12),dpi=150)509 fig = plt.figure(figsize=(12,12))510 gs = GridSpec(3, 3, height_ratios=[0.3,10,0.2], width_ratios=[0.01,2,0.01])511 512 axheader = fig.add_subplot(gs[0, :])513 ax10 = fig.add_subplot(gs[1, 0])514 ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position515 ax12 = fig.add_subplot(gs[1, 2])516 axfooter1 = fig.add_subplot(gs[-1, :])517 518 axheader.axis('off')519 ax10.axis('off')520 ax12.axis('off')521 axfooter1.axis('off')522 523 524 sns.lineplot( x= range(win,len(df_will.y_pred.rolling(window=win).mean())+1),525 y= df_will.y_pred.rolling(window=win).mean().dropna()*100,526 color=colour_palette[0],linewidth=2,ax=ax,zorder=100)527 528 ax.hlines(y=df_will.y_pred.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[0],linestyle='--',529 label=f'{player_select_full} Average: {df_will.y_pred.mean()*100:.2} xRV/100 ({p.ordinal(int(np.around(percentileofscore(df_model_2023_group_swing_plus_no.y_pred_swing,df_will.y_pred.mean(), kind="strict"))))} Percentile)')530 531 # ax.hlines(y=df_model_2023.y_pred_swing.std()*100,xmin=win,xmax=len(df_will))532 533 # sns.scatterplot( x= [976],534 # y= df_will.y_pred.rolling(window=win).mean().min()*100,535 # color=colour_palette[0],linewidth=2,ax=ax,zorder=100,s=100,edgecolor=colour_palette[7])536 537 538 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_swing.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[1],linestyle='-.',alpha=1,539 label = f'{input.level_list()} Average: {df_model_2023_group_swing_plus_no.y_pred_swing.mean()*100:.2} xRV/100')540 541 ax.legend()542 543 hard_hit_dates = [df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.9)*100,544 df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.75)*100,545 df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.25)*100,546 df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.1)*100]547 548 549 550 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.9)*100,xmin=win,xmax=len(df_will),color=colour_palette[2],linestyle='dotted',alpha=0.5,zorder=1)551 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.75)*100,xmin=win,xmax=len(df_will),color=colour_palette[3],linestyle='dotted',alpha=0.5,zorder=1)552 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.25)*100,xmin=win,xmax=len(df_will),color=colour_palette[4],linestyle='dotted',alpha=0.5,zorder=1)553 ax.hlines(y=df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.1)*100,xmin=win,xmax=len(df_will),color=colour_palette[5],linestyle='dotted',alpha=0.5,zorder=1)554 555 hard_hit_text = ['90th %','75th %','25th %','10th %']556 for i, x in enumerate(hard_hit_dates):557 ax.text(min(win+win/1000,win+win+5), x ,hard_hit_text[i], rotation=0,va='center', ha='left',558 bbox=dict(facecolor='white',alpha=0.7, edgecolor=colour_palette[2+i], pad=2),zorder=111)559 560 # # Annotate with an arrow561 # ax.annotate('June 6, 2023\nSeason Worst Decision Value', xy=(976, df_will.y_pred.rolling(window=win).mean().min()*100-0.03),562 # xytext=(976 - 150, df_will.y_pred.rolling(window=win).mean().min()*100 - 0.2),563 # arrowprops=dict(facecolor=colour_palette[7], shrink=0.01),zorder=150,fontsize=10,564 # bbox=dict(facecolor='white', edgecolor='black'),va='top')565 566 ax.set_xlim(win,len(df_will))567 #ax.set_ylim(-3.25,-1.25)568 ax.set_yticks([-3.25,-2.75,-2.25,-1.75,-1.25])569 ax.set_xlabel('Swing')570 ax.set_ylabel('Expected Run Value Added per 100 Pitches (xRV/100)')571 572 axheader.text(s=f'{player_select_full} - {win} Pitch Rolling Out of Zone Awareness Expected Run Value Added\n{input.level_list()} - {year_input}',x=0.5,y=-0.8,ha='center',va='bottom',fontsize=14)573 axfooter1.text(.05, 0.2, "By: Thomas Nestico",ha='left', va='bottom',fontsize=12)574 axfooter1.text(0.95, 0.2, "Data: MLB",ha='right', va='bottom',fontsize=12)575 576 fig.subplots_adjust(left=0.01, right=0.99, top=0.98, bottom=0.02) 577 578app = App(ui.page_fluid(579 ui.tags.base(href=base_url),580 ui.tags.div(581 {"style": "width:90%;margin: 0 auto;max-width: 1600px;"},582 ui.tags.style(583 """584 h4 {585 margin-top: 1em;font-size:35px;586 }587 h2{588 font-size:25px;589 }590 """591 ),592 shinyswatch.theme.simplex(),593 ui.tags.h4("TJStats"),594 ui.tags.i("Baseball Analytics and Visualizations"),595 # ui.markdown("""<a href='https://www.patreon.com/tj_stats'>Support me on Patreon for Access to 2024 Apps</a><sup>1</sup>"""),596 # # ui.navset_tab(597 # # ui.nav_control(598 # # ui.a(599 # # "Home",600 # # href="home/"601 # # ),602 # # ),603 # # ui.nav_menu(604 # # "Batter Charts",605 # # ui.nav_control(606 # # ui.a(607 # # "Batting Rolling",608 # # href="rolling_batter/"609 # # ),610 # # ui.a(611 # # "Spray & Damage",612 # # href="https://nesticot-tjstats-site-spray.hf.space/"613 # # ),614 # # ui.a(615 # # "Decision Value",616 # # href="decision_value/"617 # # ),618 # # # ui.a(619 # # # "Damage Model",620 # # # href="damage_model/"621 # # # ),622 # # ui.a(623 # # "Batter Scatter",624 # # href="batter_scatter/"625 # # ),626 # # # ui.a(627 # # # "EV vs LA Plot",628 # # # href="ev_angle/"629 # # # ),630 # # ui.a(631 # # "Statcast Compare",632 # # href="statcast_compare/"633 # # )634 # # ),635 # # ),636 # # ui.nav_menu(637 # # "Pitcher Charts",638 # # ui.nav_control(639 # # ui.a(640 # # "Pitcher Rolling",641 # # href="rolling_pitcher/"642 # # ),643 # # ui.a(644 # # "Pitcher Summary",645 # # href="pitching_summary_graphic_new/"646 # # ),647 # # ui.a(648 # # "Pitcher Scatter",649 # # href="pitcher_scatter/"650 # # )651 # # ),652 # # )),653 # ui.navset_tab(654 # ui.nav_control(655 # ui.a(656 # "Home",657 # href="home/"658 # ),659 # ),660 # ui.nav_menu(661 # "Batter Charts",662 # ui.nav_control(663 # ui.a(664 # "Batting Rolling",665 # href="https://nesticot-tjstats-site-rolling-batter.hf.space/"666 # ),667 # ui.a(668 # "Spray",669 # href="https://nesticot-tjstats-site-spray.hf.space/"670 # ),671 # ui.a(672 # "Decision Value",673 # href="https://nesticot-tjstats-site-decision-value.hf.space/"674 # ),675 # ui.a(676 # "Damage Model",677 # href="https://nesticot-tjstats-site-damage.hf.space/"678 # ),679 # ui.a(680 # "Batter Scatter",681 # href="https://nesticot-tjstats-site-batter-scatter.hf.space/"682 # ),683 # ui.a(684 # "EV vs LA Plot",685 # href="https://nesticot-tjstats-site-ev-angle.hf.space/"686 # ),687 # ui.a(688 # "Statcast Compare",689 # href="https://nesticot-tjstats-site-statcast-compare.hf.space/"690 # ),691 # ui.a(692 # "MLB/MiLB Cards",693 # href="https://nesticot-tjstats-site-mlb-cards.hf.space/"694 # )695 # ),696 # ),697 # ui.nav_menu(698 # "Pitcher Charts",699 # ui.nav_control(700 # ui.a(701 # "Pitcher Rolling",702 # href="https://nesticot-tjstats-site-rolling-pitcher.hf.space/"703 # ),704 # ui.a(705 # "Pitcher Summary",706 # href="https://nesticot-tjstats-site-pitching-summary-graphic-new.hf.space/"707 # ),708 # ui.a(709 # "Pitcher Scatter",710 # href="https://nesticot-tjstats-site-pitcher-scatter.hf.space"711 # )712 # ),713 # )), 714 ui.row(715 ui.layout_sidebar(716 717 ui.panel_sidebar(718 719 720 ui.input_numeric("pitch_min",721 "Select Pitch Minimum [min. 50] (Scatter)",722 value=100,723 min=50), 724 725 ui.input_select("name_list",726 "Select Players to List (Scatter)",727 batter_dict,728 selectize=True,729 multiple=True),730 ui.input_select("batter_id",731 "Select Batter (Rolling)",732 batter_dict,733 width=1,734 size=1,735 selectize=True),736 ui.input_numeric("rolling_window",737 "Select Rolling Window (Rolling)",738 value=100,739 min=1), 740 741 ui.input_select("level_list",742 "Select Level",743 ['MLB','AAA'],744 selected='MLB'),745 ui.input_action_button("go", "Generate",class_="btn-primary"),746 ),747 748 ui.panel_main( 749 ui.navset_tab(750 751 ui.nav("Scatter Plot",752 ui.output_plot('scatter_plot',753 width='1000px',754 height='1000px')),755 ui.nav("Rolling DV",756 ui.output_plot('dv_plot',757 width='1000px',758 height='1000px')),759 ui.nav("Rolling In-Zone",760 ui.output_plot('iz_plot',761 width='1000px',762 height='1000px')),763 ui.nav("Rolling Out-of-Zone",764 ui.output_plot('oz_plot',765 width='1000px',766 height='1000px'))767 ))768 )),)),server)