nesticot commited on
Commit
bb22c9e
1 Parent(s): 302de4b

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +421 -35
app.py CHANGED
@@ -1,45 +1,431 @@
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
+ ##### games.,py #####
2
+
3
+ # Import modules
4
+ from shiny import *
 
 
 
5
  import shinyswatch
6
+ #import plotly.express as px
7
+ from shinywidgets import output_widget, render_widget
8
+ import pandas as pd
9
+ from configure import base_url
10
+ import math
11
+ import datetime
12
+ import datasets
13
+ from datasets import load_dataset
14
+ import numpy as np
15
+ import matplotlib
16
+ from matplotlib.ticker import MaxNLocator
17
+ from matplotlib.gridspec import GridSpec
18
+ import matplotlib.pyplot as plt
19
+ from scipy.stats import gaussian_kde
20
 
21
+ ### Import Datasets
22
+ dataset = load_dataset('nesticot/mlb_data', data_files=['mlb_pitch_data_2023.csv',
23
+ 'mlb_pitch_data_2022.csv'])
24
+ dataset_train = dataset['train']
25
+ df_2023 = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True)
26
+ # Paths to data
27
+ ### Normalize Hit Locations
28
+ df_2023['hit_x'] = df_2023['hit_x'] - 126#df_2023['hit_x'].median()
29
+ df_2023['hit_y'] = -df_2023['hit_y']+204.5#df_2023['hit_y'].quantile(0.9999)
30
 
31
+ df_2023['hit_x_og'] = df_2023['hit_x']
32
+ df_2023.loc[df_2023['batter_hand'] == 'R','hit_x'] = -1*df_2023.loc[df_2023['batter_hand'] == 'R','hit_x']
 
 
 
 
 
33
 
34
+ ### Calculate Horizontal Launch Angles
35
+ df_2023['h_la'] = np.arctan(df_2023['hit_x'] / df_2023['hit_y'])*180/np.pi
36
+ conditions_ss = [
37
+ (df_2023['h_la']<-16+5/6),
38
+ (df_2023['h_la']<16+5/6)&(df_2023['h_la']>=-16+5/6),
39
+ (df_2023['h_la']>=16+5/6)
40
+ ]
41
 
42
+ choices_ss = ['Oppo','Straight','Pull']
43
+ df_2023['traj'] = np.select(conditions_ss, choices_ss, default=np.nan)
44
+ df_2023['bip'] = [1 if x > 0 else np.nan for x in df_2023['launch_speed']]
45
 
46
+ conditions_woba = [
47
+ (df_2023['event_type']=='walk'),
48
+ (df_2023['event_type']=='hit_by_pitch'),
49
+ (df_2023['event_type']=='single'),
50
+ (df_2023['event_type']=='double'),
51
+ (df_2023['event_type']=='triple'),
52
+ (df_2023['event_type']=='home_run'),
53
+ ]
54
 
55
+ choices_woba = [0.698,
56
+ 0.728,
57
+ 0.887,
58
+ 1.253,
59
+ 1.583,
60
+ 2.027]
61
 
62
+ df_2023['woba'] = np.select(conditions_woba, choices_woba, default=0)
 
 
 
 
 
 
63
 
 
 
 
 
64
 
