nesticot commited on
Commit
824716b
·
verified ·
1 Parent(s): 295b98d

Upload 7 files

Browse files
api_scraper.py CHANGED
@@ -154,41 +154,30 @@ class MLB_Scrape:
154
  return game_df
155
 
156
 
157
- # def get_data(self, game_list_input: list):
158
- # """
159
- # Retrieves live game data for a list of game IDs in parallel.
160
 
161
- # Parameters:
162
- # - game_list_input (list): A list of game IDs for which to retrieve live data.
163
 
164
- # Returns:
165
- # - data_total (list): A list of JSON responses containing live game data for each game ID.
166
- # """
167
- # data_total = []
168
- # print('This May Take a While. Progress Bar shows Completion of Data Retrieval.')
169
 
170
- # def fetch_data(game_id):
171
- # r = requests.get(f'https://statsapi.mlb.com/api/v1.1/game/{game_id}/feed/live')
172
- # return r.json()
173
 
174
- # with ThreadPoolExecutor() as executor:
175
- # futures = {executor.submit(fetch_data, game_id): game_id for game_id in game_list_input}
176
- # for future in tqdm(as_completed(futures), total=len(futures), desc="Processing", unit="iteration"):
177
- # data_total.append(future.result())
178
 
179
- # return data_total
180
-
181
-
182
- def get_data(self,game_list_input = [748540]):
183
- data_total = []
184
- #n_count = 0
185
- print('This May Take a While. Progress Bar shows Completion of Data Retrieval.')
186
- for i in tqdm(range(len(game_list_input)), desc="Processing", unit="iteration"):
187
- r = requests.get(f'https://statsapi.mlb.com/api/v1.1/game/{game_list_input[i]}/feed/live')
188
- data_total.append(r.json())
189
  return data_total
190
 
191
-
192
  def get_data_df(self, data_list):
193
  """
194
  Converts a list of game data JSON objects into a Polars DataFrame.
 
154
  return game_df
155
 
156
 
157
+ def get_data(self, game_list_input: list):
158
+ """
159
+ Retrieves live game data for a list of game IDs in parallel.
160
 
161
+ Parameters:
162
+ - game_list_input (list): A list of game IDs for which to retrieve live data.
163
 
164
+ Returns:
165
+ - data_total (list): A list of JSON responses containing live game data for each game ID.
166
+ """
167
+ data_total = []
168
+ print('This May Take a While. Progress Bar shows Completion of Data Retrieval.')
169
 
170
+ def fetch_data(game_id):
171
+ r = requests.get(f'https://statsapi.mlb.com/api/v1.1/game/{game_id}/feed/live')
172
+ return r.json()
173
 
174
+ with ThreadPoolExecutor() as executor:
175
+ futures = {executor.submit(fetch_data, game_id): game_id for game_id in game_list_input}
176
+ for future in tqdm(as_completed(futures), total=len(futures), desc="Processing", unit="iteration"):
177
+ data_total.append(future.result())
178
 
 
 
 
 
 
 
 
 
 
 
179
  return data_total
180
 
 
181
  def get_data_df(self, data_list):
182
  """
183
  Converts a list of game data JSON objects into a Polars DataFrame.
app.py CHANGED
@@ -6,6 +6,8 @@ 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
@@ -15,10 +17,8 @@ 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']
@@ -30,8 +30,8 @@ year_list = [2017,2018,2019,2020,2021,2022,2023,2024,2025]
30
 
