nesticot commited on
Commit
6c19697
1 Parent(s): fbc8a8b

Upload 28 files

Browse files
.dockerignore ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ **/__pycache__
2
+ **/.venv
3
+ **/.classpath
4
+ **/.dockerignore
5
+ **/.env
6
+ **/.git
7
+ **/.gitignore
8
+ **/.project
9
+ **/.settings
10
+ **/.toolstarget
11
+ **/.vs
12
+ **/.vscode
13
+ **/*.*proj.user
14
+ **/*.dbmdl
15
+ **/*.jfm
16
+ **/bin
17
+ **/charts
18
+ **/docker-compose*
19
+ **/compose*
20
+ **/Dockerfile*
21
+ **/node_modules
22
+ **/npm-debug.log
23
+ **/obj
24
+ **/secrets.dev.yaml
25
+ **/values.dev.yaml
26
+ LICENSE
27
+ README.md
Dockerfile CHANGED
@@ -1,20 +1,16 @@
1
  FROM python:3.9
2
 
3
- WORKDIR /code
 
 
4
 
5
- COPY ./requirements.txt /code/requirements.txt
 
 
 
6
 
7
- RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
 
8
 
9
- # Switch to the "user" user
10
- RUN useradd -m -u 1000 user
11
- USER user
12
- ENV HOME=/home/user \
13
- PATH=/home/user/.local/bin:$PATH
14
-
15
-
16
- COPY . .
17
-
18
- EXPOSE 7860
19
-
20
- CMD ["shiny", "run", "app.py", "--host", "0.0.0.0", "--port", "7860"]
 
1
  FROM python:3.9
2
 
3
+ # Install dependencies
4
+ COPY requirements.txt /app/
5
+ RUN pip install -r /app/requirements.txt
6
 
7
+ # Copy app files
8
+ COPY app_name /app/app_name/
9
+ COPY static/ /app/static/
10
+ COPY templates/ /app/templates/
11
 
12
+ # Set working directory
13
+ WORKDIR /app/app_name
14
 
15
+ # Set the command to run the app
16
+ CMD ["python", "app.py"]
 
 
 
 
 
 
 
 
 
 
__pycache__/app.cpython-311.pyc ADDED
Binary file (27.8 kB). View file
 
__pycache__/app.cpython-39.pyc ADDED
Binary file (767 Bytes). View file
 
__pycache__/batter_scatter.cpython-39.pyc ADDED
Binary file (11.4 kB). View file
 
__pycache__/configure.cpython-39.pyc ADDED
Binary file (215 Bytes). View file
 
__pycache__/damage.cpython-39.pyc ADDED
Binary file (13.8 kB). View file
 
__pycache__/decision_value.cpython-39.pyc ADDED
Binary file (15.8 kB). View file
 
__pycache__/ev_angle.cpython-39.pyc ADDED
Binary file (6.64 kB). View file
 
__pycache__/home.cpython-39.pyc ADDED
Binary file (1.69 kB). View file
 
__pycache__/spray.cpython-39.pyc ADDED
Binary file (7.73 kB). View file
 
angle_ev_list_df.csv ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ,launch_angle,launch_speed
2
+ 0,8,116.09999999999894
3
+ 1,9,115.099999999999
4
+ 2,10,114.09999999999906
5
+ 3,11,113.09999999999911
6
+ 4,12,112.09999999999917
7
+ 5,13,111.09999999999923
8
+ 6,14,110.09999999999928
9
+ 7,15,109.09999999999934
10
+ 8,16,108.0999999999994
11
+ 9,17,107.09999999999945
12
+ 10,18,106.09999999999951
13
+ 11,19,105.09999999999957
14
+ 12,20,104.09999999999962
15
+ 13,21,103.09999999999968
16
+ 14,22,102.09999999999974
17
+ 15,23,101.0999999999998
18
+ 16,24,100.09999999999985
19
+ 17,25,99.09999999999991
20
+ 18,26,98.09999999999997
21
+ 19,27,97.5
22
+ 20,28,97.5
23
+ 21,29,97.5
24
+ 22,30,98.09999999999997
25
+ 23,31,98.69999999999993
26
+ 24,32,99.39999999999989
27
+ 25,33,100.09999999999985
28
+ 26,34,100.69999999999982
29
+ 27,35,101.39999999999978
30
+ 28,36,102.09999999999974
31
+ 29,37,102.6999999999997
32
+ 30,38,103.39999999999966
33
+ 31,39,104.09999999999962
34
+ 32,40,104.69999999999959
35
+ 33,41,105.39999999999955
36
+ 34,42,106.09999999999951
37
+ 35,43,106.69999999999948
38
+ 36,44,107.39999999999944
39
+ 37,45,108.0999999999994
40
+ 38,46,108.69999999999936
41
+ 39,47,109.39999999999932
42
+ 40,48,110.09999999999928
43
+ 41,49,110.69999999999925
44
+ 42,50,111.39999999999921
app.py CHANGED
@@ -17,7 +17,11 @@ from home import home
17
  # from team_xg_rates import team_xg_rates
18
  # from gsax_comparison import gsax_comparison
19
  # from game import game
20
- # from games import games
 
 
 
 
21
  # from articles import articles
22
  # from xg_model import xg_model
23
 
