CoolFace
Apppublic

TJStatsApps/2025_decision_value

sourceHugging Facemitupdated 1y agoView on Hugging Face
0likes
app.py768 linesDownload Raw Back to root
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)