31
  level_dict = {'1':'MLB',
32
  '11':'AAA',
33
- '12':'AA',
34
- '13':'A+',
35
  '14':'A',
36
  '17':'AFL',
37
  '22':'College',
@@ -61,54 +61,48 @@ 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
@@ -148,24 +142,33 @@ if response.status_code == 200:
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",
@@ -178,6 +181,7 @@ login_ui = ui.page_fluid(
178
  )
179
 
180
 
 
181
  main_ui = ui.page_sidebar(
182
  ui.sidebar(
183
  # Row for selecting season and level
@@ -190,32 +194,47 @@ main_ui = ui.page_sidebar(
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(
@@ -249,117 +268,147 @@ def server(input, output, session):
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
@@ -368,309 +417,442 @@ def server(input, output, session):
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
-
 
 
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
 
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']
 
30
 
31
  level_dict = {'1':'MLB',
32
  '11':'AAA',
33
+ # '12':'AA',
34
+ #'13':'A+',
35
  '14':'A',
36
  '17':'AFL',
37
  '22':'College',
 
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
 
142
  continue
143
  VALID_PASSWORDS.append(BACKUP_PW)
144
  VALID_PASSWORDS.append(ADMIN_PW)
145
+ VALID_PASSWORDS.append('')
146
 
147
  from shiny import App, reactive, ui, render
148
  from shiny.ui import h2, tags
149
 
150
+ from datetime import datetime
151
+
152
+ def is_valid_date(date_str):
153
+ try:
154
+ datetime.strptime(date_str, "%Y-%m-%d") # Attempt to parse the date
155
+ return True
156
+ except ValueError:
157
+ return False # If parsing fails, it's not in the correct format
158
+
159
  # Define the login UI
160
  login_ui = ui.page_fluid(
161
  ui.card(
162
  ui.h2([
163
+ "TJStats Pitching Summary App ",
164
  ui.tags.a("(@TJStats)", href="https://twitter.com/TJStats", target="_blank")
165
  ]),
166
  ui.p(
167
  "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 ",
168
+ ui.tags.a("Patreon post", href="https://www.patreon.com/posts/116064432", target="_blank"),
169
  "."
170
  ),
171
+ ui.input_password("password", "Enter Patreon Email (or Password from Link):", width="50%"),
172
  ui.tags.input(
173
  type="checkbox",
174
  id="authenticated",
 
181
  )
182
 
183
 
184
+ # Define the UI layout for the app
185
  main_ui = ui.page_sidebar(
186
  ui.sidebar(
187
  # Row for selecting season and level
 
194
  ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
195
  # Row for selecting the player
196
  ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
197
+ # Row for selecting the date range
198
+ ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))),
199
 
 
 
 
 
200
  # Rows for selecting plots and split options
201
+ ui.row(
202
+ ui.column(4, ui.input_select('plot_id_1', 'Plot Left', function_dict, multiple=False, selected='velocity_kdes')),
203
+ ui.column(4, ui.input_select('plot_id_2', 'Plot Middle', function_dict, multiple=False, selected='tj_stuff_roling')),
204
+ ui.column(4, ui.input_select('plot_id_3', 'Plot Right', function_dict, multiple=False, selected='break_plot'))
205
+ ),
206
+ ui.row(
207
+ ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
208
+ ui.column(6, ui.input_numeric('rolling_window', 'Rolling Window (for tjStuff+ Plot)', min=1, value=50))
209
+ ),
210
+ ui.row(
211
+ ui.column(6, ui.input_switch("switch", "Custom Team?", False)),
212
+ ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False))
213
+ ),
214
 
215
  # Row for the action button to generate plot
216
  ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
217
  width="400px" # Added this parameter to control sidebar width
218
  ),
219
+
220
+ # Main content area with tabs (placed directly in page_sidebar)
221
  ui.navset_tab(
222
+ ui.nav_panel("Pitching Summary",
 
223
  ui.output_text("status"),
224
+ ui.output_plot('plot', width='2100px', height='2100px')
225
  ),
226
+ ui.nav_panel("Game Summary",
227
+ ui.output_text("status2"),
228
+ ui.output_plot('game_plot', width='2100px', height='2100px')
229
+ ),
230
+ ui.nav_panel("Table Range",
231
+ ui.output_data_frame("grid")),
232
+ ui.nav_panel("Table Game",
233
+ ui.output_data_frame("grid_game")),
234
+ id="tabset"
235
  )
236
  )
237
 
 
238
  # Combined UI with conditional panel
239
  app_ui = ui.page_fluid(
240
  ui.tags.head(
 
268
  @render.text
269
  def login_message():
270
  return ""
271
+
 
 
 
272
  @reactive.calc
273
+ @reactive.event(input.pitcher_id, input.date_id,input.split_id)
274
  def cached_data():
275
 
 
 
276
  year_input = int(input.year_input())
277
  sport_id = int(input.level_input())
278
  player_input = int(input.pitcher_id())
279
+
280
+
281
  start_date = str(input.date_id()[0])
282
  end_date = str(input.date_id()[1])
 
283
  game_list = scrape.get_player_games_list(sport_id = sport_id,
284
  season = year_input,
285
  player_id = player_input,
286
  start_date = start_date,
287
  end_date = end_date,
288
  game_type = [input.type_input()])
289
+
290
+ # if input.tabset() == 'Game Summary':
291
+ # print(year_input, sport_id, player_input, 'yup')
292
+ # print(input.date_id())
293
+ # game_list = [input.date_id()]
294
+
295
 
296
  data_list = scrape.get_data(game_list_input = game_list[:])
 
 
 
 
297
 
298
+ try:
299
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
300
+ (pl.col("pitcher_id") == player_input)&
301
+ (pl.col("is_pitch") == True)&
302
+ (pl.col("start_speed") >= 50)&
303
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
304
+
305
+ )))).with_columns(
306
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
307
+ ))
308
+ return df
309
+
310
+
311
+ except TypeError:
312
+ print("NONE")
313
+ return None
314
+
315
+ @reactive.calc
316
+ @reactive.event(input.pitcher_id, input.date_id,input.split_id,input.tabset)
317
+ def cached_data_daily():
318
+
319
+ year_input = int(input.year_input())
320
+ sport_id = int(input.level_input())
321
+ player_input = int(input.pitcher_id())
322
+
323
 
324
+ # start_date = str(input.date_id()[0])
325
+ # end_date = str(input.date_id()[1])
326
+ game_list = [int(input.date_id())]
327
+ print(game_list)
328
+
329
+ # if input.tabset() == 'Game Summary':
330
+ # print(year_input, sport_id, player_input, 'yup')
331
+ # print(input.date_id())
332
+ # game_list =
333
+
334
+
335
+ data_list = scrape.get_data(game_list_input = game_list[:])
336
+
337
+ try:
338
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
339
+ (pl.col("pitcher_id") == player_input)&
340
+ (pl.col("is_pitch") == True)&
341
+ (pl.col("start_speed") >= 50)&
342
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
343
+
344
+ )))).with_columns(
345
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
346
+ ))
347
+ return df
348
+
349
+
350
+ except TypeError:
351
+ print("NONE")
352
+ return None
353
+
354
 
355
  @render.ui
356
+ @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,input.tabset,ignore_none=False)
357
  def player_select_ui():
358
  # Get the list of pitchers for the selected level and season
359
  df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input()), game_type = [input.type_input()]).filter(
360
+ (pl.col("position").is_in(['P','TWP']))|
361
+ (pl.col("player_id").is_in([686846]))
362
+
363
+ ).sort("name")
364
 
365
  # Create a dictionary of pitcher IDs and names
366
  pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name']))
 
 
 
367
 
 
 
368
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
369
 
 
 
 
 
 
 
 
370
 
371
+ # Return a select input for choosing a pitcher
372
+ return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True)
373
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  @render.ui
375
+ @reactive.event(input.player_button, input.pitcher_id,input.year_input, input.level_input, input.type_input,input.tabset,ignore_none=False)
376
  def date_id():
377
+ if input.tabset() == 'Pitching Summary' or input.tabset() == 'Table Range':
378
+ # Create a date range input for selecting the date range within the selected year
379
+ return ui.input_date_range("date_id", "Select Date Range",
380
+ start=f"{int(input.year_input())}-01-01",
381
+ end=f"{int(input.year_input())}-12-31",
382
+ min=f"{int(input.year_input())}-01-01",
383
+ max=f"{int(input.year_input())}-12-31")
384
+
385
+ if input.tabset() == 'Game Summary' or input.tabset() == 'Table Game':
386
+ year_input = int(input.year_input())
387
+ sport_id = int(input.level_input())
388
+ player_input = int(input.pitcher_id())
389
+ print('game summary')
390
+ # start_date = str(input.date_id()[0])
391
+ # end_date = str(input.date_id()[1])
392
+
393
+ game_list = scrape.get_player_games_list(player_id = player_input,
394
+ season = year_input,
395
+ sport_id=sport_id,
396
+ game_type=[input.type_input()],
397
+ pitching = True)
398
+
399
+ schedule_df = scrape.get_schedule(year_input=[year_input],
400
+ sport_id= [sport_id],
401
+ game_type = [input.type_input()])
402
+
403
+ player_schedule_df = schedule_df.filter(pl.col('game_id').is_in(game_list)).to_pandas().sort_values('date')
404
 
405
+ player_schedule_df['def'] = player_schedule_df['date'].astype(str) + ' - ' + player_schedule_df['away'] + ' @ ' + player_schedule_df['home'] + ' '
406
+
407
+
408
+ game_dict = dict(zip(player_schedule_df['game_id'], player_schedule_df['def']))
409
+ # print(game_dict)
410
+
411
+ return ui.input_select("date_id", "Select Game", game_dict)
412
 
