nesticot commited on
Commit
05458eb
·
verified ·
1 Parent(s): 0af912b

Upload 9 files

Browse files
app.py CHANGED
@@ -1,857 +1,676 @@
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,2025]
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
- import requests
109
-
110
- import os
111
- CAMPAIGN_ID = os.getenv("CAMPAIGN_ID")
112
- ACCESS_TOKEN = os.getenv("ACCESS_TOKEN")
113
- BACKUP_PW = os.getenv("BACKUP_PW")
114
- ADMIN_PW = os.getenv("ADMIN_PW")
115
-
116
- url = f"https://www.patreon.com/api/oauth2/v2/campaigns/{CAMPAIGN_ID}/members"
117
-
118
- headers = {
119
- "Authorization": f"Bearer {ACCESS_TOKEN}"
120
- }
121
-
122
- # Simple parameters, requesting the member's email and currently entitled tiers
123
- params = {
124
- "fields[member]": "full_name,email", # Request the member's email
125
- "include": "currently_entitled_tiers", # Include the currently entitled tiers
126
- "page[size]": 1000 # Fetch up to 1000 patrons per request
127
- }
128
-
129
- response = requests.get(url, headers=headers, params=params)
130
-
131
-
132
- VALID_PASSWORDS = []
133
- if response.status_code == 200:
134
- data = response.json()
135
- for patron in data['data']:
136
- try:
137
- tiers = patron['relationships']['currently_entitled_tiers']['data']
138
- if any(tier['id'] == '9078921' for tier in tiers):
139
- full_name = patron['attributes']['email']
140
- VALID_PASSWORDS.append(full_name)
141
- except KeyError:
142
- continue
143
- VALID_PASSWORDS.append(BACKUP_PW)
144
- VALID_PASSWORDS.append(ADMIN_PW)
145
-
146
- from shiny import App, reactive, ui, render
147
- from shiny.ui import h2, tags
148
-
149
- from datetime import datetime
150
-
151
- def is_valid_date(date_str):
152
- try:
153
- datetime.strptime(date_str, "%Y-%m-%d") # Attempt to parse the date
154
- return True
155
- except ValueError:
156
- return False # If parsing fails, it's not in the correct format
157
-
158
- # Define the login UI
159
- login_ui = ui.page_fluid(
160
- ui.card(
161
- ui.h2([
162
- "TJStats Pitching Summary App ",
163
- ui.tags.a("(@TJStats)", href="https://twitter.com/TJStats", target="_blank")
164
- ]),
165
- ui.p(
166
- "This App is available to Superstar Patrons. Please enter your Patreon email address in the box below. If you're having trouble, please refer to the ",
167
- ui.tags.a("Patreon post", href="https://www.patreon.com/posts/116064432", target="_blank"),
168
- "."
169
- ),
170
- ui.input_password("password", "Enter Patreon Email (or Password from Link):", width="50%"),
171
- ui.tags.input(
172
- type="checkbox",
173
- id="authenticated",
174
- value=False,
175
- disabled=True
176
- ),
177
- ui.input_action_button("login", "Login", class_="btn-primary"),
178
- ui.output_text("login_message"),
179
- )
180
- )
181
-
182
-
183
- # Define the UI layout for the app
184
- main_ui = ui.page_sidebar(
185
- ui.sidebar(
186
- # Row for selecting season and level
187
- ui.row(
188
- ui.column(4, ui.input_select('year_input', 'Select Season', year_list, selected=2025)),
189
- ui.column(4, ui.input_select('level_input', 'Select Level', level_dict)),
190
- ui.column(4, ui.input_select('type_input', 'Select Type', type_dict,selected='R'))
191
- ),
192
- # Row for the action button to get player list
193
- ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
194
- # Row for selecting the player
195
- ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
196
- # Row for selecting the date range
197
- ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))),
198
-
199
- # Rows for selecting plots and split options
200
- ui.row(
201
- ui.column(4, ui.input_select('plot_id_1', 'Plot Left', function_dict, multiple=False, selected='velocity_kdes')),
202
- ui.column(4, ui.input_select('plot_id_2', 'Plot Middle', function_dict, multiple=False, selected='tj_stuff_roling')),
203
- ui.column(4, ui.input_select('plot_id_3', 'Plot Right', function_dict, multiple=False, selected='break_plot'))
204
- ),
205
- ui.row(
206
- ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
207
- ui.column(6, ui.input_numeric('rolling_window', 'Rolling Window (for tjStuff+ Plot)', min=1, value=50))
208
- ),
209
- ui.row(
210
- ui.column(6, ui.input_switch("switch", "Custom Team?", False)),
211
- ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False))
212
- ),
213
-
214
- # Row for the action button to generate plot
215
- ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
216
- width="400px" # Added this parameter to control sidebar width
217
- ),
218
-
219
- # Main content area with tabs (placed directly in page_sidebar)
220
- ui.navset_tab(
221
- ui.nav_panel("Pitching Summary",
222
- ui.output_text("status"),
223
- ui.output_plot('plot', width='2100px', height='2100px')
224
- ),
225
- ui.nav_panel("Game Summary",
226
- ui.output_text("status2"),
227
- ui.output_plot('game_plot', width='2100px', height='2100px')
228
- ),
229
- ui.nav_panel("Table Range",
230
- ui.output_data_frame("grid")),
231
- ui.nav_panel("Table Game",
232
- ui.output_data_frame("grid_game")),
233
- id="tabset"
234
- )
235
- )
236
-
237
- # Combined UI with conditional panel
238
- app_ui = ui.page_fluid(
239
- ui.tags.head(
240
- ui.tags.script(src="script.js")
241
- ),
242
-
243
- ui.panel_conditional(
244
- "!input.authenticated",
245
- login_ui
246
- ),
247
- ui.panel_conditional(
248
- "input.authenticated",
249
- main_ui
250
- )
251
- )
252
-
253
-
254
- def server(input, output, session):
255
-
256
- @reactive.Effect
257
- @reactive.event(input.login)
258
- def check_password():
259
- if input.password() in VALID_PASSWORDS:
260
- ui.update_checkbox("authenticated", value=True)
261
- ui.update_text("login_message", value="")
262
- else:
263
- ui.update_text("login_message", value="Invalid password!")
264
- ui.update_text("password", value="")
265
-
266
- @output
267
- @render.text
268
- def login_message():
269
- return ""
270
-
271
- @reactive.calc
272
- @reactive.event(input.pitcher_id, input.date_id,input.split_id)
273
- def cached_data():
274
-
275
- year_input = int(input.year_input())
276
- sport_id = int(input.level_input())
277
- player_input = int(input.pitcher_id())
278
-
279
-
280
- start_date = str(input.date_id()[0])
281
- end_date = str(input.date_id()[1])
282
- game_list = scrape.get_player_games_list(sport_id = sport_id,
283
- season = year_input,
284
- player_id = player_input,
285
- start_date = start_date,
286
- end_date = end_date,
287
- game_type = [input.type_input()])
288
-
289
- # if input.tabset() == 'Game Summary':
290
- # print(year_input, sport_id, player_input, 'yup')
291
- # print(input.date_id())
292
- # game_list = [input.date_id()]
293
-
294
-
295
- data_list = scrape.get_data(game_list_input = game_list[:])
296
-
297
- try:
298
- df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
299
- (pl.col("pitcher_id") == player_input)&
300
- (pl.col("is_pitch") == True)&
301
- (pl.col("start_speed") >= 50)&
302
- (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
303
-
304
- )))).with_columns(
305
- pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
306
- ))
307
- return df
308
-
309
-
310
- except TypeError:
311
- print("NONE")
312
- return None
313
-
314
- @reactive.calc
315
- @reactive.event(input.pitcher_id, input.date_id,input.split_id,input.tabset)
316
- def cached_data_daily():
317
-
318
- year_input = int(input.year_input())
319
- sport_id = int(input.level_input())
320
- player_input = int(input.pitcher_id())
321
-
322
-
323
- # start_date = str(input.date_id()[0])
324
- # end_date = str(input.date_id()[1])
325
- game_list = [int(input.date_id())]
326
- print(game_list)
327
-
328
- # if input.tabset() == 'Game Summary':
329
- # print(year_input, sport_id, player_input, 'yup')
330
- # print(input.date_id())
331
- # game_list =
332
-
333
-
334
- data_list = scrape.get_data(game_list_input = game_list[:])
335
-
336
- try:
337
- df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
338
- (pl.col("pitcher_id") == player_input)&
339
- (pl.col("is_pitch") == True)&
340
- (pl.col("start_speed") >= 50)&
341
- (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
342
-
343
- )))).with_columns(
344
- pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
345
- ))
346
- return df
347
-
348
-
349
- except TypeError:
350
- print("NONE")
351
- return None
352
-
353
-
354
- @render.ui
355
- @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,input.tabset,ignore_none=False)
356
- def player_select_ui():
357
- # Get the list of pitchers for the selected level and season
358
- df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input()), game_type = [input.type_input()]).filter(
359
- (pl.col("position").is_in(['P','TWP']))|
360
- (pl.col("player_id").is_in([686846]))
361
-
362
- ).sort("name")
363
-
364
- # Create a dictionary of pitcher IDs and names
365
- pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name']))
366
-
367
-
368
-
369
-
370
- # Return a select input for choosing a pitcher
371
- return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True)
372
-
373
- @render.ui
374
- @reactive.event(input.player_button, input.pitcher_id,input.year_input, input.level_input, input.type_input,input.tabset,ignore_none=False)
375
- def date_id():
376
- if input.tabset() == 'Pitching Summary' or input.tabset() == 'Table Range':
377
- # Create a date range input for selecting the date range within the selected year
378
- return ui.input_date_range("date_id", "Select Date Range",
379
- start=f"{int(input.year_input())}-01-01",
380
- end=f"{int(input.year_input())}-12-31",
381
- min=f"{int(input.year_input())}-01-01",
382
- max=f"{int(input.year_input())}-12-31")
383
-
384
- if input.tabset() == 'Game Summary' or input.tabset() == 'Table Game':
385
- year_input = int(input.year_input())
386
- sport_id = int(input.level_input())
387
- player_input = int(input.pitcher_id())
388
- print('game summary')
389
- # start_date = str(input.date_id()[0])
390
- # end_date = str(input.date_id()[1])
391
-
392
- game_list = scrape.get_player_games_list(player_id = player_input,
393
- season = year_input,
394
- sport_id=sport_id,
395
- game_type=[input.type_input()],
396
- pitching = True)
397
-
398
- schedule_df = scrape.get_schedule(year_input=[year_input],
399
- sport_id= [sport_id],
400
- game_type = [input.type_input()])
401
-
402
- player_schedule_df = schedule_df.filter(pl.col('game_id').is_in(game_list)).to_pandas().sort_values('date')
403
-
404
- player_schedule_df['def'] = player_schedule_df['date'].astype(str) + ' - ' + player_schedule_df['away'] + ' @ ' + player_schedule_df['home'] + ' '
405
-
406
-
407
- game_dict = dict(zip(player_schedule_df['game_id'], player_schedule_df['def']))
408
- # print(game_dict)
409
-
410
- return ui.input_select("date_id", "Select Game", game_dict)
411
-
412
- @output
413
- @render.text
414
- def status():
415
- # Only show status when generating
416
- if input.generate == 0:
417
- return ""
418
- return ""
419
-
420
-
421
-
422
-
423
- @output
424
- @render.plot
425
- @reactive.event(input.generate_plot, ignore_none=False)
426
- def plot():
427
- # Show progress/loading notification
428
-
429
-
430
-
431
- with ui.Progress(min=0, max=1) as p:
432
- p.set(message="Generating plot", detail="This may take a while...")
433
-
434
-
435
- p.set(0.3, "Gathering data...")
436
- year_input = int(input.year_input())
437
- sport_id = int(input.level_input())
438
- player_input = int(input.pitcher_id())
439
- start_date = str(input.date_id()[0])
440
- end_date = str(input.date_id()[1])
441
- if not is_valid_date(start_date):
442
- fig = plt.figure(figsize=(26,26))
443
- fig.text(x=0.1,y=0.9,s='Select Date Range and Generate Plot',fontsize=36,ha='left')
444
- return fig
445
- print(year_input, sport_id, player_input, start_date, end_date)
446
-
447
- df = cached_data()
448
-
449
- if df is None:
450
- fig = plt.figure(figsize=(26,26))
451
- fig.text(x=0.1,y=0.9,s='No Statcast Data For This Pitcher',fontsize=36,ha='left')
452
- return fig
453
-
454
- df = df.clone()
455
-
456
-
457
- p.set(0.6, "Creating plot...")
458
-
459
-
460
- #plt.rcParams["figure.figsize"] = [10,10]
461
- fig = plt.figure(figsize=(26,26))
462
- plt.rcParams.update({'figure.autolayout': True})
463
- fig.set_facecolor('white')
464
- sns.set_theme(style="whitegrid", palette=colour_palette)
465
- print('this is the one plot')
466
-
467
- gs = gridspec.GridSpec(6, 8,
468
- height_ratios=[6,20,12,36,36,6],
469
- width_ratios=[4,18,18,18,18,18,18,4])
470
-
471
-
472
- gs.update(hspace=0.2, wspace=0.5)
473
-
474
- # Define the positions of each subplot in the grid
475
- ax_headshot = fig.add_subplot(gs[1,1:3])
476
- ax_bio = fig.add_subplot(gs[1,3:5])
477
- ax_logo = fig.add_subplot(gs[1,5:7])
478
-
479
- ax_season_table = fig.add_subplot(gs[2,1:7])
480
-
481
- ax_plot_1 = fig.add_subplot(gs[3,1:3])
482
- ax_plot_2 = fig.add_subplot(gs[3,3:5])
483
- ax_plot_3 = fig.add_subplot(gs[3,5:7])
484
-
485
- ax_table = fig.add_subplot(gs[4,1:7])
486
-
487
- ax_footer = fig.add_subplot(gs[-1,1:7])
488
- ax_header = fig.add_subplot(gs[0,1:7])
489
- ax_left = fig.add_subplot(gs[:,0])
490
- ax_right = fig.add_subplot(gs[:,-1])
491
-
492
- # Hide axes for footer, header, left, and right
493
- ax_footer.axis('off')
494
- ax_header.axis('off')
495
- ax_left.axis('off')
496
- ax_right.axis('off')
497
-
498
- sns.set_theme(style="whitegrid", palette=colour_palette)
499
- fig.set_facecolor('white')
500
-
501
- df_teams = scrape.get_teams()
502
-
503
- player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
504
- player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
505
-
506
- if input.switch():
507
-
508
- # Get the logo URL from the image dictionary using the team abbreviation
509
- logo_url = input.logo_select()
510
-
511
- # Send a GET request to the logo URL
512
- response = requests.get(logo_url)
513
-
514
- # Open the image from the response content
515
- img = Image.open(BytesIO(response.content))
516
-
517
- # Display the image on the axis
518
- ax_logo.set_xlim(0, 1.3)
519
- ax_logo.set_ylim(0, 1)
520
- ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')
521
-
522
- # Turn off the axis
523
- ax_logo.axis('off')
524
-
525
- else:
526
- plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
527
-
528
- stat_summary_table(df=df,
529
- ax=ax_season_table,
530
- player_input=player_input,
531
- split=input.split_id(),
532
- sport_id=sport_id,
533
- game_type=[input.type_input()])
534
-
535
- # break_plot(df=df_plot,ax=ax2)
536
- 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]):
537
- if x == 'velocity_kdes':
538
- velocity_kdes(df,
539
- ax=y,
540
- gs=gs,
541
- gs_x=[3,4],
542
- gs_y=[z,z+2],
543
- fig=fig)
544
- if x == 'tj_stuff_roling':
545
- tj_stuff_roling(df=df,
546
- window=int(input.rolling_window()),
547
- ax=y)
548
-
549
- if x == 'tj_stuff_roling_game':
550
- tj_stuff_roling_game(df=df,
551
- window=int(input.rolling_window()),
552
- ax=y)
553
-
554
- if x == 'break_plot':
555
- break_plot(df = df,ax=y)
556
-
557
- if x == 'location_plot_lhb':
558
- location_plot(df = df,ax=y,hand='L')
559
-
560
- if x == 'location_plot_rhb':
561
- location_plot(df = df,ax=y,hand='R')
562
-
563
- summary_table(df=df,
564
- ax=ax_table)
565
-
566
- plot_footer(ax_footer)
567
-
568
- ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1)
569
- # Hide axes ticks and labels
570
- ax_watermark.set_xticks([])
571
- ax_watermark.set_yticks([])
572
- ax_watermark.set_frame_on(False) # Optional: Hide border
573
-
574
- img = Image.open('tj stats circle-01_new.jpg')
575
- # Display the image
576
- ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.1)
577
-
578
-
579
- ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1)
580
- ax_watermark2.set_xlim(0,1)
581
- ax_watermark2.set_ylim(0,1)
582
- # Hide axes ticks and labels
583
- ax_watermark2.set_xticks([])
584
- ax_watermark2.set_yticks([])
585
- ax_watermark2.set_frame_on(False) # Optional: Hide border
586
-
587
- # Open the image
588
- img = Image.open('tj stats circle-01_new.jpg')
589
- # Get the original size
590
- width, height = img.size
591
- # Calculate the new size (50% larger)
592
- new_width = int(width * 0.5)
593
- new_height = int(height * 0.5)
594
- # Resize the image
595
- img_resized = img.resize((new_width, new_height))
596
- # Display the image
597
- ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1)
598
-
599
- fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
600
-
601
-
602
-
603
-
604
- @output
605
- @render.plot
606
- @reactive.event(input.generate_plot, ignore_none=False)
607
- def game_plot():
608
- # Show progress/loading notification
609
- with ui.Progress(min=0, max=1) as p:
610
- print(input.date_id(),'TEST')
611
-
612
- if isinstance(input.date_id(), tuple):
613
- fig = plt.figure(figsize=(26,26))
614
- fig.text(x=0.1,y=0.9,s='Select Game and Generate Plot',fontsize=36,ha='left')
615
- return fig
616
-
617
-
618
- p.set(message="Generating plot", detail="This may take a while...")
619
-
620
-
621
- p.set(0.3, "Gathering data...")
622
- year_input = int(input.year_input())
623
- sport_id = int(input.level_input())
624
- player_input = int(input.pitcher_id())
625
-
626
- # print(input.game_id())
627
- # print(year_input, sport_id, player_input)
628
-
629
- # print(year_input, sport_id, player_input, start_date, end_date)
630
-
631
- df = cached_data_daily()
632
-
633
- # start_date = str(df['game_date'][0])
634
- # end_date = str(df['game_date'][0])
635
-
636
- if df is None:
637
- fig = plt.figure(figsize=(26,26))
638
- fig.text(x=0.1,y=0.9,s='No Statcast Data For This Pitcher',fontsize=36,ha='left')
639
- return fig
640
-
641
- df = df.clone()
642
-
643
-
644
- p.set(0.6, "Creating plot...")
645
-
646
-
647
- #plt.rcParams["figure.figsize"] = [10,10]
648
- fig = plt.figure(figsize=(26,26))
649
- plt.rcParams.update({'figure.autolayout': True})
650
- fig.set_facecolor('white')
651
- sns.set_theme(style="whitegrid", palette=colour_palette)
652
- print('this is the one plot')
653
-
654
- gs = gridspec.GridSpec(6, 8,
655
- height_ratios=[6,20,12,36,36,6],
656
- width_ratios=[4,18,18,18,18,18,18,4])
657
-
658
-
659
- gs.update(hspace=0.2, wspace=0.5)
660
-
661
- # Define the positions of each subplot in the grid
662
- ax_headshot = fig.add_subplot(gs[1,1:3])
663
- ax_bio = fig.add_subplot(gs[1,3:5])
664
- ax_logo = fig.add_subplot(gs[1,5:7])
665
-
666
- ax_season_table = fig.add_subplot(gs[2,1:7])
667
-
668
- ax_plot_1 = fig.add_subplot(gs[3,1:3])
669
- ax_plot_2 = fig.add_subplot(gs[3,3:5])
670
- ax_plot_3 = fig.add_subplot(gs[3,5:7])
671
-
672
- ax_table = fig.add_subplot(gs[4,1:7])
673
-
674
- ax_footer = fig.add_subplot(gs[-1,1:7])
675
- ax_header = fig.add_subplot(gs[0,1:7])
676
- ax_left = fig.add_subplot(gs[:,0])
677
- ax_right = fig.add_subplot(gs[:,-1])
678
-
679
- # Hide axes for footer, header, left, and right
680
- ax_footer.axis('off')
681
- ax_header.axis('off')
682
- ax_left.axis('off')
683
- ax_right.axis('off')
684
-
685
- sns.set_theme(style="whitegrid", palette=colour_palette)
686
- fig.set_facecolor('white')
687
-
688
- df_teams = scrape.get_teams()
689
-
690
- player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
691
- player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
692
-
693
- if input.switch():
694
-
695
- # Get the logo URL from the image dictionary using the team abbreviation
696
- logo_url = input.logo_select()
697
-
698
- # Send a GET request to the logo URL
699
- response = requests.get(logo_url)
700
-
701
- # Open the image from the response content
702
- img = Image.open(BytesIO(response.content))
703
-
704
- # Display the image on the axis
705
- ax_logo.set_xlim(0, 1.3)
706
- ax_logo.set_ylim(0, 1)
707
- ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')
708
-
709
- # Turn off the axis
710
- ax_logo.axis('off')
711
-
712
- else:
713
- plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
714
-
715
- stat_summary_table(df=df,
716
- ax=ax_season_table,
717
- player_input=player_input,
718
- split=input.split_id(),
719
- sport_id=sport_id,
720
- game_type=[input.type_input()])
721
-
722
- # break_plot(df=df_plot,ax=ax2)
723
- 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]):
724
- if x == 'velocity_kdes':
725
- velocity_kdes(df,
726
- ax=y,
727
- gs=gs,
728
- gs_x=[3,4],
729
- gs_y=[z,z+2],
730
- fig=fig)
731
- if x == 'tj_stuff_roling':
732
- tj_stuff_roling(df=df,
733
- window=int(input.rolling_window()),
734
- ax=y)
735
-
736
- if x == 'tj_stuff_roling_game':
737
- tj_stuff_roling_game(df=df,
738
- window=int(input.rolling_window()),
739
- ax=y)
740
-
741
- if x == 'break_plot':
742
- break_plot(df = df,ax=y)
743
-
744
- if x == 'location_plot_lhb':
745
- location_plot(df = df,ax=y,hand='L')
746
-
747
- if x == 'location_plot_rhb':
748
- location_plot(df = df,ax=y,hand='R')
749
-
750
- summary_table(df=df,
751
- ax=ax_table)
752
-
753
- plot_footer(ax_footer)
754
-
755
- ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1)
756
- # Hide axes ticks and labels
757
- ax_watermark.set_xticks([])
758
- ax_watermark.set_yticks([])
759
- ax_watermark.set_frame_on(False) # Optional: Hide border
760
-
761
- img = Image.open('tj stats circle-01_new.jpg')
762
- # Display the image
763
- ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.1)
764
-
765
-
766
- ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1)
767
- ax_watermark2.set_xlim(0,1)
768
- ax_watermark2.set_ylim(0,1)
769
- # Hide axes ticks and labels
770
- ax_watermark2.set_xticks([])
771
- ax_watermark2.set_yticks([])
772
- ax_watermark2.set_frame_on(False) # Optional: Hide border
773
-
774
- # Open the image
775
- img = Image.open('tj stats circle-01_new.jpg')
776
- # Get the original size
777
- width, height = img.size
778
- # Calculate the new size (50% larger)
779
- new_width = int(width * 0.5)
780
- new_height = int(height * 0.5)
781
- # Resize the image
782
- img_resized = img.resize((new_width, new_height))
783
- # Display the image
784
- ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1)
785
-
786
- fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
787
-
788
-
789
-
790
- @output
791
- @render.data_frame
792
- @reactive.event(input.generate_plot, ignore_none=False)
793
- def grid():
794
-
795
- start_date = str(input.date_id()[0])
796
- if not is_valid_date(start_date):
797
- return pd.DataFrame({"Message": ["Select range to generate table"]})
798
- df = cached_data()
799
- df = df.clone()
800
- features_table = ['start_speed',
801
- 'spin_rate',
802
- 'extension',
803
- 'ivb',
804
- 'hb',
805
- 'x0',
806
- 'z0']
807
-
808
-
809
-
810
- selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand',
811
- 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus']
812
-
813
-
814
-
815
- return render.DataGrid(
816
- df.select(selection).to_pandas().round(1),
817
- row_selection_mode='multiple',
818
- height='700px',
819
- width='fit-content',
820
- filters=True,
821
- )
822
-
823
-
824
- @output
825
- @render.data_frame
826
- @reactive.event(input.generate_plot, ignore_none=False)
827
- def grid_game():
828
- if isinstance(input.date_id(), tuple):
829
- return pd.DataFrame({"Message": ["Select game to generate table"]})
830
-
831
- df = cached_data_daily()
832
- df = df.clone()
833
- features_table = ['start_speed',
834
- 'spin_rate',
835
- 'extension',
836
- 'ivb',
837
- 'hb',
838
- 'x0',
839
- 'z0']
840
-
841
-
842
-
843
- selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand',
844
- 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus']
845
-
846
-
847
-
848
- return render.DataGrid(
849
- df.select(selection).to_pandas().round(1),
850
- row_selection_mode='multiple',
851
- height='700px',
852
- width='fit-content',
853
- filters=True,
854
- )
855
-
856
-
857
- 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
+ import requests
10
+ import joblib
11
+ from matplotlib.gridspec import GridSpec
12
+ from shiny import App, reactive, ui, render
13
+ from shiny.ui import h2, tags
14
+ import matplotlib.pyplot as plt
15
+ import matplotlib.gridspec as gridspec
16
+ import seaborn as sns
17
+ from functions.pitch_summary_functions import *
18
+ from functions.df_update import *
19
+ from shiny import App, reactive, ui, render
20
+ from shiny.ui import h2, tags
21
+ from functions.heat_map_functions import *
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,2025]
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
+ format_dict = {
65
+ 'pitch_percent': '{:.1%}',
66
+ 'pitches': '{:.0f}',
67
+ 'heart_zone_percent': '{:.1%}',
68
+ 'shadow_zone_percent': '{:.1%}',
69
+ 'chase_zone_percent': '{:.1%}',
70
+ 'waste_zone_percent': '{:.1%}',
71
+ 'csw_percent': '{:.1%}',
72
+ 'whiff_rate': '{:.1%}',
73
+ 'zone_whiff_percent': '{:.1%}',
74
+ 'chase_percent': '{:.1%}',
75
+ 'bip': '{:.0f}',
76
+ 'xwoba_percent_contact': '{:.3f}'
77
+ }
78
+
79
+ format_dict = {
80
+ 'pitch_percent': '{:.1%}',
81
+ 'pitches': '{:.0f}',
82
+ 'heart_zone_percent': '{:.1%}',
83
+ 'shadow_zone_percent': '{:.1%}',
84
+ 'chase_zone_percent': '{:.1%}',
85
+ 'waste_zone_percent': '{:.1%}',
86
+ 'csw_percent': '{:.1%}',
87
+ 'whiff_rate': '{:.1%}',
88
+ 'zone_whiff_percent': '{:.1%}',
89
+ 'chase_percent': '{:.1%}',
90
+ 'bip': '{:.0f}',
91
+ 'xwoba_percent_contact': '{:.3f}'
92
+ }
93
+ label_translation_dict = {
94
+ 'pitch_percent': 'Pitch%',
95
+ 'pitches': 'Pitches',
96
+ 'heart_zone_percent': 'Heart%',
97
+ 'shadow_zone_percent': 'Shado%',
98
+ 'chase_zone_percent': 'Chas%',
99
+ 'waste_zone_percent': 'Waste%',
100
+ 'csw_percent': 'CSW%',
101
+ 'whiff_rate': 'Whiff%',
102
+ 'zone_whiff_percent': 'Z-Whiff%',
103
+ 'chase_percent': 'O-Swing%',
104
+ 'bip': 'BBE',
105
+ 'xwoba_percent_contact': 'xwOBACON'
106
+ }
107
+
108
+ cmap_sum22 = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFB000',])
109
+ cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFFFFF','#FFB000',])
110
+ cmap_sum2 = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#FFFFFF','#FFB000','#FE6100'])
111
+ cmap_sum_r = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#FFB000','#FFFFFF','#648FFF',])
112
+
113
+
114
+ import requests
115
+
116
+ import os
117
+ CAMPAIGN_ID = os.getenv("CAMPAIGN_ID")
118
+ ACCESS_TOKEN = os.getenv("ACCESS_TOKEN")
119
+ BACKUP_PW = os.getenv("BACKUP_PW")
120
+ ADMIN_PW = os.getenv("ADMIN_PW")
121
+
122
+ url = f"https://www.patreon.com/api/oauth2/v2/campaigns/{CAMPAIGN_ID}/members"
123
+
124
+ headers = {
125
+ "Authorization": f"Bearer {ACCESS_TOKEN}"
126
+ }
127
+
128
+ # Simple parameters, requesting the member's email and currently entitled tiers
129
+ params = {
130
+ "fields[member]": "full_name,email", # Request the member's email
131
+ "include": "currently_entitled_tiers", # Include the currently entitled tiers
132
+ "page[size]": 1000 # Fetch up to 1000 patrons per request
133
+ }
134
+
135
+ response = requests.get(url, headers=headers, params=params)
136
+
137
+
138
+ VALID_PASSWORDS = []
139
+ if response.status_code == 200:
140
+ data = response.json()
141
+ for patron in data['data']:
142
+ try:
143
+ tiers = patron['relationships']['currently_entitled_tiers']['data']
144
+ if any(tier['id'] == '9078921' for tier in tiers):
145
+ full_name = patron['attributes']['email']
146
+ VALID_PASSWORDS.append(full_name)
147
+ except KeyError:
148
+ continue
149
+ VALID_PASSWORDS.append(BACKUP_PW)
150
+ VALID_PASSWORDS.append(ADMIN_PW)
151
+ # VALID_PASSWORDS.append('')
152
+
153
+ from shiny import App, reactive, ui, render
154
+ from shiny.ui import h2, tags
155
+
156
+ # Define the login UI
157
+ login_ui = ui.page_fluid(
158
+ ui.card(
159
+ ui.h2([
160
+ "TJStats Pitching Heat Maps App ",
161
+ ui.tags.a("(@TJStats)", href="https://twitter.com/TJStats", target="_blank")
162
+ ]),
163
+ ui.p(
164
+ "This App is available to Superstar Patrons. Please enter your Patreon email address in the box below. If you're having trouble, please refer to the ",
165
+ ui.tags.a("Patreon post", href="https://www.patreon.com/posts/117909954", target="_blank"),
166
+ "."
167
+ ),
168
+ ui.input_password("password", "Enter Patreon Email (or Password from Link):", width="25%"),
169
+ ui.tags.input(
170
+ type="checkbox",
171
+ id="authenticated",
172
+ value=False,
173
+ disabled=True
174
+ ),
175
+ ui.input_action_button("login", "Login", class_="btn-primary"),
176
+ ui.output_text("login_message"),
177
+ )
178
+ )
179
+
180
+
181
+ main_ui = ui.page_sidebar(
182
+ ui.sidebar(
183
+ # Row for selecting season and level
184
+ ui.row(
185
+ ui.column(4, ui.input_select('year_input', 'Select Season', year_list, selected=2024)),
186
+ ui.column(4, ui.input_select('level_input', 'Select Level', level_dict)),
187
+ ui.column(4, ui.input_select('type_input', 'Select Type', type_dict,selected='R'))
188
+ ),
189
+ # Row for the action button to get player list
190
+ ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
191
+ # Row for selecting the player
192
+ ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
193
+
194
+
195
+ ui.row(ui.input_action_button("get_pitches", "Get Pitch Types", class_="btn-secondary")),
196
+
197
+
198
+ # Rows for selecting plots and split options
199
+ ui.row(ui.column(12, ui.output_ui('pitch_type_ui', 'Select Pitch Type'))),
200
+ ui.row(ui.column(12, ui.input_select('plot_type', 'Select Plot', ['Pitch%','Whiff%','xwOBACON']))),
201
+ ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))),
202
+
203
+ # Row for the action button to generate plot
204
+ ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
205
+ width="400px" # Added this parameter to control sidebar width
206
+ ),
207
+
208
+ # Main content (former panel_main content)
209
+ ui.navset_tab(
210
+ # Tab for game summary plot
211
+ ui.nav("Pitching Summary",
212
+ ui.output_text("status"),
213
+ ui.output_plot('plot', width='1440px', height=f'{900/1600*1440}px')
214
+ ),
215
+ )
216
+ )
217
+
218
+
219
+ # Combined UI with conditional panel
220
+ app_ui = ui.page_fluid(
221
+ ui.tags.head(
222
+ ui.tags.script(src="script.js")
223
+ ),
224
+
225
+ ui.panel_conditional(
226
+ "!input.authenticated",
227
+ login_ui
228
+ ),
229
+ ui.panel_conditional(
230
+ "input.authenticated",
231
+ main_ui
232
+ )
233
+ )
234
+
235
+
236
+ def server(input, output, session):
237
+
238
+ @reactive.Effect
239
+ @reactive.event(input.login)
240
+ def check_password():
241
+ if input.password() in VALID_PASSWORDS:
242
+ ui.update_checkbox("authenticated", value=True)
243
+ ui.update_text("login_message", value="")
244
+ else:
245
+ ui.update_text("login_message", value="Invalid password!")
246
+ ui.update_text("password", value="")
247
+
248
+ @output
249
+ @render.text
250
+ def login_message():
251
+ return ""
252
+
253
+ # Instead of using @reactive.calc with @reactive.event
254
+ cached_data_value = reactive.value(None) # Initialize with None
255
+
256
+ @reactive.calc
257
+ @reactive.event(input.date_id,input.pitcher_id)
258
+ def cached_data():
259
+
260
+ if not hasattr(input, 'pitcher_id') or input.pitcher_id() is None or not hasattr(input, 'date_id') or input.date_id() is None:
261
+ return # Exit early if required inputs aren't ready
262
+ year_input = int(input.year_input())
263
+ sport_id = int(input.level_input())
264
+ player_input = int(input.pitcher_id())
265
+ start_date = str(input.date_id()[0])
266
+ end_date = str(input.date_id()[1])
267
+ # Simulate an expensive data operation
268
+ game_list = scrape.get_player_games_list(sport_id = sport_id,
269
+ season = year_input,
270
+ player_id = player_input,
271
+ start_date = start_date,
272
+ end_date = end_date,
273
+ game_type = [input.type_input()])
274
+
275
+ data_list = scrape.get_data(game_list_input = game_list[:])
276
+ df = (update.update(scrape.get_data_df(data_list = data_list).filter(
277
+ (pl.col("pitcher_id") == player_input)&
278
+ (pl.col("is_pitch") == True)
279
+
280
+
281
+ ))).with_columns(
282
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
283
+ )
284
+ return df
285
+
286
+
287
+ @render.ui
288
+ @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,ignore_none=False)
289
+ def player_select_ui():
290
+ # Get the list of pitchers for the selected level and season
291
+ df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input()), game_type = [input.type_input()]).filter(
292
+ pl.col("position").is_in(['P','TWP'])).sort("name")
293
+
294
+ # Create a dictionary of pitcher IDs and names
295
+ pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name']))
296
+
297
+ # Return a select input for choosing a pitcher
298
+ return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True)
299
+
300
+ is_loading = reactive.value(False)
301
+ data_result = reactive.value(None)
302
+
303
+ @reactive.effect
304
+ @reactive.event(input.get_pitches)
305
+ def load_data():
306
+ is_loading.set(True)
307
+ data_result.set(None) # Clear any previous data
308
+ try:
309
+ # This will fetch the data
310
+ result = cached_data()
311
+ data_result.set(result)
312
+ except Exception as e:
313
+ # Handle any errors
314
+ print(f"Error loading data: {e}")
315
+ finally:
316
+ is_loading.set(False)
317
+
318
+ @output
319
+ @render.ui
320
+ def pitch_type_ui():
321
+ # Make sure to add dependencies on both values
322
+ input.get_pitches()
323
+ loading = is_loading()
324
+ data = data_result()
325
+
326
+ # If loading, show spinner
327
+ if loading:
328
+ return ui.div(
329
+ ui.span("Loading pitch types... ", class_="me-2"),
330
+ ui.tags.div(class_="spinner-border spinner-border-sm text-primary"),
331
+ style="padding: 10px; background-color: #f8f9fa; border-radius: 5px;"
332
+ )
333
+
334
+ # If data is loaded, show dropdown
335
+ elif data is not None:
336
+ df = data
337
+ df = df.clone() if hasattr(df, 'clone') else df.copy()
338
+ pitch_dict = dict(zip(df['pitch_type'], df['pitch_description']))
339
+ return ui.input_select(
340
+ "pitch_type_input",
341
+ "Select Pitch Type",
342
+ pitch_dict,
343
+ selectize=True
344
+ )
345
+
346
+ # Initial state or after reset
347
+ else:
348
+ return ui.div(
349
+ ui.p("Click 'Get Pitch Types' to load the dropdown.", class_="text-muted"),
350
+ style="text-align: center; padding: 10px;"
351
+ ) # Empty div with instructions
352
+ @render.ui
353
+ @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,ignore_none=False)
354
+ def date_id():
355
+ # Create a date range input for selecting the date range within the selected year
356
+ return ui.input_date_range("date_id", "Select Date Range",
357
+ start=f"{int(input.year_input())}-01-01",
358
+ end=f"{int(input.year_input())}-12-31",
359
+ min=f"{int(input.year_input())}-01-01",
360
+ max=f"{int(input.year_input())}-12-31")
361
+
362
+
363
+
364
+ @output
365
+ @render.text
366
+ def status():
367
+ # Only show status when generating
368
+ if input.generate == 0:
369
+ return ""
370
+ return ""
371
+
372
+ @output
373
+ @render.plot
374
+ @reactive.event(input.generate_plot, ignore_none=False)
375
+ def plot():
376
+ # Show progress/loading notification
377
+ with ui.Progress(min=0, max=1) as p:
378
+ p.set(message="Generating plot", detail="This may take a while...")
379
+
380
+
381
+ p.set(0.3, "Gathering data...")
382
+ year_input = int(input.year_input())
383
+ sport_id = int(input.level_input())
384
+ player_input = int(input.pitcher_id())
385
+ start_date = str(input.date_id()[0])
386
+ end_date = str(input.date_id()[1])
387
+
388
+
389
+ print(year_input, sport_id, player_input, start_date, end_date)
390
+
391
+ df = cached_data()
392
+ df = df.clone()
393
+
394
+ pitch_input = input.pitch_type_input()
395
+
396
+ df_plot = pitch_heat_map(pitch_input, df)
397
+ pivot_table_l = pitch_prop(df=df_plot, hand = 'L')
398
+ pivot_table_r = pitch_prop(df=df_plot, hand = 'R')
399
+
400
+
401
+ table_left = df_update().update_summary_select(df=df_plot.filter(pl.col('batter_hand') == 'L'), selection=['pitcher_hand'])
402
+ table_left = table_left.with_columns(
403
+ (pl.col('pitches')/len(df.filter(pl.col('batter_hand') == 'L'))).alias('pitch_percent')
404
+ )
405
+
406
+ table_right = df_update().update_summary_select(df=df_plot.filter(pl.col('batter_hand') == 'R'), selection=['pitcher_hand'])
407
+ table_right = table_right.with_columns(
408
+ (pl.col('pitches')/len(df.filter(pl.col('batter_hand') == 'R'))).alias('pitch_percent')
409
+ )
410
+ try:
411
+ normalize = mcolors.Normalize(vmin=table_left['pitch_percent']*0.5,
412
+ vmax=table_left['pitch_percent']*1.5) # Define the range of values
413
+
414
+
415
+ df_colour_left = pd.DataFrame(data=[[get_color(x,normalize,cmap_sum2) for x in pivot_table_l[0]],
416
+ [get_color(x,normalize,cmap_sum2) for x in pivot_table_l[1]],
417
+ [get_color(x,normalize,cmap_sum2) for x in pivot_table_l[2]]])
418
+ df_colour_left[0] = '#ffffff'
419
+ except ValueError:
420
+ normalize = mcolors.Normalize(vmin=0,
421
+ vmax=1) # Define the range of values
422
+ df_colour_left = pd.DataFrame(data=[['#ffffff','#ffffff','#ffffff','#ffffff'],
423
+ ['#ffffff','#ffffff','#ffffff','#ffffff'],
424
+ ['#ffffff','#ffffff','#ffffff','#ffffff']])
425
+
426
+ try:
427
+ normalize = mcolors.Normalize(vmin=table_right['pitch_percent']*0.5,
428
+ vmax=table_right['pitch_percent']*1.5) # Define the range of values
429
+
430
+
431
+ df_colour_right = pd.DataFrame(data=[[get_color(x,normalize,cmap_sum2) for x in pivot_table_r[0]],
432
+ [get_color(x,normalize,cmap_sum2) for x in pivot_table_r[1]],
433
+ [get_color(x,normalize,cmap_sum2) for x in pivot_table_r[2]]])
434
+ df_colour_right[0] = '#ffffff'
435
+
436
+ except ValueError:
437
+ normalize = mcolors.Normalize(vmin=0,
438
+ vmax=1) # Define the range of values
439
+ df_colour_right = pd.DataFrame(data=[['#ffffff','#ffffff','#ffffff','#ffffff'],
440
+ ['#ffffff','#ffffff','#ffffff','#ffffff'],
441
+ ['#ffffff','#ffffff','#ffffff','#ffffff']])
442
+
443
+ table_left = table_left.select(
444
+ 'pitch_percent',
445
+ 'pitches',
446
+ 'heart_zone_percent',
447
+ 'shadow_zone_percent',
448
+ 'chase_zone_percent',
449
+ 'waste_zone_percent',
450
+ 'csw_percent',
451
+ 'whiff_rate',
452
+ 'zone_whiff_percent',
453
+ 'chase_percent',
454
+ 'bip',
455
+ 'xwoba_percent_contact').to_pandas().T
456
+
457
+ table_right = table_right.select(
458
+ 'pitch_percent',
459
+ 'pitches',
460
+ 'heart_zone_percent',
461
+ 'shadow_zone_percent',
462
+ 'chase_zone_percent',
463
+ 'waste_zone_percent',
464
+ 'csw_percent',
465
+ 'whiff_rate',
466
+ 'zone_whiff_percent',
467
+ 'chase_percent',
468
+ 'bip',
469
+ 'xwoba_percent_contact').to_pandas().T
470
+
471
+ table_right = table_right.replace({'nan%':'—'})
472
+ table_right = table_right.replace({'nan':'—'})
473
+
474
+
475
+
476
+
477
+
478
+ p.set(0.6, "Creating plot...")
479
+
480
+ import matplotlib.pyplot as plt
481
+ fig = plt.figure(figsize=(16, 9))
482
+ fig.set_facecolor('white')
483
+ sns.set_theme(style="whitegrid", palette=colour_palette)
484
+ gs = GridSpec(3, 5, height_ratios=[2,9,1],width_ratios=[1,9,1,9,1])
485
+ gs.update(hspace=0.2, wspace=0.3)
486
+
487
+ # Add subplots to the grid
488
+ ax_header = fig.add_subplot(gs[0, :])
489
+ ax_left = fig.add_subplot(gs[1, 1])
490
+ ax_right = fig.add_subplot(gs[1, 3])
491
+
492
+ axfooter = fig.add_subplot(gs[-1, :])
493
+
494
+
495
+ if input.plot_type() == 'Pitch%':
496
+ heat_map_plot(df=df_plot,
497
+ ax=ax_left,
498
+ cmap=cmap_sum2,
499
+ hand='L')
500
+
501
+ heat_map_plot(df=df_plot,
502
+ ax=ax_right,
503
+ cmap=cmap_sum2,
504
+ hand='R')
505
+
506
+
507
+ if input.plot_type() == 'Whiff%':
508
+ heat_map_plot_hex_whiff(df=df_plot,
509
+ ax=ax_left,
510
+ cmap=cmap_sum,
511
+ hand='L')
512
+
513
+ heat_map_plot_hex_whiff(df=df_plot,
514
+ ax=ax_right,
515
+ cmap=cmap_sum,
516
+ hand='R')
517
+
518
+ if input.plot_type() == 'xwOBACON':
519
+ heat_map_plot_hex_damage(df=df_plot,
520
+ ax=ax_left,
521
+ cmap=cmap_sum,
522
+ hand='L')
523
+
524
+ heat_map_plot_hex_damage(df=df_plot,
525
+ ax=ax_right,
526
+ cmap=cmap_sum,
527
+ hand='R')
528
+
529
+
530
+ # Load the image
531
+ img = mpimg.imread('images/left.png')
532
+ imagebox = OffsetImage(img, zoom=0.58) # adjust zoom as needed
533
+ ab = AnnotationBbox(imagebox, (1.25, -0.5), box_alignment=(0, 0), frameon=False)
534
+ ax_left.add_artist(ab)
535
+
536
+
537
+ # Load the image
538
+ img = mpimg.imread('images/right.png')
539
+ imagebox = OffsetImage(img, zoom=0.58) # adjust zoom as needed
540
+ # Create an AnnotationBbox
541
+ ab = AnnotationBbox(imagebox, (-1.25, -0.5), box_alignment=(1, 0), frameon=False)
542
+
543
+ ax_right.add_artist(ab)
544
+
545
+
546
+ table_plot(ax=ax_left,
547
+ table=table_left,
548
+ hand='L')
549
+
550
+ table_plot_pivot(ax=ax_left,
551
+ pivot_table=pivot_table_l,
552
+ df_colour=df_colour_left)
553
+
554
+
555
+ table_plot(ax=ax_right,
556
+ table=table_right,
557
+ hand='R')
558
+
559
+ table_plot_pivot(ax=ax_right,
560
+ pivot_table=pivot_table_r,
561
+ df_colour=df_colour_right)
562
+
563
+
564
+ from matplotlib.cm import ScalarMappable
565
+ from matplotlib.colors import Normalize
566
+ # Create a ScalarMappable with the same colormap and normalization
567
+ if input.plot_type() == 'Pitch%':
568
+ sm = ScalarMappable(cmap=cmap_sum2, norm=Normalize(vmin=0, vmax=1))
569
+
570
+ cbar = fig.colorbar(sm, ax=axfooter, orientation='horizontal',aspect=100)
571
+ cbar.set_ticks([])
572
+
573
+ cbar.set_ticks([sm.norm.vmin, sm.norm.vmax])
574
+
575
+ cbar.ax.set_xticklabels(['Least', 'Most'])
576
+ cbar.ax.tick_params(labeltop=True, labelbottom=False, labelsize=14)
577
+ labels = cbar.ax.get_xticklabels()
578
+
579
+ labels[0].set_horizontalalignment('left')
580
+ labels[-1].set_horizontalalignment('right')
581
+ labels = cbar.ax.get_xticklabels()
582
+
583
+
584
+ cbar.ax.set_xticklabels(labels)
585
+ cbar.ax.tick_params(length=0)
586
+
587
+ if input.plot_type() == 'Whiff%':
588
+ sm = ScalarMappable(cmap=cmap_sum, norm=Normalize(vmin=0.15, vmax=0.35))
589
+
590
+ cbar = fig.colorbar(sm, ax=axfooter, orientation='horizontal',aspect=100)
591
+ cbar.set_ticks([])
592
+
593
+ cbar.set_ticks([sm.norm.vmin, sm.norm.vmax])
594
+
595
+ cbar.ax.set_xticklabels(['15%', '35%'])
596
+ cbar.ax.tick_params(labeltop=True, labelbottom=False, labelsize=14)
597
+ labels = cbar.ax.get_xticklabels()
598
+
599
+ labels[0].set_horizontalalignment('left')
600
+ labels[-1].set_horizontalalignment('right')
601
+ labels = cbar.ax.get_xticklabels()
602
+
603
+
604
+ cbar.ax.set_xticklabels(labels)
605
+ cbar.ax.tick_params(length=0)
606
+
607
+
608
+ if input.plot_type() == 'xwOBACON':
609
+ sm = ScalarMappable(cmap=cmap_sum_r, norm=Normalize(vmin=0.25, vmax=0.5))
610
+
611
+ cbar = fig.colorbar(sm, ax=axfooter, orientation='horizontal',aspect=100)
612
+ cbar.set_ticks([])
613
+
614
+ cbar.set_ticks([sm.norm.vmin, sm.norm.vmax])
615
+
616
+ cbar.ax.set_xticklabels(['.000', '.500'])
617
+ cbar.ax.tick_params(labeltop=True, labelbottom=False, labelsize=14)
618
+ labels = cbar.ax.get_xticklabels()
619
+
620
+ labels[0].set_horizontalalignment('left')
621
+ labels[-1].set_horizontalalignment('right')
622
+ labels = cbar.ax.get_xticklabels()
623
+
624
+
625
+ cbar.ax.set_xticklabels(labels)
626
+ cbar.ax.tick_params(length=0)
627
+
628
+
629
+ axfooter.text(x=0.02,y=1,s='By: Thomas Nestico\n @TJStats',fontname='Calibri',ha='left',fontsize=14,va='top')
630
+ axfooter.text(x=1-0.02,y=1,s='Data: MLB',ha='right',fontname='Calibri',fontsize=14,va='top')
631
+
632
+ axfooter.axis('off')
633
+
634
+ # Display the image on the axis
635
+ ax_header.set_xlim(-12,12)
636
+ ax_header.set_ylim(0, 2)
637
+
638
+
639
+ if input.plot_type() == 'Pitch%':
640
+ ax_header.text(x=0,y=2,s=f"{df_plot['pitcher_name'][0]} - {df_plot['pitcher_hand'][0]}HP\n{df_plot['pitch_description'][0]} Pitch Frequency",ha='center',fontsize=24,va='top')
641
+ if input.plot_type() == 'Whiff%':
642
+ ax_header.text(x=0,y=2,s=f"{df_plot['pitcher_name'][0]} - {df_plot['pitcher_hand'][0]}HP\n{df_plot['pitch_description'][0]} Whiff%",ha='center',fontsize=24,va='top')
643
+ if input.plot_type() == 'xwOBACON':
644
+ ax_header.text(x=0,y=2,s=f"{df_plot['pitcher_name'][0]} - {df_plot['pitcher_hand'][0]}HP\n{df_plot['pitch_description'][0]} xwOBACON",ha='center',fontsize=24,va='top')
645
+
646
+ ax_header.text(x=0,y=0.7,s=f"{year_input} {level_dict[str(sport_id)]} Season",ha='center',fontsize=16,va='top')
647
+ ax_header.text(x=0,y=0.3,s=f"{df_plot['game_date'][0]} to {df_plot['game_date'][-1]}",ha='center',fontsize=16,va='top',fontstyle='italic')
648
+
649
+ ax_header.axis('off')
650
+
651
+
652
+ import urllib
653
+ import urllib.request
654
+ import urllib.error
655
+ from urllib.error import HTTPError
656
+
657
+
658
+ plot_header(pitcher_id=player_input,
659
+ ax=ax_header,
660
+ df_team=scrape.get_teams(),
661
+ df_players=scrape.get_players(sport_id,year_input),
662
+ sport_id=sport_id,)
663
+
664
+
665
+
666
+
667
+
668
+
669
+ fig.subplots_adjust(left=0.03, right=0.97, top=0.97, bottom=0.03)
670
+
671
+
672
+
673
+
674
+ app = App(app_ui, server)
675
+
676
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
functions/__pycache__/app.cpython-39.pyc ADDED
Binary file (10.5 kB). View file
 
