nesticot commited on
Commit
968950a
1 Parent(s): d4271d2

Update app.py

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