Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
@@ -1,45 +1,431 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
#Import modules
|
4 |
-
from
|
5 |
-
from starlette.routing import Mount
|
6 |
-
from starlette.staticfiles import StaticFiles
|
7 |
-
from shiny import App, ui
|
8 |
import shinyswatch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
|
13 |
-
|
14 |
-
|
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 |
-
|
22 |
-
|
23 |
-
|
|
|
|
|
|
|
|
|
24 |
|
|
|
|
|
|
|
25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
|
27 |
-
|
28 |
-
|
29 |
-
|
|
|
|
|
|
|
30 |
|
31 |
-
|
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 |
-
|
45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|