functions/__pycache__/df_update.cpython-39.pyc CHANGED
Binary files a/functions/__pycache__/df_update.cpython-39.pyc and b/functions/__pycache__/df_update.cpython-39.pyc differ
 
functions/__pycache__/heat_map_functions.cpython-39.pyc ADDED
Binary file (15.5 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/df_update.py CHANGED
@@ -323,6 +323,7 @@ class df_update:
323
  (pl.col('hard_hit') / pl.col('bip_div')).alias('hard_hit_percent'),
324
  (pl.col('barrel') / pl.col('bip_div')).alias('barrel_percent'),
325
  (pl.col('zone_contact') / pl.col('zone_swing')).alias('zone_contact_percent'),
 
326
  (pl.col('zone_swing') / pl.col('in_zone')).alias('zone_swing_percent'),
327
  (pl.col('in_zone') / pl.col('pitches')).alias('zone_percent'),
328
  (pl.col('ozone_swing') / (pl.col('pitches') - pl.col('in_zone'))).alias('chase_percent'),
@@ -442,6 +443,7 @@ class df_update:
442
  (pl.col('hard_hit') / pl.col('bip_div')).alias('hard_hit_percent'),
443
  (pl.col('barrel') / pl.col('bip_div')).alias('barrel_percent'),
444
  (pl.col('zone_contact') / pl.col('zone_swing')).alias('zone_contact_percent'),
 
445
  (pl.col('zone_swing') / pl.col('in_zone')).alias('zone_swing_percent'),
446
  (pl.col('in_zone') / pl.col('pitches')).alias('zone_percent'),
447
  (pl.col('ozone_swing') / (pl.col('pitches') - pl.col('in_zone'))).alias('chase_percent'),
 
323
  (pl.col('hard_hit') / pl.col('bip_div')).alias('hard_hit_percent'),
324
  (pl.col('barrel') / pl.col('bip_div')).alias('barrel_percent'),
325
  (pl.col('zone_contact') / pl.col('zone_swing')).alias('zone_contact_percent'),
326
+ (1 - (pl.col('zone_contact') / pl.col('zone_swing'))).alias('zone_whiff_percent'),
327
  (pl.col('zone_swing') / pl.col('in_zone')).alias('zone_swing_percent'),
328
  (pl.col('in_zone') / pl.col('pitches')).alias('zone_percent'),
329
  (pl.col('ozone_swing') / (pl.col('pitches') - pl.col('in_zone'))).alias('chase_percent'),
 
443
  (pl.col('hard_hit') / pl.col('bip_div')).alias('hard_hit_percent'),
444
  (pl.col('barrel') / pl.col('bip_div')).alias('barrel_percent'),
445
  (pl.col('zone_contact') / pl.col('zone_swing')).alias('zone_contact_percent'),
446
+ (1 - (pl.col('zone_contact') / pl.col('zone_swing'))).alias('zone_whiff_percent'),
447
  (pl.col('zone_swing') / pl.col('in_zone')).alias('zone_swing_percent'),
448
  (pl.col('in_zone') / pl.col('pitches')).alias('zone_percent'),
449
  (pl.col('ozone_swing') / (pl.col('pitches') - pl.col('in_zone'))).alias('chase_percent'),
functions/heat_map_functions.py ADDED
@@ -0,0 +1,582 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ import json
4
+ from matplotlib.ticker import FuncFormatter
5
+ from matplotlib.ticker import MaxNLocator
6
+ import math
7
+ from matplotlib.patches import Ellipse
8
+ import matplotlib.transforms as transforms
9
+ import matplotlib.colors
10
+ import matplotlib.colors as mcolors
11
+ import seaborn as sns
12
+ import matplotlib.pyplot as plt
13
+ import requests
14
+ import polars as pl
15
+ from PIL import Image
16
+ import requests
17
+ from io import BytesIO
18
+ from matplotlib.offsetbox import OffsetImage, AnnotationBbox
19
+ import matplotlib.pyplot as plt
20
+ import matplotlib.gridspec as gridspec
21
+ import PIL
22
+ from matplotlib.transforms import Bbox
23
+ import matplotlib.image as mpimg
24
+ from scipy.stats import gaussian_kde
25
+ from statsmodels.nonparametric.kernel_regression import KernelReg
26
+
27
+
28
+ format_dict = {
29
+ 'pitch_percent': '{:.1%}',
30
+ 'pitches': '{:.0f}',
31
+ 'heart_zone_percent': '{:.1%}',
32
+ 'shadow_zone_percent': '{:.1%}',
33
+ 'chase_zone_percent': '{:.1%}',
34
+ 'waste_zone_percent': '{:.1%}',
35
+ 'csw_percent': '{:.1%}',
36
+ 'whiff_rate': '{:.1%}',
37
+ 'zone_whiff_percent': '{:.1%}',
38
+ 'chase_percent': '{:.1%}',
39
+ 'bip': '{:.0f}',
40
+ 'xwoba_percent_contact': '{:.3f}'
41
+ }
42
+ label_translation_dict = {
43
+ 'pitch_percent': 'Pitch%',
44
+ 'pitches': 'Pitches',
45
+ 'heart_zone_percent': 'Heart%',
46
+ 'shadow_zone_percent': 'Shadow%',
47
+ 'chase_zone_percent': 'Chase%',
48
+ 'waste_zone_percent': 'Waste%',
49
+ 'csw_percent': 'CSW%',
50
+ 'whiff_rate': 'Whiff%',
51
+ 'zone_whiff_percent': 'Z-Whiff%',
52
+ 'chase_percent': 'O-Swing%',
53
+ 'bip': 'BBE',
54
+ 'xwoba_percent_contact': 'xwOBACON'
55
+ }
56
+
57
+ def pitch_heat_map(pitch_input, df):
58
+
59
+ df = df.with_columns([
60
+ pl.col('pitcher_id').count().over(['batter_hand', 'strikes', 'balls']).alias('h_s_b'),
61
+ pl.col('pitcher_id').count().over(['batter_hand', 'strikes', 'balls', 'pitch_type']).alias('h_s_b_pitch')
62
+ ])
63
+
64
+ df = df.with_columns([
65
+ (pl.col('h_s_b_pitch') / pl.col('h_s_b')).alias('h_s_b_pitch_percent')
66
+ ])
67
+
68
+ df_plot = df.filter(pl.col('pitch_type') == pitch_input)
69
+
70
+ return df_plot
71
+
72
+ def pitch_prop(df: pl.DataFrame, hand: str = 'R') -> pd.DataFrame:
73
+ df_plot_pd = df.to_pandas()
74
+ pivot_table = (df_plot_pd[df_plot_pd['batter_hand'].isin([hand])]
75
+ .groupby(['batter_hand','strikes', 'balls'])[['h_s_b_pitch_percent']]
76
+ .mean()
77
+ .reset_index()
78
+ .pivot(index='strikes',columns='balls',values='h_s_b_pitch_percent'))
79
+ # Create a new index and columns range
80
+ new_index = range(3)
81
+ new_columns = range(4)
82
+
83
+ # Reindex the pivot table
84
+ pivot_table = pivot_table.reindex(index=new_index, columns=new_columns)
85
+
86
+ # Fill any missing values with 0
87
+ pivot_table = pivot_table.fillna(0)
88
+ df_hand = pl.DataFrame(pivot_table.reset_index())
89
+ return df_hand
90
+
91
+
92
+
93
+ # DEFINE STRIKE ZONE
94
+ strike_zone = pd.DataFrame({
95
+ 'PlateLocSide': [-0.9, -0.9, 0.9, 0.9, -0.9],
96
+ 'PlateLocHeight': [1.5, 3.5, 3.5, 1.5, 1.5]
97
+ })
98
+
99
+ ### STRIKE ZONE ###
100
+ def draw_line(axis, alpha_spot=1, catcher_p=True):
101
+ # Ensure strike_zone columns are NumPy arrays
102
+ plate_side = strike_zone['PlateLocSide'].to_numpy()
103
+ plate_height = strike_zone['PlateLocHeight'].to_numpy()
104
+
105
+ # Plot the strike zone
106
+ axis.plot(plate_side, plate_height, color='black', linewidth=1.3, zorder=3, alpha=alpha_spot)
107
+
108
+ if catcher_p:
109
+ # Add dashed lines and home plate for catcher perspective
110
+ axis.plot([-0.708, 0.708], [0.15, 0.15], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
111
+ axis.plot([-0.708, -0.708], [0.15, 0.3], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
112
+ axis.plot([-0.708, 0], [0.3, 0.5], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
113
+ axis.plot([0, 0.708], [0.5, 0.3], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
114
+ axis.plot([0.708, 0.708], [0.3, 0.15], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
115
+ else:
116
+ # Add dashed lines and home plate for other perspective
117
+ axis.plot([-0.708, 0.708], [0.4, 0.4], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
118
+ axis.plot([-0.708, -0.9], [0.4, -0.1], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
119
+ axis.plot([-0.9, 0], [-0.1, -0.35], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
120
+ axis.plot([0, 0.9], [-0.35, -0.1], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
121
+ axis.plot([0.9, 0.708], [-0.1, 0.4], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
122
+
123
+ def heat_map_plot(df:pl.DataFrame,
124
+ ax:plt.Axes,
125
+ cmap:matplotlib.colors.LinearSegmentedColormap,
126
+ hand:str):
127
+ if df.filter(pl.col('batter_hand')==hand).shape[0] > 3:
128
+ sns.kdeplot(data=df.filter(pl.col('batter_hand')==hand),
129
+ x='px',
130
+ y='pz',
131
+ cmap=cmap,
132
+ shade=True,
133
+ ax=ax,
134
+ thresh=0.3,
135
+ bw_adjust=1)
136
+ elif df.filter(pl.col('batter_hand')==hand).shape[0] > 0:
137
+ sns.scatterplot(data=df.filter(pl.col('batter_hand')==hand),
138
+ x='px',
139
+ y='pz',
140
+ cmap=cmap,
141
+ ax=ax)
142
+
143
+
144
+ draw_line(ax,alpha_spot=1,catcher_p = False)
145
+
146
+
147
+ ax.axis('off')
148
+ ax.axis('square')
149
+ ax.set_xlim(-2.75,2.75)
150
+ ax.set_ylim(-0.5,5)
151
+
152
+ def format_as_percentage(val):
153
+ return f'{val * 100:.0f}%'
154
+
155
+
156
+ def table_plot(ax:plt.Axes,
157
+ table:pl.DataFrame,
158
+ hand='R'):
159
+
160
+ # Create a transformation that converts from data coordinates to axes coordinates
161
+ trans = ax.transData + ax.transAxes.inverted()
162
+
163
+ if hand == 'R':
164
+ bbox_data = Bbox.from_bounds(1.7, -0.5, 2.5, 5)
165
+
166
+ else:
167
+ bbox_data = Bbox.from_bounds(-4.2, -0.5, 2.5, 5) # replace width and height with the desired values
168
+
169
+
170
+
171
+ bbox_axes = trans.transform_bbox(bbox_data)
172
+
173
+ if hand == 'R':
174
+ ax.text(s='Against RHH',x=2.95,y=4.65,fontsize=18,fontweight='bold',ha='center')
175
+ else:
176
+ ax.text(s='Against LHH',x=-2.95,y=4.65,fontsize=18,fontweight='bold',ha='center')
177
+
178
+
179
+ table = table.apply(lambda x: format_dict[x.name].format(x[0]) if x[0] != '—' else '—', axis=1)
180
+ table.index = [label_translation_dict[x] for x in table.index]
181
+
182
+
183
+
184
+ table_plot = ax.table(cellText=table.reset_index().values,
185
+ loc='right',
186
+ cellLoc='center',
187
+ colWidths=[0.52,0.3],
188
+ bbox=bbox_axes.bounds,zorder=100)
189
+
190
+
191
+ min_font_size = 14
192
+ # Set table properties
193
+ table_plot.auto_set_font_size(False)
194
+ #table.set_fontsize(min(min_font_size,max(min_font_size/((len(label_labels)/4)),10)))
195
+ table_plot.set_fontsize(min_font_size)
196
+ #table_left_plot.scale(1,3)
197
+ # Calculate the bbox in axes coordinates
198
+ bbox_data = Bbox.from_bounds(-1.25, 5, 2.5, 1) # replace width and height with the desired values
199
+ bbox_axes = trans.transform_bbox(bbox_data)
200
+
201
+
202
+
203
+ def table_plot_pivot(ax:plt.Axes,
204
+ pivot_table:pl.DataFrame,
205
+ df_colour:pd.DataFrame):
206
+
207
+
208
+ trans = ax.transData + ax.transAxes.inverted()
209
+ bbox_data = Bbox.from_bounds(-0.75, 5, 2.5, 1) # replace width and height with the desired values
210
+ bbox_axes = trans.transform_bbox(bbox_data)
211
+
212
+ table_plot_pivot = ax.table(cellText=[[format_as_percentage(val) for val in row] for row in pivot_table.select(pivot_table.columns[-4:]).to_numpy()],
213
+ colLabels =pivot_table.columns[-4:],
214
+ rowLabels =[' 0 ',' 1 ',' 2 '],
215
+ loc='center',
216
+ cellLoc='center',
217
+ colWidths=[0.3,0.3,0.30,0.3],
218
+ bbox=bbox_axes.bounds,zorder=100,
219
+ cellColours = df_colour[df_colour.columns[-4:]].values)
220
+
221
+
222
+ min_font_size = 11
223
+ # Set table properties
224
+ table_plot_pivot.auto_set_font_size(False)
225
+ #table.set_fontsize(min(min_font_size,max(min_font_size/((len(label_labels)/4)),10)))
226
+ table_plot_pivot.set_fontsize(min_font_size)
227
+
228
+
229
+ ax.text(x=-2.0, y=5.08, s='Strikes', rotation=90,fontweight='bold')
230
+ ax.text(x=0, y=6.05, s='Balls',fontweight='bold',ha='center')
231
+
232
+
233
+ def plot_header(pitcher_id: str, ax: plt.Axes, df_team: pl.DataFrame, df_players: pl.DataFrame,sport_id:int):
234
+ """
235
+ Display the team logo for the given pitcher on the specified axis.
236
+ Parameters
237
+ ----------
238
+ pitcher_id : str
239
+ The ID of the pitcher.
240
+ ax : plt.Axes
241
+ The axis to display the logo on.
242
+ df_team : pl.DataFrame
243
+ The DataFrame containing team data.
244
+ df_players : pl.DataFrame
245
+ The DataFrame containing player data.
246
+ """
247
+ # List of MLB teams and their corresponding ESPN logo URLs
248
+ mlb_teams = [
249
+ {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
250
+ {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
251
+ {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
252
+ {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
253
+ {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
254
+ {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
255
+ {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
256
+ {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
257
+ {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
258
+ {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
259
+ {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
260
+ {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
261
+ {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
262
+ {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
263
+ {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
264
+ {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
265
+ {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
266
+ {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
267
+ {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
268
+ {"team": "OAK", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
269
+ {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
270
+ {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
271
+ {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
272
+ {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
273
+ {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
274
+ {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
275
+ {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
276
+ {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
277
+ {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
278
+ {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"},
279
+ {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
280
+ ]
281
+
282
+ try:
283
+ # Construct the URL for the player's headshot image based on sport ID
284
+ if int(sport_id) == 1:
285
+ url = f'https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:67:current.png/w_640,q_auto:best/v1/people/{pitcher_id}/headshot/silo/current.png'
286
+ else:
287
+ url = f'https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto/w_640/v1/people/{pitcher_id}/headshot/milb/current.png'
288
+
289
+ # Send a GET request to the URL and open the image from the response content
290
+ response = requests.get(url)
291
+ img = Image.open(BytesIO(response.content))
292
+
293
+ # Display the image on the axis
294
+ ax.imshow(img, extent=[-11.5, -9.5, 0, 2] if sport_id == 1 else [-11.5+2/6, -9.5-2/6, 0, 2], origin='upper')
295
+ except PIL.UnidentifiedImageError:
296
+ ax.axis('off')
297
+
298
+
299
+
300
+ try:
301
+ # Create a DataFrame from the list of dictionaries
302
+ df_image = pd.DataFrame(mlb_teams)
303
+ image_dict = df_image.set_index('team')['logo_url'].to_dict()
304
+
305
+ # Get the team ID for the given pitcher
306
+ team_id = df_players.filter(pl.col('player_id') == pitcher_id)['team'][0]
307
+
308
+ # Construct the URL to fetch team data
309
+ url_team = f'https://statsapi.mlb.com/api/v1/teams/{team_id}'
310
+
311
+ # Send a GET request to the team URL and parse the JSON response
312
+ data_team = requests.get(url_team).json()
313
+
314
+ # Extract the team abbreviation
315
+ if data_team['teams'][0]['id'] in df_team['parent_org_id']:
316
+ team_abb = df_team.filter(pl.col('team_id') == data_team['teams'][0]['id'])['parent_org_abbreviation'][0]
317
+ else:
318
+ team_abb = df_team.filter(pl.col('parent_org_id') == data_team['teams'][0]['parentOrgId'])['parent_org_abbreviation'][0]
319
+
320
+ # Get the logo URL from the image dictionary using the team abbreviation
321
+ logo_url = image_dict[team_abb]
322
+
323
+ # Send a GET request to the logo URL
324
+ response = requests.get(logo_url)
325
+
326
+ # Open the image from the response content
327
+ img = Image.open(BytesIO(response.content))
328
+
329
+
330
+ ax.imshow(img, extent=[9.5, 11.5, 0, 2], origin='upper')
331
+
332
+ # Turn off the axis
333
+ # ax.axis('off')
334
+
335
+
336
+ except (KeyError,IndexError) as e:
337
+ ax.axis('off')
338
+ return
339
+
340
+
341
+
342
+
343
+ # DEFINE STRIKE ZONE
344
+ strike_zone = pd.DataFrame({
345
+ 'PlateLocSide': [-0.9, -0.9, 0.9, 0.9, -0.9],
346
+ 'PlateLocHeight': [1.5, 3.5, 3.5, 1.5, 1.5]
347
+ })
348
+
349
+ ### STRIKE ZONE ###
350
+ def draw_line(axis, alpha_spot=1, catcher_p=True):
351
+ # Ensure strike_zone columns are NumPy arrays
352
+ plate_side = strike_zone['PlateLocSide'].to_numpy()
353
+ plate_height = strike_zone['PlateLocHeight'].to_numpy()
354
+
355
+ # Plot the strike zone
356
+ axis.plot(plate_side, plate_height, color='black', linewidth=1.3, zorder=3, alpha=alpha_spot)
357
+
358
+ if catcher_p:
359
+ # Add dashed lines and home plate for catcher perspective
360
+ axis.plot([-0.708, 0.708], [0.15, 0.15], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
361
+ axis.plot([-0.708, -0.708], [0.15, 0.3], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
362
+ axis.plot([-0.708, 0], [0.3, 0.5], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
363
+ axis.plot([0, 0.708], [0.5, 0.3], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
364
+ axis.plot([0.708, 0.708], [0.3, 0.15], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
365
+ else:
366
+ # Add dashed lines and home plate for other perspective
367
+ axis.plot([-0.708, 0.708], [0.4, 0.4], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
368
+ axis.plot([-0.708, -0.9], [0.4, -0.1], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
369
+ axis.plot([-0.9, 0], [-0.1, -0.35], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
370
+ axis.plot([0, 0.9], [-0.35, -0.1], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
371
+ axis.plot([0.9, 0.708], [-0.1, 0.4], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
372
+
373
+ from matplotlib.patches import Rectangle
374
+ # Function to draw the strike zone and home plate
375
+ # Import necessary libraries
376
+ import matplotlib.pyplot as plt
377
+ import seaborn as sns
378
+ from matplotlib.patches import Rectangle
379
+ from matplotlib import gridspec
380
+ import numpy as np
381
+ import pandas as pd
382
+ from statsmodels.nonparametric.kernel_regression import KernelReg
383
+
384
+
385
+ # DEFINE STRIKE ZONE
386
+ strike_zone = pd.DataFrame({
387
+ 'PlateLocSide': [-0.9, -0.9, 0.9, 0.9, -0.9],
388
+ 'PlateLocHeight': [1.5, 3.5, 3.5, 1.5, 1.5]
389
+ })
390
+
391
+ ### STRIKE ZONE ###
392
+ def draw_line(axis, alpha_spot=1, catcher_p=True):
393
+ # Ensure strike_zone columns are NumPy arrays
394
+ plate_side = strike_zone['PlateLocSide'].to_numpy()
395
+ plate_height = strike_zone['PlateLocHeight'].to_numpy()
396
+
397
+ # Plot the strike zone
398
+ axis.plot(plate_side, plate_height, color='black', linewidth=1.3, zorder=3, alpha=alpha_spot)
399
+
400
+ if catcher_p:
401
+ # Add dashed lines and home plate for catcher perspective
402
+ axis.plot([-0.708, 0.708], [0.15, 0.15], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
403
+ axis.plot([-0.708, -0.708], [0.15, 0.3], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
404
+ axis.plot([-0.708, 0], [0.3, 0.5], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
405
+ axis.plot([0, 0.708], [0.5, 0.3], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
406
+ axis.plot([0.708, 0.708], [0.3, 0.15], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
407
+ else:
408
+ # Add dashed lines and home plate for other perspective
409
+ axis.plot([-0.708, 0.708], [0.4, 0.4], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
410
+ axis.plot([-0.708, -0.9], [0.4, -0.1], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
411
+ axis.plot([-0.9, 0], [-0.1, -0.35], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
412
+ axis.plot([0, 0.9], [-0.35, -0.1], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
413
+ axis.plot([0.9, 0.708], [-0.1, 0.4], color='black', linewidth=1, alpha=alpha_spot, zorder=1)
414
+
415
+
416
+
417
+ def heat_map_plot_hex_whiff(df:pl.DataFrame,
418
+ ax:plt.Axes,
419
+ cmap:matplotlib.colors.LinearSegmentedColormap,
420
+ hand:str):
421
+
422
+ # Generate a grid of x and z coordinates for the strike zone area
423
+
424
+ heatmap_df = df.filter((pl.col('batter_hand')==hand)&((pl.col('is_swing')))).to_pandas() # Load your data here
425
+ heatmap_df['is_whiff'] = heatmap_df['is_whiff'].fillna(0)
426
+
427
+ bin_size = max(0.1, min(0.1, 1 / np.sqrt(len(heatmap_df))))
428
+
429
+ zone_df = pd.DataFrame(columns=['px', 'pz'])
430
+ for x in np.arange(-2.75, 2.85,bin_size):
431
+ for y in np.arange(-0.5, 5.6,bin_size):
432
+ zone_df.loc[len(zone_df)] = [round(x,1), round(y,1)]
433
+
434
+
435
+
436
+ heatmap_df.loc[heatmap_df['px'].notna(),'kde_x'] = np.clip(heatmap_df.loc[heatmap_df['px'].notna(),'px'].astype('float').mul(10).astype('int').div(10),
437
+ -2.75,
438
+ 2.75)
439
+ heatmap_df.loc[heatmap_df['pz'].notna(),'kde_z'] = np.clip(heatmap_df.loc[heatmap_df['pz'].notna(),'pz'].astype('float').mul(10).astype('int').div(10),
440
+ -0.5,
441
+ 5)
442
+
443
+
444
+ # Dynamically determine bandwidth for KDE
445
+ # bandwidth = np.clip(heatmap_df.shape[0] / 2000, 0.2, 0.2)
446
+ bandwidth = np.clip(1 / np.sqrt(len(df)), 0.3, 0.5)
447
+
448
+ # Kernel Regression for smoothing the metric values
449
+ v_center = 0.25
450
+ kde_df = pd.merge(zone_df,
451
+ heatmap_df
452
+ .dropna(subset=['is_whiff', 'px', 'pz'])
453
+ [['kde_x', 'kde_z', 'is_whiff']],
454
+ how='left',
455
+ left_on=['px', 'pz'],
456
+ right_on=['kde_x', 'kde_z']).fillna({'is_whiff': v_center})
457
+
458
+
459
+
460
+ kernel_regression = KernelReg(endog=kde_df['is_whiff'],
461
+ exog=[kde_df['px'], kde_df['pz']],
462
+ bw=[bandwidth,bandwidth],
463
+ var_type='cc')
464
+
465
+ kde_df['kernel_stat'] = kernel_regression.fit([kde_df['px'], kde_df['pz']])[0]
466
+ kde_df = kde_df.pivot_table(columns='px', index='pz', values='kernel_stat', aggfunc='mean')
467
+ kde_df = kde_df.round(3)
468
+
469
+ # Set up a gridspec layout for heatmap and colorbar
470
+
471
+ from matplotlib.colors import LinearSegmentedColormap
472
+
473
+ # Define the custom color palette
474
+ kde_min = '#648FFF' # Blue
475
+ kde_mid = '#ffffff' # White
476
+ kde_max = '#FFB000' # Orange
477
+ # Create a custom colormap
478
+ # kde_palette = LinearSegmentedColormap.from_list("kde_palette", [kde_min, kde_mid, kde_max])
479
+ # kde_palette = (sns.color_palette(f'blend:{kde_min},{kde_mid}', n_colors=101)[:-1] +
480
+ # sns.color_palette(f'blend:{kde_mid},{kde_max}', n_colors=101)[:-1])
481
+ # kde_palette = (sns.color_palette(f'blend:{kde_min},{kde_mid}', n_colors=101)[:-1] +
482
+ # sns.color_palette(f'blend:{kde_mid},{kde_max}', n_colors=101)[:-1])
483
+
484
+ # # Generate the heatmap
485
+ # heatmap = sns.heatmap(data=kde_df, cmap=kde_palette, center=v_center, vmin=0.15, vmax=0.35, cbar=False, ax=ax)
486
+
487
+ ax.imshow(kde_df.values, extent=[-2.25, 2.25, -0.5, 5], origin='lower', cmap=cmap, vmin=0.15, vmax=0.35,
488
+ interpolation='bilinear')# Customize axes
489
+ ax.axis('square')
490
+ ax.set(xlabel=None, ylabel=None)
491
+ ax.set_xlim(-2.75, 2.75)
492
+ ax.set_ylim(-0.5, 5)
493
+ # ax.set_xticks([])
494
+ # ax.set_yticks([])
495
+ # ax.invert_yaxis()
496
+ # ax.grid(False)
497
+ ax.axis('off')
498
+
499
+ draw_line(ax,alpha_spot=1,catcher_p = False)
500
+
501
+
502
+ def heat_map_plot_hex_damage(df:pl.DataFrame,
503
+ ax:plt.Axes,
504
+ cmap:matplotlib.colors.LinearSegmentedColormap,
505
+ hand:str):
506
+
507
+ heatmap_df = df.filter((pl.col('batter_hand')==hand)&((pl.col('launch_speed')>0))).to_pandas() # Load your data here
508
+ heatmap_df['woba_pred_contact'] = heatmap_df['woba_pred_contact'].fillna(0)
509
+ bin_size = max(0.2, min(0.3, 1 / np.sqrt(len(heatmap_df))))
510
+
511
+ # Generate a grid of x and z coordinates for the strike zone area
512
+ zone_df = pd.DataFrame(columns=['px', 'pz'])
513
+ for x in np.arange(-2.75, 2.95,bin_size):
514
+ for y in np.arange(-0.5, 5.7,bin_size):
515
+ zone_df.loc[len(zone_df)] = [round(x,1), round(y,1)]
516
+
517
+
518
+ heatmap_df.loc[heatmap_df['px'].notna(),'kde_x'] = np.clip(heatmap_df.loc[heatmap_df['px'].notna(),'px'].astype('float').mul(10).astype('int').div(10),
519
+ -2.75,
520
+ 2.75)
521
+ heatmap_df.loc[heatmap_df['pz'].notna(),'kde_z'] = np.clip(heatmap_df.loc[heatmap_df['pz'].notna(),'pz'].astype('float').mul(10).astype('int').div(10),
522
+ -0.5,
523
+ 5)
524
+
525
+
526
+
527
+ # Dynamically determine bandwidth for KDE
528
+ # bandwidth = np.clip(heatmap_df.shape[0] / 2000, 0.2, 0.2)
529
+ bandwidth = np.clip(1 / np.sqrt(len(df)), 0.3, 0.5)
530
+
531
+ # Kernel Regression for smoothing the metric values
532
+ v_center = 0.375
533
+ kde_df = pd.merge(zone_df,
534
+ heatmap_df
535
+ .dropna(subset=['woba_pred_contact', 'px', 'pz'])
536
+ [['kde_x', 'kde_z', 'woba_pred_contact']],
537
+ how='left',
538
+ left_on=['px', 'pz'],
539
+ right_on=['kde_x', 'kde_z']).fillna({'woba_pred_contact': v_center})
540
+
541
+
542
+
543
+ kernel_regression = KernelReg(endog=kde_df['woba_pred_contact'],
544
+ exog=[kde_df['px'], kde_df['pz']],
545
+ bw=[bandwidth,bandwidth],
546
+ var_type='cc')
547
+
548
+ kde_df['kernel_stat'] = kernel_regression.fit([kde_df['px'], kde_df['pz']])[0]
549
+ kde_df = kde_df.pivot_table(columns='px', index='pz', values='kernel_stat', aggfunc='mean')
550
+ kde_df = kde_df.round(3)
551
+
552
+ # Set up a gridspec layout for heatmap and colorbar
553
+
554
+ from matplotlib.colors import LinearSegmentedColormap
555
+
556
+ # Define the custom color palette
557
+ kde_min = '#648FFF' # Blue
558
+ kde_mid = '#ffffff' # White
559
+ kde_max = '#FFB000' # Orange
560
+ # Create a custom colormap
561
+ # kde_palette = LinearSegmentedColormap.from_list("kde_palette", [kde_min, kde_mid, kde_max])
562
+ # kde_palette = (sns.color_palette(f'blend:{kde_min},{kde_mid}', n_colors=101)[:-1] +
563
+ # sns.color_palette(f'blend:{kde_mid},{kde_max}', n_colors=101)[:-1])
564
+ # kde_palette = (sns.color_palette(f'blend:{kde_min},{kde_mid}', n_colors=101)[:-1] +
565
+ # sns.color_palette(f'blend:{kde_mid},{kde_max}', n_colors=101)[:-1])
566
+
567
+ # # Generate the heatmap
568
+ # heatmap = sns.heatmap(data=kde_df, cmap=kde_palette, center=v_center, vmin=0.15, vmax=0.35, cbar=False, ax=ax)
569
+
570
+ ax.imshow(kde_df.values, extent=[-2.25, 2.25, -0.5, 5], origin='lower', cmap=cmap, vmin=0.25, vmax=0.5,
571
+ interpolation='bilinear')# Customize axes
572
+ ax.axis('square')
573
+ ax.set(xlabel=None, ylabel=None)
574
+ ax.set_xlim(-2.75, 2.75)
575
+ ax.set_ylim(-0.5, 5)
576
+ # ax.set_xticks([])
577
+ # ax.set_yticks([])
578
+ # ax.invert_yaxis()
579
+ # ax.grid(False)
580
+ ax.axis('off')
581
+
582
+ draw_line(ax,alpha_spot=1,catcher_p = False)
functions/pitch_summary_functions.py CHANGED
The diff for this file is too large to render. See raw diff
 
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