nesticot commited on
Commit
b02077b
·
verified ·
1 Parent(s): 9f78c29

Upload 34 files

Browse files
__pycache__/api_scraper.cpython-39.pyc ADDED
Binary file (28.1 kB). View file
 
__pycache__/app.cpython-39.pyc ADDED
Binary file (13.8 kB). View file
 
__pycache__/df_update.cpython-39.pyc ADDED
Binary file (14 kB). View file
 
__pycache__/stuff_apply.cpython-39.pyc ADDED
Binary file (1.33 kB). View file
 
api_scraper.py CHANGED
The diff for this file is too large to render. See raw diff
 
app.py CHANGED
@@ -1,406 +1,641 @@
1
- import polars as pl
2
- import numpy as np
3
- import pandas as pd
4
- import api_scraper
5
- scrape = api_scraper.MLB_Scrape()
6
- from functions import df_update
7
- from functions import pitch_summary_functions
8
- update = df_update.df_update()
9
- from stuff_model import feature_engineering as fe
10
- from stuff_model import stuff_apply
11
- import requests
12
- import joblib
13
- from matplotlib.gridspec import GridSpec
14
- from shiny import App, reactive, ui, render
15
- from shiny.ui import h2, tags
16
- import matplotlib.pyplot as plt
17
- import matplotlib.gridspec as gridspec
18
- import seaborn as sns
19
- from functions.pitch_summary_functions import *
20
- from shiny import App, reactive, ui, render
21
- from shiny.ui import h2, tags
22
-
23
- colour_palette = ['#FFB000','#648FFF','#785EF0',
24
- '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
25
-
26
-
27
- year_list = [2017,2018,2019,2020,2021,2022,2023,2024]
28
-
29
-
30
-
31
- level_dict = {'1':'MLB',
32
- '11':'AAA',
33
- # '12':'AA',
34
- #'13':'A+',
35
- '14':'A',
36
- '17':'AFL',
37
- '22':'College',
38
- '21':'Prospects',
39
- '51':'International' }
40
-
41
- function_dict={
42
- 'velocity_kdes':'Velocity Distributions',
43
- 'break_plot':'Pitch Movement',
44
- 'tj_stuff_roling':'Rolling tjStuff+ by Pitch',
45
- 'tj_stuff_roling_game':'Rolling tjStuff+ by Game',
46
- 'location_plot_lhb':'Locations vs LHB',
47
- 'location_plot_rhb':'Locations vs RHB',
48
- }
49
-
50
-
51
- split_dict = {'all':'All',
52
- 'left':'LHH',
53
- 'right':'RHH'}
54
-
55
- split_dict_hand = {'all':['L','R'],
56
- 'left':['L'],
57
- 'right':['R']}
58
-
59
-
60
- type_dict = {'R':'Regular Season',
61
- 'S':'Spring',
62
- 'P':'Playoffs' }
63
-
64
-
65
-
66
- # List of MLB teams and their corresponding ESPN logo URLs
67
- mlb_teams = [
68
- {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
69
- {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
70
- {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
71
- {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
72
- {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
73
- {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
74
- {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
75
- {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
76
- {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
77
- {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
78
- {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
79
- {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
80
- {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
81
- {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
82
- {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
83
- {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
84
- {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
85
- {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
86
- {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
87
- {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
88
- {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
89
- {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
90
- {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
91
- {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
92
- {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
93
- {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
94
- {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
95
- {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
96
- {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
97
- {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"},
98
- {"team": "ZZZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png&w=500&h=500"}
99
- ]
100
-
101
-
102
- df_image = pd.DataFrame(mlb_teams)
103
- image_dict = df_image.set_index('team')['logo_url'].to_dict()
104
- image_dict_flip = df_image.set_index('logo_url')['team'].to_dict()
105
-
106
-
107
-
108
- # # Define the features to be used for training
109
- # features_table = ['start_speed',
110
- # 'spin_rate',
111
- # 'extension',
112
- # 'ivb',
113
- # 'hb',
114
- # 'x0',
115
- # 'z0',
116
- # 'tj_stuff_plus']
117
-
118
- from shiny import App, reactive, ui, render
119
- from shiny.ui import h2, tags
120
-
121
- # Define the UI layout for the app
122
- app_ui = ui.page_sidebar(
123
- ui.sidebar(
124
- # Row for selecting season and level
125
- ui.row(
126
- ui.column(4, ui.input_select('year_input', 'Select Season', year_list, selected=2024)),
127
- ui.column(4, ui.input_select('level_input', 'Select Level', level_dict)),
128
- ui.column(4, ui.input_select('type_input', 'Select Type', type_dict,selected='R'))
129
- ),
130
- # Row for the action button to get player list
131
- ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
132
- # Row for selecting the player
133
- ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
134
- # Row for selecting the date range
135
- ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))),
136
-
137
- # Rows for selecting plots and split options
138
- ui.row(
139
- ui.column(4, ui.input_select('plot_id_1', 'Plot Left', function_dict, multiple=False, selected='velocity_kdes')),
140
- ui.column(4, ui.input_select('plot_id_2', 'Plot Middle', function_dict, multiple=False, selected='tj_stuff_roling')),
141
- ui.column(4, ui.input_select('plot_id_3', 'Plot Right', function_dict, multiple=False, selected='break_plot'))
142
- ),
143
- ui.row(
144
- ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
145
- ui.column(6, ui.input_numeric('rolling_window', 'Rolling Window (for tjStuff+ Plot)', min=1, value=50))
146
- ),
147
- ui.row(
148
- ui.column(6, ui.input_switch("switch", "Custom Team?", False)),
149
- ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False))
150
- ),
151
-
152
- # Row for the action button to generate plot
153
- ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
154
- width="400px" # Added this parameter to control sidebar width
155
- ),
156
-
157
- # Main content area with tabs (placed directly in page_sidebar)
158
- ui.navset_tab(
159
- ui.nav_panel("Pitching Summary",
160
- ui.output_text("status"),
161
- ui.output_plot('plot', width='2100px', height='2100px')
162
- ),
163
- ui.nav_panel("Summary Table",
164
- ui.output_data_frame("grid"))
165
- )
166
- )
167
-
168
-
169
- def server(input, output, session):
170
-
171
- @reactive.calc
172
- @reactive.event(input.pitcher_id, input.date_id,input.split_id)
173
- def cached_data():
174
-
175
- year_input = int(input.year_input())
176
- sport_id = int(input.level_input())
177
- player_input = int(input.pitcher_id())
178
- start_date = str(input.date_id()[0])
179
- end_date = str(input.date_id()[1])
180
- # Simulate an expensive data operation
181
- game_list = scrape.get_player_games_list(sport_id = sport_id,
182
- season = year_input,
183
- player_id = player_input,
184
- start_date = start_date,
185
- end_date = end_date,
186
- game_type = [input.type_input()])
187
-
188
- data_list = scrape.get_data(game_list_input = game_list[:])
189
- df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
190
- (pl.col("pitcher_id") == player_input)&
191
- (pl.col("is_pitch") == True)&
192
- (pl.col("start_speed") >= 50)&
193
- (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
194
-
195
- )))).with_columns(
196
- pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
197
- ))
198
- return df
199
-
200
- @render.ui
201
- @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,ignore_none=False)
202
- def player_select_ui():
203
- # Get the list of pitchers for the selected level and season
204
- df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input()), game_type = [input.type_input()]).filter(
205
- (pl.col("position").is_in(['P','TWP']))|
206
- (pl.col("player_id").is_in([686846]))
207
-
208
- ).sort("name")
209
-
210
- # Create a dictionary of pitcher IDs and names
211
- pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name']))
212
-
213
-
214
-
215
-
216
- # Return a select input for choosing a pitcher
217
- return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True)
218
-
219
- @render.ui
220
- @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,ignore_none=False)
221
- def date_id():
222
- # Create a date range input for selecting the date range within the selected year
223
- return ui.input_date_range("date_id", "Select Date Range",
224
- start=f"{int(input.year_input())}-01-01",
225
- end=f"{int(input.year_input())}-12-31",
226
- min=f"{int(input.year_input())}-01-01",
227
- max=f"{int(input.year_input())}-12-31")
228
- @output
229
- @render.text
230
- def status():
231
- # Only show status when generating
232
- if input.generate == 0:
233
- return ""
234
- return ""
235
-
236
- @output
237
- @render.plot
238
- @reactive.event(input.generate_plot, ignore_none=False)
239
- def plot():
240
- # Show progress/loading notification
241
- with ui.Progress(min=0, max=1) as p:
242
- p.set(message="Generating plot", detail="This may take a while...")
243
-
244
-
245
- p.set(0.3, "Gathering data...")
246
- year_input = int(input.year_input())
247
- sport_id = int(input.level_input())
248
- player_input = int(input.pitcher_id())
249
- start_date = str(input.date_id()[0])
250
- end_date = str(input.date_id()[1])
251
-
252
- print(year_input, sport_id, player_input, start_date, end_date)
253
-
254
- df = cached_data()
255
- df = df.clone()
256
-
257
- p.set(0.6, "Creating plot...")
258
-
259
-
260
- #plt.rcParams["figure.figsize"] = [10,10]
261
- fig = plt.figure(figsize=(26,26))
262
- plt.rcParams.update({'figure.autolayout': True})
263
- fig.set_facecolor('white')
264
- sns.set_theme(style="whitegrid", palette=colour_palette)
265
- print('this is the one plot')
266
-
267
- gs = gridspec.GridSpec(6, 8,
268
- height_ratios=[5,20,12,36,36,7],
269
- width_ratios=[4,18,18,18,18,18,18,4])
270
-
271
-
272
- gs.update(hspace=0.2, wspace=0.5)
273
-
274
- # Define the positions of each subplot in the grid
275
- ax_headshot = fig.add_subplot(gs[1,1:3])
276
- ax_bio = fig.add_subplot(gs[1,3:5])
277
- ax_logo = fig.add_subplot(gs[1,5:7])
278
-
279
- ax_season_table = fig.add_subplot(gs[2,1:7])
280
-
281
- ax_plot_1 = fig.add_subplot(gs[3,1:3])
282
- ax_plot_2 = fig.add_subplot(gs[3,3:5])
283
- ax_plot_3 = fig.add_subplot(gs[3,5:7])
284
-
285
- ax_table = fig.add_subplot(gs[4,1:7])
286
-
287
- ax_footer = fig.add_subplot(gs[-1,1:7])
288
- ax_header = fig.add_subplot(gs[0,1:7])
289
- ax_left = fig.add_subplot(gs[:,0])
290
- ax_right = fig.add_subplot(gs[:,-1])
291
-
292
- # Hide axes for footer, header, left, and right
293
- ax_footer.axis('off')
294
- ax_header.axis('off')
295
- ax_left.axis('off')
296
- ax_right.axis('off')
297
-
298
- sns.set_theme(style="whitegrid", palette=colour_palette)
299
- fig.set_facecolor('white')
300
-
301
- df_teams = scrape.get_teams()
302
-
303
- player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
304
- player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
305
-
306
- if input.switch():
307
-
308
- # Get the logo URL from the image dictionary using the team abbreviation
309
- logo_url = input.logo_select()
310
-
311
- # Send a GET request to the logo URL
312
- response = requests.get(logo_url)
313
-
314
- # Open the image from the response content
315
- img = Image.open(BytesIO(response.content))
316
-
317
- # Display the image on the axis
318
- ax_logo.set_xlim(0, 1.3)
319
- ax_logo.set_ylim(0, 1)
320
- ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')
321
-
322
- # Turn off the axis
323
- ax_logo.axis('off')
324
-
325
- else:
326
- plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
327
-
328
- stat_summary_table(df=df,
329
- ax=ax_season_table,
330
- player_input=player_input,
331
- split=input.split_id(),
332
- sport_id=sport_id,
333
- game_type=[input.type_input()])
334
-
335
- # break_plot(df=df_plot,ax=ax2)
336
- for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax_plot_1,ax_plot_2,ax_plot_3],[1,3,5]):
337
- if x == 'velocity_kdes':
338
- velocity_kdes(df,
339
- ax=y,
340
- gs=gs,
341
- gs_x=[3,4],
342
- gs_y=[z,z+2],
343
- fig=fig)
344
- if x == 'tj_stuff_roling':
345
- tj_stuff_roling(df=df,
346
- window=int(input.rolling_window()),
347
- ax=y)
348
-
349
- if x == 'tj_stuff_roling_game':
350
- tj_stuff_roling_game(df=df,
351
- window=int(input.rolling_window()),
352
- ax=y)
353
-
354
- if x == 'break_plot':
355
- break_plot(df = df,ax=y)
356
-
357
- if x == 'location_plot_lhb':
358
- location_plot(df = df,ax=y,hand='L')
359
-
360
- if x == 'location_plot_rhb':
361
- location_plot(df = df,ax=y,hand='R')
362
-
363
- summary_table(df=df,
364
- ax=ax_table)
365
-
366
- plot_footer(ax_footer)
367
-
368
- fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
369
-
370
-
371
- @output
372
- @render.data_frame
373
- @reactive.event(input.generate_plot, ignore_none=False)
374
- def grid():
375
-
376
- df = cached_data()
377
- df = df.clone()
378
- features_table = ['start_speed',
379
- 'spin_rate',
380
- 'extension',
381
- 'ivb',
382
- 'hb',
383
- 'x0',
384
- 'z0']
385
-
386
-
387
-
388
- selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand',
389
- 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus']
390
-
391
-
392
-
393
- return render.DataGrid(
394
- df.select(selection).to_pandas().round(1),
395
- row_selection_mode='multiple',
396
- height='700px',
397
- width='fit-content',
398
- filters=True,
399
- )
400
-
401
-
402
- app = App(app_ui, server)
403
-
404
-
405
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  app = App(app_ui, server)
 
1
+ import polars as pl
2
+ import numpy as np
3
+ import pandas as pd
4
+ import api_scraper
5
+ scrape = api_scraper.MLB_Scrape()
6
+ from functions import df_update
7
+ from functions import pitch_summary_functions
8
+ update = df_update.df_update()
9
+ from stuff_model import feature_engineering as fe
10
+ from stuff_model import stuff_apply
11
+ import requests
12
+ import joblib
13
+ from matplotlib.gridspec import GridSpec
14
+ from shiny import App, reactive, ui, render
15
+ from shiny.ui import h2, tags
16
+ import matplotlib.pyplot as plt
17
+ import matplotlib.gridspec as gridspec
18
+ import seaborn as sns
19
+ from functions.pitch_summary_functions import *
20
+ from shiny import App, reactive, ui, render
21
+ from shiny.ui import h2, tags
22
+ # from functions.PitchPlotFunctions import *
23
+ import functions.PitchPlotFunctions as ppf
24
+ ploter = ppf.PitchPlotFunctions()
25
+ from shiny.plotutils import brushed_points
26
+ from pytabulator import TableOptions, Tabulator, output_tabulator, render_tabulator, theme
27
+ theme.tabulator_site()
28
+
29
+ colour_palette = ['#FFB000','#648FFF','#785EF0',
30
+ '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
31
+ cmap_sum = mcolors.LinearSegmentedColormap.from_list("", ['#648FFF', '#FFFFFF', '#FFB000'])
32
+
33
+ year_list = [2017,2018,2019,2020,2021,2022,2023,2024]
34
+
35
+
36
+
37
+ level_dict = {'1':'MLB',
38
+ '11':'AAA',
39
+ '12':'AA',
40
+ '13':'A+',
41
+ '14':'A',
42
+ '17':'AFL',
43
+ '22':'College',
44
+ '21':'Prospects',
45
+ '51':'International' }
46
+
47
+ function_dict={
48
+ 'velocity_kdes':'Velocity Distributions',
49
+ 'break_plot':'Pitch Movement',
50
+ 'tj_stuff_roling':'Rolling tjStuff+ by Pitch',
51
+ 'tj_stuff_roling_game':'Rolling tjStuff+ by Game',
52
+ 'location_plot_lhb':'Locations vs LHB',
53
+ 'location_plot_rhb':'Locations vs RHB',
54
+ }
55
+
56
+
57
+ split_dict = {'all':'All',
58
+ 'left':'LHH',
59
+ 'right':'RHH'}
60
+
61
+ split_dict_hand = {'all':['L','R'],
62
+ 'left':['L'],
63
+ 'right':['R']}
64
+
65
+ ### PITCH COLOURS ###
66
+
67
+ # Dictionary to map pitch types to their corresponding colors and names
68
+ pitch_colours = {
69
+ ## Fastballs ##
70
+ 'FF': {'colour': '#FF007D', 'name': '4-Seam Fastball'},
71
+ 'FA': {'colour': '#FF007D', 'name': 'Fastball'},
72
+ 'SI': {'colour': '#98165D', 'name': 'Sinker'},
73
+ 'FC': {'colour': '#BE5FA0', 'name': 'Cutter'},
74
+
75
+ ## Offspeed ##
76
+ 'CH': {'colour': '#F79E70', 'name': 'Changeup'},
77
+ 'FS': {'colour': '#FE6100', 'name': 'Splitter'},
78
+ 'SC': {'colour': '#F08223', 'name': 'Screwball'},
79
+ 'FO': {'colour': '#FFB000', 'name': 'Forkball'},
80
+
81
+ ## Sliders ##
82
+ 'SL': {'colour': '#67E18D', 'name': 'Slider'},
83
+ 'ST': {'colour': '#1BB999', 'name': 'Sweeper'},
84
+ 'SV': {'colour': '#376748', 'name': 'Slurve'},
85
+
86
+ ## Curveballs ##
87
+ 'KC': {'colour': '#311D8B', 'name': 'Knuckle Curve'},
88
+ 'CU': {'colour': '#3025CE', 'name': 'Curveball'},
89
+ 'CS': {'colour': '#274BFC', 'name': 'Slow Curve'},
90
+ 'EP': {'colour': '#648FFF', 'name': 'Eephus'},
91
+
92
+ ## Others ##
93
+ 'KN': {'colour': '#867A08', 'name': 'Knuckleball'},
94
+ 'KN': {'colour': '#867A08', 'name': 'Knuckle Ball'},
95
+ 'PO': {'colour': '#472C30', 'name': 'Pitch Out'},
96
+ 'UN': {'colour': '#9C8975', 'name': 'Unknown'},
97
+ }
98
+
99
+ # Create dictionaries for pitch types and their attributes
100
+ dict_colour = {key: value['colour'] for key, value in pitch_colours.items()}
101
+ dict_pitch = {key: value['name'] for key, value in pitch_colours.items()}
102
+ dict_pitch_desc_type = {value['name']: key for key, value in pitch_colours.items()}
103
+ dict_pitch_desc_type.update({'Four-Seam Fastball':'FF'})
104
+ dict_pitch_desc_type.update({'All':'All'})
105
+ dict_pitch_name = {value['name']: value['colour'] for key, value in pitch_colours.items()}
106
+ dict_pitch_name.update({'Four-Seam Fastball':'#FF007D'})
107
+ dict_pitch_name.update({'4-Seam':'#FF007D'})
108
+
109
+
110
+ from shiny import App, reactive, ui, render
111
+ from shiny.ui import h2, tags
112
+
113
+ # Define the UI layout for the app
114
+ app_ui = ui.page_fluid(
115
+ ui.layout_sidebar(
116
+ ui.panel_sidebar(
117
+ # Row for selecting season and level
118
+ ui.row(
119
+ ui.column(6, ui.input_select('year_input', 'Select Season', year_list, selected=2024)),
120
+ ui.column(6, ui.input_select('level_input', 'Select Level', level_dict))
121
+ ),
122
+ # Row for the action button to get player list
123
+ ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
124
+ # Row for selecting the player
125
+ ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
126
+ # Row for selecting the date range
127
+ ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))),
128
+
129
+ ui.row(
130
+ ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
131
+ ),
132
+ # Row for the action button to generate plot
133
+ ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
134
+ ui.row(ui.input_action_button("generate_table", "Generate Table", class_="btn-warning")),
135
+
136
+ ),
137
+
138
+
139
+ ui.panel_main(
140
+ # ui.navset_tab(
141
+ # Tab for game summary plot
142
+ # ui.nav(
143
+ # "Pitching Summary",
144
+ ui.card(
145
+ {"style": "width: 870px;"},
146
+ ui.head_content(
147
+ ui.tags.script(src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.min.js"),
148
+ ui.tags.script("""
149
+ async function downloadSVG() {
150
+ const content = document.getElementById('capture-section');
151
+
152
+ // Create a new SVG element
153
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
154
+ const bbox = content.getBoundingClientRect();
155
+
156
+ // Set SVG attributes
157
+ svg.setAttribute('width', bbox.width);
158
+ svg.setAttribute('height', bbox.height);
159
+ svg.setAttribute('viewBox', `0 0 ${bbox.width} ${bbox.height}`);
160
+
161
+ // Create foreignObject to contain HTML content
162
+ const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
163
+ foreignObject.setAttribute('width', '100%');
164
+ foreignObject.setAttribute('height', '100%');
165
+ foreignObject.setAttribute('x', '0');
166
+ foreignObject.setAttribute('y', '0');
167
+
168
+ // Clone the content and its styles
169
+ const clonedContent = content.cloneNode(true);
170
+
171
+ // Add necessary style context
172
+ const style = document.createElement('style');
173
+ Array.from(document.styleSheets).forEach(sheet => {
174
+ try {
175
+ Array.from(sheet.cssRules).forEach(rule => {
176
+ style.innerHTML += rule.cssText + '\\n';
177
+ });
178
+ } catch (e) {
179
+ console.warn('Could not access stylesheet rules');
180
+ }
181
+ });
182
+
183
+ // Create a wrapper div to hold styles and content
184
+ const wrapper = document.createElement('div');
185
+ wrapper.appendChild(style);
186
+ wrapper.appendChild(clonedContent);
187
+
188
+ foreignObject.appendChild(wrapper);
189
+ svg.appendChild(foreignObject);
190
+
191
+ // Convert to SVG string with XML declaration and DTD
192
+ const svgString = new XMLSerializer().serializeToString(svg);
193
+ const svgBlob = new Blob([
194
+ '<?xml version="1.0" standalone="no"?>\\n',
195
+ '<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\\n',
196
+ svgString
197
+ ], {type: 'image/svg+xml;charset=utf-8'});
198
+
199
+ // Create and trigger download
200
+ const url = URL.createObjectURL(svgBlob);
201
+ const link = document.createElement('a');
202
+ link.href = url;
203
+ link.download = 'plot_and_table.svg';
204
+ document.body.appendChild(link);
205
+ link.click();
206
+ document.body.removeChild(link);
207
+ URL.revokeObjectURL(url);
208
+ }
209
+
210
+ $(document).on('click', '#capture_btn', function() {
211
+ downloadSVG();
212
+ });
213
+ """)
214
+ ),
215
+ ui.output_text("status"),
216
+ ui.div(
217
+ {
218
+ "id": "capture-section",
219
+ "style": "background-color: white; padding: 0; margin-left: 20px; margin-right: 20px; margin-top: 20px; margin-bottom: 20px;" # Added margin-right
220
+ },
221
+ # Plot section with relative positioning for brush
222
+ ui.div(
223
+ {"style": "position: relative;"},
224
+ ui.output_ui("plot_ui")
225
+ ),
226
+ # Table section
227
+ ui.div(
228
+ {"style": "margin-top: 20px;"},
229
+ ui.row(ui.tags.b("Pitches in Selection"), ui.output_table("in_brush")),
230
+
231
+
232
+ ),
233
+ ui.div({"style": "height: 20px;"})
234
+ ),
235
+ ui.input_action_button("capture_btn", "Save as SVG", class_="btn-primary"),
236
+ )
237
+ # ),
238
+ # )
239
+ )
240
+ )
241
+ )
242
+
243
+
244
+ def server(input, output, session):
245
+
246
+ @reactive.calc
247
+ @reactive.event(input.pitcher_id, input.date_id,input.split_id)
248
+ def cached_data():
249
+
250
+ year_input = int(input.year_input())
251
+ sport_id = int(input.level_input())
252
+ player_input = int(input.pitcher_id())
253
+ start_date = str(input.date_id()[0])
254
+ end_date = str(input.date_id()[1])
255
+ # Simulate an expensive data operation
256
+ game_list = scrape.get_player_games_list(sport_id = sport_id,
257
+ season = year_input,
258
+ player_id = player_input,
259
+ start_date = start_date,
260
+ end_date = end_date)
261
+
262
+ data_list = scrape.get_data(game_list_input = game_list[:])
263
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
264
+ (pl.col("pitcher_id") == player_input)&
265
+ (pl.col("is_pitch") == True)&
266
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
267
+
268
+ )))).with_columns(
269
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
270
+ ))
271
+
272
+ df = df.with_columns(
273
+ prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"),
274
+ prop=pl.col('is_pitch').sum().over("pitch_type")
275
+ )
276
+
277
+ return df
278
+
279
+ @render.ui
280
+ @reactive.event(input.player_button, input.level_input,input.year_input, ignore_none=False)
281
+ def player_select_ui():
282
+ # Get the list of pitchers for the selected level and season
283
+ df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input())).filter(
284
+ pl.col("position").is_in(['P'])).sort("name")
285
+
286
+ # Create a dictionary of pitcher IDs and names
287
+ pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name']))
288
+
289
+ # Return a select input for choosing a pitcher
290
+ return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True)
291
+
292
+ @render.ui
293
+ @reactive.event(input.player_button,input.pitcher_id,input.year_input, ignore_none=False)
294
+ def date_id():
295
+ # Create a date range input for selecting the date range within the selected year
296
+ return ui.input_date_range("date_id", "Select Date Range",
297
+ start=f"{int(input.year_input())}-01-01",
298
+ end=f"{int(input.year_input())}-03-31",
299
+ min=f"{int(input.year_input())}-01-01",
300
+ max=f"{int(input.year_input())}-12-31")
301
+ @output
302
+ @render.text
303
+ def status():
304
+ # Only show status when generating
305
+ if input.generate == 0:
306
+ return ""
307
+ return ""
308
+
309
+ @render.ui
310
+ @reactive.event(input.generate_plot)
311
+ def plot_ui():
312
+ brush_opts_kwargs = {}
313
+ brush_opts_kwargs["direction"] = 'xy'
314
+ brush_opts_kwargs["delay"] = 60 # Optional: adds a small delay for better performance
315
+ brush_opts_kwargs["delay_type"] = "throttle"
316
+
317
+
318
+ return ui.output_plot('plot',
319
+ width='800px',
320
+ height='800px',
321
+ brush=ui.brush_opts(**brush_opts_kwargs))
322
+
323
+ @render.table
324
+ @reactive.event(input.plot_brush, input.generate_table) # Note: changed to match the brush ID
325
+ def in_brush():
326
+ # if input.plot_brush() is None: # Note: changed to match the brush ID
327
+ # return None
328
+ brushed_df = pl.DataFrame(brushed_points(
329
+ cached_data().to_pandas(),
330
+ input.plot_brush(),
331
+ xvar="hb",
332
+ yvar="ivb",
333
+ all_rows=False
334
+ ))
335
+
336
+
337
+ brushed_df_final = (((brushed_df.group_by(['pitcher_id', 'pitch_description'])
338
+ .agg([
339
+ pl.col('is_pitch').drop_nans().count().alias('pitches'),
340
+ pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
341
+ pl.col('vb').drop_nans().mean().round(1).alias('vb'),
342
+ pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
343
+ pl.col('hb').drop_nans().mean().round(1).alias('hb'),
344
+ pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
345
+ pl.col('x0').drop_nans().mean().round(1).alias('x0'),
346
+ pl.col('z0').drop_nans().mean().round(1).alias('z0'),
347
+ pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
348
+ ])
349
+ .with_columns(
350
+ (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id'))
351
+ # .round(1)
352
+ # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
353
+ .alias('proportion')
354
+ )
355
+ )).sort('proportion', descending=True).
356
+ select(["pitch_description", "pitches", "proportion", "start_speed", "ivb", "hb",
357
+ "spin_rate", "x0", "z0",'tj_stuff_plus'])
358
+ .with_columns(
359
+ pl.when(pl.col("pitch_description") == "Four-Seam Fastball")
360
+ .then(pl.lit("4-Seam"))
361
+ .otherwise(pl.col("pitch_description"))
362
+ .alias("pitch_description")
363
+ )
364
+ .rename({
365
+ 'pitch_description': 'Pitch Type',
366
+ 'pitches': 'Pitches',
367
+ 'proportion': 'Prop',
368
+ 'start_speed': 'Velo',
369
+ 'ivb': 'iVB',
370
+ 'hb': 'HB',
371
+ 'spin_rate': 'Spin',
372
+ 'x0': 'hRel',
373
+ 'z0': 'vRel',
374
+ 'tj_stuff_plus': 'tjStuff+'
375
+ }))
376
+
377
+ # brushed_df_final = brushed_df_final
378
+
379
+ # print(brushed_df_final)
380
+
381
+ def change_font(val):
382
+ if val == "Cutter":
383
+ return "color: red; font-weight: bold;"
384
+ else:
385
+ ''
386
+ return "font-weight: bold;"
387
+ df_brush_style = (brushed_df_final.to_pandas().style.set_precision(1)
388
+
389
+ .set_properties(**{'border': '3 px'},overwrite=False).set_table_styles([{
390
+ 'selector': 'caption',
391
+ 'props': [
392
+ ('color', ''),
393
+ ('fontname', 'Century Gothic'),
394
+ ('font-size', '16px'),
395
+ ('font-style', 'italic'),
396
+ ('font-weight', ''),
397
+ ('text-align', 'centre'),
398
+ ]
399
+
400
+ },{'selector' :'th', 'props':[('font-size', '16px'),('text-align', 'center'),('Height','px'),('color','black'),('border', '1px black solid !important')]},{'selector' :'td', 'props':[('text-align', 'center'),('font-size', '16px'),('color','black')]}],overwrite=False)
401
+ .set_properties(**{'background-color':'White','index':'White','min-width':'72px'},overwrite=False)
402
+ .set_table_styles([{'selector': 'th:first-child', 'props': [('background-color', 'white')]}],overwrite=False)
403
+ .set_table_styles([{'selector': 'tr:first-child', 'props': [('background-color', 'white')]}],overwrite=False)
404
+ .set_table_styles([{'selector': 'tr', 'props': [('line-height', '20px')]}],overwrite=False)
405
+ .set_properties(**{'Height': '8px'},**{'text-align': 'center'},overwrite=False)
406
+ .hide_index()
407
+ .set_properties(**{'border': '1px black solid !important'})
408
+ .format('{:.0%}',subset=(brushed_df_final.columns[2]))
409
+ .format('{:.0f}',subset=(brushed_df_final.columns[6]))
410
+ .format('{:.0f}',subset=(brushed_df_final.columns[-1]))
411
+ .set_properties(subset=brushed_df_final.columns, **{'height': '30px'})
412
+ .set_table_styles([{'selector': 'thead th', 'props': [('height', '30px')]}], overwrite=False)
413
+ # .set_table_styles([{'selector': 'table', 'props': [('width', '100px')]}], overwrite=False)
414
+ .set_table_styles([{'selector': 'thead th:nth-child(1)', 'props': [('min-width', '125px')]}], overwrite=False)
415
+ .set_table_styles([{'selector': 'thead th:nth-child(2)', 'props': [('min-width', '40px')]}], overwrite=False)
416
+ .set_table_styles([{'selector': 'thead th:nth-child(3)', 'props': [('min-width', '40px')]}], overwrite=False)
417
+ .set_table_styles([{'selector': 'thead th:nth-child(4)', 'props': [('min-width', '40px')]}], overwrite=False)
418
+ .set_table_styles([{'selector': 'thead th:nth-child(5)', 'props': [('min-width', '40px')]}], overwrite=False)
419
+ .set_table_styles([{'selector': 'thead th:nth-child(6)', 'props': [('min-width', '40px')]}], overwrite=False)
420
+ .set_table_styles([{'selector': 'thead th:nth-child(7)', 'props': [('min-width', '40px')]}], overwrite=False)
421
+ .set_table_styles([{'selector': 'thead th:nth-child(8)', 'props': [('min-width', '40px')]}], overwrite=False)
422
+ .background_gradient(cmap=cmap_sum,subset = (brushed_df_final.columns[-1]),vmin=80,vmax=120)
423
+ .applymap(lambda x: f'background-color: {dict_pitch_name.get(x, "")}', subset=['Pitch Type'])
424
+
425
+
426
+ )
427
+
428
+ return df_brush_style
429
+
430
+ # return Tabulator(
431
+ # brushed_df.to_pandas(),
432
+ # table_options=TableOptions(
433
+ # height=800,
434
+ # resizable_column_fit=True,
435
+ # )
436
+ # )
437
+ # return brushed_points(
438
+ # ((brushed_df.group_by(['pitcher_id', 'pitch_description'])
439
+ # .agg([
440
+ # pl.col('is_pitch').drop_nans().count().alias('pitches'),
441
+ # pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
442
+ # pl.col('vb').drop_nans().mean().round(1).alias('vb'),
443
+ # pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
444
+ # pl.col('hb').drop_nans().mean().round(1).alias('hb'),
445
+ # pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
446
+ # pl.col('x0').drop_nans().mean().round(1).alias('x0'),
447
+ # pl.col('z0').drop_nans().mean().round(1).alias('z0'),
448
+ # pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
449
+ # ])
450
+ # .with_columns(
451
+ # (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id') * 100)
452
+ # .round(1)
453
+ # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
454
+ # .alias('proportion')
455
+ # )
456
+ # )).sort('proportion', descending=True).
457
+ # select(["pitch_description", "pitches", "proportion", "start_speed", "ivb", "hb",
458
+ # "spin_rate", "x0", "z0",'tj_stuff_plus'])
459
+ # .rename({
460
+ # 'pitch_description': 'Pitch Type',
461
+ # 'pitches': 'Pitches',
462
+ # 'proportion': 'Proportion',
463
+ # 'start_speed': 'Velocity',
464
+ # 'ivb': 'iVB',
465
+ # 'hb': 'HB',
466
+ # 'spin_rate': 'Spin Rate',
467
+ # 'x0': 'hRel',
468
+ # 'z0': 'vRel',
469
+ # 'tj_stuff_plus': 'tjStuff+'
470
+ # }).to_pandas(),
471
+ # input.plot_brush(), # Note: changed to match the brush ID
472
+ # xvar="HB", # Replace "x" with your actual x-axis column name
473
+ # yvar="iVB", # Replace "y" with your actual y-axis column name
474
+ # all_rows=False
475
+ # )
476
+
477
+
478
+
479
+ # return brushed_points(
480
+ # ((cached_data().group_by(['pitcher_id', 'pitch_description'])
481
+ # .agg([
482
+ # pl.col('is_pitch').drop_nans().count().alias('pitches'),
483
+ # pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
484
+ # pl.col('vb').drop_nans().mean().round(1).alias('vb'),
485
+ # pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
486
+ # pl.col('hb').drop_nans().mean().round(1).alias('hb'),
487
+ # pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
488
+ # pl.col('x0').drop_nans().mean().round(1).alias('x0'),
489
+ # pl.col('z0').drop_nans().mean().round(1).alias('z0'),
490
+ # pl.col('tj_stuff_plus').drop_nans().mean().round(0).alias('tj_stuff_plus'),
491
+ # ])
492
+ # .with_columns(
493
+ # (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id') * 100)
494
+ # .round(1)
495
+ # .map_elements(lambda x: f"{x}%", return_dtype=pl.Utf8) # Properly append "%"
496
+ # .alias('proportion')
497
+ # )
498
+ # )).sort('proportion', descending=True).
499
+ # select(["pitch_description", "pitches", "proportion", "start_speed", "ivb", "hb",
500
+ # "spin_rate", "x0", "z0",'tj_stuff_plus'])
501
+ # .rename({
502
+ # 'pitch_description': 'Pitch Type',
503
+ # 'pitches': 'Pitches',
504
+ # 'proportion': 'Prop',
505
+ # 'start_speed': 'Velocity',
506
+ # 'ivb': 'iVB',
507
+ # 'hb': 'HB',
508
+ # 'spin_rate': 'Spin Rate',
509
+ # 'x0': 'hRel',
510
+ # 'z0': 'vRel',
511
+ # 'tj_stuff_plus': 'tjStuff+'
512
+ # }).to_pandas(),
513
+ # input.plot_brush(), # Note: changed to match the brush ID
514
+ # xvar="HB", # Replace "x" with your actual x-axis column name
515
+ # yvar="iVB", # Replace "y" with your actual y-axis column name
516
+ # all_rows=False
517
+ # )
518
+ # @output
519
+ @render.plot
520
+ @reactive.event(input.generate_plot)
521
+ def plot():
522
+ # Show progress/loading notification
523
+ with ui.Progress(min=0, max=1) as p:
524
+ p.set(message="Generating plot", detail="This may take a while...")
525
+
526
+
527
+ p.set(0.3, "Gathering data...")
528
+ year_input = int(input.year_input())
529
+ sport_id = int(input.level_input())
530
+ player_input = int(input.pitcher_id())
531
+ start_date = str(input.date_id()[0])
532
+ end_date = str(input.date_id()[1])
533
+
534
+ print(year_input, sport_id, player_input, start_date, end_date)
535
+
536
+ df = cached_data()
537
+ df = df.clone()
538
+
539
+ p.set(0.6, "Creating plot...")
540
+
541
+
542
+ ploter.final_plot(
543
+ df=df,
544
+ pitcher_id=player_input,
545
+ plot_picker='short_form_movement',#plot_picker,
546
+ sport_id=sport_id)
547
+
548
+
549
+ # #plt.rcParams["figure.figsize"] = [10,10]
550
+ # fig = plt.figure(figsize=(26,26))
551
+ # plt.rcParams.update({'figure.autolayout': True})
552
+ # fig.set_facecolor('white')
553
+ # sns.set_theme(style="whitegrid", palette=colour_palette)
554
+ # print('this is the one plot')
555
+
556
+ # gs = gridspec.GridSpec(6, 8,
557
+ # height_ratios=[5,20,12,36,36,7],
558
+ # width_ratios=[4,18,18,18,18,18,18,4])
559
+
560
+
561
+ # gs.update(hspace=0.2, wspace=0.5)
562
+
563
+ # # Define the positions of each subplot in the grid
564
+ # ax_headshot = fig.add_subplot(gs[1,1:3])
565
+ # ax_bio = fig.add_subplot(gs[1,3:5])
566
+ # ax_logo = fig.add_subplot(gs[1,5:7])
567
+
568
+ # ax_season_table = fig.add_subplot(gs[2,1:7])
569
+
570
+ # ax_plot_1 = fig.add_subplot(gs[3,1:3])
571
+ # ax_plot_2 = fig.add_subplot(gs[3,3:5])
572
+ # ax_plot_3 = fig.add_subplot(gs[3,5:7])
573
+
574
+ # ax_table = fig.add_subplot(gs[4,1:7])
575
+
576
+ # ax_footer = fig.add_subplot(gs[-1,1:7])
577
+ # ax_header = fig.add_subplot(gs[0,1:7])
578
+ # ax_left = fig.add_subplot(gs[:,0])
579
+ # ax_right = fig.add_subplot(gs[:,-1])
580
+
581
+ # # Hide axes for footer, header, left, and right
582
+ # ax_footer.axis('off')
583
+ # ax_header.axis('off')
584
+ # ax_left.axis('off')
585
+ # ax_right.axis('off')
586
+
587
+ # sns.set_theme(style="whitegrid", palette=colour_palette)
588
+ # fig.set_facecolor('white')
589
+
590
+ # df_teams = scrape.get_teams()
591
+
592
+ # player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
593
+ # player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
594
+ # plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
595
+
596
+ # stat_summary_table(df=df,
597
+ # ax=ax_season_table,
598
+ # player_input=player_input,
599
+ # split=input.split_id(),
600
+ # sport_id=sport_id)
601
+
602
+ # # break_plot(df=df_plot,ax=ax2)
603
+ # for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax_plot_1,ax_plot_2,ax_plot_3],[1,3,5]):
604
+ # if x == 'velocity_kdes':
605
+ # velocity_kdes(df,
606
+ # ax=y,
607
+ # gs=gs,
608
+ # gs_x=[3,4],
609
+ # gs_y=[z,z+2],
610
+ # fig=fig)
611
+ # if x == 'tj_stuff_roling':
612
+ # tj_stuff_roling(df=df,
613
+ # window=int(input.rolling_window()),
614
+ # ax=y)
615
+
616
+ # if x == 'tj_stuff_roling_game':
617
+ # tj_stuff_roling_game(df=df,
618
+ # window=int(input.rolling_window()),
619
+ # ax=y)
620
+
621
+ # if x == 'break_plot':
622
+ # break_plot(df = df,ax=y)
623
+
624
+ # if x == 'location_plot_lhb':
625
+ # location_plot(df = df,ax=y,hand='L')
626
+
627
+ # if x == 'location_plot_rhb':
628
+ # location_plot(df = df,ax=y,hand='R')
629
+
630
+ # summary_table(df=df,
631
+ # ax=ax_table)
632
+
633
+ # plot_footer(ax_footer)
634
+
635
+ # fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
636
+
637
+ # fig.savefig('test.svg')
638
+
639
+
640
+
641
  app = App(app_ui, server)
app_streamlit.py ADDED
@@ -0,0 +1,253 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import seaborn as sns
2
+ import streamlit as st
3
+ from st_aggrid import AgGrid, GridOptionsBuilder, GridUpdateMode
4
+ import PitchPlotFunctions as ppf
5
+ import requests
6
+ import polars as pl
7
+ from datetime import date
8
+ import api_scraper
9
+
10
+
11
+
12
+ # Display the app title and description
13
+ st.markdown("""
14
+ ## MLB & AAA Pitch Plots App
15
+
16
+ ##### By: Thomas Nestico ([@TJStats](https://x.com/TJStats))
17
+ ##### Code: [GitHub Repo](https://github.com/tnestico/streamlit_pitch_plots)
18
+ ##### Data: [MLB](https://baseballsavant.mlb.com/)
19
+
20
+ #### About
21
+ This Streamlit app retrieves MLB and AAA Pitching Data for a selected pitcher from the MLB Stats API and is accessed using my [MLB Stats API Scraper](https://github.com/tnestico/mlb_scraper).
22
+
23
+ The app outputs the pitcher's data into both a plot and table to illustrate and summarize the data.
24
+ It can also display data for games currently in progress.
25
+
26
+ *More information about the data and plots is shown at the bottom of this page.*
27
+
28
+ """
29
+ )
30
+
31
+ # Initialize the plotter object from PitchPlotFunctions
32
+ ploter = ppf.PitchPlotFunctions()
33
+ # Initialize the scraper object
34
+ scraper = api_scraper.MLB_Scrape()
35
+
36
+ # Dictionary mapping league names to sport IDs
37
+ sport_id_dict = {'MLB': 1, 'AAA': 11}
38
+
39
+ # Create two columns for league and pitcher selection
40
+ st.write("#### Plot")
41
+ col_1, col_2 = st.columns(2)
42
+ with col_1:
43
+ # Select league
44
+ selected_league = st.selectbox('##### Select League', list(sport_id_dict.keys()))
45
+ selected_sport_id = sport_id_dict[selected_league]
46
+
47
+ with col_2:
48
+ # Get player data and filter for pitchers
49
+ df_player = scraper.get_players(sport_id=selected_sport_id)
50
+ df_player = df_player.filter(pl.col('position').str.contains('P'))
51
+ df_player = df_player.with_columns(
52
+ (pl.concat_str(["name", "player_id"], separator=" - ").alias("pitcher_name_id"))
53
+ )
54
+
55
+ # Select specific columns and convert to dictionary
56
+ pitcher_name_id_dict = dict(df_player.select(['pitcher_name_id', 'player_id']).iter_rows())
57
+
58
+ # Initialize session state for previous selection
59
+ if 'prev_pitcher_id' not in st.session_state:
60
+ st.session_state.prev_pitcher_id = None
61
+
62
+ # Display a selectbox for pitcher selection
63
+ selected_pitcher = st.selectbox("##### Select Pitcher", list(pitcher_name_id_dict.keys()))
64
+ pitcher_id = pitcher_name_id_dict[selected_pitcher]
65
+
66
+ # Clear cache if selection changes
67
+ if pitcher_id != st.session_state.prev_pitcher_id:
68
+ st.cache_data.clear()
69
+ st.session_state.prev_pitcher_id = pitcher_id
70
+ st.session_state.cache_cleared = False
71
+ st.write('Cache cleared!')
72
+
73
+ # Initialize session state for cache status
74
+ if 'cache_cleared' not in st.session_state:
75
+ st.session_state.cache_cleared = False
76
+
77
+ # Dictionary for batter hand selection
78
+ batter_hand_picker = {
79
+ 'All': ['L', 'R'],
80
+ 'LHH': ['L'],
81
+ 'RHH': ['R']
82
+ }
83
+
84
+ # Define date range for the season
85
+ min_date = date(2024, 3, 20)
86
+ max_date = date(2024, 11, 30)
87
+
88
+ # Create columns for input widgets
89
+ st.write("##### Filters")
90
+ col1, col2, col3 = st.columns(3)
91
+ with col1:
92
+ # Selectbox for batter handedness
93
+ batter_hand_select = st.selectbox('Batter Handedness:', list(batter_hand_picker.keys()))
94
+ batter_hand = batter_hand_picker[batter_hand_select]
95
+ with col2:
96
+ # Date input for start date
97
+ start_date = st.date_input('Start Date:',
98
+ value=min_date,
99
+ min_value=min_date,
100
+ max_value=max_date,
101
+ format="YYYY-MM-DD")
102
+ with col3:
103
+ # Date input for end date
104
+ end_date = st.date_input('End Date:',
105
+ value="default_value_today",
106
+ min_value=min_date,
107
+ max_value=max_date,
108
+ format="YYYY-MM-DD")
109
+
110
+ # Dictionary for plot type selection
111
+ plot_picker_dict = {
112
+ 'Short Form Movement': 'short_form_movement',
113
+ 'Long Form Movement': 'long_form_movement',
114
+ 'Release Points': 'release_point'
115
+ }
116
+
117
+ # Selectbox for plot type
118
+ plot_picker_select = st.selectbox('Select Plot Type:', list(plot_picker_dict.keys()))
119
+ plot_picker = plot_picker_dict[plot_picker_select]
120
+
121
+ # Extract season from start date
122
+ season = str(start_date)[0:4]
123
+
124
+ # Get list of games for the selected player and date range
125
+ player_games = scraper.get_player_games_list(player_id=pitcher_id, season=season,
126
+ start_date=str(start_date), end_date=str(end_date),
127
+ sport_id=selected_sport_id,
128
+ game_type = ['R','P'])
129
+
130
+ # Function to fetch data and cache it
131
+ @st.cache_data
132
+ def fetch_data():
133
+ data = scraper.get_data(game_list_input=player_games)
134
+ df = scraper.get_data_df(data_list=data)
135
+ return df
136
+
137
+ # Fetch data and manage cache status
138
+ if not st.session_state.cache_cleared:
139
+ df_original = fetch_data()
140
+ st.session_state.cache_cleared = True
141
+ else:
142
+ df_original = fetch_data()
143
+
144
+ # Button to generate plot
145
+ if st.button('Generate Plot'):
146
+ try:
147
+ # Convert dataframe to polars and filter based on inputs
148
+ df = ploter.df_to_polars(df_original=df_original,
149
+ pitcher_id=pitcher_id,
150
+ start_date=str(start_date),
151
+ end_date=str(end_date),
152
+ batter_hand=batter_hand)
153
+ print(df)
154
+ if len(df) == 0:
155
+ st.write('Please select different parameters.')
156
+ else:
157
+ # Generate the final plot
158
+ ploter.final_plot(
159
+ df=df,
160
+ pitcher_id=pitcher_id,
161
+ plot_picker=plot_picker,
162
+ sport_id=selected_sport_id)
163
+
164
+ # Use a container to control the width of the AgGrid display
165
+ with st.container():
166
+ # Group the data by pitch type
167
+ grouped_df = (
168
+ df.group_by(['pitcher_id', 'pitch_description'])
169
+ .agg([
170
+ pl.col('is_pitch').drop_nans().count().alias('pitches'),
171
+ pl.col('start_speed').drop_nans().mean().round(1).alias('start_speed'),
172
+ pl.col('vb').drop_nans().mean().round(1).alias('vb'),
173
+ pl.col('ivb').drop_nans().mean().round(1).alias('ivb'),
174
+ pl.col('hb').drop_nans().mean().round(1).alias('hb'),
175
+ pl.col('spin_rate').drop_nans().mean().round(0).alias('spin_rate'),
176
+ pl.col('x0').drop_nans().mean().round(1).alias('x0'),
177
+ pl.col('z0').drop_nans().mean().round(1).alias('z0'),
178
+ ])
179
+ .with_columns(
180
+ (pl.col('pitches') / pl.col('pitches').sum().over('pitcher_id') * 100).round(3).alias('proportion')
181
+ )).sort('proportion', descending=True).select(["pitch_description", "pitches", "proportion", "start_speed", "vb", "ivb", "hb",
182
+ "spin_rate", "x0", "z0"])
183
+
184
+ st.write("#### Pitching Data")
185
+ column_config_dict = {
186
+ 'pitcher_id': 'Pitcher ID',
187
+ 'pitch_description': 'Pitch Type',
188
+ 'pitches': 'Pitches',
189
+ 'start_speed': 'Velocity',
190
+ 'vb': 'VB',
191
+ 'ivb': 'iVB',
192
+ 'hb': 'HB',
193
+ 'spin_rate': 'Spin Rate',
194
+ 'proportion': st.column_config.NumberColumn("Pitch%", format="%.1f%%"),
195
+ 'x0': 'hRel',
196
+ 'z0': 'vRel',
197
+ }
198
+
199
+ st.markdown(f"""##### {selected_pitcher.split('-')[0]} {selected_league} Pitch Data""")
200
+ st.dataframe(grouped_df,
201
+ hide_index=True,
202
+ column_config=column_config_dict,
203
+ width=1500)
204
+
205
+ # Configure the AgGrid options
206
+ # gb = GridOptionsBuilder.from_dataframe(grouped_df)
207
+ # # Set display names for columns
208
+ # for col, display_name in zip(grouped_df.columns, grouped_df.columns):
209
+ # gb.configure_column(col, headerName=display_name)
210
+
211
+ # grid_options = gb.build()
212
+
213
+ # # Display the dataframe using AgGrid
214
+ # grid_response = AgGrid(
215
+ # grouped_df,
216
+ # gridOptions=grid_options,
217
+ # height=300,
218
+ # allow_unsafe_jscode=True,
219
+ # )
220
+
221
+ except IndexError:
222
+ st.write('Please select different parameters.')
223
+
224
+ # Display column and plot descriptions
225
+ st.markdown("""
226
+ #### Column Descriptions
227
+
228
+ - **`Pitch Type`**: Describes the type of pitch thrown (e.g., 4-Seam Fastball, Curveball, Slider).
229
+ - **`Pitches`**: The total number of pitches thrown by the pitcher.
230
+ - **`Pitch%`**: Proportion of pitch thrown.
231
+ - **`Velocity`**: The initial velocity of the pitch as it leaves the pitcher's hand, measured in miles per hour (mph).
232
+ - **`VB`**: Vertical Break (VB), representing the amount movement of a pitch due to spin and gravity, measured in inches (in).
233
+ - **`iVB`**: Induced Vertical Break (iVB), representing the amount movement of a pitch strictly due to the spin imparted on the ball, measured in inches (in).
234
+ - **`HB`**: Horizontal Break (HB), indicating the amount of horizontal movement of a pitch, measured in inches (in).
235
+ - **`Spin Rate`**: The rate of spin of the pitch as it is released, measured in revolutions per minute (rpm).
236
+ - **`hRel`**: The horizontal release point of the pitch, measured in feet from the center of the pitcher's mound (ft).
237
+ - **`vRel`**: The vertical release point of the pitch, measured in feet above the ground (ft).
238
+
239
+ #### Plot Descriptions
240
+
241
+ - **`Short Form Movement`**: Illustrates the movement of the pitch due to spin, where (0,0) indicates a pitch with perfect gyro-spin (e.g. Like a Football).
242
+ - **`Long Form Movement`**: Illustrates the movement of the pitch due to spin and gravity.
243
+ - **`Release Points`**: Illustrates a pitchers release points from the catcher's perspective.
244
+
245
+ #### Acknowledgements
246
+
247
+ Big thanks to [Michael Rosen](https://twitter.com/bymichaelrosen) and [Jeremy Maschino](https://twitter.com/pitchprofiler) for inspiration for this project
248
+
249
+ Check Out Michael's [Pitch Plotting App](https://pitchplotgenerator.streamlit.app/)
250
+
251
+ Check Out Jeremy's Website [Pitch Profiler](http://www.mlbpitchprofiler.com/)
252
+ """
253
+ )
functions/PitchPlotFunctions.py ADDED
@@ -0,0 +1,629 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import polars as pl
2
+ import numpy as np
3
+ import matplotlib.pyplot as plt
4
+ import seaborn as sns
5
+ from PIL import Image
6
+ import requests
7
+ from io import BytesIO
8
+ from matplotlib.offsetbox import OffsetImage, AnnotationBbox
9
+ from matplotlib.ticker import FuncFormatter
10
+ import matplotlib.transforms as transforms
11
+ from matplotlib.patches import Ellipse
12
+ import matplotlib.gridspec as gridspec
13
+ import matplotlib.patches as mpatches
14
+ import matplotlib.lines as mlines
15
+ from matplotlib.figure import Figure
16
+ import streamlit as st
17
+ import api_scraper
18
+
19
+ # Initialize the scraper
20
+ scraper = api_scraper.MLB_Scrape()
21
+
22
+ class PitchPlotFunctions:
23
+ # Define the pitch_colours method
24
+ def pitch_colours(self):
25
+ # Dictionary of pitch types and their corresponding colors and names
26
+ pitch_colours = {
27
+ 'FF': {'colour': '#FF007D', 'name': '4-Seam Fastball'},
28
+ 'FA': {'colour': '#FF007D', 'name': 'Fastball'},
29
+ 'SI': {'colour': '#98165D', 'name': 'Sinker'},
30
+ 'FC': {'colour': '#BE5FA0', 'name': 'Cutter'},
31
+ 'CH': {'colour': '#F79E70', 'name': 'Changeup'},
32
+ 'FS': {'colour': '#FE6100', 'name': 'Splitter'},
33
+ 'SC': {'colour': '#F08223', 'name': 'Screwball'},
34
+ 'FO': {'colour': '#FFB000', 'name': 'Forkball'},
35
+ 'SL': {'colour': '#67E18D', 'name': 'Slider'},
36
+ 'ST': {'colour': '#1BB999', 'name': 'Sweeper'},
37
+ 'SV': {'colour': '#376748', 'name': 'Slurve'},
38
+ 'KC': {'colour': '#311D8B', 'name': 'Knuckle Curve'},
39
+ 'CU': {'colour': '#3025CE', 'name': 'Curveball'},
40
+ 'CS': {'colour': '#274BFC', 'name': 'Slow Curve'},
41
+ 'EP': {'colour': '#648FFF', 'name': 'Eephus'},
42
+ 'KN': {'colour': '#867A08', 'name': 'Knuckleball'},
43
+ 'PO': {'colour': '#472C30', 'name': 'Pitch Out'},
44
+ 'UN': {'colour': '#9C8975', 'name': 'Unknown'},
45
+ }
46
+
47
+ # Create dictionaries mapping pitch types to their colors and names
48
+ dict_colour = dict(zip(pitch_colours.keys(), [pitch_colours[key]['colour'] for key in pitch_colours]))
49
+ dict_pitch = dict(zip(pitch_colours.keys(), [pitch_colours[key]['name'] for key in pitch_colours]))
50
+
51
+ return dict_colour, dict_pitch
52
+
53
+ # Define the sns_custom_theme method
54
+ def sns_custom_theme(self):
55
+ # Custom theme for seaborn plots
56
+ custom_theme = {
57
+ "axes.facecolor": "white",
58
+ "axes.edgecolor": ".8",
59
+ "axes.grid": True,
60
+ "axes.axisbelow": True,
61
+ "axes.labelcolor": ".15",
62
+ "figure.facecolor": "#f9f9f9",
63
+ "grid.color": ".8",
64
+ "grid.linestyle": "-",
65
+ "text.color": ".15",
66
+ "xtick.color": ".15",
67
+ "ytick.color": ".15",
68
+ "xtick.direction": "out",
69
+ "ytick.direction": "out",
70
+ "lines.solid_capstyle": "round",
71
+ "patch.edgecolor": "w",
72
+ "patch.force_edgecolor": True,
73
+ "image.cmap": "rocket",
74
+ "font.family": ["sans-serif"],
75
+ "font.sans-serif": ["Arial", "DejaVu Sans", "Liberation Sans", "Bitstream Vera Sans", "sans-serif"],
76
+ "xtick.bottom": False,
77
+ "xtick.top": False,
78
+ "ytick.left": False,
79
+ "ytick.right": False,
80
+ "axes.spines.left": True,
81
+ "axes.spines.bottom": True,
82
+ "axes.spines.right": True,
83
+ "axes.spines.top": True
84
+ }
85
+
86
+ # Color palette for the plots
87
+ colour_palette = ['#FFB000', '#648FFF', '#785EF0', '#DC267F', '#FE6100', '#3D1EB2', '#894D80', '#16AA02', '#B5592B', '#A3C1ED']
88
+
89
+ return custom_theme, colour_palette
90
+
91
+ # Define the sport_id_dict method
92
+ def sport_id_dict(self):
93
+ # Dictionary mapping sport IDs to their names
94
+ dict = {1: 'MLB', 11: 'AAA'}
95
+ return dict
96
+
97
+ # Define the team_logos method
98
+ def team_logos(self):
99
+ # List of MLB teams and their corresponding ESPN logo URLs
100
+ mlb_teams = [
101
+ {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
102
+ {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
103
+ {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
104
+ {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
105
+ {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
106
+ {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
107
+ {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
108
+ {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
109
+ {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
110
+ {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
111
+ {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
112
+ {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
113
+ {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
114
+ {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
115
+ {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
116
+ {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
117
+ {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
118
+ {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
119
+ {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
120
+ {"team": "OAK", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
121
+ {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
122
+ {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
123
+ {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
124
+ {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
125
+ {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
126
+ {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
127
+ {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
128
+ {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
129
+ {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
130
+ {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"}
131
+ ]
132
+
133
+ # Create a DataFrame from the list of dictionaries
134
+ df_image = pl.DataFrame(mlb_teams)
135
+ # Set the index to 'team' and convert 'logo_url' to a dictionary
136
+ image_dict = df_image.select(['team', 'logo_url']).to_dict(as_series=False)['logo_url']
137
+
138
+ # Convert to the desired dictionary format
139
+ image_dict = {row['team']: row['logo_url'] for row in df_image.select(['team', 'logo_url']).to_dicts()}
140
+
141
+ return image_dict
142
+
143
+ # Function to get an image from a URL and display it on the given axis
144
+ def player_headshot(self, pitcher_id: str, ax: plt.Axes, sport_id: int):
145
+ """
146
+ Fetches and displays the player's headshot image on the given axis.
147
+
148
+ Parameters:
149
+ pitcher_id (str): The ID of the pitcher.
150
+ ax (plt.Axes): The matplotlib axis to display the image on.
151
+ sport_id (int): The sport ID to determine the URL format.
152
+ """
153
+ # Construct the URL for the player's headshot image
154
+ if sport_id == 1:
155
+ url = f'https://img.mlbstatic.com/mlb-photos/image/'\
156
+ f'upload/d_people:generic:headshot:67:current.png'\
157
+ f'/w_640,q_auto:best/v1/people/{pitcher_id}/headshot/silo/current.png'
158
+ else:
159
+ url = f'https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto/w_640/v1/people/{pitcher_id}/headshot/milb/current.png'
160
+
161
+ # Send a GET request to the URL
162
+ response = requests.get(url)
163
+ # Open the image from the response content
164
+ img = Image.open(BytesIO(response.content))
165
+ # Display the image on the axis
166
+ ax.set_xlim(0, 2)
167
+ ax.set_ylim(0, 1)
168
+ ax.imshow(img, extent=[0.0, 1, 0, 1], origin='upper')
169
+ # Turn off the axis
170
+ ax.axis('off')
171
+
172
+ # Function to display player bio information on the given axis
173
+ def player_bio(self, pitcher_id: str, ax: plt.Axes, start_date: str, end_date: str, batter_hand: list):
174
+ """
175
+ Fetches and displays the player's bio information on the given axis.
176
+
177
+ Parameters:
178
+ pitcher_id (str): The ID of the pitcher.
179
+ ax (plt.Axes): The matplotlib axis to display the bio information on.
180
+ start_date (str): The start date for the bio information.
181
+ end_date (str): The end date for the bio information.
182
+ batter_hand (list): The list of batter hands (e.g., ['R'] or ['L']).
183
+ """
184
+ # Construct the URL to fetch player data
185
+ url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam"
186
+ # Send a GET request to the URL and parse the JSON response
187
+ data = requests.get(url).json()
188
+ # Extract player information from the JSON data
189
+ player_name = data['people'][0]['fullName']
190
+ pitcher_hand = data['people'][0]['pitchHand']['code']
191
+ age = data['people'][0]['currentAge']
192
+ height = data['people'][0]['height']
193
+ weight = data['people'][0]['weight']
194
+ # Display the player's name, handedness, age, height, and weight on the axis
195
+ ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=20)
196
+ ax.text(0.5, 0.65, f'{pitcher_hand}HP, Age: {age}, {height}/{weight}', va='top', ha='center', fontsize=12)
197
+ # Determine the batter hand text
198
+ if batter_hand == ['R']:
199
+ batter_hand_text = ', vs RHH'
200
+ elif batter_hand == ['L']:
201
+ batter_hand_text = ', vs LHH'
202
+ else:
203
+ batter_hand_text = ''
204
+ ax.text(0.5, 0.40, f'{start_date} to {end_date}{batter_hand_text}', va='top', ha='center', fontsize=12, fontstyle='italic')
205
+ # Turn off the axis
206
+ ax.axis('off')
207
+
208
+ # Function to display the team logo on the given axis
209
+ def plot_logo(self, pitcher_id: str, ax: plt.Axes):
210
+ """
211
+ Fetches and displays the team logo on the given axis.
212
+
213
+ Parameters:
214
+ pitcher_id (str): The ID of the pitcher.
215
+ ax (plt.Axes): The matplotlib axis to display the logo on.
216
+ """
217
+ # Construct the URL to fetch player data
218
+ url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam"
219
+ # Send a GET request to the URL and parse the JSON response
220
+ data = requests.get(url).json()
221
+ # Construct the URL to fetch team data
222
+ try:
223
+ url_team = 'https://statsapi.mlb.com/' + data['people'][0]['currentTeam']['link']
224
+ # Send a GET request to the team URL and parse the JSON response
225
+ data_team = requests.get(url_team).json()
226
+ # Get the logo URL from the image dictionary using the team abbreviation
227
+
228
+ if data_team['teams'][0]['sport']['id'] == 1:
229
+ team_abb = data_team['teams'][0]['abbreviation']
230
+ logo_url = self.team_logos()[team_abb]
231
+ else:
232
+ team_abb = data_team['teams'][0]['parentOrgId']
233
+ logo_url = self.team_logos()[dict(scraper.get_teams().select(['team_id', 'parent_org_abbreviation']).iter_rows())[team_abb]]
234
+ except KeyError:
235
+ logo_url = "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png?w=500&h=500&transparent=true"
236
+ # Send a GET request to the logo URL
237
+ response = requests.get(logo_url)
238
+ # Open the image from the response content
239
+ img = Image.open(BytesIO(response.content))
240
+ # Display the image on the axis
241
+ ax.set_xlim(0, 2)
242
+ ax.set_ylim(0, 1)
243
+ ax.imshow(img, extent=[1, 2, 0, 1], origin='upper')
244
+ # Turn off the axis
245
+ ax.axis('off')
246
+
247
+ ### PITCH ELLIPSE ###
248
+ def confidence_ellipse( self,
249
+ x:np.array,
250
+ y:np.array,
251
+ ax:plt.Axes,
252
+ n_std:float=3.0,
253
+ facecolor:str='none',
254
+ **kwargs):
255
+ """
256
+ Create a plot of the covariance confidence ellipse of *x* and *y*.
257
+ Parameters
258
+ ----------
259
+ x, y : array-like, shape (n, )
260
+ Input data.
261
+ ax : matplotlib.axes.Axes
262
+ The axes object to draw the ellipse into.
263
+ n_std : float
264
+ The number of standard deviations to determine the ellipse's radiuses.
265
+ **kwargs
266
+ Forwarded to `~matplotlib.patches.Ellipse`
267
+ Returns
268
+ -------
269
+ matplotlib.patches.Ellipse
270
+ """
271
+
272
+ if x.shape != y.shape:
273
+ raise ValueError("x and y must be the same size")
274
+ try:
275
+ cov = np.cov(x, y)
276
+ pearson = cov[0, 1]/np.sqrt(cov[0, 0] * cov[1, 1])
277
+ # Using a special case to obtain the eigenvalues of this
278
+ # two-dimensional dataset.
279
+ ell_radius_x = np.sqrt(1 + pearson)
280
+ ell_radius_y = np.sqrt(1 - pearson)
281
+ ellipse = Ellipse((0, 0), width=ell_radius_x * 2, height=ell_radius_y * 2,
282
+ facecolor=facecolor,linewidth=2,linestyle='--', **kwargs)
283
+
284
+
285
+ # Calculating the standard deviation of x from
286
+ # the squareroot of the variance and multiplying
287
+ # with the given number of standard deviations.
288
+ scale_x = np.sqrt(cov[0, 0]) * n_std
289
+ mean_x = x.mean()
290
+
291
+
292
+ # calculating the standard deviation of y ...
293
+ scale_y = np.sqrt(cov[1, 1]) * n_std
294
+ mean_y = y.mean()
295
+
296
+
297
+ transf = transforms.Affine2D() \
298
+ .rotate_deg(45) \
299
+ .scale(scale_x, scale_y) \
300
+ .translate(mean_x, mean_y)
301
+
302
+
303
+
304
+ ellipse.set_transform(transf + ax.transData)
305
+ except ValueError:
306
+ return
307
+
308
+ return ax.add_patch(ellipse)
309
+
310
+
311
+ def break_plot_big(self, df: pl.DataFrame, ax: plt.Axes, sport_id: int):
312
+ """
313
+ Plots a big break plot for the given DataFrame on the provided axis.
314
+
315
+ Parameters:
316
+ df (pl.DataFrame): The DataFrame containing pitch data.
317
+ ax (plt.Axes): The matplotlib axis to plot on.
318
+ sport_id (int): The sport ID to determine the plot title.
319
+ """
320
+ # Set font properties for different elements of the plot
321
+ font_properties = {'size': 10}
322
+ font_properties_titles = {'size': 16}
323
+ font_properties_axes = {'size': 14}
324
+
325
+ # Get unique pitch types sorted by 'prop' and 'pitch_type'
326
+ label_labels = df.sort(by=['prop', 'pitch_type'], descending=[False, True])['pitch_type'].unique()
327
+ j = 0
328
+ dict_colour, dict_pitch = self.pitch_colours()
329
+ custom_theme, colour_palette = self.sns_custom_theme()
330
+
331
+ # Loop through each pitch type and plot confidence ellipses
332
+ for label in label_labels:
333
+ subset = df.filter(pl.col('pitch_type') == label)
334
+ if len(subset) > 4:
335
+ try:
336
+ if df['pitcher_hand'][0] == 'R':
337
+ self.confidence_ellipse(subset['hb']* 1, subset['ivb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2)
338
+ if df['pitcher_hand'][0] == 'L':
339
+ self.confidence_ellipse(subset['hb'] * 1, subset['ivb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2)
340
+ except ValueError:
341
+ return
342
+ j += 1
343
+ else:
344
+ j += 1
345
+
346
+ # Plot scatter plot of pitch data
347
+ if df['pitcher_hand'][0] == 'R':
348
+ sns.scatterplot(ax=ax, x=df['hb'] * 1, y=df['ivb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=35)
349
+ if df['pitcher_hand'][0] == 'L':
350
+ sns.scatterplot(ax=ax, x=df['hb'] * 1, y=df['ivb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=35)
351
+
352
+ # Set plot limits and labels
353
+ ax.set_xlim((-25, 25))
354
+ ax.set_ylim((-25, 25))
355
+ ax.hlines(y=0, xmin=-50, xmax=50, color=colour_palette[8], alpha=0.5, linestyles='--', zorder=1)
356
+ ax.vlines(x=0, ymin=-50, ymax=50, color=colour_palette[8], alpha=0.5, linestyles='--', zorder=1)
357
+ ax.set_xlabel('Horizontal Break (in)', fontdict=font_properties_axes)
358
+ ax.set_ylabel('Induced Vertical Break (in)', fontdict=font_properties_axes)
359
+ ax.set_title(f"{self.sport_id_dict()[sport_id]} - Short Form Pitch Movement Plot", fontdict=font_properties_titles)
360
+
361
+ # Remove legend and set tick labels
362
+ ax.get_legend().remove()
363
+ ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties)
364
+ ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties)
365
+
366
+ # Add text annotations based on pitcher hand
367
+ if df['pitcher_hand'][0] == 'R':
368
+ ax.text(-24.5, -24.5, s='← Glove Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=13, zorder=3)
369
+ ax.text(24.5, -24.5, s='Arm Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=13, zorder=3)
370
+ if df['pitcher_hand'][0] == 'L':
371
+ ax.invert_xaxis()
372
+ ax.text(24.5, -24.5, s='← Arm Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=13, zorder=3)
373
+ ax.text(-24.5, -24.5, s='Glove Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=13, zorder=3)
374
+
375
+ # Set aspect ratio and format tick labels
376
+ ax.set_aspect('equal', adjustable='box')
377
+ ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
378
+ ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
379
+
380
+ ### BREAK PLOT ###
381
+ def break_plot_big_long(self, df: pl.DataFrame, ax: plt.Axes, sport_id: int):
382
+ """
383
+ Plots a long break plot for the given DataFrame on the provided axis.
384
+
385
+ Parameters:
386
+ df (pl.DataFrame): The DataFrame containing pitch data.
387
+ ax (plt.Axes): The matplotlib axis to plot on.
388
+ sport_id (int): The sport ID to determine the plot title.
389
+ """
390
+ # Set font properties for different elements of the plot
391
+ font_properties = {'size': 20}
392
+ font_properties_titles = {'size': 32}
393
+ font_properties_axes = {'size': 24}
394
+
395
+ # Get unique pitch types sorted by 'prop' and 'pitch_type'
396
+ label_labels = df.sort(by=['prop', 'pitch_type'], descending=[False, True])['pitch_type'].unique()
397
+ dict_colour, dict_pitch = self.pitch_colours()
398
+ custom_theme, colour_palette = self.sns_custom_theme()
399
+ j = 0
400
+
401
+ # Loop through each pitch type and plot confidence ellipses
402
+ for label in label_labels:
403
+ subset = df.filter(pl.col('pitch_type') == label)
404
+ print(label)
405
+ if len(subset) > 4:
406
+ try:
407
+ if df['pitcher_hand'][0] == 'R':
408
+ self.confidence_ellipse(subset['hb'], subset['vb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2)
409
+ if df['pitcher_hand'][0] == 'L':
410
+ self.confidence_ellipse(subset['hb'] * -1, subset['vb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2)
411
+ except ValueError:
412
+ return
413
+ j += 1
414
+ else:
415
+ j += 1
416
+
417
+ # Plot scatter plot of pitch data
418
+ if df['pitcher_hand'][0] == 'R':
419
+ sns.scatterplot(ax=ax, x=df['hb'] * 1, y=df['vb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
420
+ if df['pitcher_hand'][0] == 'L':
421
+ sns.scatterplot(ax=ax, x=df['hb'] * -1, y=df['vb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
422
+
423
+ # Set plot limits and labels
424
+ ax.set_xlim((-40, 40))
425
+ ax.set_ylim((-80, 0))
426
+ ax.axhline(y=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1)
427
+ ax.axvline(x=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1)
428
+ ax.set_xlabel('Horizontal Break (in)', fontdict=font_properties_axes)
429
+ ax.set_ylabel('Vertical Break (in)', fontdict=font_properties_axes)
430
+ ax.set_title(f"{self.sport_id_dict()[sport_id]} - Long Form Pitch Movement Plot", fontdict=font_properties_titles)
431
+
432
+ # Remove legend and set tick labels
433
+ ax.get_legend().remove()
434
+ ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties)
435
+ ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties)
436
+
437
+ # Add text annotations based on pitcher hand
438
+ if df['pitcher_hand'][0] == 'R':
439
+ ax.text(-39.5, -79.5, s='← Glove Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
440
+ ax.text(39.5, -79.5, s='Arm Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
441
+ if df['pitcher_hand'][0] == 'L':
442
+ ax.invert_xaxis()
443
+ ax.text(39.5, -79.5, s='← Arm Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
444
+ ax.text(-39.5, -79.5, s='Glove Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
445
+
446
+ # Set aspect ratio and format tick labels
447
+ ax.set_aspect('equal', adjustable='box')
448
+ ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
449
+ ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
450
+
451
+ ### BREAK PLOT ###
452
+ def release_point_plot(self, df: pl.DataFrame, ax: plt.Axes, sport_id: int):
453
+ """
454
+ Plots the release points for the given DataFrame on the provided axis.
455
+
456
+ Parameters:
457
+ df (pl.DataFrame): The DataFrame containing pitch data.
458
+ ax (plt.Axes): The matplotlib axis to plot on.
459
+ sport_id (int): The sport ID to determine the plot title.
460
+ """
461
+ # Set font properties for different elements of the plot
462
+ font_properties = {'size': 20}
463
+ font_properties_titles = {'size': 32}
464
+ font_properties_axes = {'size': 24}
465
+ dict_colour, dict_pitch = self.pitch_colours()
466
+ custom_theme, colour_palette = self.sns_custom_theme()
467
+
468
+ # Plot scatter plot of release points based on pitcher hand
469
+ if df['pitcher_hand'][0] == 'R':
470
+ sns.scatterplot(ax=ax, x=df['x0'] * -1, y=df['z0'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
471
+ if df['pitcher_hand'][0] == 'L':
472
+ sns.scatterplot(ax=ax, x=df['x0'] * 1, y=df['z0'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50)
473
+
474
+ # Add patches to the plot
475
+ ax.add_patch(plt.Circle((0, 10 / 12 - 18), radius=18, edgecolor='black', facecolor='#a63b17'))
476
+ ax.add_patch(plt.Rectangle((-0.5, 9 / 12), 1, 1 / 6, edgecolor='black', facecolor='white'))
477
+
478
+ # Set plot limits and labels
479
+ ax.set_xlim((-4, 4))
480
+ ax.set_ylim((0, 8))
481
+ ax.axhline(y=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1)
482
+ ax.axvline(x=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1)
483
+ ax.set_ylabel('Vertical Release (ft)', fontdict=font_properties_axes)
484
+ ax.set_xlabel('Horizontal Release (ft)', fontdict=font_properties_axes)
485
+ ax.set_title(f"{self.sport_id_dict()[sport_id]} - Release Points Catcher Perspective", fontdict=font_properties_titles)
486
+
487
+ # Remove legend and set tick labels
488
+ ax.get_legend().remove()
489
+ ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties)
490
+ ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties)
491
+
492
+ # Add text annotations based on pitcher hand
493
+ if df['pitcher_hand'][0] == 'L':
494
+ ax.text(-3.95, 0.05, s='← Glove Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
495
+ ax.text(3.95, 0.05, s='Arm Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
496
+ if df['pitcher_hand'][0] == 'R':
497
+ ax.invert_xaxis()
498
+ ax.text(3.95, 0.05, s='← Arm Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
499
+ ax.text(-3.95, 0.05, s='Glove Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3)
500
+
501
+ # Set aspect ratio and format tick labels
502
+ ax.set_aspect('equal', adjustable='box')
503
+ ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
504
+ ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
505
+
506
+ def df_to_polars(self, df_original: pl.DataFrame, pitcher_id: str, start_date: str, end_date: str, batter_hand: list):
507
+ """
508
+ Filters and processes the original DataFrame to a Polars DataFrame.
509
+
510
+ Parameters:
511
+ df_original (pl.DataFrame): The original DataFrame containing pitch data.
512
+ pitcher_id (str): The ID of the pitcher.
513
+ start_date (str): The start date for filtering the data.
514
+ end_date (str): The end date for filtering the data.
515
+ batter_hand (list): The list of batter hands (e.g., ['R'] or ['L']).
516
+
517
+ Returns:
518
+ pl.DataFrame: The filtered and processed Polars DataFrame.
519
+ """
520
+ df = df_original.clone()
521
+ df = df.filter((pl.col('pitcher_id') == pitcher_id) &
522
+ (pl.col('is_pitch')) & (pl.col('pitch_type').is_not_null()) &
523
+ (pl.col('pitch_type') != 'NaN') &
524
+ (pl.col('game_date') >= start_date) &
525
+ (pl.col('game_date') <= end_date) &
526
+ (pl.col('batter_hand').is_in(batter_hand)))
527
+ df = df.with_columns(
528
+ prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"),
529
+ prop=pl.col('is_pitch').sum().over("pitch_type")
530
+ )
531
+ return df
532
+
533
+ def final_plot(self, df: pl.DataFrame, pitcher_id: str, plot_picker: str, sport_id: int):
534
+ """
535
+ Creates a final plot with player headshot, bio, logo, and pitch movement plots.
536
+
537
+ Parameters:
538
+ df (pl.DataFrame): The DataFrame containing pitch data.
539
+ pitcher_id (str): The ID of the pitcher.
540
+ plot_picker (str): The type of plot to create ('short_form_movement', 'long_form_movement', 'release_point').
541
+ sport_id (int): The sport ID to determine the plot title.
542
+ """
543
+ # Set the theme for seaborn plots
544
+ sns.set_theme(style="whitegrid", rc=self.sns_custom_theme()[0])
545
+
546
+ # Create a figure and a gridspec with 6 rows and 5 columns
547
+ fig = plt.figure(figsize=(9, 9))
548
+ gs = gridspec.GridSpec(6, 5, figure=fig, height_ratios=[0.00000000005, 5, 30, 7, 2, 0.00000000005], width_ratios=[1, 10, 10, 10, 1])
549
+ gs.update(hspace=0.1, wspace=0.1)
550
+ # Create subplots for player headshot, bio, and logo
551
+ ax_headshot = fig.add_subplot(gs[1, 1])
552
+ ax_bio = fig.add_subplot(gs[1, 2])
553
+ ax_logo = fig.add_subplot(gs[1, 3])
554
+
555
+ # Get the start and end dates and unique batter hands from the DataFrame
556
+ start_date = df['game_date'].min()
557
+ end_date = df['game_date'].max()
558
+ batter_hand = list(df['batter_hand'].unique())
559
+
560
+ # Plot player headshot, bio, and logo
561
+ self.player_headshot(pitcher_id=pitcher_id, ax=ax_headshot, sport_id=sport_id)
562
+ self.player_bio(pitcher_id=pitcher_id, ax=ax_bio, start_date=start_date, end_date=end_date, batter_hand=batter_hand)
563
+ self.plot_logo(pitcher_id=pitcher_id, ax=ax_logo)
564
+
565
+ # Create subplot for the main plot
566
+ ax_main_plot = fig.add_subplot(gs[2, :])
567
+
568
+ # Create subplot for the legend
569
+ ax_legend = fig.add_subplot(gs[3, :])
570
+
571
+
572
+ # Create subplot for the footer
573
+ ax_footer = fig.add_subplot(gs[-2, :])
574
+
575
+ # Plot the selected pitch movement plot
576
+ if plot_picker == 'short_form_movement':
577
+ self.break_plot_big(df, ax_main_plot, sport_id=sport_id)
578
+ elif plot_picker == 'long_form_movement':
579
+ self.break_plot_big_long(df, ax_main_plot, sport_id=sport_id)
580
+ elif plot_picker == 'release_point':
581
+ self.release_point_plot(df, ax_main_plot, sport_id=sport_id)
582
+
583
+ # Sort the DataFrame and get unique pitch types
584
+ items_in_order = list(df.sort(by=['prop', 'pitch_type'], descending=[True, True])['pitch_type'].unique(maintain_order=True))
585
+
586
+ # Get pitch colors and names
587
+ dict_colour, dict_pitch = self.pitch_colours()
588
+ ordered_colors = [dict_colour[x] for x in items_in_order]
589
+ items_in_order = [dict_pitch[x] for x in items_in_order]
590
+
591
+ # Create custom legend handles with circles
592
+ legend_handles = [mlines.Line2D([], [], color=color, marker='o', linestyle='None', markersize=5, label=label) for color, label in zip(ordered_colors, items_in_order)]
593
+
594
+ # Add legend to ax_legend
595
+ if len(items_in_order) <= 5:
596
+ ax_legend.legend(handles=legend_handles, bbox_to_anchor=(0.1, 0, 0.8, 0.7), ncol=5, fancybox=True, loc='center', fontsize=10, framealpha=1.0, markerscale=2, prop={'size': 10})
597
+ else:
598
+ ax_legend.legend(handles=legend_handles, bbox_to_anchor=(0.1, 0, 0.8, 0.7), ncol=5, fancybox=True, loc='center', fontsize=10, framealpha=1.0, markerscale=2, prop={'size': 10})
599
+
600
+ # Add footer text
601
+ ax_footer.text(x=0.075, y=0, s='By: Thomas Nestico\n @TJStats', fontname='Calibri', ha='left', fontsize=12, va='bottom')
602
+ ax_footer.text(x=1-0.075, y=0, s='Data: MLB', ha='right', fontname='Calibri', fontsize=12, va='bottom')
603
+
604
+
605
+ # Create subplots for the borders
606
+
607
+ ax_top_border = fig.add_subplot(gs[0, :])
608
+ ax_left_border = fig.add_subplot(gs[:, 0])
609
+ ax_right_border = fig.add_subplot(gs[:, -1])
610
+ ax_bottom_border = fig.add_subplot(gs[-1, :])
611
+
612
+ # Turn off the axes for the border subplots
613
+ ax_top_border.axis('off')
614
+ ax_left_border.axis('off')
615
+ ax_right_border.axis('off')
616
+ ax_bottom_border.axis('off')
617
+ ax_footer.axis('off')
618
+ ax_legend.axis('off')
619
+
620
+ # Adjust layout and show the figure
621
+ # fig.tight_layout()
622
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
623
+ # st.pyplot(fig)
624
+
625
+
626
+
627
+
628
+
629
+
functions/__pycache__/PitchPlotFunctions.cpython-39.pyc ADDED
Binary file (23 kB). View file
 
functions/__pycache__/pitch_summary_functions.cpython-39.pyc CHANGED
Binary files a/functions/__pycache__/pitch_summary_functions.cpython-39.pyc and b/functions/__pycache__/pitch_summary_functions.cpython-39.pyc differ
 
functions/pitch_summary_functions.py CHANGED
@@ -50,7 +50,6 @@ pitch_colours = {
50
 
51
  ## Others ##
52
  'KN': {'colour': '#867A08', 'name': 'Knuckleball'},
53
- 'KN': {'colour': '#867A08', 'name': 'Knuckle Ball'},
54
  'PO': {'colour': '#472C30', 'name': 'Pitch Out'},
55
  'UN': {'colour': '#9C8975', 'name': 'Unknown'},
56
  }
@@ -625,7 +624,7 @@ def summary_table(df: pl.DataFrame, ax: plt.Axes):
625
  # Apply color to specific columns based on normalized values
626
  columns_to_color = [(3, 'release_speed', 0.95, 1.05), (11, 'release_extension', 0.9, 1.1), (13, None, 80, 120),
627
  (14, None, 30, 70), (15, 'in_zone_rate', 0.7, 1.3), (16, 'chase_rate', 0.7, 1.3),
628
- (17, 'whiff_rate', 0.7, 1.3), (18, 'xwobacon', 0.7, 1.3)]
629
 
630
  for col, stat, vmin_factor, vmax_factor in columns_to_color:
631
  cell_value = table.get_celld()[(i + 1, col)].get_text().get_text()
@@ -765,7 +764,7 @@ def player_bio(pitcher_id: str, ax: plt.Axes, sport_id: int, year_input: int):
765
 
766
  # Display the player's name, handedness, age, height, and weight on the axis
767
  ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=56)
768
- ax.text(0.5, 0.7, f'{pitcher_hand}HP, Age: {age}, {height}/{weight}', va='top', ha='center', fontsize=30)
769
  ax.text(0.5, 0.45, f'Season Pitching Summary', va='top', ha='center', fontsize=40)
770
 
771
  # Make API call to retrieve sports information
@@ -867,7 +866,7 @@ def plot_logo(pitcher_id: str, ax: plt.Axes, df_team: pl.DataFrame, df_players:
867
 
868
  # Turn off the axis
869
  ax.axis('off')
870
- except (KeyError,IndexError) as e:
871
  ax.axis('off')
872
  return
873
 
@@ -1056,8 +1055,7 @@ def stat_summary_table(df: pl.DataFrame,
1056
  player_input: int,
1057
  sport_id: int,
1058
  ax: plt.Axes,
1059
- split: str = 'All',
1060
- game_type: list = ['R']):
1061
  """
1062
  Create a summary table of player statistics.
1063
 
@@ -1074,18 +1072,6 @@ def stat_summary_table(df: pl.DataFrame,
1074
  split : str, optional
1075
  The split type (default is 'All').
1076
  """
1077
-
1078
- type_dict = {'R':'Regular Season',
1079
- 'S':'Spring',
1080
- 'P':'Playoffs' }
1081
-
1082
- split_title = {
1083
- 'all':'',
1084
- 'right':' vs RHH',
1085
- 'left':' vs LHH'
1086
- }
1087
-
1088
-
1089
  # Format start and end dates
1090
  start_date_format = str(pd.to_datetime(df['game_date'][0]).strftime('%m/%d/%Y'))
1091
  end_date_format = str(pd.to_datetime(df['game_date'][-1]).strftime('%m/%d/%Y'))
@@ -1093,14 +1079,11 @@ def stat_summary_table(df: pl.DataFrame,
1093
  # Determine app context based on sport ID
1094
  appContext = 'majorLeague' if sport_id == 1 else 'minorLeague'
1095
 
1096
- game_type_str = ','.join([str(x) for x in game_type])
1097
-
1098
  # Fetch player stats from MLB API
1099
  pitcher_stats_call = requests.get(
1100
- f'https://statsapi.mlb.com/api/v1/people/{player_input}?appContext={appContext}&hydrate=stats(group=[pitching],type=[byDateRange],sportId={sport_id},startDate={start_date_format},endDate={end_date_format},gameType=[{game_type_str}])'
1101
  ).json()
1102
- print('HERE')
1103
- print(f'https://statsapi.mlb.com/api/v1/people/{player_input}?appContext={appContext}&hydrate=stats(group=[pitching],type=[byDateRange],sportId={sport_id},startDate={start_date_format},endDate={end_date_format},gameType=[{game_type_str}])')
1104
  # Extract stats and create DataFrame
1105
  pitcher_stats_call_header = [x for x in pitcher_stats_call['people'][0]['stats'][0]['splits'][-1]['stat']]
1106
  pitcher_stats_call_values = [pitcher_stats_call['people'][0]['stats'][0]['splits'][-1]['stat'][x] for x in pitcher_stats_call['people'][0]['stats'][0]['splits'][-1]['stat']]
@@ -1120,11 +1103,11 @@ def stat_summary_table(df: pl.DataFrame,
1120
  if df['game_id'][0] == df['game_id'][-1]:
1121
  pitcher_stats_call_df_small = pitcher_stats_call_df.select(['inningsPitched', 'battersFaced', 'earnedRuns', 'hits', 'strikeOuts', 'baseOnBalls', 'hitByPitch', 'homeRuns', 'strikePercentage', 'whiffs'])
1122
  new_column_names = ['$\\bf{IP}$', '$\\bf{PA}$', '$\\bf{ER}$', '$\\bf{H}$', '$\\bf{K}$', '$\\bf{BB}$', '$\\bf{HBP}$', '$\\bf{HR}$', '$\\bf{Strike\%}$', '$\\bf{Whiffs}$']
1123
- title = f'{df["game_date"][0]} vs {df["batter_team"][0]} ({type_dict[game_type[0]]}){split_title[split]}'
1124
- elif sport_id != 1 or game_type[0] in ['S','P']:
1125
  pitcher_stats_call_df_small = pitcher_stats_call_df.select(['inningsPitched', 'battersFaced', 'whip', 'era', 'fip', 'k_percent', 'bb_percent', 'k_bb_percent', 'strikePercentage'])
1126
  new_column_names = ['$\\bf{IP}$', '$\\bf{PA}$', '$\\bf{WHIP}$', '$\\bf{ERA}$', '$\\bf{FIP}$', '$\\bf{K\%}$', '$\\bf{BB\%}$', '$\\bf{K-BB\%}$', '$\\bf{Strike\%}$']
1127
- title = f'{df["game_date"][0]} to {df["game_date"][-1]} ({type_dict[game_type[0]]}{split_title[split]})'
1128
  else:
1129
  fangraphs_table(df=df, ax=ax, player_input=player_input, season=int(df['game_date'][0][0:4]), split=split)
1130
  return
 
50
 
51
  ## Others ##
52
  'KN': {'colour': '#867A08', 'name': 'Knuckleball'},
 
53
  'PO': {'colour': '#472C30', 'name': 'Pitch Out'},
54
  'UN': {'colour': '#9C8975', 'name': 'Unknown'},
55
  }
 
624
  # Apply color to specific columns based on normalized values
625
  columns_to_color = [(3, 'release_speed', 0.95, 1.05), (11, 'release_extension', 0.9, 1.1), (13, None, 80, 120),
626
  (14, None, 30, 70), (15, 'in_zone_rate', 0.7, 1.3), (16, 'chase_rate', 0.7, 1.3),
627
+ (17, 'whiff_rate', 0.7, 1.3), (18, 'xwoba', 0.7, 1.3)]
628
 
629
  for col, stat, vmin_factor, vmax_factor in columns_to_color:
630
  cell_value = table.get_celld()[(i + 1, col)].get_text().get_text()
 
764
 
765
  # Display the player's name, handedness, age, height, and weight on the axis
766
  ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=56)
767
+ ax.text(0.5, 0.7, f'{pitcher_hand}HP, Age:{age}, {height}/{weight}', va='top', ha='center', fontsize=30)
768
  ax.text(0.5, 0.45, f'Season Pitching Summary', va='top', ha='center', fontsize=40)
769
 
770
  # Make API call to retrieve sports information
 
866
 
867
  # Turn off the axis
868
  ax.axis('off')
869
+ except KeyError:
870
  ax.axis('off')
871
  return
872
 
 
1055
  player_input: int,
1056
  sport_id: int,
1057
  ax: plt.Axes,
1058
+ split: str = 'All'):
 
1059
  """
1060
  Create a summary table of player statistics.
1061
 
 
1072
  split : str, optional
1073
  The split type (default is 'All').
1074
  """
 
 
 
 
 
 
 
 
 
 
 
 
1075
  # Format start and end dates
1076
  start_date_format = str(pd.to_datetime(df['game_date'][0]).strftime('%m/%d/%Y'))
1077
  end_date_format = str(pd.to_datetime(df['game_date'][-1]).strftime('%m/%d/%Y'))
 
1079
  # Determine app context based on sport ID
1080
  appContext = 'majorLeague' if sport_id == 1 else 'minorLeague'
1081
 
 
 
1082
  # Fetch player stats from MLB API
1083
  pitcher_stats_call = requests.get(
1084
+ f'https://statsapi.mlb.com/api/v1/people/{player_input}?appContext={appContext}&hydrate=stats(group=[pitching],type=[byDateRange],sportId={sport_id},startDate={start_date_format},endDate={end_date_format})'
1085
  ).json()
1086
+
 
1087
  # Extract stats and create DataFrame
1088
  pitcher_stats_call_header = [x for x in pitcher_stats_call['people'][0]['stats'][0]['splits'][-1]['stat']]
1089
  pitcher_stats_call_values = [pitcher_stats_call['people'][0]['stats'][0]['splits'][-1]['stat'][x] for x in pitcher_stats_call['people'][0]['stats'][0]['splits'][-1]['stat']]
 
1103
  if df['game_id'][0] == df['game_id'][-1]:
1104
  pitcher_stats_call_df_small = pitcher_stats_call_df.select(['inningsPitched', 'battersFaced', 'earnedRuns', 'hits', 'strikeOuts', 'baseOnBalls', 'hitByPitch', 'homeRuns', 'strikePercentage', 'whiffs'])
1105
  new_column_names = ['$\\bf{IP}$', '$\\bf{PA}$', '$\\bf{ER}$', '$\\bf{H}$', '$\\bf{K}$', '$\\bf{BB}$', '$\\bf{HBP}$', '$\\bf{HR}$', '$\\bf{Strike\%}$', '$\\bf{Whiffs}$']
1106
+ title = f'{df["game_date"][0]} vs {df["batter_team"][0]}'
1107
+ elif sport_id != 1:
1108
  pitcher_stats_call_df_small = pitcher_stats_call_df.select(['inningsPitched', 'battersFaced', 'whip', 'era', 'fip', 'k_percent', 'bb_percent', 'k_bb_percent', 'strikePercentage'])
1109
  new_column_names = ['$\\bf{IP}$', '$\\bf{PA}$', '$\\bf{WHIP}$', '$\\bf{ERA}$', '$\\bf{FIP}$', '$\\bf{K\%}$', '$\\bf{BB\%}$', '$\\bf{K-BB\%}$', '$\\bf{Strike\%}$']
1110
+ title = f'{df["game_date"][0]} to {df["game_date"][-1]}'
1111
  else:
1112
  fangraphs_table(df=df, ax=ax, player_input=player_input, season=int(df['game_date'][0][0:4]), split=split)
1113
  return
functions/statcast_2024_grouped.csv CHANGED
@@ -1,19 +1,19 @@
1
- pitch_type,pitch,release_speed,pfx_z,pfx_x,release_spin_rate,release_pos_x,release_pos_z,release_extension,delta_run_exp,swing,whiff,in_zone,out_zone,chase,xwoba,xwobacon,pitch_usage,whiff_rate,in_zone_rate,chase_rate,delta_run_exp_per_100,all
2
- CH,74155,85.46226726,5.247514143,-3.974501168,1803.342541,-0.507762986,5.740925968,6.449406057,204.631,37385,11538,28912,45151,15250,0.289735649,0.341580895,0.102188463,0.308626454,0.389886049,0.337755531,-0.275950374,
3
- CS,22,66.38181818,-7.232727273,5.176363636,2039.272727,-1.798181818,6.517727273,6.063636364,-0.629,9,2,10,12,2,0.134666667,0.1945,3.03E-05,0.222222222,0.454545455,0.166666667,2.859090909,
4
- CU,47579,79.40938533,-9.345106446,4.516206279,2568.859105,-0.676571206,5.943843838,6.401792909,93.572,19910,6150,20751,26738,7749,0.280497676,0.36671832,0.065565706,0.308890005,0.436137792,0.289812252,-0.196666597,
5
- EP,576,50.51909722,16.35729167,-3.82875,1256.715278,-0.966875,6.647100694,4.442013889,23.643,252,7,207,369,106,0.39714307,0.361505495,0.00079375,0.027777778,0.359375,0.287262873,-4.1046875,
6
- FA,635,67.81354331,15.86551181,-3.722645669,1674.014469,-1.116377953,6.317716535,4.92488189,15.495,284,29,296,339,73,0.43393491,0.388761905,0.000875055,0.102112676,0.466141732,0.215339233,-2.44015748,
7
- FC,58379,89.56435814,8.088953962,1.55092437,2389.231716,-0.974536268,5.8461769,6.403954997,-20.39,28753,6674,30002,28189,7757,0.340778229,0.370073593,0.080448524,0.23211491,0.513917676,0.275178261,0.034926943,
8
- FF,230412,94.27369496,15.72027483,-3.107441897,2296.59179,-0.768543293,5.821400777,6.524392111,-80.284,113157,24741,127386,102722,24808,0.340125691,0.388167436,0.317516664,0.218643124,0.55286183,0.241506201,0.034843671,
9
- FO,168,82.07916667,1.735714286,0.137857143,946.8154762,-0.533333333,5.891428571,6.666666667,2.539,89,29,60,108,43,0.277987474,0.3952,0.000231511,0.325842697,0.357142857,0.398148148,-1.511309524,
10
- FS,21727,86.31228886,2.979608782,-8.765506513,1302.399298,-1.464082478,5.742066553,6.508958525,-16.641,11333,3906,7982,13745,4946,0.254878506,0.344396607,0.029940648,0.344657196,0.367376996,0.359839942,0.076591338,
11
- KC,11916,81.79965592,-9.370896274,4.89529708,2444.16428,-0.878808325,5.940037764,6.434007554,-12.997,5312,1860,4858,7058,2316,0.258451373,0.364636161,0.01642071,0.350150602,0.407687143,0.328138283,0.109071836,
12
- KN,971,76.94819773,-2.945375901,-5.356498455,263.5632699,-1.230339856,5.542131823,6.45653965,12.681,426,113,428,543,130,0.287038918,0.369510345,0.001338076,0.265258216,0.440782698,0.239410681,-1.305973223,
13
- PO,55,91.24909091,13.11709091,-6.399272727,2195.381818,-1.494181818,5.861272727,6.305454545,0,0,0,1,54,0,,,7.58E-05,,0.018181818,0,0,
14
- SC,159,81.02264151,-3.105660377,-8.001509434,2050.597484,-1.053584906,6.110377358,6.064150943,4.623,58,13,63,96,20,0.353494636,0.413142857,0.000219108,0.224137931,0.396226415,0.208333333,-2.90754717,
15
- SI,116002,93.34805382,7.567078832,-6.14847607,2147.36315,-0.767198351,5.622119363,6.435364206,-32.837,53318,7390,65492,50222,12474,0.350196742,0.364144629,0.159855251,0.138602348,0.564576473,0.248377205,0.028307271,
16
- SL,116390,85.60138786,1.57598588,2.732511063,2435.570552,-0.981103401,5.761407576,6.433055359,-167.415,56606,19101,52478,63672,20396,0.281860701,0.357665208,0.16038993,0.337437727,0.45088066,0.320329187,0.143839677,
17
- ST,43821,81.85801556,1.479693298,7.821825152,2575.366192,-1.080187125,5.460724082,6.403526748,-52.968,20035,6276,19349,24472,7531,0.259780708,0.337221732,0.060387036,0.313251809,0.441546291,0.307739457,0.120873554,
18
- SV,2702,81.67483346,-4.788941525,7.356861584,2470.624859,-0.577957069,5.420762398,6.227296393,0.193,1117,339,1138,1564,479,0.290768371,0.374640553,0.003723461,0.303491495,0.421169504,0.306265985,-0.007142857,
19
- All,725669,89.15210527,7.058379139,-1.214008754,2255.676825,-0.828252978,5.758824349,6.456550519,-20.178,352163,89742,359413,365054,104080,0.314703752,0.366398,1,0.25483086,0.49528504,0.285108504,0.002780607,all
 
1
+ pitch_type,pitch,release_speed,pfx_z,pfx_x,release_spin_rate,release_pos_x,release_pos_z,release_extension,delta_run_exp,swing,whiff,in_zone,out_zone,chase,xwoba,pitch_usage,whiff_rate,in_zone_rate,chase_rate,delta_run_exp_per_100,all
2
+ CH,74155,85.46226725895522,5.247514143364433,-3.9745011679246045,1803.342540762527,-0.5077629855663421,5.740925968432281,6.449406057002311,204.631,37385,11538,28912,45151,15250,0.28973564881286695,0.10218846333521206,0.30862645446034503,0.38988604949093114,0.3377555314389493,-0.27595037421616886,
3
+ CS,22,66.38181818181819,-7.232727272727273,5.176363636363637,2039.2727272727273,-1.7981818181818183,6.5177272727272735,6.0636363636363635,-0.6290000000000001,9,2,10,12,2,0.13466666666666668,3.0316852449257168e-05,0.2222222222222222,0.45454545454545453,0.16666666666666666,2.85909090909091,
4
+ CU,47579,79.40938533133989,-9.345106445703216,4.516206279348902,2568.8591051473077,-0.6765712059634863,5.9438438375202685,6.401792908519479,93.57199999999999,19910,6150,20751,26738,7749,0.28049767649520974,0.0655657055765094,0.3088900050226017,0.4361377918829736,0.28981225222529733,-0.1966665966077471,
5
+ EP,576,50.51909722222222,16.357291666666665,-3.8287500000000003,1256.7152777777778,-0.9668749999999999,6.647100694444444,4.442013888888889,23.643,252,7,207,369,106,0.3971430703517588,0.0007937503186714604,0.027777777777777776,0.359375,0.2872628726287263,-4.104687500000001,
6
+ FA,635,67.81354330708662,15.865511811023623,-3.7226456692913388,1674.0144694533763,-1.1163779527559055,6.317716535433071,4.92488188976378,15.495,284,29,296,339,73,0.43393490999999995,0.0008750546047853774,0.10211267605633803,0.46614173228346456,0.2153392330383481,-2.4401574803149604,
7
+ FC,58379,89.56435813713696,8.08895396195288,1.5509243697478992,2389.231715947733,-0.9745362684951281,5.8461769002079365,6.403954996645393,-20.390000000000015,28753,6674,30002,28189,7757,0.34077822947428493,0.08044852405159929,0.23211490974854798,0.5139176758765994,0.2751782610238036,0.034926942907552404,
8
+ FF,230412,94.27369496062718,15.720274827472318,-3.1074418968484365,2296.591789895323,-0.7685432927147252,5.821400777026439,6.524392110813926,-80.28400000000002,113157,24741,127386,102722,24808,0.3401256910065045,0.3175166639335565,0.21864312415493517,0.5528618301130149,0.2415062012032476,0.03484367133656234,
9
+ FO,168,82.07916666666667,1.7357142857142858,0.1378571428571428,946.8154761904761,-0.5333333333333333,5.8914285714285715,6.666666666666667,2.539,89,29,60,108,43,0.27798747368421056,0.0002315105096125093,0.3258426966292135,0.35714285714285715,0.39814814814814814,-1.511309523809524,
10
+ FS,21727,86.31228885718231,2.979608781700189,-8.76550651263405,1302.3992981808108,-1.4640824780227366,5.742066553136651,6.508958525345622,-16.641000000000005,11333,3906,7982,13745,4946,0.2548785060302361,0.02994064787113684,0.34465719579987647,0.3673769963639711,0.3598399417970171,0.07659133796658538,
11
+ KC,11916,81.79965592480698,-9.370896273917422,4.895297079556898,2444.1642796967144,-0.8788083249412554,5.940037764350453,6.434007553503986,-12.997000000000003,5312,1860,4858,7058,2316,0.25845137325418993,0.016420709717515837,0.3501506024096386,0.40768714333669015,0.32813828279965995,0.10907183618663985,
12
+ KN,971,76.94819773429454,-2.9453759011328526,-5.356498455200824,263.56326987681973,-1.2303398558187437,5.542131822863028,6.45653964984552,12.681,426,113,428,543,130,0.2870389181034483,0.0013380756240103959,0.2652582159624413,0.4407826982492276,0.23941068139963168,-1.3059732234809474,
13
+ PO,55,91.24909090909091,13.11709090909091,-6.399272727272727,2195.3818181818183,-1.494181818181818,5.861272727272727,6.305454545454546,0.0,0,0,1,54,0,,7.579213112314292e-05,,0.01818181818181818,0.0,-0.0,
14
+ SC,159,81.02264150943397,-3.1056603773584905,-8.001509433962264,2050.5974842767296,-1.0535849056603774,6.110377358490566,6.064150943396227,4.623,58,13,63,96,20,0.35349463636363637,0.0002191081608832677,0.22413793103448276,0.39622641509433965,0.20833333333333334,-2.9075471698113207,
15
+ SI,116002,93.34805382235511,7.567078832293412,-6.148476070311284,2147.3631502060834,-0.7671983511070397,5.622119363257688,6.435364206296976,-32.837000000000025,53318,7390,65492,50222,12474,0.3501967420378125,0.15985525080994228,0.13860234817510034,0.5645764728194341,0.2483772052088726,0.028307270564300636,
16
+ SL,116390,85.60138786052518,1.5759858803271631,2.7325110632802407,2435.5705519351436,-0.9811034007748601,5.761407576409815,6.433055359327349,-167.41500000000002,56606,19101,52478,63672,20396,0.2818607008786495,0.16038992984404735,0.337437727449387,0.45088065985050263,0.3203291870838045,0.14383967694819144,
17
+ ST,43821,81.8580155633144,1.4796932977339632,7.821825152324228,2575.3661920073496,-1.080187124894457,5.4607240820611125,6.40352674793587,-52.96800000000001,20035,6276,19349,24472,7531,0.25978070794500324,0.0603870359626772,0.3132518093336661,0.44154629059126904,0.30773945733899966,0.12087355377558708,
18
+ SV,2702,81.67483345669874,-4.788941524796447,7.356861584011844,2470.624858757062,-0.5779570688378979,5.420762398223538,6.227296392711045,0.19299999999999926,1117,339,1138,1564,479,0.2907683709923664,0.0037234606962678577,0.3034914950760967,0.42116950407105846,0.3062659846547315,-0.007142857142857115,
19
+ All,725669,89.1521052747817,7.058379139422499,-1.2140087540219224,2255.6768252515376,-0.8282529777063689,5.758824349487279,6.456550518555369,-20.178000000000118,352163,89742,359413,365054,104080,0.3147037524825,1.0,0.25483085957354973,0.4952850404247667,0.28510850449522535,0.002780606585095976,all
output.docx ADDED
Binary file (36.7 kB). View file
 
pitcher_data_sample.csv ADDED
The diff for this file is too large to render. See raw diff
 
pitching_summary_api.ipynb ADDED
The diff for this file is too large to render. See raw diff