413
  @output
414
  @render.text
 
417
  if input.generate == 0:
418
  return ""
419
  return ""
420
+
421
+
422
 
423
+
424
  @output
425
  @render.plot
426
  @reactive.event(input.generate_plot, ignore_none=False)
427
  def plot():
428
  # Show progress/loading notification
429
+
430
+
431
+
432
  with ui.Progress(min=0, max=1) as p:
433
  p.set(message="Generating plot", detail="This may take a while...")
434
 
435
+
436
  p.set(0.3, "Gathering data...")
437
  year_input = int(input.year_input())
438
  sport_id = int(input.level_input())
439
  player_input = int(input.pitcher_id())
440
  start_date = str(input.date_id()[0])
441
  end_date = str(input.date_id()[1])
442
+ if not is_valid_date(start_date):
443
+ fig = plt.figure(figsize=(26,26))
444
+ fig.text(x=0.1,y=0.9,s='Select Date Range and Generate Plot',fontsize=36,ha='left')
445
+ return fig
446
  print(year_input, sport_id, player_input, start_date, end_date)
447
 
448
  df = cached_data()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
449
 
450
+ if df is None:
451
+ fig = plt.figure(figsize=(26,26))
452
+ fig.text(x=0.1,y=0.9,s='No Statcast Data For This Pitcher',fontsize=36,ha='left')
453
+ return fig
454
+
455
+ df = df.clone()
456
+
457
 
458
  p.set(0.6, "Creating plot...")
459
 
460
+
461
+ #plt.rcParams["figure.figsize"] = [10,10]
462
+ fig = plt.figure(figsize=(26,26))
463
+ plt.rcParams.update({'figure.autolayout': True})
464
  fig.set_facecolor('white')
465
  sns.set_theme(style="whitegrid", palette=colour_palette)
466
+ print('this is the one plot')
 
467
 
468
+ gs = gridspec.GridSpec(6, 8,
469
+ height_ratios=[6,20,12,36,36,6],
470
+ width_ratios=[4,18,18,18,18,18,18,4])
 
471
 
 
472
 
473
+ gs.update(hspace=0.2, wspace=0.5)
474
 
475
+ # Define the positions of each subplot in the grid
476
+ ax_headshot = fig.add_subplot(gs[1,1:3])
477
+ ax_bio = fig.add_subplot(gs[1,3:5])
478
+ ax_logo = fig.add_subplot(gs[1,5:7])
 
479
 
480
+ ax_season_table = fig.add_subplot(gs[2,1:7])
 
 
 
481
 
482
+ ax_plot_1 = fig.add_subplot(gs[3,1:3])
483
+ ax_plot_2 = fig.add_subplot(gs[3,3:5])
484
+ ax_plot_3 = fig.add_subplot(gs[3,5:7])
485
 
486
+ ax_table = fig.add_subplot(gs[4,1:7])
 
 
 
 
487
 
488
+ ax_footer = fig.add_subplot(gs[-1,1:7])
489
+ ax_header = fig.add_subplot(gs[0,1:7])
490
+ ax_left = fig.add_subplot(gs[:,0])
491
+ ax_right = fig.add_subplot(gs[:,-1])
492
 
493
+ # Hide axes for footer, header, left, and right
494
+ ax_footer.axis('off')
495
+ ax_header.axis('off')
496
+ ax_left.axis('off')
497
+ ax_right.axis('off')
498
 
499
+ sns.set_theme(style="whitegrid", palette=colour_palette)
500
+ fig.set_facecolor('white')
 
 
501
 
502
+ df_teams = scrape.get_teams()
503
 
504
+ player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
505
+ player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
 
 
 
506
 
507
+ if input.switch():
508
 
509
+ # Get the logo URL from the image dictionary using the team abbreviation
510
+ logo_url = input.logo_select()
511
+
512
+ # Send a GET request to the logo URL
513
+ response = requests.get(logo_url)
514
+
515
+ # Open the image from the response content
516
+ img = Image.open(BytesIO(response.content))
517
+
518
+ # Display the image on the axis
519
+ ax_logo.set_xlim(0, 1.3)
520
+ ax_logo.set_ylim(0, 1)
521
+ ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')
522
+
523
+ # Turn off the axis
524
+ ax_logo.axis('off')
525
+
526
+ else:
527
+ plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
528
+
529
+ stat_summary_table(df=df,
530
+ ax=ax_season_table,
531
+ player_input=player_input,
532
+ split=input.split_id(),
533
+ sport_id=sport_id,
534
+ game_type=[input.type_input()])
535
+
536
+ # break_plot(df=df_plot,ax=ax2)
537
+ 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]):
538
+ if x == 'velocity_kdes':
539
+ velocity_kdes(df,
540
+ ax=y,
541
+ gs=gs,
542
+ gs_x=[3,4],
543
+ gs_y=[z,z+2],
544
+ fig=fig)
545
+ if x == 'tj_stuff_roling':
546
+ tj_stuff_roling(df=df,
547
+ window=int(input.rolling_window()),
548
+ ax=y)
549
+
550
+ if x == 'tj_stuff_roling_game':
551
+ tj_stuff_roling_game(df=df,
552
+ window=int(input.rolling_window()),
553
+ ax=y)
554
 
555
+ if x == 'break_plot':
556
+ break_plot(df = df,ax=y)
557
 
558
+ if x == 'location_plot_lhb':
559
+ location_plot(df = df,ax=y,hand='L')
560
 
561
+ if x == 'location_plot_rhb':
562
+ location_plot(df = df,ax=y,hand='R')
 
563
 
564
+ summary_table(df=df,
565
+ ax=ax_table)
 
566
 
567
+ plot_footer(ax_footer)
568
 
569
+ ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1)
570
+ # Hide axes ticks and labels
571
+ ax_watermark.set_xticks([])
572
+ ax_watermark.set_yticks([])
573
+ ax_watermark.set_frame_on(False) # Optional: Hide border
574
+
575
+ img = Image.open('tj stats circle-01_new.jpg')
576
+ # Display the image
577
+ ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.1)
578
+
579
+
580
+ ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1)
581
+ ax_watermark2.set_xlim(0,1)
582
+ ax_watermark2.set_ylim(0,1)
583
+ # Hide axes ticks and labels
584
+ ax_watermark2.set_xticks([])
585
+ ax_watermark2.set_yticks([])
586
+ ax_watermark2.set_frame_on(False) # Optional: Hide border
587
+
588
+ # Open the image
589
+ img = Image.open('tj stats circle-01_new.jpg')
590
+ # Get the original size
591
+ width, height = img.size
592
+ # Calculate the new size (50% larger)
593
+ new_width = int(width * 0.5)
594
+ new_height = int(height * 0.5)
595
+ # Resize the image
596
+ img_resized = img.resize((new_width, new_height))
597
+ # Display the image
598
+ ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1)
599
 