65
+
66
+ df_2023_bip = df_2023[~df_2023['bip'].isnull()].dropna(subset=['h_la','launch_angle'])
67
+ df_2023_bip['h_la'] = df_2023_bip['h_la'].round(0)
68
+
69
+
70
+ df_2023_bip['season'] = df_2023_bip['game_date'].str[0:4].astype(int)
71
+
72
+ df_2023_bip = df_2023_bip[df_2023_bip['season'] == 2023]
73
+ df_2022_bip = df_2023_bip[df_2023_bip['season'] == 2022]
74
+
75
+ batter_dict = df_2023_bip.sort_values('batter_name').set_index('batter_id')['batter_name'].to_dict()
76
+
77
+
78
+
79
+
80
+
81
+ def server(input,output,session):
82
+ @output
83
+ @render.plot(alt="plot")
84
+ @reactive.event(input.go, ignore_none=False)
85
+ def plot():
86
+
87
+ batter_id_select = int(input.batter_id())
88
+ df_batter_2023 = df_2023_bip.loc[(df_2023_bip['batter_id'] == batter_id_select)&(df_2023_bip['season']==2023)]
89
+ df_batter_2022 = df_2023_bip.loc[(df_2023_bip['batter_id'] == batter_id_select)&(df_2023_bip['season']==2022)]
90
+
91
+ df_non_batter_2023 = df_2023_bip.loc[(df_2023_bip['batter_id'] != batter_id_select)&(df_2023_bip['season']==2023)]
92
+ df_non_batter_2022 = df_2023_bip.loc[(df_2023_bip['batter_id'] != batter_id_select)&(df_2023_bip['season']==2022)]
93
+
94
+ traj_df = df_batter_2023.groupby(['traj'])['launch_speed'].count() / len(df_batter_2023)
95
+ trajectory_df = df_batter_2023.groupby(['trajectory'])['launch_speed'].count() / len(df_batter_2023)#.loc['Oppo']
96
+
97
+
98
+
99
+
100
+ colour_palette = ['#FFB000','#648FFF','#785EF0',
101
+ '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
102
+
103
+ fig = plt.figure(figsize=(10, 10))
104
+
105
+
106
+
107
+ # Create a 2x2 grid of subplots using GridSpec
108
+ gs = GridSpec(3, 3, width_ratios=[0.1,0.8,0.1], height_ratios=[0.1,0.8,0.1])
109
+
110
+ # ax00 = fig.add_subplot(gs[0, 0])
111
+ ax01 = fig.add_subplot(gs[0, :]) # Subplot at the top-right position
112
+ # ax02 = fig.add_subplot(gs[0, 2])
113
+ # Subplot spanning the entire bottom row
114
+ ax10 = fig.add_subplot(gs[1, 0])
115
+ ax11 = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
116
+ ax12 = fig.add_subplot(gs[1, 2])
117
+ # ax20 = fig.add_subplot(gs[2, 0])
118
+ ax21 = fig.add_subplot(gs[2, :]) # Subplot at the top-right position
119
+ # ax22 = fig.add_subplot(gs[2, 2])
120
+
121
+ initial_position = ax12.get_position()
122
+
123
+ # Change the size of the axis
124
+ # new_width = 0.06 # Set your desired width
125
+ # new_height = 0.4 # Set your desired height
126
+ # new_position = [initial_position.x0-0.01, initial_position.y0+0.065, new_width, new_height]
127
+ # ax12.set_position(new_position)
128
+
129
+ cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],'#ffffff',colour_palette[0]])
130
+ # Generate two sets of two-dimensional data
131
+ # data1 = np.random.multivariate_normal([0, 0], [[1, 0.5], [0.5, 1]], 1000)
132
+ # data2 = np.random.multivariate_normal([3, 3], [[1, -0.5], [-0.5, 1]], 1000)
133
+ bat_hand = df_batter_2023.groupby('batter_hand')['launch_speed'].count().sort_values(ascending=False).index[0]
134
+
135
+ bat_hand_value = 1
136
+
137
+ if bat_hand == 'R':
138
+ bat_hand_value = -1
139
+
140
+ kde1_df = df_batter_2023[['h_la','launch_angle']]
141
+ kde1_df['h_la'] = kde1_df['h_la'] * bat_hand_value
142
+ kde2_df = df_non_batter_2023[['h_la','launch_angle']].sample(n=50000, random_state=42)
143
+ kde2_df['h_la'] = kde2_df['h_la'] * bat_hand_value
144
+
145
+
146
+ # Calculate 2D KDE for each dataset
147
+ kde1 = gaussian_kde(kde1_df.values.T)
148
+ kde2 = gaussian_kde(kde2_df.values.T)
149
+
150
+ # Generate a grid of points for evaluation
151
+ x, y = np.meshgrid(np.arange(-45, 46,1 ), np.arange(-30, 61,1 ))
152
+ positions = np.vstack([x.ravel(), y.ravel()])
153
+
154
+ # Evaluate the KDEs on the grid
155
+ kde1_values = np.reshape(kde1(positions).T, x.shape)
156
+ kde2_values = np.reshape(kde2(positions).T, x.shape)
157
+
158
+ # Subtract one KDE from the other
159
+ result_kde_values = kde1_values - kde2_values
160
+
161
+ # Normalize the array to the range [0, 1]
162
+ # result_kde_values = (result_kde_values - np.min(result_kde_values)) / (np.max(result_kde_values) - np.min(result_kde_values))
163
+ result_kde_values = (result_kde_values - np.mean(result_kde_values)) / (np.std(result_kde_values))
164
+
165
+ result_kde_values = np.clip(result_kde_values, -3, 3)
166
+ # # Plot the original KDEs
167
+ # plt.contourf(x, y, kde1_values, cmap='Blues', alpha=0.5, levels=20)
168
+ # plt.contourf(x, y, kde2_values, cmap='Reds', alpha=0.5, levels=20)
169
+
170
+ # Plot the subtracted KDE
171
+ # Set the number of levels and midrange value
172
+ # Set the number of levels and midrange value
173
+ num_levels = 14
174
+ midrange_value = 0
175
+
176
+ # Create a filled contour plot with specified levels
177
+ levels = np.linspace(-3, 3, num_levels)
178
+
179
+ batter_plot = ax11.contourf(x, y, result_kde_values, cmap=cmap_hue, levels=levels, vmin=-3, vmax=3)
180
+
181
+
182
+ ax11.hlines(y=10,xmin=45,xmax=-45,color=colour_palette[3],linewidth=1)
183
+ ax11.hlines(y=25,xmin=45,xmax=-45,color=colour_palette[3],linewidth=1)
184
+ ax11.hlines(y=50,xmin=45,xmax=-45,color=colour_palette[3],linewidth=1)
185
+
186
+ ax11.vlines(x=-15,ymin=-30,ymax=60,color=colour_palette[3],linewidth=1)
187
+ ax11.vlines(x=15,ymin=-30,ymax=60,color=colour_palette[3],linewidth=1)
188
+ #ax11.axis('square')
189
+ #ax11.axis('off')
190
+ #ax.hlines(y=10,xmin=-45,xmax=-45)
191
+ # Add labels and legend
192
+ #plt.xlabel('X-axis')
193
+ #plt.ylabel('Y-axis')
194
+ #ax.plot('equal')
195
+ #plt.gca().set_aspect('equal')
196
+
197
+ #Choose a mappable (can be any plot or image)
198
+ ax12.set_ylim(0,1)
199
+ cbar = plt.colorbar(batter_plot, cax=ax12, orientation='vertical',shrink=1)
200
+ cbar.set_ticks([])
201
+ # Set the colorbar to have 13 levels
202
+ cbar_locator = MaxNLocator(nbins=13)
203
+ cbar.locator = cbar_locator
204
+ cbar.update_ticks()
205
+ #cbar.set_clim(vmin=-3, vmax=)
206
+ # Set ticks and tick labels
207
+ # cbar.set_ticks(np.linspace(-3, 3, 13))
208
+ # cbar.set_ticklabels(np.linspace(0, 3, 13))
209
+ cbar.set_ticks([])
210
+
211
+
212
+
213
+
214
+ ax10.text(s=f"Pop Up\n({trajectory_df.loc['popup']:.1%})",
215
+ x=1,
216
+ y=0.95,va='center',ha='right',fontsize=16)
217
+ # Choose a mappable (can be any plot or image)
218
+ ax10.text(s=f"Fly Ball\n({trajectory_df.loc['fly_ball']:.1%})",
219
+ x=1,
220
+ y=0.75,va='center',ha='right',fontsize=16)
221
+
222
+ ax10.text(s=f"Line\nDrive\n({trajectory_df.loc['line_drive']:.1%})",
223
+ x=1,
224
+ y=0.53,va='center',ha='right',fontsize=16)
225
+
226
+
227
+ ax10.text(s=f"Ground\nBall\n({trajectory_df.loc['ground_ball']:.1%})",
228
+ x=1,
229
+ y=0.23,va='center',ha='right',fontsize=16)
230
+ #ax12.axis(True)
231
+ # Set equal aspect ratio for the contour plot
232
+
233
+ if bat_hand == 'R':
234
+
235
+
236
+ ax21.text(s=f"Pull\n({traj_df.loc['Pull']:.1%})",
237
+ x=0.2+1/16*0.8,
238
+ y=1,va='top',ha='center',fontsize=16)
239
+
240
+ ax21.text(s=f"Straight\n({traj_df.loc['Straight']:.1%})",
241
+ x=0.5,
242
+ y=1,va='top',ha='center',fontsize=16)
243
+
244
+ ax21.text(s=f"Oppo\n({traj_df.loc['Oppo']:.1%})",
245
+ x=0.8-1/16*0.8,
246
+ y=1,va='top',ha='center',fontsize=16)
247
+
248
+ else:
249
+
250
+ ax21.text(s=f"Pull\n({traj_df.loc['Pull']:.1%})",
251
+ x=0.8-1/16*0.8,
252
+ y=1,va='top',ha='center',fontsize=16)
253
+
254
+ ax21.text(s=f"Straight\n({traj_df.loc['Straight']:.1%})",
255
+ x=0.5,
256
+ y=1,va='top',ha='center',fontsize=16)
257
+
258
+ ax21.text(s=f"Oppo\n({traj_df.loc['Oppo']:.1%})",
259
+ x=0.2+1/16*0.8,
260
+ y=1,va='top',ha='center',fontsize=16)
261
+
262
+ # Define the initial position of the axis
263
+
264
+ # Customize colorbar properties
265
+ # cbar = fig.colorbar(orientation='vertical', pad=0.1,ax=ax12)
266
+ #cbar.set_label('Difference', rotation=270, labelpad=15)
267
+ # Show the plot
268
+ # ax21.text(0.0, 0., "By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12)
269
+ # ax21.text(1, 0., "Data: MLB",ha='right', va='bottom',fontsize=12)
270
+ # ax21.text(0.5, 0., "Inspired by @blandalytics",ha='center', va='bottom',fontsize=12)
271
+
272
+ # ax00.axis('off')
273
+ ax01.axis('off')
274
+ # ax02.axis('off')
275
+ ax10.axis('off')
276
+ #ax11.axis('off')
277
+ #ax12.axis('off')
278
+ # ax20.axis('off')
279
+ ax21.axis('off')
280
+ # ax22.axis('off')
281
+
282
+ ax21.text(0.0, 0., "By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12)
283
+ ax21.text(0.98, 0., "Data: MLB",ha='right', va='bottom',fontsize=12)
284
+ ax21.text(0.5, 0., "Inspired by @blandalytics",ha='center', va='bottom',fontsize=12)
285
+
286
+
287
+ ax11.set_xticks([])
288
+ ax11.set_yticks([])
289
+
290
+ # ax12.text(s='Same',x=np.mean([x for x in ax12.get_xlim()]),y=np.median([x for x in ax12.get_ylim()]),
291
+ # va='center',ha='center',fontsize=12)
292
+
293
+ # ax12.text(s='More\nOften',x=0.5,y=0.74,
294
+ # va='top',ha='center',fontsize=12)
295
+
296
+ ax12.text(s='+3σ',x=0.5,y=3-1/14*3,
297
+ va='center',ha='center',fontsize=12)
298
+
299
+ ax12.text(s='+2σ',x=0.5,y=2-1/14*2,
300
+ va='center',ha='center',fontsize=12)
301
+
302
+ ax12.text(s='+1σ',x=0.5,y=1-1/14*1,
303
+ va='center',ha='center',fontsize=12)
304
+
305
+
306
+ ax12.text(s='±0σ',x=0.5,y=0,
307
+ va='center',ha='center',fontsize=12)
308
+
309
+ ax12.text(s='-1σ',x=0.5,y=-1-1/14*-1,
310
+ va='center',ha='center',fontsize=12)
311
+
312
+ ax12.text(s='-2σ',x=0.5,y=-2-1/14*-2,
313
+ va='center',ha='center',fontsize=12)
314
+
315
+ ax12.text(s='-3σ',x=0.5,y=-3-1/14*-3,
316
+ va='center',ha='center',fontsize=12)
317
+
318
+ # # ax12.text(s='Less\nOften',x=0.5,y=0.26,
319
+ # # va='bottom',ha='center',fontsize=12)
320
+
321
+ ax01.text(s=f"{df_batter_2023['batter_name'].values[0]}'s 2023 Batted Ball Tendencies",
322
+ x=0.5,
323
+ y=0.8,va='top',ha='center',fontsize=20)
324
+
325
+ ax01.text(s=f"(Compared to rest of MLB)",
326
+ x=0.5,
327
+ y=0.3,va='top',ha='center',fontsize=16)
328
+
329
+ #plt.show()
330
+
331
+ App(ui.page_fluid(
332
+ ui.tags.base(href=base_url),
333
+ ui.tags.div(
334
+ {"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
335
+ ui.tags.style(
336
+ """
337
+ h4 {
338
+ margin-top: 1em;font-size:35px;
339
+ }
340
+ h2{
341
+ font-size:25px;
342
+ }
343
+ """
344
+ ),
345
+ shinyswatch.theme.simplex(),
346
+ ui.tags.h4("TJStats"),
347
+ ui.tags.i("Baseball Analytics and Visualizations"),
348
+ ui.markdown("""<a href='https://www.patreon.com/tj_stats'>Support me on Patreon for Access to 2024 Apps</a><sup>1</sup>"""),
349
+ ui.navset_tab(
350
+ ui.nav_control(
351
+ ui.a(
352
+ "Home",
353
+ href="home/"
354
+ ),
355
+ ),
356
+ ui.nav_menu(
357
+ "Batter Charts",
358
+ ui.nav_control(
359
+ ui.a(
360
+ "Batting Rolling",
361
+ href="rolling_batter/"
362
+ ),
363
+ ui.a(
364
+ "Spray",
365
+ href="spray/"
366
+ ),
367
+ ui.a(
368
+ "Decision Value",
369
+ href="decision_value/"
370
+ ),
371
+ ui.a(
372
+ "Damage Model",
373
+ href="damage_model/"
374
+ ),
375
+ ui.a(
376
+ "Batter Scatter",
377
+ href="batter_scatter/"
378
+ ),
379
+ ui.a(
380
+ "EV vs LA Plot",
381
+ href="ev_angle/"
382
+ ),
383
+ ui.a(
384
+ "Statcast Compare",
385
+ href="statcast_compare/"
386
+ )
387
+ ,
388
+ ui.a(
389
+ "MLB/MiLB Cards",
390
+ href="statcast_compare/"
391
+ )
392
+ ),
393
+ ),
394
+ ui.nav_menu(
395
+ "Pitcher Charts",
396
+ ui.nav_control(
397
+ ui.a(
398
+ "Pitcher Rolling",
399
+ href="rolling_pitcher/"
400
+ ),
401
+ ui.a(
402
+ "Pitcher Summary",
403
+ href="pitching_summary_graphic_new/"
404
+ ),
405
+ ui.a(
406
+ "Pitcher Scatter",
407
+ href="pitcher_scatter/"
408
+ )
409
+ ),
410
+ )),ui.row(
411
+ ui.layout_sidebar(
412
+
413
+ ui.panel_sidebar(
414
+ ui.input_select("batter_id",
415
+ "Select Batter",
416
+ batter_dict,
417
+ width=1,
418
+ size=1,
419
+ selectize=True),
420
+ ui.input_action_button("go", "Generate",class_="btn-primary",
421
+ )),
422
+
423
+ ui.panel_main(
424
+ ui.navset_tab(
425
+
426
+ ui.nav("2023 vs MLB",
427
+ ui.output_plot('plot',
428
+ width='1000px',
429
+ height='1000px')),
430
+ ))
431
+ )),)),server)