@@ -31,7 +35,11 @@ routes = [
31
  # Mount('/skater-xg-percentages', app=on_ice_xgfp),
32
  # Mount('/team-xg-rates', app=team_xg_rates),
33
  # Mount('/gsax-comparison',app=gsax_comparison),
34
- # Mount('/games',app=games),
 
 
 
 
35
  # Mount('/game/{game_id}',app=game),
36
  # Mount('/articles',app=articles),
37
  # Mount('/xg-model',app=xg_model)
 
17
  # from team_xg_rates import team_xg_rates
18
  # from gsax_comparison import gsax_comparison
19
  # from game import game
20
+ from spray import spray
21
+ from decision_value import decision_value
22
+ from damage import damage
23
+ from batter_scatter import batter_scatter
24
+ from ev_angle import ev_angle
25
  # from articles import articles
26
  # from xg_model import xg_model
27
 
 
35
  # Mount('/skater-xg-percentages', app=on_ice_xgfp),
36
  # Mount('/team-xg-rates', app=team_xg_rates),
37
  # Mount('/gsax-comparison',app=gsax_comparison),
38
+ Mount('/spray',app=spray),
39
+ Mount('/decision_value',app=decision_value),
40
+ Mount('/damage_model',app=damage),
41
+ Mount('/batter_scatter',app=batter_scatter),
42
+ Mount('/ev_angle',app=ev_angle),
43
  # Mount('/game/{game_id}',app=game),
44
  # Mount('/articles',app=articles),
45
  # Mount('/xg-model',app=xg_model)
batter_scatter.py ADDED
@@ -0,0 +1,499 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
24
+ exit_velo_df_codes_summ_batter = pd.read_csv('summary_batter.csv',index_col=[0])
25
+ #exit_velo_df_codes_summ = pd.read_csv('summary_pitcher.csv',index_col=[0])
26
+
27
+ exit_velo_df_codes_summ_non_level = pd.read_csv('summary_batter_level.csv',index_col=[0]).reset_index(drop=True)
28
+
29
+ exit_velo_df_codes_summ_non_level['levels'] = exit_velo_df_codes_summ_non_level.levels.str.split(', ')
30
+
31
+ exit_velo_df_codes_summ_non_level = exit_velo_df_codes_summ_non_level.rename(columns={'levels':'level'})
32
+
33
+
34
+
35
+ print(exit_velo_df_codes_summ_batter.bb_minus_k_percent)
36
+
37
+ batter_dict_stat = { 'sweet_spot_percent':{'x_axis':'SweetSpot%','title':'SweetSpot%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
38
+ 'max_launch_speed':{'x_axis':'Max Exit Velocity','title':'Max Exit Velocity','flip_p':False,'decimal_format':'string_0','percent_adjust':1},
39
+ 'launch_speed_90':{'x_axis':'90th Percentile EV','title':'90th Percentile EV','flip_p':False,'decimal_format':'string_0','percent_adjust':1},
40
+ 'launch_speed':{'x_axis':'Exit Velocity','title':'Exit Velocity','flip_p':False,'decimal_format':'string_0','percent_adjust':1},
41
+ 'launch_angle':{'x_axis':'Launch Angle','title':'Launch Angle','flip_p':False,'decimal_format':'string_0','percent_adjust':100},
42
+ 'avg':{'x_axis':'AVG','title':'AVG','flip_p':False,'decimal_format':'string_3','percent_adjust':100},
43
+ 'obp':{'x_axis':'OBP','title':'OBP','flip_p':False,'decimal_format':'string_3','percent_adjust':100},
44
+ 'slg':{'x_axis':'SLG','title':'SLG','flip_p':False,'decimal_format':'string_3','percent_adjust':100},
45
+ 'ops':{'x_axis':'OPS','title':'OPS','flip_p':False,'decimal_format':'string_3','percent_adjust':100},
46
+ 'k_percent':{'x_axis':'K%','title':'K%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100},
47
+ 'bb_percent':{'x_axis':'BB%','title':'BB%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
48
+ 'bb_over_k_percent':{'x_axis':'BB/K','title':'BB/K','flip_p':False,'decimal_format':'string_1','percent_adjust':100},
49
+ 'bb_minus_k_percent':{'x_axis':'BB%-K%','title':'BB%-K%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
50
+ 'csw_percent':{'x_axis':'CSW%','title':'CSW%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100},
51
+ 'woba_percent':{'x_axis':'wOBA','title':'wOBA','flip_p':False,'decimal_format':'string_3','percent_adjust':100},
52
+ 'hard_hit_percent':{'x_axis':'HardHit%','title':'HardHit%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
53
+ 'barrel_percent':{'x_axis':'Barrel%','title':'Barrel%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
54
+ 'zone_contact_percent':{'x_axis':'Z-Contact%','title':'Z-Contact%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
55
+ 'zone_swing_percent':{'x_axis':'Z-Swing%','title':'Z-Swing%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
56
+ 'zone_percent':{'x_axis':'Zone%','title':'Zone%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
57
+ 'chase_percent':{'x_axis':'O-Swing%','title':'O-Swing%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100},
58
+ 'chase_contact':{'x_axis':'O-Contact%','title':'O-Contact%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100},
59
+ 'swing_percent':{'x_axis':'Swing%','title':'Swing%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100},
60
+ 'whiff_rate':{'x_axis':'Whiff%','title':'Whiff%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100},
61
+ 'swstr_rate':{'x_axis':'SwStr%','title':'SwStr%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100},
62
+ }
63
+
64
+ batter_dict_stat_small = { 'sweet_spot_percent':'SweetSpot%',
65
+ 'max_launch_speed':'Max Exit Velocity',
66
+ 'launch_speed_90':'90th Percentile EV',
67
+ 'launch_speed':'Exit Velocity',
68
+ 'launch_angle':'Launch Angle',
69
+ 'avg':'AVG',
70
+ 'obp':'OBP',
71
+ 'slg':'SLG',
72
+ 'ops':'OPS',
73
+ 'k_percent':'K%',
74
+ 'bb_percent':'BB%',
75
+ 'bb_over_k_percent':'BB/K',
76
+ 'bb_minus_k_percent':'BB%-K%',
77
+ 'csw_percent':'CSW%',
78
+ 'woba_percent':'wOBA',
79
+ 'hard_hit_percent':'HardHit%',
80
+ 'barrel_percent':'Barrel%',
81
+ 'zone_contact_percent':'Z-Contact%',
82
+ 'zone_swing_percent':'Z-Swing%',
83
+ 'zone_percent':'Zone%',
84
+ 'chase_percent':'O-Swing%',
85
+ 'chase_contact':'O-Contact%',
86
+ 'swing_percent':'Swing%',
87
+ 'whiff_rate':'Whiff%',
88
+ 'swstr_rate':'SwStr%',
89
+ }
90
+
91
+
92
+ colour_palette = ['#FFB000','#648FFF','#785EF0',
93
+ '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
94
+
95
+ level_dict = {'MLB':'MLB','AAA':'AAA','AA':'AA','A+':'A+','A':'A','ROK':'ROK'}
96
+
97
+ batter_test_df = exit_velo_df_codes_summ_batter.sort_values(by='batter').drop_duplicates(subset='batter_id').reset_index(drop=True)[['batter_id','batter']]#['pitcher'].to_dict()
98
+ batter_test_df = batter_test_df.set_index('batter_id')
99
+
100
+
101
+ def decimal_format_assign(x):
102
+ if x['decimal_format'] == 'percent_1':
103
+ return mtick.PercentFormatter(1,decimals=1)
104
+ if x['decimal_format'] == 'string_3':
105
+ return mtick.FormatStrFormatter('%.3f')
106
+ if x['decimal_format'] == 'string_0':
107
+ return mtick.FormatStrFormatter('%.0f')
108
+ if x['decimal_format'] == 'string_1':
109
+ return mtick.FormatStrFormatter('%.1f')
110
+
111
+
112
+ #test_df = test_df[test_df.pitcher == 'Chris Bassitt'].append(test_df[test_df.pitcher != 'Chris Bassitt'])
113
+
114
+ batter_dict = batter_test_df['batter'].to_dict()
115
+
116
+ exit_velo_df_codes_summ_batter.position = exit_velo_df_codes_summ_batter.position.replace(['LF','RF','CF','TWP'],['OF','OF','OF','DH'])
117
+ exit_velo_df_codes_summ_non_level.position = exit_velo_df_codes_summ_non_level.position.replace(['LF','RF','CF','TWP'],['OF','OF','OF','DH'])
118
+
119
+ position_list = ['All'] + list(exit_velo_df_codes_summ_batter.position.unique())
120
+ team_list = ['All'] + sorted(list(exit_velo_df_codes_summ_batter.parent_org_abb.unique()))
121
+
122
+
123
+
124
+ def server(input,output,session):
125
+
126
+
127
+ @output
128
+ @render.plot(alt="A histogram")
129
+ def plot():
130
+ sns.set_theme(style="whitegrid", palette="pastel")
131
+ print(input.level_id())
132
+ print(input.n())
133
+ print('we made it here',input.team_id(),input.position_id())
134
+ if input.group_level():
135
+ data_df = exit_velo_df_codes_summ_non_level.copy()
136
+
137
+ turth_list = []
138
+ #turth_list_2 = []
139
+ for x in range(0,len(data_df.level)):
140
+ turth_list_2 = []
141
+ for y in range(0,len(data_df.level[x])):
142
+ #print(level_list[x][y])
143
+ turth_list_2.append(data_df.level[x][y] in input.level_id())
144
+ turth_list.append(turth_list_2)
145
+
146
+ final_check_list = [True if True in x else False for x in turth_list]
147
+
148
+
149
+ data_df = data_df[(data_df.pa >= input.n())&(data_df.age <= input.n_age())&(final_check_list)]
150
+
151
+
152
+ else:
153
+
154
+
155
+ data_df = exit_velo_df_codes_summ_batter.copy()
156
+ data_df = data_df[(data_df.pa >= input.n())&(data_df.age <= input.n_age())&(data_df.level.isin(input.level_id()))]
157
+ print(data_df)
158
+ if 'All' in input.team_id():
159
+ print('nice')#data_df = data_df[(data_df.pa >= input.n())&(data_df.age <= input.n_age())].reset_index(drop=True)
160
+
161
+ else:
162
+ data_df = data_df[(data_df.parent_org_abb.isin(input.team_id()))].reset_index(drop=True)
163
+
164
+ if 'All' in input.position_id():
165
+ print('nice')#data_df = data_df[(data_df.level.isin(input.level_id()))&(data_df.pa >= input.n())&(data_df.age <= input.n_age())].reset_index(drop=True)
166
+
167
+ else:
168
+ data_df = data_df[(data_df.position.isin(input.position_id()))].reset_index(drop=True)
169
+
170
+
171
+ #print('we made it here')
172
+ print(data_df)
173
+ data_df = data_df.sort_values(by='level').reset_index(drop=True)
174
+ print(batter_dict_stat[input.stat_x()]['flip_p'])
175
+
176
+
177
+
178
+ x_flip = batter_dict_stat[input.stat_x()]['flip_p']
179
+ y_flip = batter_dict_stat[input.stat_y()]['flip_p']
180
+ cbr_flip = batter_dict_stat[input.stat_z()]['flip_p']
181
+
182
+
183
+
184
+ data_df[input.stat_x()+'_percent'] = data_df[input.stat_x()].rank(pct=True,ascending=abs(x_flip-1))
185
+
186
+ data_df[input.stat_y()+'_percent'] = data_df[input.stat_y()].rank(pct=True,ascending=abs(y_flip-1))
187
+
188
+ data_df[input.stat_z()+'_percent'] = data_df[input.stat_z()].rank(pct=True,ascending=abs(cbr_flip-1))
189
+
190
+
191
+
192
+ fig, ax = plt.subplots(1, 1, figsize=(9, 9))
193
+
194
+ #data_df['bb_over_obp'] = data_df['bb']/data_df['k']
195
+
196
+ #data_df[input.stat_z()]= data_df[input.stat_z()].fillna(-100000)
197
+
198
+
199
+ if cbr_flip:
200
+ cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[0],colour_palette[3],colour_palette[1]])
201
+ norm = plt.Normalize(data_df[input.stat_z()].min(), data_df[input.stat_z()].max())
202
+
203
+ else:
204
+ cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],colour_palette[3],colour_palette[0]])
205
+ norm = plt.Normalize(data_df[input.stat_z()].min(), data_df[input.stat_z()].max())
206
+
207
+ sm = plt.cm.ScalarMappable(cmap=cmap_hue, norm=norm)
208
+ print('we made it here')
209
+
210
+ # sns.regplot(x = stat_x, y = stat_y, data=data_df, color = colour_palette[6],ax=ax,scatter=False,
211
+ # line_kws=dict(alpha=0.3,linewidth=2,zorder=1))
212
+ # scatter_plot = sns.scatterplot(x = stat_x, y = stat_y, data=data_df, color = colour_palette[0],ax=ax,hue=stat_z,palette=cmap_hue)
213
+
214
+
215
+
216
+ # r, p = sp.stats.pearsonr(data_df[input.stat_x()], data_df[input.stat_y()])
217
+ # ax = plt.gca()
218
+ # # ax.text(.25, 0.3, 'r={:.2f}, p={:.2g}'.format(r, p),
219
+ # # transform=ax.transAxes, fontsize=12)
220
+
221
+ # ax.annotate('R²={:.2f}'.format(r, p), ( math.ceil(data_df[input.stat_x()].max()*batter_dict_stat[input.stat_x()]['percent_adjust']/5)*5/batter_dict_stat[input.stat_x()]['percent_adjust']*(1-batter_dict_stat[input.stat_x()]['flip_p']),
222
+ # math.floor(data_df[input.stat_y()].min()*batter_dict_stat[input.stat_y()]['percent_adjust']/5)*5/batter_dict_stat[input.stat_y()]['percent_adjust']*(1-batter_dict_stat[input.stat_y()]['flip_p'])),
223
+ # fontsize=18,fontname='Century Gothic',ha='right')
224
+
225
+ if input.group_level():
226
+ scatter = sns.scatterplot(x = input.stat_x(), y = input.stat_y(), data=data_df, color = '#b3b3b3')
227
+ #ax.get_legend().remove()
228
+ scatter = sns.scatterplot(x = input.stat_x(), y = input.stat_y(), data=data_df, color = colour_palette[0],ax=ax,hue=input.stat_z(),palette=cmap_hue)
229
+ else:
230
+ scatter = sns.scatterplot(x = input.stat_x(), y = input.stat_y(), data=data_df, color = '#b3b3b3',style='level')
231
+ #ax.get_legend().remove()
232
+ scatter = sns.scatterplot(x = input.stat_x(), y = input.stat_y(), data=data_df, color = colour_palette[0],ax=ax,hue=input.stat_z(),palette=cmap_hue,style='level')
233
+ sns.set_theme(style="whitegrid", palette="pastel")
234
+
235
+ fig.set_facecolor('#F0F0F0')
236
+ ax.set_facecolor('white')
237
+
238
+ print('we made it here')
239
+ # for i in range(0,len(pitch_group_unique)):
240
+ # data_df = elly_zone_df[elly_zone_df.pitch_group==pitch_group_unique[i]]
241
+ # len_df.append(len(data_df))
242
+ # sns.lineplot(x=range(1,len(data_df)+1),y=data_df.swings.rolling(window=rolling_window_input).sum()/data_df.pitches.rolling(window=rolling_window_input).sum(),color=colour_palette[i],linewidth=3,ax=ax,
243
+ # label=f'{pitch_group_unique[i]} (Season Average {float(data_df.swings.sum()/data_df.pitches.sum()):.1%})',zorder=i+10)
244
+ # ax.hlines(xmin=0,xmax=len(elly_zone_df),y=data_df.swings.sum()/data_df.pitches.sum(),color=colour_palette[i],linewidth=3,linestyle='-.',alpha=0.4,zorder=i)
245
+
246
+ ts=[]
247
+ print(input.player_id())
248
+
249
+ print(len(data_df))
250
+ if input.names():
251
+ for i in range(len(data_df)):
252
+ if (data_df[input.stat_x()+'_percent'][i] < input.n_percent_bot_x() or data_df[input.stat_x()+'_percent'][i] > 1 - input.n_percent_top_x() ) \
253
+ or (data_df[input.stat_y()+'_percent'][i] < input.n_percent_bot_y() or data_df[input.stat_y()+'_percent'][i] > 1 -input.n_percent_top_y()) \
254
+ or (data_df[input.stat_z()+'_percent'][i] < input.n_percent_bot_z() or data_df[input.stat_z()+'_percent'][i] > 1 -input.n_percent_top_z() )\
255
+ or (str(data_df.batter_id[i]) in (input.player_id())):
256
+ # print(data_df.batter[i])
257
+ # ax.annotate(data_df.batter[i], xy=((data_df[input.stat_x()][i])+0.025/batter_dict_stat[input.stat_x()]['percent_adjust'], data_df[input.stat_y()][i]+0.01/batter_dict_stat[input.stat_x()]['percent_adjust']), xytext=(-20,20),
258
+ # textcoords='offset points', ha='center', va='bottom',fontsize=7,
259
+ # bbox=dict(boxstyle='round,pad=0', fc=colour_palette[6], alpha=0.0),
260
+ # arrowprops=dict(arrowstyle='->', connectionstyle="angle,angleA=-90,angleB=-10,rad=2",
261
+ # color=colour_palette[8]))
262
+
263
+ #if data_df['batter'][i] != 'Jo Adell':
264
+ # ax.annotate(data_df.batter[i], (data_df[input.stat_x()][i]-len(data_df.batter[i])*0.00025, data_df[input.stat_y()][i]+0.001),fontsize=8)
265
+ ts.append(ax.text(data_df[input.stat_x()][i], data_df[input.stat_y()][i], data_df.batter[i],fontsize=8))
266
+
267
+
268
+
269
+ ax.hlines(xmin=(math.floor((data_df[input.stat_x()].min()*batter_dict_stat[input.stat_x()]['percent_adjust']-0.01)/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'],
270
+ xmax= (math.ceil((data_df[input.stat_x()].max()*batter_dict_stat[input.stat_x()]['percent_adjust']+0.01)/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'],
271
+ y=data_df[input.stat_y()].mean(),color='gray',linewidth=3,linestyle='dotted',alpha=0.4)
272
+
273
+ print('we made it here')
274
+
275
+ ax.vlines(ymin=(math.floor((data_df[input.stat_y()].min()*batter_dict_stat[input.stat_y()]['percent_adjust']-0.01)/5))*5/batter_dict_stat[input.stat_y()]['percent_adjust'],
276
+ ymax= (math.ceil((data_df[input.stat_y()].max()*batter_dict_stat[input.stat_y()]['percent_adjust']+0.01)/5))*5/batter_dict_stat[input.stat_y()]['percent_adjust'],
277
+ x=data_df[input.stat_x()].mean(),color='gray',linewidth=3,linestyle='dotted',alpha=0.4)
278
+
279
+ print(data_df[input.stat_x()].min())
280
+ print(batter_dict_stat[input.stat_x()]['percent_adjust'])
281
+ print((math.floor((data_df[input.stat_x()].min()*batter_dict_stat[input.stat_x()]['percent_adjust']-0.01)/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'])
282
+
283
+
284
+ ax.set_xlim((math.floor((data_df[input.stat_x()].min()*batter_dict_stat[input.stat_x()]['percent_adjust'])/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'],
285
+ (math.ceil((data_df[input.stat_x()].max()*batter_dict_stat[input.stat_x()]['percent_adjust'])/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'])
286
+
287
+
288
+ ax.set_ylim((math.floor((data_df[input.stat_y()].min()*batter_dict_stat[input.stat_y()]['percent_adjust'])/5))*5/batter_dict_stat[input.stat_y()]['percent_adjust'],
289
+ (math.ceil((data_df[input.stat_y()].max()*batter_dict_stat[input.stat_y()]['percent_adjust'])/5))*5/batter_dict_stat[input.stat_y()]['percent_adjust'])
290
+
291
+
292
+
293
+ title_level = str([x .strip("\'")for x in input.level_id()]).strip('[').strip(']').replace("'",'')
294
+
295
+ if title_level == 'AAA, AA, A+, A':
296
+ title_level='MiLB'
297
+ #title_level = input.level_id()[0]
298
+ if input.n_age() >= 50:
299
+ title_spot = f'{title_level} Batter {batter_dict_stat[input.stat_y()]["title"]} vs {batter_dict_stat[input.stat_x()]["title"]} (min. {input.n()} PA)'
300
+
301
+ else:
302
+ title_spot = f'{title_level} Batter {batter_dict_stat[input.stat_y()]["title"]} vs {batter_dict_stat[input.stat_x()]["title"]} (min. {input.n()} PA, Max Age {input.n_age()})'
303
+
304
+ ax.set_title(title_spot, fontsize=24/(len(title_spot)*0.03),fontname='Century Gothic')
305
+ # #vals = ax.get_yticks()
306
+ ax.set_xlabel(batter_dict_stat[input.stat_x()]['x_axis'], fontsize=16,fontname='Century Gothic')
307
+ ax.set_ylabel(batter_dict_stat[input.stat_y()]['x_axis'], fontsize=16,fontname='Century Gothic')
308
+
309
+
310
+ if input.group_level():
311
+ ax.get_legend().remove()
312
+
313
+ if not input.group_level():
314
+ if len(input.level_id()) > 1:
315
+ h,l = scatter.get_legend_handles_labels()
316
+ l[-(len(input.level_id())+1)] = 'Level'
317
+ ax.legend(h[-(len(input.level_id())+1):],l[-(len(input.level_id())+1):], borderaxespad=0.1,loc=0)
318
+
319
+ else:
320
+ ax.get_legend().remove()
321
+
322
+ #plt.show(g)
323
+ # ax.figure.colorbar(sm, ax=ax)
324
+
325
+ cbar = ax.figure.colorbar(sm, ax=ax,format=decimal_format_assign(x=batter_dict_stat[input.stat_z()]),orientation='vertical',aspect=30)
326
+ cbar.set_label(batter_dict_stat[input.stat_z()]['x_axis'])
327
+ #fig.axes[0].invert_yaxis()
328
+ print('we made it here5')
329
+ fig.subplots_adjust(wspace=.02, hspace=.02)
330
+ # ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
331
+ #ax.set_yticks([0,0.1,0.2,0.3,0.4,0.5])
332
+ # fig.colorbar(plot_dist, ax=ax)
333
+ # fig.colorbar(plot_dist)
334
+
335
+ if batter_dict_stat[input.stat_x()]['flip_p']:
336
+ fig.axes[0].invert_xaxis()
337
+
338
+ if batter_dict_stat[input.stat_y()]['flip_p']:
339
+ fig.axes[0].invert_yaxis()
340
+
341
+
342
+ # ax.xaxis.set_major_formatter(mtick.PercentFormatter(1,decimals=0))
343
+ # ax.yaxis.set_major_formatter(mtick.PercentFormatter(1))
344
+
345
+
346
+
347
+
348
+
349
+ print('we made it here6')
350
+
351
+ ax.xaxis.set_major_formatter(decimal_format_assign(x=batter_dict_stat[input.stat_x()]))
352
+ ax.yaxis.set_major_formatter(decimal_format_assign(x=batter_dict_stat[input.stat_y()]))
353
+
354
+
355
+ print('we made it here7')
356
+ # ax.text(0.5, 0.5, '/u/tomstoms', transform=ax.transAxes,
357
+ # fontsize=60, color='gray', alpha=0.075,
358
+ # ha='center', va='center', rotation=45)
359
+
360
+ print(ts)
361
+ adjust_text(ts,
362
+ arrowprops=dict(arrowstyle="-", color=colour_palette[4], lw=1),ax=ax)
363
+
364
+ #ax.legend(fontsize='16')
365
+ fig.text(x=0.03,y=0.02,s='By: @TJStats',fontname='Century Gothic')
366
+ fig.text(x=1-0.03,y=0.02,s='Data: MLB',ha='right',fontname='Century Gothic')
367
+ fig.tight_layout()
368
+ #matplotlib.rcParams["figure.dpi"] = 600
369
+ #plt.show()
370
+
371
+
372
+ batter_scatter = App(ui.page_fluid(
373
+ ui.tags.base(href=base_url),
374
+ ui.tags.div(
375
+ {"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
376
+ ui.tags.style(
377
+ """
378
+ h4 {
379
+ margin-top: 1em;font-size:35px;
380
+ }
381
+ h2{
382
+ font-size:25px;
383
+ }
384
+ """
385
+ ),
386
+ shinyswatch.theme.simplex(),
387
+ ui.tags.h4("TJStats"),
388
+ ui.tags.i("Baseball Analytics and Visualizations"),
389
+ ui.navset_tab(
390
+ ui.nav_control(
391
+ ui.a(
392
+ "Home",
393
+ href="home/"
394
+ ),
395
+ ),
396
+ ui.nav_menu(
397
+ "Batter Charts",
398
+ ui.nav_control(
399
+ ui.a(
400
+ "Spray",
401
+ href="spray/"
402
+ ),
403
+ ui.a(
404
+ "Decision Value",
405
+ href="decision_value/"
406
+ ),
407
+ ui.a(
408
+ "Damage Model",
409
+ href="damage_model/"
410
+ ),
411
+ ui.a(
412
+ "Batter Scatter",
413
+ href="batter_scatter/"
414
+ ),
415
+ ui.a(
416
+ "EV vs LA Plot",
417
+ href="ev_angle/"
418
+ )
419
+ ),
420
+ ),
421
+ ui.nav_menu(
422
+ "Goalie Charts",
423
+ ui.nav_control(
424
+ ui.a(
425
+ "GSAx Timeline",
426
+ href="gsax-timeline/"
427
+ ),
428
+ ui.a(
429
+ "GSAx Leaderboard",
430
+ href="gsax-leaderboard/"
431
+ ),
432
+ ui.a(
433
+ "GSAx Comparison",
434
+ href="gsax-comparison/"
435
+ )
436
+ ),
437
+ ),ui.nav_menu(
438
+ "Team Charts",
439
+ ui.nav_control(
440
+ ui.a(
441
+ "Team xG Rates",
442
+ href="team-xg-rates/"
443
+ ),
444
+ ),
445
+ ),ui.nav_control(
446
+ ui.a(
447
+ "Games",
448
+ href="games/"
449
+ ),
450
+ ),ui.nav_control(
451
+ ui.a(
452
+ "About",
453
+ href="about/"
454
+ ),
455
+ ),ui.nav_control(
456
+ ui.a(
457
+ "Articles",
458
+ href="articles/"
459
+ ),
460
+ )),ui.row(
461
+ ui.layout_sidebar(
462
+
463
+
464
+
465
+ ui.panel_sidebar(
466
+ #ui.input_select("id", "Select Batter",batter_dict,selected=675911,width=1,size=1),
467
+ ui.row(
468
+ ui.column(4,ui.input_select("level_id", "Select Level",level_dict,width=1,size=1,multiple=True,selected='MLB',selectize=True),),
469
+ ui.column(4,ui.input_select("team_id", "Select Team",team_list,width=1,size=1,multiple=True,selected='All',selectize=True),),
470
+ ui.column(4,ui.input_select("position_id", "Select Position",position_list,width=1,size=1,selected='All',multiple=True,selectize=True))),
471
+ ui.row(
472
+ ui.column(6,ui.input_numeric("n", "Minimum PA", value=100)),
473
+ ui.column(6,ui.input_numeric("n_age", "Maximum Age", value=50))),
474
+ ui.row(
475
+ ui.column(4,ui.input_select("stat_x", "X-Axis",batter_dict_stat_small,selected='k_percent',width=1,size=1)),
476
+ ui.column(4,ui.input_select("stat_y", "Y-Axis",batter_dict_stat_small,selected='bb_percent',width=1,size=1)),
477
+ ui.column(4,ui.input_select("stat_z", "Colour-Bar Axis",batter_dict_stat_small,selected='bb_over_k_percent',width=1,size=1))),
478
+
479
+ ui.row(
480
+ ui.column(6,ui.input_numeric("n_percent_top_x", "Top 'n' Percentile X-Labels", value=0.01)),
481
+ ui.column(6,ui.input_numeric("n_percent_bot_x", "Bottom 'n' Percentile X-Labels", value=0.01))),
482
+ ui.row(
483
+ ui.column(6,ui.input_numeric("n_percent_top_y", "Top 'n' Percentile Y-Labels", value=0.01)),
484
+ ui.column(6,ui.input_numeric("n_percent_bot_y", "Bottom 'n' Percentile Y-Labels", value=0.01))),
485
+ ui.row(
486
+ ui.column(6,ui.input_numeric("n_percent_top_z", "Top 'n' Percentile Z-Labels", value=0.01)),
487
+ ui.column(6,ui.input_numeric("n_percent_bot_z", "Bottom 'n' Percentile Z-Labels", value=0.01))),
488
+
489
+ ui.input_select("player_id", "Label Player",batter_dict,width=1,size=1,multiple=True,selectize=True),
490
+ ui.row(
491
+ ui.input_switch("names", "Toggle Names"),
492
+ ui.input_switch("group_level", "Group Levels")),
493
+ ),
494
+
495
+ ui.panel_main(
496
+ ui.output_plot("plot",height = "1000px",width="1000px")
497
+ ,
498
+ ),
499
+ )),)),server)
damage.py ADDED
@@ -0,0 +1,610 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True)
27
+ print(df_2023)
28
+ ### Normalize Hit Locations
29
+
30
+ df_2023['season'] = df_2023['game_date'].str[0:4].astype(int)
31
+ # df_2023['hit_x'] = df_2023['hit_x'] - df_2023['hit_x'].median()
32
+ # df_2023['hit_y'] = -df_2023['hit_y']+df_2023['hit_y'].quantile(0.9999)
33
+
34
+ df_2023['hit_x'] = df_2023['hit_x'] - 126#df_2023['hit_x'].median()
35
+ df_2023['hit_y'] = -df_2023['hit_y']+204.5#df_2023['hit_y'].quantile(0.9999)
36
+
37
+ df_2023['hit_x_og'] = df_2023['hit_x']
38
+ df_2023.loc[df_2023['batter_hand'] == 'R','hit_x'] = -1*df_2023.loc[df_2023['batter_hand'] == 'R','hit_x']
39
+ df_2023['h_la'] = np.arctan(df_2023['hit_x'] / df_2023['hit_y'])*180/np.pi
40
+ conditions_ss = [
41
+ (df_2023['h_la']<-15),
42
+ (df_2023['h_la']<15)&(df_2023['h_la']>=-15),
43
+ (df_2023['h_la']>=15)
44
+ ]
45
+
46
+ choices_ss = ['Oppo','Straight','Pull']
47
+ df_2023['traj'] = np.select(conditions_ss, choices_ss, default=np.nan)
48
+ df_2023['bip'] = [1 if x > 0 else np.nan for x in df_2023['launch_speed']]
49
+
50
+ conditions_woba = [
51
+ (df_2023['event_type']=='walk'),
52
+ (df_2023['event_type']=='hit_by_pitch'),
53
+ (df_2023['event_type']=='single'),
54
+ (df_2023['event_type']=='double'),
55
+ (df_2023['event_type']=='triple'),
56
+ (df_2023['event_type']=='home_run'),
57
+ ]
58
+
59
+
60
+ choices_woba = [1,
61
+ 1,
62
+ 1,
63
+ 2,
64
+ 3,
65
+ 4]
66
+
67
+
68
+ # choices_woba = [0.698,
69
+ # 0.728,
70
+ # 0.887,
71
+ # 1.253,
72
+ # 1.583,
73
+ # 2.027]
74
+
75
+ df_2023['woba'] = np.select(conditions_woba, choices_woba, default=0)
76
+
77
+ choices_woba_train = [1,
78
+ 1,
79
+ 1,
80
+ 2,
81
+ 3,
82
+ 4]
83
+
84
+ df_2023['woba_train'] = np.select(conditions_woba, choices_woba_train, default=0)
85
+
86
+
87
+ df_2023_bip = df_2023[~df_2023['bip'].isnull()].dropna(subset=['h_la','launch_angle'])
88
+ df_2023_bip['h_la'] = df_2023_bip['h_la'].round(0)
89
+
90
+
91
+ df_2023_bip['season'] = df_2023_bip['game_date'].str[0:4].astype(int)
92
+
93
+ df_2023_bip = df_2023[~df_2023['bip'].isnull()].dropna(subset=['launch_angle','bip'])
94
+ df_2023_bip_train = df_2023_bip[df_2023_bip['season'] == 2023]
95
+
96
+ batter_dict = df_2023_bip.sort_values('batter_name').set_index('batter_id')['batter_name'].to_dict()
97
+
98
+ features = ['launch_angle','launch_speed','h_la']
99
+ target = ['woba_train']
100
+
101
+ df_2023_bip_train = df_2023_bip_train.dropna(subset=features)
102
+
103
+ import joblib
104
+ # # Dump the model to a file named 'model.joblib'
105
+ model = joblib.load('xtb_model.joblib')
106
+
107
+
108
+ df_2023_bip_train['y_pred'] = [sum(x) for x in model.predict_proba(df_2023_bip_train[features]) * ([0,1,2,3,4])]
109
+ # df_2023_bip_train['y_pred_noh'] = [sum(x) for x in model_noh.predict_proba(df_2023_bip_train[['launch_angle','launch_speed']]) * ([0,0.887,1.253,1.583,2.027])]
110
+
111
+ df_2023_output = df_2023_bip_train.groupby(['batter_id','batter_name']).agg(
112
+ bip = ('y_pred','count'),
113
+ y_pred = ('y_pred','sum'),
114
+ slgcon = ('woba','mean'),
115
+ xslgcon = ('y_pred','mean'),
116
+ launch_speed = ('launch_speed','mean'),
117
+ launch_angle_std = ('launch_angle','median'),
118
+ h_la_std = ('h_la','mean'))
119
+
120
+ df_2023_output_copy = df_2023_output.copy()
121
+ # df_2023_output = df_2023_output[df_2023_output['bip'] > 100]
122
+ # df_2023_output[df_2023_output['bip'] > 100].sort_values(by='h_la_std',ascending=True).head(20)
123
+
124
+ import pandas as pd
125
+ import numpy as np
126
+
127
+
128
+ # Create grid coordinates
129
+ x = np.arange(30, 121,1 )
130
+ y = np.arange(-30, 61,1 )
131
+ z = np.arange(-45, 46,1 )
132
+
133
+ # Create a meshgrid
134
+ X, Y, Z = np.meshgrid(x, y, z, indexing='ij')
135
+ # Flatten the meshgrid to get x and y coordinates
136
+ x_flat = X.flatten()
137
+ y_flat = Y.flatten()
138
+ z_flat = Z.flatten()
139
+
140
+ # Create a DataFrame
141
+ df = pd.DataFrame({'launch_speed': x_flat, 'launch_angle': y_flat,'h_la':z_flat})
142
+
143
+ df['y_pred'] = [sum(x) for x in model.predict_proba(df[features]) * ([0,1,2,3,4])]
144
+
145
+
146
+ import matplotlib
147
+
148
+ colour_palette = ['#FFB000','#648FFF','#785EF0',
149
+ '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
150
+
151
+ cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],'#ffffff',colour_palette[0]])
152
+ cmap_hue2 = matplotlib.colors.LinearSegmentedColormap.from_list("",['#ffffff',colour_palette[0]])
153
+
154
+
155
+ from matplotlib.pyplot import text
156
+ import inflect
157
+ from scipy.stats import percentileofscore
158
+ p = inflect.engine()
159
+
160
+
161
+
162
+
163
+ def server(input,output,session):
164
+
165
+
166
+ @output
167
+ @render.plot(alt="hex_plot")
168
+ def hex_plot():
169
+
170
+ if input.batter_id() is "":
171
+ fig = plt.figure(figsize=(12, 12))
172
+ fig.text(s='Please Select a Batter',x=0.5,y=0.5)
173
+ return
174
+
175
+ batter_select_id = int(input.batter_id())
176
+ # batter_select_name = 'Edouard Julien'
177
+ quant = int(input.quant())/100
178
+ df_batter_og = df_2023_bip_train[df_2023_bip_train['batter_id']==batter_select_id]
179
+ # df_batter_og = df_2023_bip_train[df_2023_bip_train['batter_name']==batter_select_name]
180
+ df_batter = df_batter_og[df_batter_og['launch_speed'] >= df_batter_og['launch_speed'].quantile(quant)]
181
+ # df_batter_best_speed = df_batter['launch_speed'].mean().round()
182
+
183
+ # df_bip_league = df_2023_bip_train[df_2023_bip_train['launch_speed'] >= df_2023_bip_train['launch_speed'].quantile(quant)]
184
+
185
+ import pandas as pd
186
+ import numpy as np
187
+
188
+
189
+ # Create grid coordinates
190
+ #x = np.arange(30, 121,1 )
191
+ y_b = np.arange(df_batter['launch_angle'].median()-df_batter['launch_angle'].std(),
192
+ df_batter['launch_angle'].median()+df_batter['launch_angle'].std(),1 )
193
+
194
+ z_b = np.arange(df_batter['h_la'].median()-df_batter['h_la'].std(),
195
+ df_batter['h_la'].median()+df_batter['h_la'].std(),1 )
196
+
197
+ # Create a meshgrid
198
+ Y_b, Z_b = np.meshgrid( y_b,z_b, indexing='ij')
199
+ # Flatten the meshgrid to get x and y coordinates
200
+
201
+ y_flat_b = Y_b.flatten()
202
+ z_flat_b = Z_b.flatten()
203
+
204
+ # Create a DataFrame
205
+ df_batter_base = pd.DataFrame({'launch_angle': y_flat_b,'h_la':z_flat_b,'c':[0]*len(y_flat_b)})
206
+
207
+ # df_batter_base['y_pred'] = [sum(x) for x in model.predict_proba(df_batter_base[features]) * ([0,1,2,3,4])]
208
+
209
+ from matplotlib.gridspec import GridSpec
210
+ # fig,ax = plt.subplots(figsize=(12, 12),dpi=150)
211
+ fig = plt.figure(figsize=(12,12))
212
+ gs = GridSpec(4, 3, height_ratios=[0.5,10,1.5,0.2], width_ratios=[0.05,0.9,0.05])
213
+
214
+ axheader = fig.add_subplot(gs[0, :])
215
+ ax10 = fig.add_subplot(gs[1, 0])
216
+ ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
217
+ ax12 = fig.add_subplot(gs[1, 2])
218
+ ax2_ = fig.add_subplot(gs[2, :])
219
+ axfooter1 = fig.add_subplot(gs[-1, :])
220
+
221
+ axheader.axis('off')
222
+ ax10.axis('off')
223
+ ax12.axis('off')
224
+ ax2_.axis('off')
225
+ axfooter1.axis('off')
226
+
227
+
228
+
229
+ extents = [-45,45,-30,60]
230
+
231
+ def hexLines(a=None,i=None,off=[0,0]):
232
+ '''regular hexagon segment lines as `(xy1,xy2)` in clockwise
233
+ order with points in line sorted top to bottom
234
+ for irregular hexagon pass both `a` (vertical) and `i` (horizontal)'''
235
+ if a is None: a = 2 / np.sqrt(3) * i;
236
+ if i is None: i = np.sqrt(3) / 2 * a;
237
+ h = a / 2
238
+ xy = np.array([ [ [ 0, a], [ i, h] ],
239
+ [ [ i, h], [ i,-h] ],
240
+ [ [ i,-h], [ 0,-a] ],
241
+ [ [-i,-h], [ 0,-a] ], #flipped
242
+ [ [-i, h], [-i,-h] ], #flipped
243
+ [ [ 0, a], [-i, h] ] #flipped
244
+ ])
245
+ return xy+off;
246
+
247
+
248
+ h = ax.hexbin(x=df_batter_base['h_la'],
249
+ y=df_batter_base['launch_angle'],
250
+ gridsize=25,
251
+ edgecolors='k',
252
+ extent=extents,mincnt=1,lw=2,zorder=-3,)
253
+
254
+ # cfg = {**cfg,'vmin':h.get_clim()[0], 'vmax':h.get_clim()[1]}
255
+ # plt.hexbin( ec="black" ,lw=6,zorder=4,mincnt=2,**cfg,alpha=0.1)
256
+ # plt.hexbin( ec="#ffffff",lw=1,zorder=5,mincnt=2,**cfg,alpha=0.1)
257
+
258
+
259
+ ax.hexbin(x=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['h_la'],
260
+ y=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['launch_angle'],
261
+ C=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['y_pred'],
262
+ gridsize=25,
263
+ vmin=0,
264
+ vmax=4,
265
+ cmap=cmap_hue2,
266
+ extent=extents,zorder=-3)
267
+
268
+
269
+ # Get the counts and centers of the hexagons
270
+ counts = ax.hexbin(x=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['h_la'],
271
+ y=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['launch_angle'],
272
+ C=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['y_pred'],
273
+ gridsize=25,
274
+ vmin=0,
275
+ vmax=4,
276
+ cmap=cmap_hue2,
277
+ extent=extents).get_array()
278
+
279
+ bin_centers = ax.hexbin(x=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['h_la'],
280
+ y=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['launch_angle'],
281
+ C=df[(df['launch_angle']>=-30)&(df['launch_angle']<=60)&(df['launch_speed']>=df_batter['launch_speed'].median())&(df['launch_speed']<=df_batter['launch_speed'].max())]['y_pred'],
282
+ gridsize=25,
283
+ vmin=0,
284
+ vmax=4,
285
+ cmap=cmap_hue2,
286
+ extent=extents).get_offsets()
287
+
288
+ # Add text with the values of "C" to each hexagon
289
+ for count, (x, y) in zip(counts, bin_centers):
290
+ if count >= 1:
291
+ ax.text(x, y, f'{count:.1f}', color='black', ha='center', va='center',fontsize=7)
292
+
293
+
294
+
295
+ #get hexagon centers that should be highlighted
296
+ verts = h.get_offsets()
297
+ cnts = h.get_array()
298
+ highl = verts[cnts > .5*cnts.max()]
299
+
300
+ #create hexagon lines
301
+ a = ((verts[0,1]-verts[1,1])/3).round(6)
302
+ i = ((verts[1:,0]-verts[:-1,0])/2).round(6)
303
+ i = i[i>0][0]
304
+ lines = np.concatenate([hexLines(a,i,off) for off in highl])
305
+
306
+ #select contour lines and draw
307
+ uls,c = np.unique(lines.round(4),axis=0,return_counts=True)
308
+ for l in uls[c==1]: ax.plot(*l.transpose(),'w-',lw=2,scalex=False,scaley=False,color=colour_palette[1],zorder=100)
309
+
310
+
311
+ # Plot filled hexagons
312
+ for hc in highl:
313
+ hx = hc[0] + np.array([0, i, i, 0, -i, -i])
314
+ hy = hc[1] + np.array([a, a/2, -a/2, -a, -a/2, a/2])
315
+ ax.fill(hx, hy, color=colour_palette[1], alpha=0.15, edgecolor=None) # Adjust color and alpha as needed
316
+
317
+ # # Create grid coordinates
318
+ # #x = np.arange(30, 121,1 )
319
+ # y_b = np.arange(df_bip_league['launch_angle'].median()-df_bip_league['launch_angle'].std(),
320
+ # df_bip_league['launch_angle'].median()+df_bip_league['launch_angle'].std(),1 )
321
+
322
+ # z_b = np.arange(df_bip_league['h_la'].median()-df_bip_league['h_la'].std(),
323
+ # df_bip_league['h_la'].median()+df_bip_league['h_la'].std(),1 )
324
+
325
+ # # Create a meshgrid
326
+ # Y_b, Z_b = np.meshgrid( y_b,z_b, indexing='ij')
327
+ # # Flatten the meshgrid to get x and y coordinates
328
+
329
+ # y_flat_b = Y_b.flatten()
330
+ # z_flat_b = Z_b.flatten()
331
+
332
+ # # Create a DataFrame
333
+ # df_league_base = pd.DataFrame({'launch_angle': y_flat_b,'h_la':z_flat_b,'c':[0]*len(y_flat_b)})
334
+
335
+ # h_league = ax.hexbin(x=df_league_base['h_la'],
336
+ # y=df_league_base['launch_angle'],
337
+ # gridsize=25,
338
+ # edgecolors=colour_palette[1],
339
+ # extent=extents,mincnt=1,lw=2,zorder=-3,)
340
+
341
+ # #get hexagon centers that should be highlighted
342
+ # verts = h_league.get_offsets()
343
+ # cnts = h_league.get_array()
344
+ # highl = verts[cnts > .5*cnts.max()]
345
+
346
+ # #create hexagon lines
347
+ # a = ((verts[0,1]-verts[1,1])/3).round(6)
348
+ # i = ((verts[1:,0]-verts[:-1,0])/2).round(6)
349
+ # i = i[i>0][0]
350
+ # lines = np.concatenate([hexLines(a,i,off) for off in highl])
351
+
352
+ # #select contour lines and draw
353
+ # uls,c = np.unique(lines.round(4),axis=0,return_counts=True)
354
+ # for l in uls[c==1]: ax.plot(*l.transpose(),'w-',lw=2,scalex=False,scaley=False,color=colour_palette[3],zorder=99)
355
+
356
+
357
+ axheader.text(s=f"{df_batter['batter_name'].values[0]} - {int(quant*100)}th% EV and Greater Batted Ball Tendencies",x=0.5,y=0.2,fontsize=20,ha='center',va='bottom')
358
+ axheader.text(s=f"2023 Season",x=0.5,y=-0.1,fontsize=14,ha='center',va='top')
359
+
360
+ ax.set_xlabel(f"Horizontal Spray Angle (°)",fontsize=12)
361
+ ax.set_ylabel(f"Vertical Launch Angle (°)",fontsize=12)
362
+
363
+ ax2_.text(x=0.5,
364
+ y=0.0,
365
+
366
+ s="Notes:\n" \
367
+ f"- {int(quant*100)}th% EV and Greater BBE is defined as a batter's top {100 - int(quant*100)}% hardest hit BBE\n" \
368
+ f"- Colour Scale and Number Labels Represents the Expected Total Bases for a batter's range of Best Speeds\n" \
369
+ f"- Shaded Area Represents the 2-D Region bounded by ±1σ Launch Angle and Horizontal Spray Angle on batter's Best Speed BBE\n"\
370
+ f"- {df_batter['batter_name'].values[0]} {int(quant*100)}th% EV and Greater BBE Range from {df_batter['launch_speed'].min():.0f} to {df_batter['launch_speed'].max():.0f} mph ({len(df_batter)} BBE)\n"\
371
+ f"- Positive Horizontal Spray Angle Represents a BBE hit in same direction as batter handedness (i.e. Pulled)" ,
372
+
373
+ fontsize=11,
374
+ fontstyle='oblique',
375
+ va='bottom',
376
+ ha='center',
377
+ bbox=dict(facecolor='white', edgecolor='black'),ma='left')
378
+
379
+ axfooter1.text(0.05, 0.5, "By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12)
380
+ axfooter1.text(0.95, 0.5, "Data: MLB",ha='right', va='bottom',fontsize=12)
381
+
382
+ if df_batter['batter_hand'].values[0] == 'R':
383
+ ax.invert_xaxis()
384
+ ax.grid(False)
385
+ ax.axis('equal')
386
+ # Adjusting subplot to center it within the figure
387
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.975, bottom=0.025)
388
+
389
+ #ax.text(f"Vertical Spray Angle (°)")
390
+
391
+
392
+ @output
393
+ @render.plot(alt="roll_plot")
394
+ def roll_plot():
395
+ # player_select = 'Nolan Gorman'
396
+ # player_select_full =player_select
397
+
398
+ if input.batter_id() is "":
399
+ fig = plt.figure(figsize=(12, 12))
400
+ fig.text(s='Please Select a Batter',x=0.5,y=0.5)
401
+ return
402
+
403
+ # df_will = df_model_2023[df_model_2023.batter_name == player_select].sort_values(by=['game_date','start_time'])
404
+ # df_will = df_will[df_will['is_swing'] != 1]
405
+ batter_select_id = int(input.batter_id())
406
+ # batter_select_name = 'Edouard Julien'
407
+ df_batter_og = df_2023_bip_train[df_2023_bip_train['batter_id']==batter_select_id]
408
+ batter_select_name = df_batter_og['batter_name'].values[0]
409
+ win = min(int(input.rolling_window()),len(df_batter_og))
410
+ df_2023_output = df_2023_output_copy[df_2023_output_copy['bip'] >= win]
411
+ sns.set_theme(style="whitegrid", palette="pastel")
412
+ #fig, ax = plt.subplots(1, 1, figsize=(10, 10),dpi=300)
413
+
414
+ from matplotlib.gridspec import GridSpec
415
+ # fig,ax = plt.subplots(figsize=(12, 12),dpi=150)
416
+ fig = plt.figure(figsize=(12,12))
417
+ gs = GridSpec(3, 3, height_ratios=[0.3,10,0.2], width_ratios=[0.01,2,0.01])
418
+
419
+ axheader = fig.add_subplot(gs[0, :])
420
+ ax10 = fig.add_subplot(gs[1, 0])
421
+ ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
422
+ ax12 = fig.add_subplot(gs[1, 2])
423
+ axfooter1 = fig.add_subplot(gs[-1, :])
424
+
425
+ axheader.axis('off')
426
+ ax10.axis('off')
427
+ ax12.axis('off')
428
+ axfooter1.axis('off')
429
+
430
+
431
+ sns.lineplot( x= range(win,len(df_batter_og.y_pred.rolling(window=win).mean())+1),
432
+ y= df_batter_og.y_pred.rolling(window=win).mean().dropna(),
433
+ color=colour_palette[0],linewidth=2,ax=ax)
434
+
435
+ ax.hlines(y=df_batter_og.y_pred.mean(),xmin=win,xmax=len(df_batter_og),color=colour_palette[0],linestyle='--',
436
+ label=f'{batter_select_name} Average: {df_batter_og.y_pred.mean():.3f} xSLGCON ({p.ordinal(int(np.around(percentileofscore(df_2023_output["xslgcon"],df_batter_og.y_pred.mean(), kind="strict"))))} Percentile)')
437
+
438
+ # ax.hlines(y=df_model_2023.y_pred_no_swing.std()*100,xmin=win,xmax=len(df_will))
439
+
440
+ # sns.scatterplot( x= [976],
441
+ # y= df_will.y_pred.rolling(window=win).mean().min()*100,
442
+ # color=colour_palette[0],linewidth=2,ax=ax,zorder=100,s=100,edgecolor=colour_palette[7])
443
+
444
+
445
+ ax.hlines(y=df_2023_bip_train['y_pred'].mean(),xmin=win,xmax=len(df_batter_og),color=colour_palette[1],linestyle='-.',alpha=1,
446
+ label = f'MLB Average: {df_2023_bip_train["y_pred"].mean():.3f} xSLGCON')
447
+
448
+ ax.legend()
449
+
450
+ hard_hit_dates = [df_2023_output['xslgcon'].quantile(0.9),
451
+ df_2023_output['xslgcon'].quantile(0.75),
452
+ df_2023_output['xslgcon'].quantile(0.25),
453
+ df_2023_output['xslgcon'].quantile(0.1)]
454
+
455
+
456
+
457
+ ax.hlines(y=df_2023_output['xslgcon'].quantile(0.9),xmin=win,xmax=len(df_batter_og),color=colour_palette[2],linestyle='dotted',alpha=0.5,zorder=1)
458
+ ax.hlines(y=df_2023_output['xslgcon'].quantile(0.75),xmin=win,xmax=len(df_batter_og),color=colour_palette[3],linestyle='dotted',alpha=0.5,zorder=1)
459
+ ax.hlines(y=df_2023_output['xslgcon'].quantile(0.25),xmin=win,xmax=len(df_batter_og),color=colour_palette[4],linestyle='dotted',alpha=0.5,zorder=1)
460
+ ax.hlines(y=df_2023_output['xslgcon'].quantile(0.1),xmin=win,xmax=len(df_batter_og),color=colour_palette[5],linestyle='dotted',alpha=0.5,zorder=1)
461
+
462
+ hard_hit_text = ['90th %','75th %','25th %','10th %']
463
+ for i, x in enumerate(hard_hit_dates):
464
+ ax.text(min(win+win/50,win+win+5), x ,hard_hit_text[i], rotation=0,va='center', ha='left',
465
+ bbox=dict(facecolor='white',alpha=0.7, edgecolor=colour_palette[2+i], pad=2),zorder=11)
466
+
467
+ # # Annotate with an arrow
468
+ # ax.annotate('June 6, 2023\nSeason Worst Decision Value', xy=(976, df_will.y_pred.rolling(window=win).mean().min()*100-0.03),
469
+ # xytext=(976 - 150, df_will.y_pred.rolling(window=win).mean().min()*100 - 0.2),
470
+ # arrowprops=dict(facecolor=colour_palette[7], shrink=0.01),zorder=150,fontsize=10,
471
+ # bbox=dict(facecolor='white', edgecolor='black'),va='top')
472
+
473
+ ax.set_xlim(win,len(df_batter_og))
474
+ # ax.set_ylim(0.2,max(1,))
475
+
476
+ ax.set_yticks([0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9,1])
477
+
478
+ ax.set_xlabel('Balls In Play')
479
+ ax.set_ylabel('Expected Total Bases per Ball In Play (xSLGCON)')
480
+
481
+ from matplotlib.ticker import FormatStrFormatter
482
+
483
+ ax.yaxis.set_major_formatter(FormatStrFormatter('%.3f'))
484
+
485
+ axheader.text(s=f'{batter_select_name} - MLB - {win} Rolling BIP Expected Slugging on Contact (xSLGCON)',x=0.5,y=-0.5,ha='center',va='bottom',fontsize=14)
486
+ axfooter1.text(.05, 0.2, "By: Thomas Nestico",ha='left', va='bottom',fontsize=12)
487
+ axfooter1.text(0.95, 0.2, "Data: MLB",ha='right', va='bottom',fontsize=12)
488
+
489
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.98, bottom=0.02)
490
+
491
+ damage = App(ui.page_fluid(
492
+ ui.tags.base(href=base_url),
493
+ ui.tags.div(
494
+ {"style": "width:95%;margin: 0 auto;max-width: 1600px;"},
495
+ ui.tags.style(
496
+ """
497
+ h4 {
498
+ margin-top: 1em;font-size:35px;
499
+ }
500
+ h2{
501
+ font-size:25px;
502
+ }
503
+ """
504
+ ),
505
+ shinyswatch.theme.simplex(),
506
+ ui.tags.h4("TJStats"),
507
+ ui.tags.i("Baseball Analytics and Visualizations"),
508
+ ui.navset_tab(
509
+ ui.nav_control(
510
+ ui.a(
511
+ "Home",
512
+ href="home/"
513
+ ),
514
+ ),
515
+ ui.nav_menu(
516
+ "Batter Charts",
517
+ ui.nav_control(
518
+ ui.a(
519
+ "Spray",
520
+ href="spray/"
521
+ ),
522
+ ui.a(
523
+ "Decision Value",
524
+ href="decision_value/"
525
+ ),
526
+ ui.a(
527
+ "Damage Model",
528
+ href="damage_model/"
529
+ ),
530
+ ui.a(
531
+ "Batter Scatter",
532
+ href="batter_scatter/"
533
+ ),
534
+ ui.a(
535
+ "EV vs LA Plot",
536
+ href="ev_angle/"
537
+ )
538
+ ),
539
+ ),
540
+ ui.nav_menu(
541
+ "Goalie Charts",
542
+ ui.nav_control(
543
+ ui.a(
544
+ "GSAx Timeline",
545
+ href="gsax-timeline/"
546
+ ),
547
+ ui.a(
548
+ "GSAx Leaderboard",
549
+ href="gsax-leaderboard/"
550
+ ),
551
+ ui.a(
552
+ "GSAx Comparison",
553
+ href="gsax-comparison/"
554
+ )
555
+ ),
556
+ ),ui.nav_menu(
557
+ "Team Charts",
558
+ ui.nav_control(
559
+ ui.a(
560
+ "Team xG Rates",
561
+ href="team-xg-rates/"
562
+ ),
563
+ ),
564
+ ),ui.nav_control(
565
+ ui.a(
566
+ "Games",
567
+ href="games/"
568
+ ),
569
+ ),ui.nav_control(
570
+ ui.a(
571
+ "About",
572
+ href="about/"
573
+ ),
574
+ ),ui.nav_control(
575
+ ui.a(
576
+ "Articles",
577
+ href="articles/"
578
+ ),
579
+ )),ui.row(
580
+ ui.layout_sidebar(
581
+
582
+ ui.panel_sidebar(
583
+ ui.input_select("batter_id",
584
+ "Select Batter",
585
+ batter_dict,
586
+ width=1,
587
+ size=1,
588
+ selectize=True),
589
+ ui.input_numeric("quant",
590
+ "Select Percentile",
591
+ value=50,
592
+ min=0,max=100),
593
+ ui.input_numeric("rolling_window",
594
+ "Select Rolling Window",
595
+ value=50,
596
+ min=1)),
597
+
598
+ ui.panel_main(
599
+ ui.navset_tab(
600
+
601
+ ui.nav("Damage Hex",
602
+ ui.output_plot('hex_plot',
603
+ width='1200px',
604
+ height='1200px')),
605
+ ui.nav("Damage Roll",
606
+ ui.output_plot('roll_plot',
607
+ width='1200px',
608
+ height='1200px'))
609
+ ))
610
+ )),)),server)
decision_value.py ADDED
@@ -0,0 +1,683 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ def scatter_plot():
150
+
151
+ if input.batter_id() is "":
152
+ fig = plt.figure(figsize=(12, 12))
153
+ fig.text(s='Please Select a Batter',x=0.5,y=0.5)
154
+ return
155
+ print(df_model_2023_group_swing_plus_no_copy)
156
+ print(input.level_list())
157
+ 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()]
158
+ print('this one')
159
+ print(df_model_2023_group_swing_plus_no)
160
+ batter_select_id = int(input.batter_id())
161
+ # batter_select_name = 'Edouard Julien'
162
+ #max(1,int(input.pitch_min()))
163
+ plot_min = max(250,int(input.pitch_min()))
164
+ df_model_2023_group_swing_plus_no = df_model_2023_group_swing_plus_no[df_model_2023_group_swing_plus_no.pitches >= plot_min]
165
+ ## Plot In-Zone vs Out-of-Zone Awareness
166
+ sns.set_theme(style="whitegrid", palette="pastel")
167
+ # fig, ax = plt.subplots(1,1,figsize=(12,12))
168
+ fig = plt.figure(figsize=(12,12))
169
+ gs = GridSpec(3, 3, height_ratios=[0.6,10,0.2], width_ratios=[0.25,0.50,0.25])
170
+
171
+ axheader = fig.add_subplot(gs[0, :])
172
+ #ax10 = fig.add_subplot(gs[1, 0])
173
+ ax = fig.add_subplot(gs[1, :]) # Subplot at the top-right position
174
+ #ax12 = fig.add_subplot(gs[1, 2])
175
+ axfooter1 = fig.add_subplot(gs[-1, 0])
176
+ axfooter2 = fig.add_subplot(gs[-1, 1])
177
+ axfooter3 = fig.add_subplot(gs[-1, 2])
178
+
179
+ cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],colour_palette[3],colour_palette[0]])
180
+ 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)
181
+
182
+ sns.scatterplot(
183
+ x=df_model_2023_group_swing_plus_no['y_pred_swing']*100,
184
+ y=df_model_2023_group_swing_plus_no['y_pred_no_swing']*100,
185
+ hue=df_model_2023_group_swing_plus_no['y_pred']*100,
186
+ size=df_model_2023_group_swing_plus_no['pitches_swing']/df_model_2023_group_swing_plus_no['pitches'],
187
+ palette=cmap_hue,ax=ax)
188
+
189
+ sm = plt.cm.ScalarMappable(cmap=cmap_hue, norm=norm)
190
+ cbar = plt.colorbar(sm, cax=axfooter2, orientation='horizontal',shrink=1)
191
+ cbar.set_label('Decision Value xRV/100 Pitches',fontsize=12)
192
+
193
+ ax.hlines(xmin=(math.floor((df_model_2023_group_swing_plus_no['y_pred_swing'].min()*100*100-0.01)/5))*5/100,
194
+ xmax= (math.ceil((df_model_2023_group_swing_plus_no['y_pred_swing'].max()**100100+0.01)/5))*5/100,
195
+ y=df_model_2023_group_swing_plus_no['y_pred_no_swing'].mean()*100,color='gray',linewidth=3,linestyle='dotted',alpha=0.4)
196
+
197
+ ax.vlines(ymin=(math.floor((df_model_2023_group_swing_plus_no['y_pred_no_swing'].min()*100*100-0.01)/5))*5/100,
198
+ ymax= (math.ceil((df_model_2023_group_swing_plus_no['y_pred_no_swing'].max()*100*100+0.01)/5))*5/100,
199
+ x=df_model_2023_group_swing_plus_no['y_pred_swing'].mean()*100,color='gray',linewidth=3,linestyle='dotted',alpha=0.4)
200
+
201
+ x_lim_min = (math.floor((df_model_2023_group_swing_plus_no['y_pred_swing'].min()*100*100)/5))*5/100
202
+ x_lim_max = (math.ceil((df_model_2023_group_swing_plus_no['y_pred_swing'].max()*100*100)/5))*5/100
203
+
204
+ y_lim_min = (math.floor((df_model_2023_group_swing_plus_no['y_pred_no_swing'].min()*100*100)/5))*5/100
205
+ y_lim_max = (math.ceil((df_model_2023_group_swing_plus_no['y_pred_no_swing'].max()*100*100)/5))*5/100
206
+
207
+ ax.set_xlim(x_lim_min,x_lim_max)
208
+ ax.set_ylim(y_lim_min,y_lim_max)
209
+
210
+ ax.tick_params(axis='both', which='major', labelsize=12)
211
+
212
+ ax.set_xlabel('Out-of-Zone Awareness Value xRV/100 Swings',fontsize=16)
213
+ ax.set_ylabel('In-Zone Awareness Value xRV/100 Takes',fontsize=16)
214
+ ax.get_legend().remove()
215
+
216
+
217
+ ts=[]
218
+
219
+
220
+ # thresh = 0.5
221
+ # thresh_2 = -0.9
222
+ # for i in range(len(df_model_2023_group_swing_plus_no)):
223
+ # if (df_model_2023_group_swing_plus_no['y_pred'].values[i]*100) >= thresh or \
224
+ # (df_model_2023_group_swing_plus_no['y_pred'].values[i]*100) <= thresh_2 or \
225
+ # (str(df_model_2023_group_swing_plus_no.index.get_level_values(0).values[i]) in (input.name_list())) :
226
+ # ts.append(ax.text(x=df_model_2023_group_swing_plus_no['y_pred_swing'].values[i]*100,
227
+ # y=df_model_2023_group_swing_plus_no['y_pred_no_swing'].values[i]*100,
228
+ # s=df_model_2023_group_swing_plus_no.index.get_level_values(1).values[i],
229
+ # fontsize=8))
230
+ thresh = 0.5
231
+ thresh_2 = -0.9
232
+ for i in range(len(df_model_2023_group_swing_plus_no)):
233
+ 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 \
234
+ (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 \
235
+ (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 \
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.02) or \
237
+ (df_model_2023_group_swing_plus_no['y_pred'].values[i]) >= df_model_2023_group_swing_plus_no['y_pred'].quantile(0.98) or \
238
+ (df_model_2023_group_swing_plus_no['y_pred'].values[i]) <= df_model_2023_group_swing_plus_no['y_pred'].quantile(0.02) or \
239
+ (str(df_model_2023_group_swing_plus_no.index.get_level_values(0).values[i]) in (input.name_list())) :
240
+ ts.append(ax.text(x=df_model_2023_group_swing_plus_no['y_pred_swing'].values[i]*100,
241
+ y=df_model_2023_group_swing_plus_no['y_pred_no_swing'].values[i]*100,
242
+ s=df_model_2023_group_swing_plus_no.index.get_level_values(1).values[i],
243
+ fontsize=8))
244
+
245
+ 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',
246
+ bbox=dict(facecolor='white', edgecolor='black'))
247
+ # 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',
248
+ # bbox=dict(facecolor='white', edgecolor='black'))
249
+ 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',
250
+ bbox=dict(facecolor='white', edgecolor='black'))
251
+
252
+ adjust_text(ts,
253
+ arrowprops=dict(arrowstyle="-", color=colour_palette[4], lw=1),ax=ax)
254
+
255
+ axfooter1.axis('off')
256
+ axfooter3.axis('off')
257
+ axheader.axis('off')
258
+
259
+ 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')
260
+
261
+ axfooter1.text(0.05, -0.5,"By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12)
262
+ axfooter3.text(0.95, -0.5, "Data: MLB",ha='right', va='bottom',fontsize=12)
263
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.975, bottom=0.025)
264
+
265
+ @output
266
+ @render.plot(alt="hex_plot")
267
+ def dv_plot():
268
+
269
+ if input.batter_id() is "":
270
+ fig = plt.figure(figsize=(12, 12))
271
+ fig.text(s='Please Select a Batter',x=0.5,y=0.5)
272
+ return
273
+
274
+ player_select = int(input.batter_id())
275
+ player_select_full = batter_dict[player_select]
276
+
277
+
278
+ df_will = df_model_2023[df_model_2023.batter_id == player_select].sort_values(by=['game_date','start_time'])
279
+ df_will = df_will[df_will['level']==input.level_list()]
280
+ # df_will['y_pred'] = df_will['y_pred'] - df_will['y_pred'].mean()
281
+
282
+ win = max(1,int(input.rolling_window()))
283
+ sns.set_theme(style="whitegrid", palette="pastel")
284
+ #fig, ax = plt.subplots(1, 1, figsize=(10, 10),dpi=300)
285
+
286
+ from matplotlib.gridspec import GridSpec
287
+ # fig,ax = plt.subplots(figsize=(12, 12),dpi=150)
288
+ fig = plt.figure(figsize=(12,12))
289
+ gs = GridSpec(3, 3, height_ratios=[0.3,10,0.2], width_ratios=[0.01,2,0.01])
290
+
291
+ axheader = fig.add_subplot(gs[0, :])
292
+ ax10 = fig.add_subplot(gs[1, 0])
293
+ ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
294
+ ax12 = fig.add_subplot(gs[1, 2])
295
+ axfooter1 = fig.add_subplot(gs[-1, :])
296
+
297
+ axheader.axis('off')
298
+ ax10.axis('off')
299
+ ax12.axis('off')
300
+ axfooter1.axis('off')
301
+
302
+
303
+ sns.lineplot( x= range(win,len(df_will.y_pred.rolling(window=win).mean())+1),
304
+ y= df_will.y_pred.rolling(window=win).mean().dropna()*100,
305
+ color=colour_palette[0],linewidth=2,ax=ax,zorder=100)
306
+
307
+ ax.hlines(y=df_will.y_pred.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[0],linestyle='--',
308
+ 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)')
309
+
310
+ # ax.hlines(y=df_model_2023.y_pred.std()*100,xmin=win,xmax=len(df_will))
311
+
312
+ # sns.scatterplot( x= [976],
313
+ # y= df_will.y_pred.rolling(window=win).mean().min()*100,
314
+ # color=colour_palette[0],linewidth=2,ax=ax,zorder=100,s=100,edgecolor=colour_palette[7])
315
+
316
+
317
+ 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,
318
+ label = f'{input.level_list()} Average: {df_model_2023_group_swing_plus_no.y_pred.mean()*100:.2f} xRV/100')
319
+
320
+ ax.legend()
321
+
322
+ hard_hit_dates = [df_model_2023_group_swing_plus_no.y_pred.quantile(0.9)*100,
323
+ df_model_2023_group_swing_plus_no.y_pred.quantile(0.75)*100,
324
+ df_model_2023_group_swing_plus_no.y_pred.quantile(0.25)*100,
325
+ df_model_2023_group_swing_plus_no.y_pred.quantile(0.1)*100]
326
+
327
+
328
+
329
+ 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)
330
+ 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)
331
+ 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)
332
+ 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)
333
+
334
+ hard_hit_text = ['90th %','75th %','25th %','10th %']
335
+ for i, x in enumerate(hard_hit_dates):
336
+ ax.text(min(win+win/1000,win+win+5), x ,hard_hit_text[i], rotation=0,va='center', ha='left',
337
+ bbox=dict(facecolor='white',alpha=0.7, edgecolor=colour_palette[2+i], pad=2),zorder=11)
338
+
339
+ # # Annotate with an arrow
340
+ # ax.annotate('June 6, 2023\nSeason Worst Decision Value', xy=(976, df_will.y_pred.rolling(window=win).mean().min()*100-0.03),
341
+ # xytext=(976 - 150, df_will.y_pred.rolling(window=win).mean().min()*100 - 0.2),
342
+ # arrowprops=dict(facecolor=colour_palette[7], shrink=0.01),zorder=150,fontsize=10,
343
+ # bbox=dict(facecolor='white', edgecolor='black'),va='top')
344
+
345
+ ax.set_xlim(win,len(df_will))
346
+ #ax.set_ylim(-1.5,1.5)
347
+ ax.set_yticks([-1.5,-1,-0.5,0,0.5,1,1.5])
348
+ ax.set_xlabel('Pitch')
349
+ ax.set_ylabel('Expected Run Value Added per 100 Pitches (xRV/100)')
350
+
351
+ 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)
352
+ axfooter1.text(.05, 0.2, "By: Thomas Nestico",ha='left', va='bottom',fontsize=12)
353
+ axfooter1.text(0.95, 0.2, "Data: MLB",ha='right', va='bottom',fontsize=12)
354
+
355
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.98, bottom=0.02)
356
+ #fig.set_facecolor(colour_palette[5])
357
+
358
+ @output
359
+ @render.plot(alt="hex_plot")
360
+ def iz_plot():
361
+
362
+ if input.batter_id() is "":
363
+ fig = plt.figure(figsize=(12, 12))
364
+ fig.text(s='Please Select a Batter',x=0.5,y=0.5)
365
+ return
366
+
367
+ player_select = int(input.batter_id())
368
+ player_select_full = batter_dict[player_select]
369
+
370
+
371
+ df_will = df_model_2023[df_model_2023.batter_id == player_select].sort_values(by=['game_date','start_time'])
372
+ df_will = df_will[df_will['level']==input.level_list()]
373
+ df_will = df_will[df_will['is_swing'] != 1]
374
+
375
+ win = max(1,int(input.rolling_window()))
376
+ sns.set_theme(style="whitegrid", palette="pastel")
377
+ #fig, ax = plt.subplots(1, 1, figsize=(10, 10),dpi=300)
378
+
379
+ from matplotlib.gridspec import GridSpec
380
+ # fig,ax = plt.subplots(figsize=(12, 12),dpi=150)
381
+ fig = plt.figure(figsize=(12,12))
382
+ gs = GridSpec(3, 3, height_ratios=[0.3,10,0.2], width_ratios=[0.01,2,0.01])
383
+
384
+ axheader = fig.add_subplot(gs[0, :])
385
+ ax10 = fig.add_subplot(gs[1, 0])
386
+ ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
387
+ ax12 = fig.add_subplot(gs[1, 2])
388
+ axfooter1 = fig.add_subplot(gs[-1, :])
389
+
390
+ axheader.axis('off')
391
+ ax10.axis('off')
392
+ ax12.axis('off')
393
+ axfooter1.axis('off')
394
+
395
+
396
+ sns.lineplot( x= range(win,len(df_will.y_pred.rolling(window=win).mean())+1),
397
+ y= df_will.y_pred.rolling(window=win).mean().dropna()*100,
398
+ color=colour_palette[0],linewidth=2,ax=ax,zorder=100)
399
+
400
+ ax.hlines(y=df_will.y_pred.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[0],linestyle='--',
401
+ 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)')
402
+
403
+ # ax.hlines(y=df_model_2023.y_pred_no_swing.std()*100,xmin=win,xmax=len(df_will))
404
+
405
+ # sns.scatterplot( x= [976],
406
+ # y= df_will.y_pred.rolling(window=win).mean().min()*100,
407
+ # color=colour_palette[0],linewidth=2,ax=ax,zorder=100,s=100,edgecolor=colour_palette[7])
408
+
409
+
410
+ 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,
411
+ label = f'{input.level_list()} Average: {df_model_2023_group_swing_plus_no.y_pred_no_swing.mean()*100:.2} xRV/100')
412
+
413
+ ax.legend()
414
+
415
+ hard_hit_dates = [df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.9)*100,
416
+ df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.75)*100,
417
+ df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.25)*100,
418
+ df_model_2023_group_swing_plus_no.y_pred_no_swing.quantile(0.1)*100]
419
+
420
+
421
+
422
+ 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)
423
+ 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)
424
+ 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)
425
+ 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)
426
+
427
+ hard_hit_text = ['90th %','75th %','25th %','10th %']
428
+ for i, x in enumerate(hard_hit_dates):
429
+ ax.text(min(win+win/1000,win+win+5), x ,hard_hit_text[i], rotation=0,va='center', ha='left',
430
+ bbox=dict(facecolor='white',alpha=0.7, edgecolor=colour_palette[2+i], pad=2),zorder=11)
431
+
432
+ # # Annotate with an arrow
433
+ # ax.annotate('June 6, 2023\nSeason Worst Decision Value', xy=(976, df_will.y_pred.rolling(window=win).mean().min()*100-0.03),
434
+ # xytext=(976 - 150, df_will.y_pred.rolling(window=win).mean().min()*100 - 0.2),
435
+ # arrowprops=dict(facecolor=colour_palette[7], shrink=0.01),zorder=150,fontsize=10,
436
+ # bbox=dict(facecolor='white', edgecolor='black'),va='top')
437
+
438
+ ax.set_xlim(win,len(df_will))
439
+ ax.set_yticks([1.0,1.5,2.0,2.5,3.0])
440
+ # ax.set_ylim(1,3)
441
+
442
+ ax.set_xlabel('Takes')
443
+ ax.set_ylabel('Expected Run Value Added per 100 Pitches (xRV/100)')
444
+
445
+ 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)
446
+ axfooter1.text(.05, 0.2, "By: Thomas Nestico",ha='left', va='bottom',fontsize=12)
447
+ axfooter1.text(0.95, 0.2, "Data: MLB",ha='right', va='bottom',fontsize=12)
448
+
449
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.98, bottom=0.02)
450
+
451
+ @output
452
+ @render.plot(alt="hex_plot")
453
+ def oz_plot():
454
+ if input.batter_id() is "":
455
+ fig = plt.figure(figsize=(12, 12))
456
+ fig.text(s='Please Select a Batter',x=0.5,y=0.5)
457
+ return
458
+
459
+ player_select = int(input.batter_id())
460
+ player_select_full = batter_dict[player_select]
461
+
462
+
463
+
464
+ df_will = df_model_2023[df_model_2023.batter_id == player_select].sort_values(by=['game_date','start_time'])
465
+ df_will = df_will[df_will['level']==input.level_list()]
466
+ df_will = df_will[df_will['is_swing'] == 1]
467
+
468
+ win = max(1,int(input.rolling_window()))
469
+ sns.set_theme(style="whitegrid", palette="pastel")
470
+ #fig, ax = plt.subplots(1, 1, figsize=(10, 10),dpi=300)
471
+
472
+ from matplotlib.gridspec import GridSpec
473
+ # fig,ax = plt.subplots(figsize=(12, 12),dpi=150)
474
+ fig = plt.figure(figsize=(12,12))
475
+ gs = GridSpec(3, 3, height_ratios=[0.3,10,0.2], width_ratios=[0.01,2,0.01])
476
+
477
+ axheader = fig.add_subplot(gs[0, :])
478
+ ax10 = fig.add_subplot(gs[1, 0])
479
+ ax = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
480
+ ax12 = fig.add_subplot(gs[1, 2])
481
+ axfooter1 = fig.add_subplot(gs[-1, :])
482
+
483
+ axheader.axis('off')
484
+ ax10.axis('off')
485
+ ax12.axis('off')
486
+ axfooter1.axis('off')
487
+
488
+
489
+ sns.lineplot( x= range(win,len(df_will.y_pred.rolling(window=win).mean())+1),
490
+ y= df_will.y_pred.rolling(window=win).mean().dropna()*100,
491
+ color=colour_palette[0],linewidth=2,ax=ax,zorder=100)
492
+
493
+ ax.hlines(y=df_will.y_pred.mean()*100,xmin=win,xmax=len(df_will),color=colour_palette[0],linestyle='--',
494
+ 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)')
495
+
496
+ # ax.hlines(y=df_model_2023.y_pred_swing.std()*100,xmin=win,xmax=len(df_will))
497
+
498
+ # sns.scatterplot( x= [976],
499
+ # y= df_will.y_pred.rolling(window=win).mean().min()*100,
500
+ # color=colour_palette[0],linewidth=2,ax=ax,zorder=100,s=100,edgecolor=colour_palette[7])
501
+
502
+
503
+ 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,
504
+ label = f'{input.level_list()} Average: {df_model_2023_group_swing_plus_no.y_pred_swing.mean()*100:.2} xRV/100')
505
+
506
+ ax.legend()
507
+
508
+ hard_hit_dates = [df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.9)*100,
509
+ df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.75)*100,
510
+ df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.25)*100,
511
+ df_model_2023_group_swing_plus_no.y_pred_swing.quantile(0.1)*100]
512
+
513
+
514
+
515
+ 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)
516
+ 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)
517
+ 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)
518
+ 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)
519
+
520
+ hard_hit_text = ['90th %','75th %','25th %','10th %']
521
+ for i, x in enumerate(hard_hit_dates):
522
+ ax.text(min(win+win/1000,win+win+5), x ,hard_hit_text[i], rotation=0,va='center', ha='left',
523
+ bbox=dict(facecolor='white',alpha=0.7, edgecolor=colour_palette[2+i], pad=2),zorder=11)
524
+
525
+ # # Annotate with an arrow
526
+ # ax.annotate('June 6, 2023\nSeason Worst Decision Value', xy=(976, df_will.y_pred.rolling(window=win).mean().min()*100-0.03),
527
+ # xytext=(976 - 150, df_will.y_pred.rolling(window=win).mean().min()*100 - 0.2),
528
+ # arrowprops=dict(facecolor=colour_palette[7], shrink=0.01),zorder=150,fontsize=10,
529
+ # bbox=dict(facecolor='white', edgecolor='black'),va='top')
530
+
531
+ ax.set_xlim(win,len(df_will))
532
+ #ax.set_ylim(-3.25,-1.25)
533
+ ax.set_yticks([-3.25,-2.75,-2.25,-1.75,-1.25])
534
+ ax.set_xlabel('Swing')
535
+ ax.set_ylabel('Expected Run Value Added per 100 Pitches (xRV/100)')
536
+
537
+ 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)
538
+ axfooter1.text(.05, 0.2, "By: Thomas Nestico",ha='left', va='bottom',fontsize=12)
539
+ axfooter1.text(0.95, 0.2, "Data: MLB",ha='right', va='bottom',fontsize=12)
540
+
541
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.98, bottom=0.02)
542
+
543
+ decision_value = App(ui.page_fluid(
544
+ ui.tags.base(href=base_url),
545
+ ui.tags.div(
546
+ {"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
547
+ ui.tags.style(
548
+ """
549
+ h4 {
550
+ margin-top: 1em;font-size:35px;
551
+ }
552
+ h2{
553
+ font-size:25px;
554
+ }
555
+ """
556
+ ),
557
+ shinyswatch.theme.simplex(),
558
+ ui.tags.h4("TJStats"),
559
+ ui.tags.i("Baseball Analytics and Visualizations"),
560
+ ui.navset_tab(
561
+ ui.nav_control(
562
+ ui.a(
563
+ "Home",
564
+ href="home/"
565
+ ),
566
+ ),
567
+ ui.nav_menu(
568
+ "Batter Charts",
569
+ ui.nav_control(
570
+ ui.a(
571
+ "Spray",
572
+ href="spray/"
573
+ ),
574
+ ui.a(
575
+ "Decision Value",
576
+ href="decision_value/"
577
+ ),
578
+ ui.a(
579
+ "Damage Model",
580
+ href="damage_model/"
581
+ ),
582
+ ui.a(
583
+ "Batter Scatter",
584
+ href="batter_scatter/"
585
+ ),
586
+ ui.a(
587
+ "EV vs LA Plot",
588
+ href="ev_angle/"
589
+ )
590
+ ),
591
+ ),
592
+ ui.nav_menu(
593
+ "Goalie Charts",
594
+ ui.nav_control(
595
+ ui.a(
596
+ "GSAx Timeline",
597
+ href="gsax-timeline/"
598
+ ),
599
+ ui.a(
600
+ "GSAx Leaderboard",
601
+ href="gsax-leaderboard/"
602
+ ),
603
+ ui.a(
604
+ "GSAx Comparison",
605
+ href="gsax-comparison/"
606
+ )
607
+ ),
608
+ ),ui.nav_menu(
609
+ "Team Charts",
610
+ ui.nav_control(
611
+ ui.a(
612
+ "Team xG Rates",
613
+ href="team-xg-rates/"
614
+ ),
615
+ ),
616
+ ),ui.nav_control(
617
+ ui.a(
618
+ "Games",
619
+ href="games/"
620
+ ),
621
+ ),ui.nav_control(
622
+ ui.a(
623
+ "About",
624
+ href="about/"
625
+ ),
626
+ ),ui.nav_control(
627
+ ui.a(
628
+ "Articles",
629
+ href="articles/"
630
+ ),
631
+ )),ui.row(
632
+ ui.layout_sidebar(
633
+
634
+ ui.panel_sidebar(
635
+
636
+
637
+ ui.input_numeric("pitch_min",
638
+ "Select Pitch Minimum [min. 250] (Scatter)",
639
+ value=500,
640
+ min=250),
641
+
642
+ ui.input_select("name_list",
643
+ "Select Players to List (Scatter)",
644
+ batter_dict,
645
+ selectize=True,
646
+ multiple=True),
647
+ ui.input_select("batter_id",
648
+ "Select Batter (Rolling)",
649
+ batter_dict,
650
+ width=1,
651
+ size=1,
652
+ selectize=True),
653
+ ui.input_numeric("rolling_window",
654
+ "Select Rolling Window (Rolling)",
655
+ value=100,
656
+ min=1),
657
+
658
+ ui.input_select("level_list",
659
+ "Select Level",
660
+ ['MLB','AAA'],
661
+ selected='MLB')),
662
+
663
+ ui.panel_main(
664
+ ui.navset_tab(
665
+
666
+ ui.nav("Scatter Plot",
667
+ ui.output_plot('scatter_plot',
668
+ width='1000px',
669
+ height='1000px')),
670
+ ui.nav("Rolling DV",
671
+ ui.output_plot('dv_plot',
672
+ width='1000px',
673
+ height='1000px')),
674
+ ui.nav("Rolling In-Zone",
675
+ ui.output_plot('iz_plot',
676
+ width='1000px',
677
+ height='1000px')),
678
+ ui.nav("Rolling Out-of-Zone",
679
+ ui.output_plot('oz_plot',
680
+ width='1000px',
681
+ height='1000px'))
682
+ ))
683
+ )),)),server)
ev_angle.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ from matplotlib.pyplot import text
23
+
24
+
25
+ ### Import Datasets
26
+ dataset = load_dataset('nesticot/mlb_data', data_files=['mlb_pitch_data_2023.csv' ])
27
+ dataset_train = dataset['train']
28
+ exit_velo_df = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True)
29
+
30
+ colour_palette = ['#FFB000','#648FFF','#785EF0',
31
+ '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
32
+
33
+
34
+ #exit_velo_df = pd.read_csv('exit_velo_df.csv',index_col=[0])
35
+
36
+ conditions = [
37
+ (exit_velo_df['launch_speed'].isna()),
38
+ (exit_velo_df['launch_speed']*1.5 - exit_velo_df['launch_angle'] >= 117 ) & (exit_velo_df['launch_speed'] + exit_velo_df['launch_angle'] >= 124) & (exit_velo_df['launch_speed'] > 98) & (exit_velo_df['launch_angle'] >= 8) & (exit_velo_df['launch_angle'] <= 50)
39
+ ]
40
+
41
+ choices = [False,True]
42
+ exit_velo_df['barrel'] = np.select(conditions, choices, default=np.nan)
43
+
44
+ test_df = exit_velo_df.sort_values(by='batter_name').drop_duplicates(subset='batter_id').reset_index(drop=True)[['batter_id','batter_name']]#['pitcher'].to_dict()
45
+ test_df = test_df.set_index('batter_id')
46
+
47
+ #test_df = test_df[test_df.pitcher == 'Chris Bassitt'].append(test_df[test_df.pitcher != 'Chris Bassitt'])
48
+
49
+ batter_dict = test_df['batter_name'].to_dict()
50
+
51
+ colour_palette = ['#FFB000','#648FFF','#785EF0',
52
+ '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
53
+
54
+ angle_ev_list_df = pd.read_csv('angle_ev_list_df.csv')
55
+ ev_ranges = list(np.arange(97.5,130,0.1))
56
+ angle_ranges = list(range(8,51))
57
+ #print
58
+
59
+
60
+ def server(input,output,session):
61
+ @output
62
+ @render.plot(alt="A histogram")
63
+ def plot():
64
+ data_df = exit_velo_df[exit_velo_df.batter_id==int(input.id())]
65
+ #pitch_list = exit_velo_df_small.pitch_type.unique()
66
+ sns.set_theme(style="whitegrid", palette="pastel")
67
+ fig, ax = plt.subplots(1, 1, figsize=(10, 10))
68
+
69
+
70
+
71
+ if input.plot_id() == 'dist':
72
+ sns.histplot(x=data_df.launch_angle,y=data_df.launch_speed,cbar=colour_palette,binwidth=(5,2.5),ax=ax,cbar_kws=dict(shrink=.75,label='Count'),binrange=(
73
+ (math.floor((min(data_df.launch_angle.dropna())/5))*5,math.ceil((max(data_df.launch_angle.dropna())/5))*5),(math.floor((min(data_df.launch_speed.dropna())/5))*5,math.ceil((max(data_df.launch_speed.dropna())/5))*5)))
74
+ if input.plot_id() == 'scatter':
75
+ sns.scatterplot(x=data_df.launch_angle,y=data_df.launch_speed,color=colour_palette[1])
76
+ ax.set_xlim(math.floor((min(data_df.launch_angle.dropna())/10))*10,math.ceil((max(data_df.launch_angle.dropna())/10))*10)
77
+ #ticks=np.arange(revels.values.min(),revels.values.max()+1 )
78
+ sns.lineplot(x=angle_ev_list_df.launch_angle,y=angle_ev_list_df.launch_speed,color=colour_palette[0])
79
+ ax.vlines(x=angle_ev_list_df.launch_angle[0],ymin=angle_ev_list_df.launch_speed[0],ymax=ev_ranges[-1],color=colour_palette[0])
80
+ ax.vlines(x=angle_ev_list_df.launch_angle[len(angle_ev_list_df)-1],ymin=angle_ev_list_df.launch_speed[len(angle_ev_list_df)-1],ymax=ev_ranges[-1],color=colour_palette[0])
81
+
82
+ groundball = f'{sum(data_df.launch_angle.dropna()<=10)/len(data_df.launch_angle.dropna()):.1%}'
83
+ linedrive = f'{sum((data_df.launch_angle.dropna()<=25) & (data_df.launch_angle.dropna()>10))/len(data_df.launch_angle.dropna()):.1%}'
84
+ flyball = f'{sum((data_df.launch_angle.dropna()<=50) & (data_df.launch_angle.dropna()>25))/len(data_df.launch_angle.dropna()):.1%}'
85
+ popup = f'{sum(data_df.launch_angle.dropna()>50)/len(data_df.launch_angle.dropna()):.1%}'
86
+ percentages_list = [groundball,linedrive,flyball,popup]
87
+
88
+ hard_hit_percent = f'{sum(data_df.launch_speed.dropna()>=95)/len(data_df.launch_speed.dropna()):.1%}'
89
+
90
+ barrel_percentage = f'{data_df.barrel.dropna().sum()/len(data_df.launch_angle.dropna()):.1%}'
91
+
92
+ plt.text(x=27, y=math.ceil((max(data_df.launch_speed.dropna())/5))*5+5-3, s=f'Barrel% {barrel_percentage}',ha='left',bbox=dict(facecolor='white',alpha=0.8, edgecolor=colour_palette[4], pad=5))
93
+
94
+
95
+ sample_dates = np.array([math.floor((min(data_df.launch_angle.dropna())/10))*10,10,25,50])
96
+ sample_text = [f'Groundball ({groundball})',f'Line Drive ({linedrive})',f'Fly Ball ({flyball})',f'Pop-up ({popup})']
97
+
98
+ hard_hit_dates = [95]
99
+ hard_hit_text = [f'Hard Hit% ({hard_hit_percent})']
100
+
101
+
102
+
103
+ #sample_dates = mdates.date2num(sample_dates)
104
+ plt.hlines(y=hard_hit_dates,xmin=math.floor((min(data_df.launch_angle.dropna())/10))*10, xmax=math.ceil((max(data_df.launch_angle.dropna())/10))*10, color = colour_palette[4],linestyles='--')
105
+ plt.vlines(x=sample_dates, ymin=0, ymax=130, color = colour_palette[3],linestyles='--')
106
+
107
+
108
+ # ax.vlines(x=10,ymin=0,ymax=ev_ranges[-1],color=colour_palette[3],linestyles='--')
109
+ # ax.vlines(x=25,ymin=0,ymax=ev_ranges[-1],color=colour_palette[3],linestyles='--')
110
+ # ax.vlines(x=50,ymin=0,ymax=ev_ranges[-1],color=colour_palette[3],linestyles='--')
111
+
112
+
113
+
114
+ for i, x in enumerate(hard_hit_dates):
115
+ text(math.ceil((max(data_df.launch_angle.dropna())/10))*10-2.5, x+1.25,hard_hit_text[i], rotation=0, ha='right',
116
+ bbox=dict(facecolor='white',alpha=0.5, edgecolor=colour_palette[4], pad=5))
117
+
118
+
119
+ for i, x in enumerate(sample_dates):
120
+ text(x+0.75, (math.floor((min(data_df.launch_speed.dropna())/5))*5)+1,sample_text[i], rotation=90, verticalalignment='bottom',
121
+ bbox=dict(facecolor='white',alpha=0.5, edgecolor=colour_palette[3], pad=5))
122
+ #ax.vlines(x=math.floor((min(data_df.launch_angle.dropna())/10))*10+1,ymin=0,ymax=ev_ranges[-1],color=colour_palette[3],linestyles='--')
123
+
124
+ ax.set_xlim((math.floor((min(data_df.launch_angle.dropna())/10))*10,math.ceil((max(data_df.launch_angle.dropna())/10))*10))
125
+ ax.set_ylim((math.floor((min(data_df.launch_speed.dropna())/5))*5,math.ceil((max(data_df.launch_speed.dropna())/5))*5+5))
126
+ # ax.set_xlim(-90,90)
127
+ # ax.set_ylim(0,125)
128
+ ax.set_title(f'MLB - {data_df.batter_name.unique()[0]} Launch Angle vs EV Plot', fontsize=18,fontname='Century Gothic',)
129
+ #vals = ax.get_yticks()
130
+ ax.set_xlabel('Launch Angle', fontsize=16,fontname='Century Gothic')
131
+ ax.set_ylabel('Exit Velocity', fontsize=16,fontname='Century Gothic')
132
+ ax.fill_between(angle_ev_list_df.launch_angle, 130, angle_ev_list_df.launch_speed, interpolate=True, color=colour_palette[3],alpha=0.1,label='Barrel')
133
+ #fig.colorbar(plot_dist, ax=ax)
134
+ #fig.colorbar(plot_dist)
135
+ #fig.axes[0].invert_yaxis()
136
+ ax.legend(fontsize='16',loc='upper left')
137
+ fig.text(x=0.03,y=0.02,s='By: @TJStats')
138
+ fig.text(x=1-0.03,y=0.02,s='Data: MLB',ha='right')
139
+
140
+ # fig.text(x=0.25,y=0.02,s='Data: MLB',ha='right')
141
+ # fig.text(x=0.25,y=0.02,s='Data: MLB',ha='right')
142
+ # fig.text(x=0.25,y=0.02,s='Data: MLB',ha='right')
143
+ #cbar = plt.colorbar()
144
+ #fig.subplots_adjust(wspace=.02, hspace=.02)
145
+ #ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
146
+ fig.set_facecolor('white')
147
+ fig.tight_layout()
148
+ #matplotlib.rcParams["figure.dpi"] = 300
149
+
150
+ # ax.set_xlim(input.n(),exit_velo_df_small.pitch.max())
151
+ #ax.axis('off')
152
+
153
+ #fig.set_facecolor('white')
154
+ #fig.tight_layout()
155
+ #ax.hist(exit_velo_df[exit_velo_df.pitcher_id==int(input.id())]['pitch_velocity'],input.n(),density=True)
156
+ #plt.show()
157
+ #return g
158
+
159
+ # This is a shiny.App object. It must be named `app`.
160
+
161
+ # fig, ax = plt.subplots()
162
+ #print(input.pitcher_id())
163
+ # print(input)
164
+ # plt.hist(x=exit_velo_df[exit_velo_df.pitcher_id==input.x()]['pitch_velocity'])
165
+ # plt.show()
166
+
167
+
168
+ ev_angle = App(ui.page_fluid(
169
+ ui.tags.base(href=base_url),
170
+ ui.tags.div(
171
+ {"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
172
+ ui.tags.style(
173
+ """
174
+ h4 {
175
+ margin-top: 1em;font-size:35px;
176
+ }
177
+ h2{
178
+ font-size:25px;
179
+ }
180
+ """
181
+ ),
182
+ shinyswatch.theme.simplex(),
183
+ ui.tags.h4("TJStats"),
184
+ ui.tags.i("Baseball Analytics and Visualizations"),
185
+ ui.navset_tab(
186
+ ui.nav_control(
187
+ ui.a(
188
+ "Home",
189
+ href="home/"
190
+ ),
191
+ ),
192
+ ui.nav_menu(
193
+ "Batter Charts",
194
+ ui.nav_control(
195
+ ui.a(
196
+ "Spray",
197
+ href="spray/"
198
+ ),
199
+ ui.a(
200
+ "Decision Value",
201
+ href="decision_value/"
202
+ ),
203
+ ui.a(
204
+ "Damage Model",
205
+ href="damage_model/"
206
+ ),
207
+ ui.a(
208
+ "Batter Scatter",
209
+ href="batter_scatter/"
210
+ ),
211
+ ui.a(
212
+ "EV vs LA Plot",
213
+ href="ev_angle/"
214
+ )
215
+ ),
216
+ ),
217
+ ui.nav_menu(
218
+ "Goalie Charts",
219
+ ui.nav_control(
220
+ ui.a(
221
+ "GSAx Timeline",
222
+ href="gsax-timeline/"
223
+ ),
224
+ ui.a(
225
+ "GSAx Leaderboard",
226
+ href="gsax-leaderboard/"
227
+ ),
228
+ ui.a(
229
+ "GSAx Comparison",
230
+ href="gsax-comparison/"
231
+ )
232
+ ),
233
+ ),ui.nav_menu(
234
+ "Team Charts",
235
+ ui.nav_control(
236
+ ui.a(
237
+ "Team xG Rates",
238
+ href="team-xg-rates/"
239
+ ),
240
+ ),
241
+ ),ui.nav_control(
242
+ ui.a(
243
+ "Games",
244
+ href="games/"
245
+ ),
246
+ ),ui.nav_control(
247
+ ui.a(
248
+ "About",
249
+ href="about/"
250
+ ),
251
+ ),ui.nav_control(
252
+ ui.a(
253
+ "Articles",
254
+ href="articles/"
255
+ ),
256
+ )),ui.row(
257
+ ui.layout_sidebar(
258
+
259
+
260
+
261
+
262
+ ui.panel_sidebar(
263
+ ui.input_select("id", "Select Batter",batter_dict,width=1),
264
+ ui.input_select("plot_id", "Select Plot",{'scatter':'Scatter Plot','dist':'Distribution Plot'},width=1)
265
+ ),
266
+
267
+ ui.panel_main(
268
+ ui.output_plot("plot",height = "1000px",width="1000px")
269
+ ),
270
+ )),)),server)
home.py CHANGED
@@ -14,7 +14,7 @@ from configure import base_url
14
  home = App(ui.page_fluid(
15
  ui.tags.base(href=base_url),
16
  ui.tags.div(
17
- {"style": "width:75%;margin: 0 auto;max-width: 1500px;"},
18
  ui.tags.style(
19
  """
20
  h4 {
@@ -25,8 +25,9 @@ home = App(ui.page_fluid(
25
  }
26
  """
27
  ),
28
- shinyswatch.theme.darkly(),ui.tags.h4("Stats By Zach"),
29
- ui.tags.i("A website for hockey analytics"),
 
30
  ui.navset_tab(
31
  ui.nav_control(
32
  ui.a(
@@ -35,16 +36,28 @@ home = App(ui.page_fluid(
35
  ),
36
  ),
37
  ui.nav_menu(
38
- "Skater Charts",
39
  ui.nav_control(
40
  ui.a(
41
- "On-Ice xG Rates",
42
- href="skater-xg-rates/"
43
  ),
44
  ui.a(
45
- "On-Ice xGF%",
46
- href="skater-xg-percentages/"
47
  ),
 
 
 
 
 
 
 
 
 
 
 
 
48
  ),
49
  ),
50
  ui.nav_menu(
@@ -86,4 +99,4 @@ home = App(ui.page_fluid(
86
  "Articles",
87
  href="articles/"
88
  ),
89
- )),ui.tags.br(),ui.tags.h5("Welcome to Stats By Zach!"),ui.tags.h6("The 2023-24 NHL regular season is here, and the StatsByZach website is officially up and running for it! As I've state before, this website is still a work in progress, with lots of work to be done in terms of styling and compatibility especially. Along with that, I am focusing on finding a new hosting solution, adding more charts, and some prerformace enhancements as well. Thank you for paying the site a visit, and I do hope you can use my data to better understand the NHL. The website gets updated daily, and I try to make improvements on a regular basis, so please do visit the site often, and feel free to reach out to me on Twitter @StatsByZach for any feedback or suggestions. Enjoy the site, and happy hockey season!"))), None)
 
14
  home = App(ui.page_fluid(
15
  ui.tags.base(href=base_url),
16
  ui.tags.div(
17
+ {"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
18
  ui.tags.style(
19
  """
20
  h4 {
 
25
  }
26
  """
27
  ),
28
+ shinyswatch.theme.simplex(),
29
+ ui.tags.h4("TJStats"),
30
+ ui.tags.i("Baseball Analytics and Visualizations"),
31
  ui.navset_tab(
32
  ui.nav_control(
33
  ui.a(
 
36
  ),
37
  ),
38
  ui.nav_menu(
39
+ "Batter Charts",
40
  ui.nav_control(
41
  ui.a(
42
+ "Spray",
43
+ href="spray/"
44
  ),
45
  ui.a(
46
+ "Decision Value",
47
+ href="decision_value/"
48
  ),
49
+ ui.a(
50
+ "Damage Model",
51
+ href="damage_model/"
52
+ ),
53
+ ui.a(
54
+ "Batter Scatter",
55
+ href="batter_scatter/"
56
+ ),
57
+ ui.a(
58
+ "EV vs LA Plot",
59
+ href="ev_angle/"
60
+ )
61
  ),
62
  ),
63
  ui.nav_menu(
 
99
  "Articles",
100
  href="articles/"
101
  ),
102
+ )),ui.tags.br(),ui.tags.h5("Welcome to TJStats!"),ui.tags.h6(""))), None)
manifest.json ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": 1,
3
+ "locale": "en_CA.cp1252",
4
+ "metadata": {
5
+ "appmode": "python-shiny",
6
+ "entrypoint": "app"
7
+ },
8
+ "python": {
9
+ "version": "3.9.13",
10
+ "package_manager": {
11
+ "name": "pip",
12
+ "version": "23.1",
13
+ "package_file": "requirements.txt"
14
+ }
15
+ },
16
+ "files": {
17
+ "requirements.txt": {
18
+ "checksum": "d9ed48c97a63d10c292a5a380e83b577"
19
+ },
20
+ ".dockerignore": {
21
+ "checksum": "abd9778fec883c042a294e6f8ec4d95f"
22
+ },
23
+ "Dockerfile": {
24
+ "checksum": "ad90b7a7fd00daa424a233ac74027c3f"
25
+ },
26
+ "app.py": {
27
+ "checksum": "9ede9917d07c102f70acc73a105debdf"
28
+ },
29
+ "runtime.txt": {
30
+ "checksum": "d500af0d9f1bed234ef59fa020da5bf1"
31
+ }
32
+ }
33
+ }
no_swing.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c3da3e7ab2b513b87d05e90ae30c788ac819dfcaa7cc1cd9943fc13d2958a00f
3
+ size 279409
requirements.txt CHANGED
@@ -1,18 +1,310 @@
1
  # requirements.txt generated by rsconnect-python on 2023-04-20 21:38:50.254957
2
- #adjustText==0.8
3
- #dataframe_image==0.1.11
4
- datasets==2.16.1
5
- inflect==7.0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  matplotlib==3.5.1
7
- numpy==1.23.5
8
- pandas==1.5.2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  Pillow==9.0.1
10
- plotly==5.14.1
11
- #Pillow==10.0.0
12
- Requests==2.31.0
13
- scipy==1.11.1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  seaborn==0.11.1
15
- shiny==0.6.1
16
- shinywidgets
17
- shinyswatch
18
- starlette==0.26.1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # requirements.txt generated by rsconnect-python on 2023-04-20 21:38:50.254957
2
+ adjustText==0.7.3
3
+ aiohttp==3.8.1
4
+ aiosignal==1.2.0
5
+ altair==4.2.0
6
+ ansicolors==1.1.8
7
+ anyio==3.6.2
8
+ appdirs==1.4.4
9
+ argon2-cffi==21.1.0
10
+ asgiref==3.5.2
11
+ aspose-words==22.10.0
12
+ asttokens==2.2.1
13
+ async-timeout==4.0.2
14
+ atomicwrites==1.4.0
15
+ attrs==20.3.0
16
+ backcall==0.2.0
17
+ bandit==1.7.4
18
+ baseball-scraper==0.4.10
19
+ beautifulsoup4==4.9.3
20
+ bleach==4.1.0
21
+ branca==0.4.2
22
+ bs4==0.0.1
23
+ cachetools==5.2.0
24
+ cairocffi==1.2.0
25
+ CairoSVG==2.6.0
26
+ calfem-python==3.5.9
27
+ certifi==2020.12.5
28
+ cffi==1.14.5
29
+ chardet==4.0.0
30
+ charset-normalizer==2.0.11
31
+ chart-studio==1.1.0
32
+ click==8.1.3
33
+ click-plugins==1.1.1
34
+ cligj==0.7.2
35
+ colorama==0.4.4
36
+ colour==0.1.5
37
+ commonmark==0.9.1
38
+ contextvars==2.4
39
+ coverage==6.5.0
40
+ cryptography==38.0.1
41
+ cssselect==1.1.0
42
+ cssselect2==0.4.1
43
+ cssutils==2.2.0
44
+ cycler==0.10.0
45
+ dash==2.9.3
46
+ dash-core-components==2.0.0
47
+ dash-html-components==2.0.0
48
+ dash-table==5.0.0
49
+ dataframe-image==0.1.7
50
+ DateTime==4.3
51
+ decorator==4.4.2
52
+ decorest==0.0.6
53
+ defusedxml==0.7.1
54
+ Deprecated==1.2.13
55
+ deserialize==1.8.1
56
+ df2img==0.2.9
57
+ Django==4.1.1
58
+ dmsh==0.2.19
59
+ docopt==0.6.2
60
+ entrypoints==0.3
61
+ et-xmlfile==1.0.1
62
+ executing==1.2.0
63
+ ezdxf==0.17.2
64
+ fastapi==0.95.0
65
+ fastjsonschema==2.16.3
66
+ flake8==5.0.4
67
+ Flask==2.2.3
68
+ folium==0.12.1.post1
69
+ fonttools==4.31.2
70
+ frozenlist==1.3.0
71
+ fsspec==2023.4.0
72
+ fuzzywuzzy==0.18.0
73
+ geomdl==5.3.1
74
+ gitdb==4.0.9
75
+ GitPython==3.1.29
76
+ gmsh==4.9.5
77
+ google-api-core==2.10.1
78
+ google-api-python-client==2.63.0
79
+ google-auth==2.12.0
80
+ google-auth-httplib2==0.1.0
81
+ google-auth-oauthlib==0.5.3
82
+ google-spreadsheet==0.0.6
83
+ googleapis-common-protos==1.56.4
84
+ graphviz==0.19.1
85
+ greenlet==1.1.3.post0
86
+ gspread==5.5.0
87
+ h11==0.12.0
88
+ h5py==3.6.0
89
+ hockey-rink==0.1.1
90
+ hockey-scraper==1.37.1
91
+ hockeyjockey==1.2
92
+ html2image==2.0.1
93
+ html5lib==1.1
94
+ htmltools==0.2.1
95
+ httpcore==0.15.0
96
+ httplib2==0.20.4
97
+ httpx==0.23.0
98
+ humanize==4.6.0
99
+ idna==2.10
100
+ image==1.5.33
101
+ imageio==2.15.0
102
+ imageio-ffmpeg==0.4.8
103
+ imgkit==1.2.2
104
+ immutables==0.19
105
+ importlib-metadata==6.0.0
106
+ importlib-resources==5.12.0
107
+ iniconfig==1.1.1
108
+ ipykernel==5.5.0
109
+ ipython==8.11.0
110
+ ipython-genutils==0.2.0
111
+ itsdangerous==2.1.2
112
+ jdcal==1.4.1
113
+ jedi==0.18.0
114
+ Jinja2==3.0.1
115
+ joblib==1.1.0
116
+ jsonschema==3.2.0
117
+ jupyter-client==6.1.11
118
+ jupyter-core==4.7.1
119
+ jupyterlab-pygments==0.1.2
120
+ kaleido==0.2.1
121
+ kiwisolver==1.3.1
122
+ libhockey==0.23.0
123
+ linkify-it-py==2.0.0
124
+ lxml==4.6.2
125
+ markdown-it-py==2.2.0
126
+ MarkupSafe==2.1.2
127
  matplotlib==3.5.1
128
+ matplotlib-inline==0.1.6
129
+ mccabe==0.7.0
130
+ mdit-py-plugins==0.3.5
131
+ mdurl==0.1.2
132
+ menyou==1.0
133
+ meshio==5.3.4
134
+ meshplex==0.17.2
135
+ meshzoo==0.9.4
136
+ mistune==0.8.4
137
+ MLB-StatsAPI==1.4.2
138
+ more-itertools==8.12.0
139
+ moviepy==1.0.3
140
+ mpmath==1.2.1
141
+ multidict==6.0.2
142
+ munch==2.5.0
143
+ mysql-connector-python==8.0.24
144
+ natsort==7.1.1
145
+ nbclient==0.5.4
146
+ nbconvert==6.4.0
147
+ nbformat==5.1.3
148
+ ndim==0.1.6
149
+ nest-asyncio==1.5.1
150
+ networkx==2.6.3
151
+ nhl-logo-scraper==1.1.0
152
+ nhlpy==0.3.0
153
+ nibabel==3.2.2
154
+ notebook==6.4.4
155
+ npx==0.1.1
156
+ numpy==1.22.3
157
+ oauth2client==4.1.3
158
+ oauthlib==3.2.1
159
+ openpyxl==3.0.5
160
+ optimesh==0.8.7
161
+ orthopy==0.9.5
162
+ packaging==21.3
163
+ pandas==1.2.0
164
+ pandocfilters==1.5.0
165
+ parso==0.8.1
166
+ patsy==0.5.1
167
+ pbr==5.11.0
168
+ pdfkit==1.0.0
169
+ pickleshare==0.7.5
170
  Pillow==9.0.1
171
+ pins==0.8.0
172
+ pip==23.1
173
+ platformdirs==2.5.1
174
+ plotly==4.1.1
175
+ pluggy==0.13.1
176
+ praw==7.7.0
177
+ prawcore==2.3.0
178
+ proglog==0.1.10
179
+ prometheus-client==0.11.0
180
+ prompt-toolkit==3.0.38
181
+ protobuf==4.21.6
182
+ psutil==5.9.3
183
+ pure-eval==0.2.2
184
+ py==1.10.0
185
+ pyaml==20.4.0
186
+ pyarrow==5.0.0
187
+ pyasn1==0.4.8
188
+ pyasn1-modules==0.2.8
189
+ pybaseball==2.2.1
190
+ pybind11==2.9.1
191
+ pycairo==1.23.0
192
+ pycodestyle==2.9.1
193
+ pycparser==2.20
194
+ pydantic==1.10.7
195
+ pyfiglet==0.8.post1
196
+ pyflakes==2.5.0
197
+ PyGithub==1.55
198
+ Pygments==2.8.0
199
+ pygsheets==2.0.5
200
+ PyHockeyStats==0.5.2
201
+ PyJWT==2.6.0
202
+ pymesh==1.0.2
203
+ PyNaCl==1.4.0
204
+ PyOpenGL==3.1.6
205
+ pyOpenSSL==22.1.0
206
+ pyparsing==3.0.7
207
+ pyperclip==1.8.2
208
+ pyproj==3.2.1
209
+ PyQt5==5.15.6
210
+ PyQt5-Qt5==5.15.2
211
+ PyQt5-sip==12.9.1
212
+ PyQtWebEngine==5.15.5
213
+ PyQtWebEngine-Qt5==5.15.2
214
+ pyrsistent==0.17.3
215
+ pytest==7.1.3
216
+ pytest_check==1.0.5
217
+ python-dateutil==2.8.2
218
+ python-dotenv==0.21.0
219
+ python-mlb-statsapi==0.3.9
220
+ python-multipart==0.0.6
221
+ pytools==2022.1.1
222
+ pytz==2022.7.1
223
+ PyVTK==0.5.18
224
+ PyWavelets==1.2.0
225
+ pywin32==305
226
+ pywinpty==1.1.4
227
+ PyYAML==5.3.1
228
+ pyzmq==25.0.1
229
+ quadpy==0.16.10
230
+ rauth==0.7.3
231
+ requests==2.28.1
232
+ requests-mock==1.10.0
233
+ requests-oauthlib==1.3.1
234
+ retrying==1.3.3
235
+ rfc3986==1.5.0
236
+ rhino-shapley-interop==0.0.4
237
+ rhino3dm==7.15.0
238
+ rich==12.0.0
239
+ riotwatcher==3.2.4
240
+ rsa==4.9
241
+ scikit-image==0.19.1
242
+ scikit-learn==1.0.1
243
+ scipy==1.6.0
244
  seaborn==0.11.1
245
+ selenium==3.141.0
246
+ semver==2.13.0
247
+ Send2Trash==1.8.0
248
+ Shapely==1.7.1
249
+ shiny==0.3.0
250
+ six==1.16.0
251
+ sklearn==0.0
252
+ smmap==5.0.0
253
+ sniffio==1.3.0
254
+ soupsieve==2.4
255
+ spotipy==2.18.0
256
+ SQLAlchemy==1.4.42
257
+ sqlparse==0.4.2
258
+ stack-data==0.6.2
259
+ starlette==0.26.1
260
+ statsmodels==0.12.2
261
+ stevedore==4.1.1
262
+ stringcase==1.2.0
263
+ sympy==1.10
264
+ tabulate==0.9.0
265
+ tekore==4.5.0
266
+ tenacity==8.2.2
267
+ terminado==0.12.1
268
+ termplotlib==0.3.9
269
+ testpath==0.5.0
270
+ threadpoolctl==3.0.0
271
+ tifffile==2022.2.2
272
+ tinycss2==1.2.1
273
+ toml==0.10.2
274
+ tomli==2.0.1
275
+ toolz==0.11.2
276
+ TopDownHockey-Scraper==2.0.1
277
+ tornado==6.2
278
+ tqdm==4.62.3
279
+ traitlets==5.9.0
280
+ triangle==20220202
281
+ trimeshpy==0.0.2
282
+ tweepy==4.13.0
283
+ typing_extensions==4.5.0
284
+ tzdata==2022.2
285
+ uc-micro-py==1.0.1
286
+ Unidecode==1.3.4
287
+ update-checker==0.18.0
288
+ uritemplate==4.1.1
289
+ urllib3==1.26.15
290
+ uvicorn==0.21.1
291
+ vega-datasets==0.9.0
292
+ visvis==1.13.0
293
+ vtk==9.1.0
294
+ wcwidth==0.2.6
295
+ webdriver-manager==3.8.5
296
+ webencodings==0.5.1
297
+ websocket-client==1.5.1
298
+ websockets==11.0.1
299
+ Werkzeug==2.2.3
300
+ wget==3.2
301
+ wrapt==1.12.1
302
+ wslink==1.4.3
303
+ xmltodict==0.12.0
304
+ xxhash==3.2.0
305
+ yahoo-oauth==2.0
306
+ yarl==1.7.2
307
+ yffpy==2.11.0
308
+ yfpy==9.1.0
309
+ zipp==3.15.0
310
+ zope.interface==5.4.0
runtime.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ python-3.9.13
spray.py ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ def plot():
85
+
86
+ batter_id_select = int(input.batter_id())
87
+ df_batter_2023 = df_2023_bip.loc[(df_2023_bip['batter_id'] == batter_id_select)&(df_2023_bip['season']==2023)]
88
+ df_batter_2022 = df_2023_bip.loc[(df_2023_bip['batter_id'] == batter_id_select)&(df_2023_bip['season']==2022)]
89
+
90
+ df_non_batter_2023 = df_2023_bip.loc[(df_2023_bip['batter_id'] != batter_id_select)&(df_2023_bip['season']==2023)]
91
+ df_non_batter_2022 = df_2023_bip.loc[(df_2023_bip['batter_id'] != batter_id_select)&(df_2023_bip['season']==2022)]
92
+
93
+ traj_df = df_batter_2023.groupby(['traj'])['launch_speed'].count() / len(df_batter_2023)
94
+ trajectory_df = df_batter_2023.groupby(['trajectory'])['launch_speed'].count() / len(df_batter_2023)#.loc['Oppo']
95
+
96
+
97
+
98
+
99
+ colour_palette = ['#FFB000','#648FFF','#785EF0',
100
+ '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
101
+
102
+ fig = plt.figure(figsize=(10, 10))
103
+
104
+
105
+
106
+ # Create a 2x2 grid of subplots using GridSpec
107
+ gs = GridSpec(3, 3, width_ratios=[0.1,0.8,0.1], height_ratios=[0.1,0.8,0.1])
108
+
109
+ # ax00 = fig.add_subplot(gs[0, 0])
110
+ ax01 = fig.add_subplot(gs[0, :]) # Subplot at the top-right position
111
+ # ax02 = fig.add_subplot(gs[0, 2])
112
+ # Subplot spanning the entire bottom row
113
+ ax10 = fig.add_subplot(gs[1, 0])
114
+ ax11 = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position
115
+ ax12 = fig.add_subplot(gs[1, 2])
116
+ # ax20 = fig.add_subplot(gs[2, 0])
117
+ ax21 = fig.add_subplot(gs[2, :]) # Subplot at the top-right position
118
+ # ax22 = fig.add_subplot(gs[2, 2])
119
+
120
+ initial_position = ax12.get_position()
121
+
122
+ # Change the size of the axis
123
+ # new_width = 0.06 # Set your desired width
124
+ # new_height = 0.4 # Set your desired height
125
+ # new_position = [initial_position.x0-0.01, initial_position.y0+0.065, new_width, new_height]
126
+ # ax12.set_position(new_position)
127
+
128
+ cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],'#ffffff',colour_palette[0]])
129
+ # Generate two sets of two-dimensional data
130
+ # data1 = np.random.multivariate_normal([0, 0], [[1, 0.5], [0.5, 1]], 1000)
131
+ # data2 = np.random.multivariate_normal([3, 3], [[1, -0.5], [-0.5, 1]], 1000)
132
+ bat_hand = df_batter_2023.groupby('batter_hand')['launch_speed'].count().sort_values(ascending=False).index[0]
133
+
134
+ bat_hand_value = 1
135
+
136
+ if bat_hand == 'R':
137
+ bat_hand_value = -1
138
+
139
+ kde1_df = df_batter_2023[['h_la','launch_angle']]
140
+ kde1_df['h_la'] = kde1_df['h_la'] * bat_hand_value
141
+ kde2_df = df_non_batter_2023[['h_la','launch_angle']].sample(n=50000, random_state=42)
142
+ kde2_df['h_la'] = kde2_df['h_la'] * bat_hand_value
143
+
144
+
145
+ # Calculate 2D KDE for each dataset
146
+ kde1 = gaussian_kde(kde1_df.values.T)
147
+ kde2 = gaussian_kde(kde2_df.values.T)
148
+
149
+ # Generate a grid of points for evaluation
150
+ x, y = np.meshgrid(np.arange(-45, 46,1 ), np.arange(-30, 61,1 ))
151
+ positions = np.vstack([x.ravel(), y.ravel()])
152
+
153
+ # Evaluate the KDEs on the grid
154
+ kde1_values = np.reshape(kde1(positions).T, x.shape)
155
+ kde2_values = np.reshape(kde2(positions).T, x.shape)
156
+
157
+ # Subtract one KDE from the other
158
+ result_kde_values = kde1_values - kde2_values
159
+
160
+ # Normalize the array to the range [0, 1]
161
+ # result_kde_values = (result_kde_values - np.min(result_kde_values)) / (np.max(result_kde_values) - np.min(result_kde_values))
162
+ result_kde_values = (result_kde_values - np.mean(result_kde_values)) / (np.std(result_kde_values))
163
+
164
+ result_kde_values = np.clip(result_kde_values, -3, 3)
165
+ # # Plot the original KDEs
166
+ # plt.contourf(x, y, kde1_values, cmap='Blues', alpha=0.5, levels=20)
167
+ # plt.contourf(x, y, kde2_values, cmap='Reds', alpha=0.5, levels=20)
168
+
169
+ # Plot the subtracted KDE
170
+ # Set the number of levels and midrange value
171
+ # Set the number of levels and midrange value
172
+ num_levels = 14
173
+ midrange_value = 0
174
+
175
+ # Create a filled contour plot with specified levels
176
+ levels = np.linspace(-3, 3, num_levels)
177
+
178
+ batter_plot = ax11.contourf(x, y, result_kde_values, cmap=cmap_hue, levels=levels, vmin=-3, vmax=3)
179
+
180
+
181
+ ax11.hlines(y=10,xmin=45,xmax=-45,color=colour_palette[3],linewidth=1)
182
+ ax11.hlines(y=25,xmin=45,xmax=-45,color=colour_palette[3],linewidth=1)
183
+ ax11.hlines(y=50,xmin=45,xmax=-45,color=colour_palette[3],linewidth=1)
184
+
185
+ ax11.vlines(x=-15,ymin=-30,ymax=60,color=colour_palette[3],linewidth=1)
186
+ ax11.vlines(x=15,ymin=-30,ymax=60,color=colour_palette[3],linewidth=1)
187
+ #ax11.axis('square')
188
+ #ax11.axis('off')
189
+ #ax.hlines(y=10,xmin=-45,xmax=-45)
190
+ # Add labels and legend
191
+ #plt.xlabel('X-axis')
192
+ #plt.ylabel('Y-axis')
193
+ #ax.plot('equal')
194
+ #plt.gca().set_aspect('equal')
195
+
196
+ #Choose a mappable (can be any plot or image)
197
+ ax12.set_ylim(0,1)
198
+ cbar = plt.colorbar(batter_plot, cax=ax12, orientation='vertical',shrink=1)
199
+ cbar.set_ticks([])
200
+ # Set the colorbar to have 13 levels
201
+ cbar_locator = MaxNLocator(nbins=13)
202
+ cbar.locator = cbar_locator
203
+ cbar.update_ticks()
204
+ #cbar.set_clim(vmin=-3, vmax=)
205
+ # Set ticks and tick labels
206
+ # cbar.set_ticks(np.linspace(-3, 3, 13))
207
+ # cbar.set_ticklabels(np.linspace(0, 3, 13))
208
+ cbar.set_ticks([])
209
+
210
+
211
+
212
+
213
+ ax10.text(s=f"Pop Up\n({trajectory_df.loc['popup']:.1%})",
214
+ x=1,
215
+ y=0.95,va='center',ha='right',fontsize=16)
216
+ # Choose a mappable (can be any plot or image)
217
+ ax10.text(s=f"Fly Ball\n({trajectory_df.loc['fly_ball']:.1%})",
218
+ x=1,
219
+ y=0.75,va='center',ha='right',fontsize=16)
220
+
221
+ ax10.text(s=f"Line\nDrive\n({trajectory_df.loc['line_drive']:.1%})",
222
+ x=1,
223
+ y=0.53,va='center',ha='right',fontsize=16)
224
+
225
+
226
+ ax10.text(s=f"Ground\nBall\n({trajectory_df.loc['ground_ball']:.1%})",
227
+ x=1,
228
+ y=0.23,va='center',ha='right',fontsize=16)
229
+ #ax12.axis(True)
230
+ # Set equal aspect ratio for the contour plot
231
+
232
+ if bat_hand == 'R':
233
+
234
+
235
+ ax21.text(s=f"Pull\n({traj_df.loc['Pull']:.1%})",
236
+ x=0.2+1/16*0.8,
237
+ y=1,va='top',ha='center',fontsize=16)
238
+
239
+ ax21.text(s=f"Straight\n({traj_df.loc['Straight']:.1%})",
240
+ x=0.5,
241
+ y=1,va='top',ha='center',fontsize=16)
242
+
243
+ ax21.text(s=f"Oppo\n({traj_df.loc['Oppo']:.1%})",
244
+ x=0.8-1/16*0.8,
245
+ y=1,va='top',ha='center',fontsize=16)
246
+
247
+ else:
248
+
249
+ ax21.text(s=f"Pull\n({traj_df.loc['Pull']:.1%})",
250
+ x=0.8-1/16*0.8,
251
+ y=1,va='top',ha='center',fontsize=16)
252
+
253
+ ax21.text(s=f"Straight\n({traj_df.loc['Straight']:.1%})",
254
+ x=0.5,
255
+ y=1,va='top',ha='center',fontsize=16)
256
+
257
+ ax21.text(s=f"Oppo\n({traj_df.loc['Oppo']:.1%})",
258
+ x=0.2+1/16*0.8,
259
+ y=1,va='top',ha='center',fontsize=16)
260
+
261
+ # Define the initial position of the axis
262
+
263
+ # Customize colorbar properties
264
+ # cbar = fig.colorbar(orientation='vertical', pad=0.1,ax=ax12)
265
+ #cbar.set_label('Difference', rotation=270, labelpad=15)
266
+ # Show the plot
267
+ # ax21.text(0.0, 0., "By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12)
268
+ # ax21.text(1, 0., "Data: MLB",ha='right', va='bottom',fontsize=12)
269
+ # ax21.text(0.5, 0., "Inspired by @blandalytics",ha='center', va='bottom',fontsize=12)
270
+
271
+ # ax00.axis('off')
272
+ ax01.axis('off')
273
+ # ax02.axis('off')
274
+ ax10.axis('off')
275
+ #ax11.axis('off')
276
+ #ax12.axis('off')
277
+ # ax20.axis('off')
278
+ ax21.axis('off')
279
+ # ax22.axis('off')
280
+
281
+ ax21.text(0.0, 0., "By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12)
282
+ ax21.text(0.98, 0., "Data: MLB",ha='right', va='bottom',fontsize=12)
283
+ ax21.text(0.5, 0., "Inspired by @blandalytics",ha='center', va='bottom',fontsize=12)
284
+
285
+
286
+ ax11.set_xticks([])
287
+ ax11.set_yticks([])
288
+
289
+ # ax12.text(s='Same',x=np.mean([x for x in ax12.get_xlim()]),y=np.median([x for x in ax12.get_ylim()]),
290
+ # va='center',ha='center',fontsize=12)
291
+
292
+ # ax12.text(s='More\nOften',x=0.5,y=0.74,
293
+ # va='top',ha='center',fontsize=12)
294
+
295
+ ax12.text(s='+3σ',x=0.5,y=3-1/14*3,
296
+ va='center',ha='center',fontsize=12)
297
+
298
+ ax12.text(s='+2σ',x=0.5,y=2-1/14*2,
299
+ va='center',ha='center',fontsize=12)
300
+
301
+ ax12.text(s='+1σ',x=0.5,y=1-1/14*1,
302
+ va='center',ha='center',fontsize=12)
303
+
304
+
305
+ ax12.text(s='±0σ',x=0.5,y=0,
306
+ va='center',ha='center',fontsize=12)
307
+
308
+ ax12.text(s='-1σ',x=0.5,y=-1-1/14*-1,
309
+ va='center',ha='center',fontsize=12)
310
+
311
+ ax12.text(s='-2σ',x=0.5,y=-2-1/14*-2,
312
+ va='center',ha='center',fontsize=12)
313
+
314
+ ax12.text(s='-3σ',x=0.5,y=-3-1/14*-3,
315
+ va='center',ha='center',fontsize=12)
316
+
317
+ # # ax12.text(s='Less\nOften',x=0.5,y=0.26,
318
+ # # va='bottom',ha='center',fontsize=12)
319
+
320
+ ax01.text(s=f"{df_batter_2023['batter_name'].values[0]}'s 2023 Batted Ball Tendencies",
321
+ x=0.5,
322
+ y=0.8,va='top',ha='center',fontsize=20)
323
+
324
+ ax01.text(s=f"(Compared to rest of MLB)",
325
+ x=0.5,
326
+ y=0.3,va='top',ha='center',fontsize=16)
327
+
328
+ #plt.show()
329
+
330
+ spray = App(ui.page_fluid(
331
+ ui.tags.base(href=base_url),
332
+ ui.tags.div(
333
+ {"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
334
+ ui.tags.style(
335
+ """
336
+ h4 {
337
+ margin-top: 1em;font-size:35px;
338
+ }
339
+ h2{
340
+ font-size:25px;
341
+ }
342
+ """
343
+ ),
344
+ shinyswatch.theme.simplex(),
345
+ ui.tags.h4("Stats By Zach"),
346
+ ui.tags.i("A website for hockey analytics"),
347
+ ui.navset_tab(
348
+ ui.nav_control(
349
+ ui.a(
350
+ "Home",
351
+ href="home/"
352
+ ),
353
+ ),
354
+ ui.nav_menu(
355
+ "Batter Charts",
356
+ ui.nav_control(
357
+ ui.a(
358
+ "Spray",
359
+ href="spray/"
360
+ ),
361
+ ui.a(
362
+ "Decision Value",
363
+ href="decision_value/"
364
+ ),
365
+ ui.a(
366
+ "Damage Model",
367
+ href="damage_model/"
368
+ ),
369
+ ui.a(
370
+ "Batter Scatter",
371
+ href="batter_scatter/"
372
+ ),
373
+ ui.a(
374
+ "EV vs LA Plot",
375
+ href="ev_angle/"
376
+ )
377
+ ),
378
+ ),
379
+ ui.nav_menu(
380
+ "Goalie Charts",
381
+ ui.nav_control(
382
+ ui.a(
383
+ "GSAx Timeline",
384
+ href="gsax-timeline/"
385
+ ),
386
+ ui.a(
387
+ "GSAx Leaderboard",
388
+ href="gsax-leaderboard/"
389
+ ),
390
+ ui.a(
391
+ "GSAx Comparison",
392
+ href="gsax-comparison/"
393
+ )
394
+ ),
395
+ ),ui.nav_menu(
396
+ "Team Charts",
397
+ ui.nav_control(
398
+ ui.a(
399
+ "Team xG Rates",
400
+ href="team-xg-rates/"
401
+ ),
402
+ ),
403
+ ),ui.nav_control(
404
+ ui.a(
405
+ "Games",
406
+ href="games/"
407
+ ),
408
+ ),ui.nav_control(
409
+ ui.a(
410
+ "About",
411
+ href="about/"
412
+ ),
413
+ ),ui.nav_control(
414
+ ui.a(
415
+ "Articles",
416
+ href="articles/"
417
+ ),
418
+ )),ui.row(
419
+ ui.layout_sidebar(
420
+
421
+ ui.panel_sidebar(
422
+ ui.input_select("batter_id",
423
+ "Select Batter",
424
+ batter_dict,
425
+ width=1,
426
+ size=1,
427
+ selectize=True)),
428
+
429
+ ui.panel_main(
430
+ ui.navset_tab(
431
+
432
+ ui.nav("2023 vs MLB",
433
+ ui.output_plot('plot',
434
+ width='1000px',
435
+ height='1000px')),
436
+ ))
437
+ )),)),server)
summary_batter.csv ADDED
The diff for this file is too large to render. See raw diff
 
summary_batter_level.csv ADDED
The diff for this file is too large to render. See raw diff
 
swing.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4fef4a66363e5f3fdc70ae45c5382bd986c800ff8bf9296a1f9b334461e70fd4
3
+ size 262137
xtb_model.joblib ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a0b08278fdbb8c3bc2af79a65e9563e8b8d2251072ae90fc772ce7b4d2937b7d
3
+ size 14497085