600
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
 
 
601
 
602
 
 
 
 
 
 
603
 
604
+
605
+ @output
606
+ @render.plot
607
+ @reactive.event(input.generate_plot, ignore_none=False)
608
+ def game_plot():
609
+ # Show progress/loading notification
610
+ with ui.Progress(min=0, max=1) as p:
611
+ print(input.date_id(),'TEST')
612
 
613
+ if isinstance(input.date_id(), tuple):
614
+ fig = plt.figure(figsize=(26,26))
615
+ fig.text(x=0.1,y=0.9,s='Select Game and Generate Plot',fontsize=36,ha='left')
616
+ return fig
617
+
618
 
619
+ p.set(message="Generating plot", detail="This may take a while...")
 
 
620
 
621
+
622
+ p.set(0.3, "Gathering data...")
623
+ year_input = int(input.year_input())
624
+ sport_id = int(input.level_input())
625
+ player_input = int(input.pitcher_id())
626
+
627
+ # print(input.game_id())
628
+ # print(year_input, sport_id, player_input)
629
 
630
+ # print(year_input, sport_id, player_input, start_date, end_date)
631
 
632
+ df = cached_data_daily()
 
633
 
634
+ # start_date = str(df['game_date'][0])
635
+ # end_date = str(df['game_date'][0])
636
+
637
+ if df is None:
638
+ fig = plt.figure(figsize=(26,26))
639
+ fig.text(x=0.1,y=0.9,s='No Statcast Data For This Pitcher',fontsize=36,ha='left')
640
+ return fig
641
+
642
+ df = df.clone()
643
+
644
 
645
+ p.set(0.6, "Creating plot...")
 
646
 
647
+
648
+ #plt.rcParams["figure.figsize"] = [10,10]
649
+ fig = plt.figure(figsize=(26,26))
650
+ plt.rcParams.update({'figure.autolayout': True})
651
+ fig.set_facecolor('white')
652
+ sns.set_theme(style="whitegrid", palette=colour_palette)
653
+ print('this is the one plot')
654
 
655
+ gs = gridspec.GridSpec(6, 8,
656
+ height_ratios=[6,20,12,36,36,6],
657
+ width_ratios=[4,18,18,18,18,18,18,4])
658
 
 
 
 
659
 
660
+ gs.update(hspace=0.2, wspace=0.5)
661
 
662
+ # Define the positions of each subplot in the grid
663
+ ax_headshot = fig.add_subplot(gs[1,1:3])
664
+ ax_bio = fig.add_subplot(gs[1,3:5])
665
+ ax_logo = fig.add_subplot(gs[1,5:7])
666
 
667
+ ax_season_table = fig.add_subplot(gs[2,1:7])
668
 
669
+ ax_plot_1 = fig.add_subplot(gs[3,1:3])
670
+ ax_plot_2 = fig.add_subplot(gs[3,3:5])
671
+ ax_plot_3 = fig.add_subplot(gs[3,5:7])
672
 
673
+ ax_table = fig.add_subplot(gs[4,1:7])
 
674
 
675
+ ax_footer = fig.add_subplot(gs[-1,1:7])
676
+ ax_header = fig.add_subplot(gs[0,1:7])
677
+ ax_left = fig.add_subplot(gs[:,0])
678
+ ax_right = fig.add_subplot(gs[:,-1])
679
 
680
+ # Hide axes for footer, header, left, and right
681
+ ax_footer.axis('off')
682
+ ax_header.axis('off')
683
+ ax_left.axis('off')
684
+ ax_right.axis('off')
685
 
686
+ sns.set_theme(style="whitegrid", palette=colour_palette)
687
+ fig.set_facecolor('white')
 
688
 
689
+ df_teams = scrape.get_teams()
690
 
691
+ player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
692
+ player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
693
 
694
+ if input.switch():
695
 
696
+ # Get the logo URL from the image dictionary using the team abbreviation
697
+ logo_url = input.logo_select()
698
+
699
+ # Send a GET request to the logo URL
700
+ response = requests.get(logo_url)
701
+
702
+ # Open the image from the response content
703
+ img = Image.open(BytesIO(response.content))
704
+
705
+ # Display the image on the axis
706
+ ax_logo.set_xlim(0, 1.3)
707
+ ax_logo.set_ylim(0, 1)
708
+ ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')
709
+
710
+ # Turn off the axis
711
+ ax_logo.axis('off')
712
+
713
+ else:
714
+ plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
715
+
716
+ stat_summary_table(df=df,
717
+ ax=ax_season_table,
718
+ player_input=player_input,
719
+ split=input.split_id(),
720
+ sport_id=sport_id,
721
+ game_type=[input.type_input()])
722
+
723
+ # break_plot(df=df_plot,ax=ax2)
724
+ 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]):
725
+ if x == 'velocity_kdes':
726
+ velocity_kdes(df,
727
+ ax=y,
728
+ gs=gs,
729
+ gs_x=[3,4],
730
+ gs_y=[z,z+2],
731
+ fig=fig)
732
+ if x == 'tj_stuff_roling':
733
+ tj_stuff_roling(df=df,
734
+ window=int(input.rolling_window()),
735
+ ax=y)
736
+
737
+ if x == 'tj_stuff_roling_game':
738
+ tj_stuff_roling_game(df=df,
739
+ window=int(input.rolling_window()),
740
+ ax=y)
741
 
742
+ if x == 'break_plot':
743
+ break_plot(df = df,ax=y)
744
 
745
+ if x == 'location_plot_lhb':
746
+ location_plot(df = df,ax=y,hand='L')
 
747
 
748
+ if x == 'location_plot_rhb':
749
+ location_plot(df = df,ax=y,hand='R')
750
 
751
+ summary_table(df=df,
752
+ ax=ax_table)
 
 
 
 
753
 
754
+ plot_footer(ax_footer)
 
755
 
756
+ ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1)
757
+ # Hide axes ticks and labels
758
+ ax_watermark.set_xticks([])
759
+ ax_watermark.set_yticks([])
760
+ ax_watermark.set_frame_on(False) # Optional: Hide border
761
+
762
+ img = Image.open('tj stats circle-01_new.jpg')
763
+ # Display the image
764
+ ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.1)
765
+
766
+
767
+ ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1)
768
+ ax_watermark2.set_xlim(0,1)
769
+ ax_watermark2.set_ylim(0,1)
770
+ # Hide axes ticks and labels
771
+ ax_watermark2.set_xticks([])
772
+ ax_watermark2.set_yticks([])
773
+ ax_watermark2.set_frame_on(False) # Optional: Hide border
774
+
775
+ # Open the image
776
+ img = Image.open('tj stats circle-01_new.jpg')
777
+ # Get the original size
778
+ width, height = img.size
779
+ # Calculate the new size (50% larger)
780
+ new_width = int(width * 0.5)
781
+ new_height = int(height * 0.5)
782
+ # Resize the image
783
+ img_resized = img.resize((new_width, new_height))
784
+ # Display the image
785
+ ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1)
786
 
787
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
788
 
 
 
 
 
789
 
790
 
791
+ @output
792
+ @render.data_frame
793
+ @reactive.event(input.generate_plot, ignore_none=False)
794
+ def grid():
 
795
 
796
+ start_date = str(input.date_id()[0])
797
+ if not is_valid_date(start_date):
798
+ return pd.DataFrame({"Message": ["Select range to generate table"]})
799
+ df = cached_data()
800
+ df = df.clone()
801
+ features_table = ['start_speed',
802
+ 'spin_rate',
803
+ 'extension',
804
+ 'ivb',
805
+ 'hb',
806
+ 'x0',
807
+ 'z0']
808
+
809
+
810
+
811
+ selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand',
812
+ 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus']
813
 
814
 
815
 
816
+ return render.DataGrid(
817
+ df.select(selection).to_pandas().round(1),
818
+ row_selection_mode='multiple',
819
+ height='700px',
820
+ width='fit-content',
821
+ filters=True,
822
+ )
823
 
824
 
825
+ @output
826
+ @render.data_frame
827
+ @reactive.event(input.generate_plot, ignore_none=False)
828
+ def grid_game():
829
+ if isinstance(input.date_id(), tuple):
830
+ return pd.DataFrame({"Message": ["Select game to generate table"]})
831
+
832
+ df = cached_data_daily()
833
+ df = df.clone()
834
+ features_table = ['start_speed',
835
+ 'spin_rate',
836
+ 'extension',
837
+ 'ivb',
838
+ 'hb',
839
+ 'x0',
840
+ 'z0']
841
+
842
+
843
+
844
+ selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand',
845
+ 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus']
846
 
847
 
 
848
 
849
+ return render.DataGrid(
850
+ df.select(selection).to_pandas().round(1),
851
+ row_selection_mode='multiple',
852
+ height='700px',
853
+ width='fit-content',
854
+ filters=True,
855
+ )
856
 
857
+
858
+ app = App(app_ui, server)
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__/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,7 +323,6 @@ 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
- (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,7 +442,6 @@ class df_update:
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'),
 
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
  (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'),
functions/pitch_summary_functions.py CHANGED
@@ -19,6 +19,7 @@ from matplotlib.offsetbox import OffsetImage, AnnotationBbox
19
  import matplotlib.pyplot as plt
20
  import matplotlib.gridspec as gridspec
21
  import PIL
 
22
 
23
 
24
  ### PITCH COLOURS ###
@@ -50,6 +51,7 @@ pitch_colours = {
50
 
51
  ## Others ##
52
  'KN': {'colour': '#867A08', 'name': 'Knuckleball'},
 
53
  'PO': {'colour': '#472C30', 'name': 'Pitch Out'},
54
  'UN': {'colour': '#9C8975', 'name': 'Unknown'},
55
  }
@@ -393,6 +395,17 @@ def break_plot(df: pl.DataFrame, ax: plt.Axes):
393
  ax.set_xlim((-25, 25))
394
  ax.set_ylim((-25, 25))
395
 
 
 
 
 
 
 
 
 
 
 
 
396
  # Add horizontal and vertical lines
397
  ax.hlines(y=0, xmin=-50, xmax=50, color=colour_palette[8], alpha=0.5, linestyles='--', zorder=1)
398
  ax.vlines(x=0, ymin=-50, ymax=50, color=colour_palette[8], alpha=0.5, linestyles='--', zorder=1)
@@ -400,7 +413,7 @@ def break_plot(df: pl.DataFrame, ax: plt.Axes):
400
  # Set axis labels and title
401
  ax.set_xlabel('Horizontal Break (in)', fontdict=font_properties_axes)
402
  ax.set_ylabel('Induced Vertical Break (in)', fontdict=font_properties_axes)
403
- ax.set_title("Pitch Breaks", fontdict=font_properties_titles)
404
 
405
  # Remove legend
406
  ax.get_legend().remove()
@@ -543,8 +556,8 @@ def summary_table(df: pl.DataFrame, ax: plt.Axes):
543
  pl.col('spin_rate').mean().alias('spin_rate'),
544
  pl.col('vaa').mean().alias('vaa'),
545
  pl.col('haa').mean().alias('haa'),
546
- pl.col('z0').mean().alias('z0'),
547
- pl.col('x0').mean().alias('x0'),
548
  pl.col('extension').mean().alias('extension'),
549
  (((pl.col('spin_direction').mean() + 180) % 360 // 30) +
550
  (((pl.col('spin_direction').mean() + 180) % 360 % 30 / 30 / 100 * 60).round(2) * 10).round(0) // 1.5 / 4)
@@ -624,7 +637,7 @@ def summary_table(df: pl.DataFrame, ax: plt.Axes):
624
  # Apply color to specific columns based on normalized values
625
  columns_to_color = [(3, 'release_speed', 0.95, 1.05), (11, 'release_extension', 0.9, 1.1), (13, None, 80, 120),
626
  (14, None, 30, 70), (15, 'in_zone_rate', 0.7, 1.3), (16, 'chase_rate', 0.7, 1.3),
627
- (17, 'whiff_rate', 0.7, 1.3), (18, 'xwoba', 0.7, 1.3)]
628
 
629
  for col, stat, vmin_factor, vmax_factor in columns_to_color:
630
  cell_value = table.get_celld()[(i + 1, col)].get_text().get_text()
@@ -764,7 +777,7 @@ def player_bio(pitcher_id: str, ax: plt.Axes, sport_id: int, year_input: int):
764
 
765
  # Display the player's name, handedness, age, height, and weight on the axis
766
  ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=56)
767
- ax.text(0.5, 0.7, f'{pitcher_hand}HP, Age:{age}, {height}/{weight}', va='top', ha='center', fontsize=30)
768
  ax.text(0.5, 0.45, f'Season Pitching Summary', va='top', ha='center', fontsize=40)
769
 
770
  # Make API call to retrieve sports information
@@ -1055,7 +1068,8 @@ def stat_summary_table(df: pl.DataFrame,
1055
  player_input: int,
1056
  sport_id: int,
1057
  ax: plt.Axes,
1058
- split: str = 'All'):
 
1059
  """
1060
  Create a summary table of player statistics.
1061
 
@@ -1072,6 +1086,18 @@ def stat_summary_table(df: pl.DataFrame,
1072
  split : str, optional
1073
  The split type (default is 'All').
1074
  """
 
 
 
 
 
 
 
 
 
 
 
 
1075
  # Format start and end dates
1076
  start_date_format = str(pd.to_datetime(df['game_date'][0]).strftime('%m/%d/%Y'))
1077
  end_date_format = str(pd.to_datetime(df['game_date'][-1]).strftime('%m/%d/%Y'))
@@ -1079,11 +1105,14 @@ def stat_summary_table(df: pl.DataFrame,
1079
  # Determine app context based on sport ID
1080
  appContext = 'majorLeague' if sport_id == 1 else 'minorLeague'
1081
 
 
 
1082
  # Fetch player stats from MLB API
1083
  pitcher_stats_call = requests.get(
1084
- f'https://statsapi.mlb.com/api/v1/people/{player_input}?appContext={appContext}&hydrate=stats(group=[pitching],type=[byDateRange],sportId={sport_id},startDate={start_date_format},endDate={end_date_format})'
1085
  ).json()
1086
-
 
1087
  # Extract stats and create DataFrame
1088
  pitcher_stats_call_header = [x for x in pitcher_stats_call['people'][0]['stats'][0]['splits'][-1]['stat']]
1089
  pitcher_stats_call_values = [pitcher_stats_call['people'][0]['stats'][0]['splits'][-1]['stat'][x] for x in pitcher_stats_call['people'][0]['stats'][0]['splits'][-1]['stat']]
@@ -1103,11 +1132,11 @@ def stat_summary_table(df: pl.DataFrame,
1103
  if df['game_id'][0] == df['game_id'][-1]:
1104
  pitcher_stats_call_df_small = pitcher_stats_call_df.select(['inningsPitched', 'battersFaced', 'earnedRuns', 'hits', 'strikeOuts', 'baseOnBalls', 'hitByPitch', 'homeRuns', 'strikePercentage', 'whiffs'])
1105
  new_column_names = ['$\\bf{IP}$', '$\\bf{PA}$', '$\\bf{ER}$', '$\\bf{H}$', '$\\bf{K}$', '$\\bf{BB}$', '$\\bf{HBP}$', '$\\bf{HR}$', '$\\bf{Strike\%}$', '$\\bf{Whiffs}$']
1106
- title = f'{df["game_date"][0]} vs {df["batter_team"][0]}'
1107
- elif sport_id != 1:
1108
  pitcher_stats_call_df_small = pitcher_stats_call_df.select(['inningsPitched', 'battersFaced', 'whip', 'era', 'fip', 'k_percent', 'bb_percent', 'k_bb_percent', 'strikePercentage'])
1109
  new_column_names = ['$\\bf{IP}$', '$\\bf{PA}$', '$\\bf{WHIP}$', '$\\bf{ERA}$', '$\\bf{FIP}$', '$\\bf{K\%}$', '$\\bf{BB\%}$', '$\\bf{K-BB\%}$', '$\\bf{Strike\%}$']
1110
- title = f'{df["game_date"][0]} to {df["game_date"][-1]}'
1111
  else:
1112
  fangraphs_table(df=df, ax=ax, player_input=player_input, season=int(df['game_date'][0][0:4]), split=split)
1113
  return
@@ -1121,149 +1150,3 @@ def stat_summary_table(df: pl.DataFrame,
1121
  # Add title to the plot
1122
  ax.text(0.5, 0.9, title, va='bottom', ha='center', fontsize=36, fontstyle='italic')
1123
  ax.axis('off')
1124
-
1125
-
1126
-
1127
- def stat_daily_summary(df: pl.DataFrame,
1128
- data: list,
1129
- player_input: int,
1130
- sport_id: int,
1131
- ax: plt.Axes):
1132
-
1133
-
1134
- pk_list = []
1135
- pitcher_id_list = []
1136
- summary_list = []
1137
- ip_list = []
1138
- pa_list = []
1139
- er_list = []
1140
- hit_list = []
1141
- k_list = []
1142
- bb_list = []
1143
- hbp_list = []
1144
- strikes_list = []
1145
- hr_list = []
1146
- test_list = []
1147
- game_pk_list = []
1148
- pitches_list = []
1149
-
1150
-
1151
- # 'inningsPitched','battersFaced','earnedRuns','hits','strikeOuts','baseOnBalls','hitByPitch'
1152
-
1153
- for y in range(0,len(data)):
1154
-
1155
- pk_list.append([data[y]['gameData']['game']['pk'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
1156
- pk_list.append([data[y]['gameData']['game']['pk'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1157
-
1158
- pitcher_id_list.append([x for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
1159
- pitcher_id_list.append([x for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1160
-
1161
-
1162
- ip_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['inningsPitched'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
1163
- ip_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['inningsPitched'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1164
-
1165
- pa_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['battersFaced'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
1166
- pa_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['battersFaced'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1167
-
1168
- er_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['earnedRuns'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
1169
- er_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['earnedRuns'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1170
-
1171
- hit_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['hits'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
1172
- hit_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['hits'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1173
-
1174
- k_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['strikeOuts'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
1175
- k_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['strikeOuts'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1176
-
1177
- bb_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['baseOnBalls'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
1178
- bb_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['baseOnBalls'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1179
-
1180
- hbp_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['hitByPitch'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
1181
- hbp_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['hitByPitch'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1182
-
1183
- strikes_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['strikes'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
1184
- strikes_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['strikes'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1185
-
1186
- pitches_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['pitchesThrown'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1187
- pitches_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['pitchesThrown'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1188
-
1189
-
1190
- hr_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['homeRuns'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
1191
- hr_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['homeRuns'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1192
-
1193
- summary_list.append([data[y]['liveData']['boxscore']['teams']['away']['players']['ID'+str(x)]['stats']['pitching']['summary'] for x in data[y]['liveData']['boxscore']['teams']['away']['pitchers']])
1194
- summary_list.append([data[y]['liveData']['boxscore']['teams']['home']['players']['ID'+str(x)]['stats']['pitching']['summary'] for x in data[y]['liveData']['boxscore']['teams']['home']['pitchers']])
1195
-
1196
- test_list.append([x for x in data[y]['liveData']['plays']['allPlays']])
1197
- game_pk_list.append([data[y]['gameData']['game']['pk'] for x in data[y]['liveData']['plays']['allPlays']])
1198
-
1199
- flat_list_pk = [item for sublist in pk_list for item in sublist]
1200
- flat_list_pitcher_id = [item for sublist in pitcher_id_list for item in sublist]
1201
- flat_list_summary = [item for sublist in summary_list for item in sublist]
1202
- flat_list_hits = [item for sublist in hit_list for item in sublist]
1203
- flat_list_k = [item for sublist in k_list for item in sublist]
1204
- flat_list_bb = [item for sublist in bb_list for item in sublist]
1205
- flat_list_pa = [item for sublist in pa_list for item in sublist]
1206
- flat_list_ip = [item for sublist in ip_list for item in sublist]
1207
- flat_list_hbp= [item for sublist in hbp_list for item in sublist]
1208
- flat_list_strikes = [item for sublist in strikes_list for item in sublist]
1209
- flat_list_hr= [item for sublist in hr_list for item in sublist]
1210
- flat_list_er= [item for sublist in er_list for item in sublist]
1211
- flat_list_pitches= [item for sublist in pitches_list for item in sublist]
1212
-
1213
-
1214
-
1215
- pitcher_summary_df = pl.DataFrame(data={'game_id':flat_list_pk,
1216
- 'pitcher_id':flat_list_pitcher_id,
1217
- 'summary':flat_list_summary,
1218
- 'hits':flat_list_hits,
1219
- 'k':flat_list_k,
1220
- 'bb':flat_list_bb,
1221
- 'pa':flat_list_pa,
1222
- 'hbp':flat_list_hbp,
1223
- 'strikes':flat_list_strikes,
1224
- 'hr':flat_list_hr,
1225
- 'ip':flat_list_ip,
1226
- 'er':flat_list_er,
1227
- 'pitches':flat_list_pitches})
1228
-
1229
-
1230
-
1231
- # Add additional calculated columns
1232
- pitcher_summary_df = pitcher_summary_df.filter(pl.col('pitcher_id')==player_input).with_columns(
1233
- pl.lit(df['is_whiff'].sum()).alias('whiffs'),
1234
- ((pl.col('strikes'))/(pl.col('pitches'))*100).round(1).cast(pl.Utf8).str.concat('%').alias('strikePercentage')
1235
- )
1236
-
1237
- # Determine columns and title based on game count and sport ID
1238
-
1239
- pitcher_stats_call_df_small = pitcher_summary_df.select(['ip',
1240
- 'pa',
1241
- 'er',
1242
- 'hits',
1243
- 'k',
1244
- 'bb',
1245
- 'hbp',
1246
- 'hr',
1247
- 'strikePercentage',
1248
- 'whiffs'])
1249
-
1250
- new_column_names = ['$\\bf{IP}$', '$\\bf{PA}$', '$\\bf{ER}$', '$\\bf{H}$', '$\\bf{K}$', '$\\bf{BB}$', '$\\bf{HBP}$', '$\\bf{HR}$', '$\\bf{Strike\%}$', '$\\bf{Whiffs}$']
1251
- title = f'{df["game_date"][0]} vs {df["batter_team"][0]}'
1252
-
1253
- table_fg = ax.table(cellText=pitcher_stats_call_df_small.to_numpy(), colLabels=pitcher_stats_call_df_small.columns, cellLoc='center',
1254
- bbox=[0.0, 0.1, 1, 0.7])
1255
-
1256
- min_font_size = 20
1257
- table_fg.set_fontsize(min_font_size)
1258
-
1259
-
1260
- new_column_names = ['$\\bf{IP}$','$\\bf{PA}$','$\\bf{ER}$','$\\bf{H}$','$\\bf{K}$','$\\bf{BB}$','$\\bf{HBP}$','$\\bf{HR}$','$\\bf{Strike\%}$','$\\bf{Whiffs}$']
1261
- # #new_column_names = ['Pitch Name', 'Pitch%', 'Velocity', 'Spin Rate','Exit Velocity', 'Whiff%', 'CSW%']
1262
- for i, col_name in enumerate(new_column_names):
1263
- table_fg.get_celld()[(0, i)].get_text().set_text(col_name)
1264
-
1265
- ax.axis('off')
1266
-
1267
- # Add title to the plot
1268
- ax.text(0.5, 0.9, title, va='bottom', ha='center', fontsize=36, fontstyle='italic')
1269
- ax.axis('off')
 
19
  import matplotlib.pyplot as plt
20
  import matplotlib.gridspec as gridspec
21
  import PIL
22
+ from stuff_model import calculate_arm_angles as caa
23
 
24
 
25
  ### PITCH COLOURS ###
 
51
 
52
  ## Others ##
53
  'KN': {'colour': '#867A08', 'name': 'Knuckleball'},
54
+ 'KN': {'colour': '#867A08', 'name': 'Knuckle Ball'},
55
  'PO': {'colour': '#472C30', 'name': 'Pitch Out'},
56
  'UN': {'colour': '#9C8975', 'name': 'Unknown'},
57
  }
 
395
  ax.set_xlim((-25, 25))
396
  ax.set_ylim((-25, 25))
397
 
398
+
399
+ df_aa = caa.calculate_arm_angles(df,df['pitcher_id'][0])['arm_angle']
400
+
401
+ # Plot average arm angle
402
+ mean_arm_angle = df_aa.mean()
403
+ x_end = 30
404
+ y_end = x_end * np.tan(np.radians(mean_arm_angle))
405
+ ax.plot([0, x_end], [0, y_end], color='grey', linestyle='--', linewidth=2,zorder=0)
406
+
407
+
408
+
409
  # Add horizontal and vertical lines
410
  ax.hlines(y=0, xmin=-50, xmax=50, color=colour_palette[8], alpha=0.5, linestyles='--', zorder=1)
411
  ax.vlines(x=0, ymin=-50, ymax=50, color=colour_palette[8], alpha=0.5, linestyles='--', zorder=1)
 
413
  # Set axis labels and title
414
  ax.set_xlabel('Horizontal Break (in)', fontdict=font_properties_axes)
415
  ax.set_ylabel('Induced Vertical Break (in)', fontdict=font_properties_axes)
416
+ ax.set_title(f"Pitch Breaks - Arm Angle: {mean_arm_angle:.0f}°", fontdict=font_properties_titles)
417
 
418
  # Remove legend
419
  ax.get_legend().remove()
 
556
  pl.col('spin_rate').mean().alias('spin_rate'),
557
  pl.col('vaa').mean().alias('vaa'),
558
  pl.col('haa').mean().alias('haa'),
559
+ pl.col('release_pos_z').mean().alias('z0'),
560
+ pl.col('release_pos_x').mean().alias('x0'),
561
  pl.col('extension').mean().alias('extension'),
562
  (((pl.col('spin_direction').mean() + 180) % 360 // 30) +
563
  (((pl.col('spin_direction').mean() + 180) % 360 % 30 / 30 / 100 * 60).round(2) * 10).round(0) // 1.5 / 4)
 
637
  # Apply color to specific columns based on normalized values
638
  columns_to_color = [(3, 'release_speed', 0.95, 1.05), (11, 'release_extension', 0.9, 1.1), (13, None, 80, 120),
639
  (14, None, 30, 70), (15, 'in_zone_rate', 0.7, 1.3), (16, 'chase_rate', 0.7, 1.3),
640
+ (17, 'whiff_rate', 0.7, 1.3), (18, 'xwobacon', 0.7, 1.3)]
641
 
642
  for col, stat, vmin_factor, vmax_factor in columns_to_color:
643
  cell_value = table.get_celld()[(i + 1, col)].get_text().get_text()
 
777
 
778
  # Display the player's name, handedness, age, height, and weight on the axis
779
  ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=56)
780
+ ax.text(0.5, 0.7, f'{pitcher_hand}HP, Age: {age}, {height}/{weight}', va='top', ha='center', fontsize=30)
781
  ax.text(0.5, 0.45, f'Season Pitching Summary', va='top', ha='center', fontsize=40)
782
 
783
  # Make API call to retrieve sports information
 
1068
  player_input: int,
1069
  sport_id: int,
1070
  ax: plt.Axes,
1071
+ split: str = 'All',
1072
+ game_type: list = ['R']):
1073
  """
1074
  Create a summary table of player statistics.
1075
 
 
1086
  split : str, optional
1087
  The split type (default is 'All').
1088
  """
1089
+
1090
+ type_dict = {'R':'Regular Season',
1091
+ 'S':'Spring',
1092
+ 'P':'Playoffs' }
1093
+
1094
+ split_title = {
1095
+ 'all':'',
1096
+ 'right':' vs RHH',
1097
+ 'left':' vs LHH'
1098
+ }
1099
+
1100
+
1101
  # Format start and end dates
1102
  start_date_format = str(pd.to_datetime(df['game_date'][0]).strftime('%m/%d/%Y'))
1103
  end_date_format = str(pd.to_datetime(df['game_date'][-1]).strftime('%m/%d/%Y'))
 
1105
  # Determine app context based on sport ID
1106
  appContext = 'majorLeague' if sport_id == 1 else 'minorLeague'
1107
 
1108
+ game_type_str = ','.join([str(x) for x in game_type])
1109
+
1110
  # Fetch player stats from MLB API
1111
  pitcher_stats_call = requests.get(
1112
+ f'https://statsapi.mlb.com/api/v1/people/{player_input}?appContext={appContext}&hydrate=stats(group=[pitching],type=[byDateRange],sportId={sport_id},startDate={start_date_format},endDate={end_date_format},gameType=[{game_type_str}])'
1113
  ).json()
1114
+ print('HERE')
1115
+ print(f'https://statsapi.mlb.com/api/v1/people/{player_input}?appContext={appContext}&hydrate=stats(group=[pitching],type=[byDateRange],sportId={sport_id},startDate={start_date_format},endDate={end_date_format},gameType=[{game_type_str}])')
1116
  # Extract stats and create DataFrame
1117
  pitcher_stats_call_header = [x for x in pitcher_stats_call['people'][0]['stats'][0]['splits'][-1]['stat']]
1118
  pitcher_stats_call_values = [pitcher_stats_call['people'][0]['stats'][0]['splits'][-1]['stat'][x] for x in pitcher_stats_call['people'][0]['stats'][0]['splits'][-1]['stat']]
 
1132
  if df['game_id'][0] == df['game_id'][-1]:
1133
  pitcher_stats_call_df_small = pitcher_stats_call_df.select(['inningsPitched', 'battersFaced', 'earnedRuns', 'hits', 'strikeOuts', 'baseOnBalls', 'hitByPitch', 'homeRuns', 'strikePercentage', 'whiffs'])
1134
  new_column_names = ['$\\bf{IP}$', '$\\bf{PA}$', '$\\bf{ER}$', '$\\bf{H}$', '$\\bf{K}$', '$\\bf{BB}$', '$\\bf{HBP}$', '$\\bf{HR}$', '$\\bf{Strike\%}$', '$\\bf{Whiffs}$']
1135
+ title = f'{df["game_date"][0]} vs {df["batter_team"][0]} ({type_dict[game_type[0]]}){split_title[split]}'
1136
+ elif sport_id != 1 or game_type[0] in ['S','P']:
1137
  pitcher_stats_call_df_small = pitcher_stats_call_df.select(['inningsPitched', 'battersFaced', 'whip', 'era', 'fip', 'k_percent', 'bb_percent', 'k_bb_percent', 'strikePercentage'])
1138
  new_column_names = ['$\\bf{IP}$', '$\\bf{PA}$', '$\\bf{WHIP}$', '$\\bf{ERA}$', '$\\bf{FIP}$', '$\\bf{K\%}$', '$\\bf{BB\%}$', '$\\bf{K-BB\%}$', '$\\bf{Strike\%}$']
1139
+ title = f'{df["game_date"][0]} to {df["game_date"][-1]} ({type_dict[game_type[0]]}{split_title[split]})'
1140
  else:
1141
  fangraphs_table(df=df, ax=ax, player_input=player_input, season=int(df['game_date'][0][0:4]), split=split)
1142
  return
 
1150
  # Add title to the plot
1151
  ax.text(0.5, 0.9, title, va='bottom', ha='center', fontsize=36, fontstyle='italic')
1152
  ax.axis('off')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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,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
 
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