nesticot commited on
Commit
e67a659
·
verified ·
1 Parent(s): b97f105

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +842 -842
app.py CHANGED
@@ -1,843 +1,843 @@
1
- import polars as pl
2
- import api_scraper
3
- import pandas as pd
4
- scrape = api_scraper.MLB_Scrape()
5
-
6
- # import df_update
7
- # update = df_update.df_update()
8
- from matplotlib.colors import LinearSegmentedColormap, Normalize
9
- import numpy as np
10
- import requests
11
- from io import BytesIO
12
- from PIL import Image
13
- from matplotlib.gridspec import GridSpec
14
-
15
- import matplotlib.pyplot as plt
16
- import matplotlib.patches as patches
17
- import PIL
18
-
19
- level_dict = {
20
- '11':'AAA',
21
- '14':'A',}
22
-
23
-
24
-
25
- def player_bio(pitcher_id: str, ax: plt.Axes, sport_id: int, year_input: int):
26
- """
27
- Display the player's bio information on the given axis.
28
- Parameters
29
- ----------
30
- pitcher_id : str
31
- The player's ID.
32
- ax : plt.Axes
33
- The axis to display the bio information on.
34
- sport_id : int
35
- The sport ID (1 for MLB, other for minor leagues).
36
- year_input : int
37
- The season year.
38
- """
39
- # Construct the URL to fetch player data
40
- url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam"
41
-
42
- # Send a GET request to the URL and parse the JSON response
43
- data = requests.get(url).json()
44
-
45
- # Extract player information from the JSON data
46
- player_name = data['people'][0]['fullName']
47
- position = data['people'][0]['primaryPosition']['abbreviation']
48
- bat_side = data['people'][0]['batSide']['code']
49
- pitcher_hand = data['people'][0]['pitchHand']['code']
50
- age = data['people'][0]['currentAge']
51
- height = data['people'][0]['height']
52
- weight = data['people'][0]['weight']
53
-
54
- # Display the player's name, handedness, age, height, and weight on the axis
55
- ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=30)
56
- ax.text(0.5, 0.65, f'{position}, B/T: {bat_side}/{pitcher_hand}, Age: {age}, {height}/{weight}', va='top', ha='center', fontsize=20)
57
- if position == 'P':
58
- ax.text(0.5, 0.38, f'Season Pitching Percentiles', va='top', ha='center', fontsize=16)
59
- else:
60
- ax.text(0.5, 0.41, f'Season Batting Percentiles', va='top', ha='center', fontsize=16)
61
-
62
- # Make API call to retrieve sports information
63
- response = requests.get(url='https://statsapi.mlb.com/api/v1/sports').json()
64
-
65
- # Convert the JSON response into a Polars DataFrame
66
- df_sport_id = pl.DataFrame(response['sports'])
67
- abb = df_sport_id.filter(pl.col('id') == sport_id)['abbreviation'][0]
68
-
69
- # Display the season and sport abbreviation
70
- ax.text(0.5, 0.20, f'{year_input} {abb} Season', va='top', ha='center', fontsize=14, fontstyle='italic')
71
-
72
- # Turn off the axis
73
- ax.axis('off')
74
-
75
-
76
- df_teams = scrape.get_teams()
77
- team_dict = dict(zip(df_teams['team_id'],df_teams['parent_org_abbreviation']))
78
-
79
-
80
- # List of MLB teams and their corresponding ESPN logo URLs
81
- mlb_teams = [
82
- {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
83
- {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
84
- {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
85
- {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
86
- {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
87
- {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
88
- {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
89
- {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
90
- {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
91
- {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
92
- {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
93
- {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
94
- {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
95
- {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
96
- {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
97
- {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
98
- {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
99
- {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
100
- {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
101
- {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
102
- {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
103
- {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
104
- {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
105
- {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
106
- {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
107
- {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
108
- {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
109
- {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
110
- {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
111
- {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"},
112
- {"team": "ZZZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png&w=500&h=500"}
113
- ]
114
-
115
- df_image = pd.DataFrame(mlb_teams)
116
- image_dict = df_image.set_index('team')['logo_url'].to_dict()
117
- image_dict_flip = df_image.set_index('logo_url')['team'].to_dict()
118
-
119
- # level_dict = {'1':'MLB',
120
- # '11':'AAA'}
121
-
122
- level_dict = {
123
- '11':'AAA',
124
- '14':'A (FSL)',}
125
-
126
-
127
- level_dict_file = {
128
- '11':'aaa',
129
- '14':'a',}
130
-
131
-
132
-
133
- year_list = [2024]
134
-
135
-
136
- from shiny import App, reactive, ui, render
137
- from shiny.ui import h2, tags
138
-
139
- # Define the UI layout for the app
140
- app_ui = ui.page_fluid(
141
-
142
-
143
- ui.tags.div(
144
- {"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
145
- ui.tags.style(
146
- """
147
- h4 {
148
- margin-top: 1em;font-size:35px;
149
- }
150
- h2{
151
- font-size:25px;
152
- }
153
- """
154
- ),
155
-
156
- ui.tags.h4("TJStats"),
157
- ui.tags.i("Baseball Analytics and Visualizations"),
158
- ui.markdown("""<a href='https://x.com/TJStats'>Follow me on Twitter</a><sup>1</sup>"""),
159
- ui.markdown("""<a href='https://www.patreon.com/tj_stats'>Support me on Patreon for Access to 2024 Apps</a><sup>1</sup>"""),
160
-
161
- ui.markdown("### MiLB Statcast Batting Summaries"),
162
- ui.markdown("""This Shiny App allows you to generate Baseball Savant-style percentile bars for MiLB players in the 2024 Season.
163
- Currently, MiLB Statcast is only available for AAA and A (Florida State League) levels."""),
164
-
165
- ui.layout_sidebar(
166
- ui.panel_sidebar(
167
- # Row for selecting season and level
168
- ui.row(
169
- ui.column(6, ui.input_select('year_input', 'Select Season', year_list, selected=2024)),
170
- ui.column(6, ui.input_select('level_input', 'Select Level', level_dict)),
171
- ),
172
- # Row for the action button to get player list
173
- ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
174
- # Row for selecting the player
175
- ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
176
-
177
- ui.row(
178
- ui.column(6, ui.input_switch("switch", "Custom Team?", False)),
179
- ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False))
180
- ),
181
-
182
- # Row for the action button to generate plot
183
- ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
184
- width=3,
185
- ),
186
-
187
- ui.panel_main(
188
- ui.navset_tab(
189
- # Tab for game summary plot
190
- ui.nav("Batter Summary",
191
- ui.output_text("status_batter"),
192
- ui.output_plot('batter_plot', width='1200px', height='1200px')
193
- ),
194
- ui.nav("Pitcher Summary",
195
- ui.output_text("status_pitcher"),
196
- ui.output_plot('pitcher_plot', width='1200px', height='1200px')
197
- )
198
- ,id="tabset"
199
- )
200
- )
201
- )
202
- )
203
- )
204
-
205
- def server(input, output, session):
206
- @render.ui
207
- @reactive.event(input.player_button,input.tabset, ignore_none=False)
208
- def player_select_ui():
209
- if input.tabset() == "Batter Summary":
210
- #Get the list of pitchers for the selected level and season
211
- df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input())).filter(
212
- ~pl.col("position").is_in(['P','TWP'])).sort("name")
213
-
214
-
215
-
216
- # Create a dictionary of pitcher IDs and names
217
- batter_dict_pos = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['position']))
218
-
219
- year = int(input.year_input())
220
- sport_id = int(input.level_input())
221
- batter_summary = pl.read_csv(f'data/statcast/batter_summary_{level_dict_file[str(sport_id)]}_{year}.csv').sort('batter_name',descending=False)
222
- # Map elements in Polars DataFrame from a dictionary
223
- batter_summary = batter_summary.with_columns(
224
- pl.col("batter_id").map_elements(lambda x: batter_dict_pos.get(x, x)).alias("position")
225
- )
226
-
227
-
228
- batter_dict_pos = dict(zip(batter_summary['batter_id'], batter_summary['batter_name']))
229
- # Create a dictionary of pitcher IDs and names
230
- batter_dict = dict(zip(batter_summary['batter_id'], batter_summary['batter_name'] + ' - ' + batter_summary['position']))
231
-
232
- # Return a select input for choosing a pitcher
233
- return ui.input_select("batter_id", "Select Batter", batter_dict, selectize=True)
234
-
235
- if input.tabset() == "Pitcher Summary":
236
- #Get the list of pitchers for the selected level and season
237
- df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input())).filter(
238
- pl.col("position").is_in(['P','TWP'])).sort("name")
239
-
240
-
241
-
242
- # Create a dictionary of pitcher IDs and names
243
- batter_dict_pos = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['position']))
244
-
245
- year = int(input.year_input())
246
- sport_id = int(input.level_input())
247
- batter_summary = pl.read_csv(f'data/statcast/pitcher_summary_{level_dict_file[str(sport_id)]}_{year}.csv').sort('pitcher_name',descending=False)
248
- # Map elements in Polars DataFrame from a dictionary
249
- batter_summary = batter_summary.with_columns(
250
- pl.col("pitcher_id").map_elements(lambda x: batter_dict_pos.get(x, x)).alias("position")
251
- )
252
-
253
-
254
- batter_dict_pos = dict(zip(batter_summary['pitcher_id'], batter_summary['pitcher_name']))
255
- # Create a dictionary of pitcher IDs and names
256
- batter_dict = dict(zip(batter_summary['pitcher_id'], batter_summary['pitcher_name'] + ' - ' + batter_summary['position']))
257
-
258
- # Return a select input for choosing a pitcher
259
- return ui.input_select("pitcher_id", "Select Batter", batter_dict, selectize=True)
260
-
261
-
262
-
263
- @output
264
- @render.plot
265
- @reactive.event(input.generate_plot, ignore_none=False)
266
- def batter_plot():
267
-
268
-
269
- merged_dict = {
270
- "woba_percent": { "format": '.3f', "percentile_flip": False, "stat_title": "wOBA" },
271
- "xwoba_percent": { "format": '.3f', "percentile_flip": False, "stat_title": "xwOBA" },
272
- "launch_speed": { "format": '.1f', "percentile_flip": False, "stat_title": "Average EV"},
273
- "launch_speed_90": { "format": '.1f', "percentile_flip": False, "stat_title": "90th% EV"},
274
- "max_launch_speed": { "format": '.1f', "percentile_flip": False, "stat_title": "Max EV"},
275
- "barrel_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Barrel%" },
276
- "hard_hit_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Hard-Hit%" },
277
- "sweet_spot_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "LA Sweet-Spot%" },
278
- "zone_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Zone%" },
279
- "zone_swing_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Z-Swing%" },
280
- "chase_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "O-Swing%" },
281
- "whiff_rate": { "format": '.1%', "percentile_flip": True, "stat_title": "Whiff%" },
282
- "k_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "K%" },
283
- "bb_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "BB%" },
284
- "pull_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Pull%" },
285
- "pulled_fly_ball_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Pull FB%" },
286
- }
287
- # Show progress/loading notification
288
- with ui.Progress(min=0, max=1) as p:
289
-
290
- def draw_baseball_savant_percentiles(new_player_metrics, new_player_percentiles, colors=None,
291
- sport_id=None,
292
- year_input=None):
293
- """
294
- Draw Baseball Savant-style percentile bars with proper alignment and scaling.
295
-
296
- :param new_player_metrics: DataFrame containing new player metrics.
297
- :param new_player_percentiles: DataFrame containing new player percentiles.
298
- :param colors: List of colors for bars (optional, red/blue default).
299
- """
300
- # Extract player information
301
- batter_id = new_player_metrics['batter_id'][0]
302
- player_name = batter_name_id[batter_id]
303
- stats = [merged_dict[x]['stat_title'] for x in merged_dict.keys()]
304
-
305
- # Calculate percentiles and values
306
- percentiles = [int((1 - x) * 100) if merged_dict[stat]["percentile_flip"] else int(x * 100) for x, stat in zip(new_player_percentiles.select(merged_dict.keys()).to_numpy()[0], merged_dict.keys())]
307
- percentiles = np.clip(percentiles, 1, 100)
308
- values = [str(f'{x:{merged_dict[stat]["format"]}}').strip('%') for x, stat in zip(new_player_metrics.select(merged_dict.keys()).to_numpy()[0], merged_dict.keys())]
309
-
310
- # Get team logo URL
311
- logo_url = image_dict[team_dict[player_team_dict[batter_id]]]
312
-
313
- # Create a custom colormap
314
- color_list = ['#3661AD', '#B4CFD1', '#D82129']
315
- cmap = LinearSegmentedColormap.from_list("custom_cmap", color_list)
316
- norm = Normalize(vmin=0.1, vmax=0.9)
317
- norm_percentiles = norm(percentiles / 100)
318
- colors = [cmap(p) for p in norm_percentiles]
319
-
320
- # Figure setup
321
- num_stats = len(stats)
322
- bar_height = 4.5
323
- spacing = 1
324
- fig_height = (bar_height + spacing) * num_stats
325
- fig = plt.figure(figsize=(12, 12))
326
- gs = GridSpec(6, 5, height_ratios=[0.1, 1.5, 0.9, 0.9, 7.6, 0.1], width_ratios=[0.2, 1.5, 7, 1.5, 0.2])
327
-
328
- # Define subplots
329
- ax_title = fig.add_subplot(gs[1, 2])
330
- ax_table = fig.add_subplot(gs[2, :])
331
- ax_fv_table = fig.add_subplot(gs[3, :])
332
- ax_fv_table.axis('off')
333
- ax = fig.add_subplot(gs[4, :])
334
- ax_logo = fig.add_subplot(gs[1, 3])
335
-
336
- ax.set_xlim(-1, 99)
337
- ax.set_ylim(-1, 99)
338
- ax.set_aspect("equal")
339
- ax.axis("off")
340
-
341
- # Draw each bar
342
- for i, (stat, percentile, value, color) in enumerate(zip(stats, percentiles, values, colors)):
343
- y = fig_height - (i + 1) * (bar_height + spacing)
344
- ax.add_patch(patches.Rectangle((0, y + bar_height / 4), 100, bar_height / 2, color="#C7DCDC", lw=0))
345
- ax.add_patch(patches.Rectangle((0, y), percentile, bar_height, color=color, lw=0))
346
- circle_y = y + bar_height - bar_height / 2
347
- circle = plt.Circle((percentile, circle_y), bar_height / 2, color=color, ec='white', lw=1.5, zorder=10)
348
- ax.add_patch(circle)
349
- fs = 14
350
- ax.text(percentile, circle_y, f"{percentile}", ha="center", va="center", fontsize=10, color='white', zorder=10, fontweight='bold')
351
- ax.text(-5, y + bar_height / 2, stat, ha="right", va="center", fontsize=fs)
352
- ax.text(115, y + bar_height / 2, str(value), ha="right", va="center", fontsize=fs, zorder=5)
353
- if i < len(stats) and i > 0:
354
- ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=-33, xmax=0)
355
- ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=100, xmax=115)
356
-
357
- # Draw vertical lines for 10%, 50%, and 90% with labels
358
- for x, label, align, color in zip([10, 50, 90], ["Poor", "Average", "Great"], ['center', 'center', 'center'], color_list):
359
- ax.axvline(x=x, ymin=0, ymax=1, color='#FFF', linestyle='-', lw=1, zorder=1, alpha=0.5)
360
- ax.text(x, fig_height + 4, label, ha=align, va='center', fontsize=12, fontweight='bold', color=color)
361
- triangle = patches.RegularPolygon((x, fig_height + 1), 3, radius=1, orientation=0, color=color, zorder=2)
362
- ax.add_patch(triangle)
363
-
364
- # # Title
365
- # ax_title.set_ylim(0, 1)
366
- # ax_title.text(0.5, 0.5, f"{player_name} - {player_position_dict[batter_id]}\nPercentile Rankings - 2024 AAA", ha="center", va="center", fontsize=24)
367
- # ax_title.axis("off")
368
- player_bio(batter_id, ax=ax_title, sport_id=sport_id, year_input=year_input)
369
-
370
- # Add team logo
371
- #response = requests.get(logo_url)
372
- if input.switch():
373
- response = requests.get(input.logo_select())
374
- else:
375
- response = requests.get(logo_url)
376
- img = Image.open(BytesIO(response.content))
377
- ax_logo.imshow(img)
378
- ax_logo.axis("off")
379
- ax.axis('equal')
380
-
381
- # Metrics data table
382
- metrics_data = {
383
- "Pitches": new_player_metrics['pitches'][0],
384
- "PA": new_player_metrics['pa'][0],
385
- "BIP": new_player_metrics['bip'][0],
386
- "HR": f"{new_player_metrics['home_run'][0]:.0f}",
387
- "AVG": f"{new_player_metrics['avg'][0]:.3f}",
388
- "OBP": f"{new_player_metrics['obp'][0]:.3f}",
389
- "SLG": f"{new_player_metrics['slg'][0]:.3f}",
390
- "OPS": f"{new_player_metrics['obp'][0] + new_player_metrics['slg'][0]:.3f}",
391
- }
392
- df_table = pd.DataFrame(metrics_data, index=[0])
393
- ax_table.axis('off')
394
- table = ax_table.table(cellText=df_table.values, colLabels=df_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])
395
- for key, cell in table.get_celld().items():
396
- if key[0] == 0:
397
- cell.set_text_props(fontweight='bold')
398
- table.auto_set_font_size(False)
399
- table.set_fontsize(12)
400
- table.scale(1, 1.5)
401
-
402
- # Additional subplots for spacing
403
- ax_top = fig.add_subplot(gs[0, :])
404
- ax_bot = fig.add_subplot(gs[-1, :])
405
- ax_top.axis('off')
406
- ax_bot.axis('off')
407
- ax_bot.text(0.05, 2, "By: Thomas Nestico (@TJStats)", ha="left", va="center", fontsize=14)
408
- ax_bot.text(0.95, 2, "Data: MLB, Fangraphs", ha="right", va="center", fontsize=14)
409
- fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
410
-
411
- # Player headshot
412
- ax_headshot = fig.add_subplot(gs[1, 1])
413
- try:
414
- url = f'https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto/w_640/v1/people/{batter_id}/headshot/milb/current.png'
415
- response = requests.get(url)
416
- img = Image.open(BytesIO(response.content))
417
- ax_headshot.set_xlim(0, 1)
418
- ax_headshot.set_ylim(0, 1)
419
- ax_headshot.imshow(img, extent=[1/6, 5/6, 0, 1], origin='upper')
420
- except PIL.UnidentifiedImageError:
421
- ax_headshot.axis('off')
422
- #return
423
- ax_headshot.axis('off')
424
- ax_table.set_title('Season Summary', style='italic')
425
-
426
- # Fangraphs scouting grades table
427
- print(batter_id)
428
-
429
- if batter_id not in dict_mlb_fg.keys():
430
- ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))
431
- return
432
- df_fv_table = df_prospects[(df_prospects['minorMasterId'] == dict_mlb_fg[batter_id])][['cFV', 'Hit', 'Game', 'Raw', 'Spd', 'Fld']].reset_index(drop=True)
433
- ax_fv_table.axis('off')
434
- if df_fv_table.empty:
435
- ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))
436
- return
437
- df_fv_table.columns = ['FV', 'Hit', 'Game', 'Raw', 'Spd', 'Fld']
438
- table_fv = ax_fv_table.table(cellText=df_fv_table.values, colLabels=df_fv_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])
439
- for key, cell in table_fv.get_celld().items():
440
- if key[0] == 0:
441
- cell.set_text_props(fontweight='bold')
442
- table_fv.auto_set_font_size(False)
443
- table_fv.set_fontsize(12)
444
- table_fv.scale(1, 1.5)
445
- ax_fv_table.set_title('Fangraphs Scouting Grades', style='italic')
446
-
447
-
448
-
449
- #plt.show()
450
-
451
-
452
- def calculate_new_player_percentiles(player_id, new_player_metrics, player_summary_filtered):
453
- """
454
- Calculate percentiles for a new player's metrics.
455
-
456
- :param player_id: ID of the player.
457
- :param new_player_metrics: DataFrame containing new player metrics.
458
- :param player_summary_filtered: Filtered player summary DataFrame.
459
- :return: DataFrame containing new player percentiles.
460
- """
461
- filtered_summary_clone = player_summary_filtered[['batter_id'] + stat_list].filter(pl.col('batter_id') != player_id).clone()
462
- combined_data = pl.concat([filtered_summary_clone, new_player_metrics], how="vertical").to_pandas()
463
- combined_percentiles = pl.DataFrame(pd.concat([combined_data['batter_id'], combined_data[stat_list].rank(pct=True)], axis=1))
464
- new_player_percentiles = combined_percentiles.filter(pl.col('batter_id') == player_id)
465
- return new_player_percentiles
466
-
467
-
468
-
469
- p.set(message="Generating plot", detail="This may take a while...")
470
-
471
-
472
- p.set(0.3, "Gathering data...")
473
-
474
- # Example: New player's metrics
475
- year = int(input.year_input())
476
- sport_id = int(input.level_input())
477
- batter_id = int(input.batter_id())
478
-
479
-
480
- df_player = scrape.get_players(sport_id=sport_id,season=year)
481
- batter_name_id = dict(zip(df_player['player_id'],df_player['name']))
482
- player_team_dict = dict(zip(df_player['player_id'],df_player['team']))
483
- player_position_dict = dict(zip(df_player['player_id'],df_player['position']))
484
-
485
-
486
- batter_summary = pl.read_csv(f'data/statcast/batter_summary_{level_dict_file[str(sport_id)]}_{year}.csv')
487
- df_prospects = pd.read_csv(f'data/prospects/prospects_{year}.csv')
488
- df_rosters = pd.read_csv(f'data/rosters/fangraphs_rosters_{year}.csv')
489
- df_small = df_rosters[['minorbamid','minormasterid']].dropna()
490
- dict_mlb_fg=dict(zip(df_small['minorbamid'].astype(int),df_small['minormasterid']))
491
-
492
-
493
-
494
-
495
- batter_summary_filter = batter_summary.filter((pl.col('pa') >= 300) & (pl.col('launch_speed') >= 0))
496
- stat_list = batter_summary.columns[2:]
497
- batter_summary_filter_pd = batter_summary_filter.to_pandas()
498
- new_player_metrics = batter_summary.filter(pl.col('batter_id') == batter_id)[['batter_id'] + stat_list]
499
-
500
- # Get percentiles for the new player
501
- new_player_percentiles = calculate_new_player_percentiles(batter_id, new_player_metrics, batter_summary_filter)
502
-
503
- p.set(0.6, "Creating plot...")
504
- # Draw Baseball Savant-style percentile bars
505
- draw_baseball_savant_percentiles(new_player_metrics=new_player_metrics,
506
- new_player_percentiles=new_player_percentiles,
507
- sport_id=sport_id,
508
- year_input=year)
509
-
510
- @output
511
- @render.plot
512
- @reactive.event(input.generate_plot, ignore_none=False)
513
- def pitcher_plot():
514
- merged_dict = {
515
- "avg_start_speed_ff": { "format": '.1f', "percentile_flip": False, "stat_title": "Fastball Velocity" },
516
- "extension": { "format": '.1f', "percentile_flip": False, "stat_title": "Extension" },
517
- "woba_percent": { "format": '.3f', "percentile_flip": True, "stat_title": "wOBA" },
518
- "xwoba_percent": { "format": '.3f', "percentile_flip": True, "stat_title": "xwOBA" },
519
- "launch_speed": { "format": '.1f', "percentile_flip": True, "stat_title": "Average EV"},
520
- "barrel_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "Barrel%" },
521
- "hard_hit_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "Hard-Hit%" },
522
- "whiff_rate": { "format": '.1%', "percentile_flip": False, "stat_title": "Whiff%" },
523
- "zone_contact_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "Z-Contact%" },
524
- "zone_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Zone%" },
525
- "chase_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "O-Swing%" },
526
- "csw_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "CSW%" },
527
- "k_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "K%" },
528
- "bb_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "BB%" },
529
- "k_minus_bb_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "K - BB%" },
530
- "ground_ball_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "GB%" },
531
- }
532
-
533
- with ui.Progress(min=0, max=1) as p:
534
-
535
- def draw_baseball_savant_percentiles(new_player_metrics, new_player_percentiles, colors=None,
536
- sport_id=None,
537
- year_input=None):
538
- """
539
- Draw Baseball Savant-style percentile bars with proper alignment and scaling.
540
-
541
- :param new_player_metrics: DataFrame containing new player metrics.
542
- :param new_player_percentiles: DataFrame containing new player percentiles.
543
- :param colors: List of colors for bars (optional, red/blue default).
544
- """
545
- # Extract player information
546
- pitcher_id = new_player_metrics['pitcher_id'][0]
547
- player_name = pitcher_name_id[pitcher_id]
548
- stats = [merged_dict[x]['stat_title'] for x in merged_dict.keys()]
549
-
550
- # Calculate percentiles and values
551
- percentiles = [int((1 - x) * 100) if merged_dict[stat]["percentile_flip"] else int(x * 100) for x, stat in zip(new_player_percentiles.select(merged_dict.keys()).to_numpy()[0], merged_dict.keys())]
552
- percentiles = np.clip(percentiles, 1, 100)
553
- values = [str(f'{x:{merged_dict[stat]["format"]}}').strip('%') for x, stat in zip(new_player_metrics.select(merged_dict.keys()).to_numpy()[0], merged_dict.keys())]
554
-
555
- # Get team logo URL
556
- logo_url = image_dict[team_dict[player_team_dict[pitcher_id]]]
557
-
558
- # Create a custom colormap
559
- color_list = ['#3661AD', '#B4CFD1', '#D82129']
560
- cmap = LinearSegmentedColormap.from_list("custom_cmap", color_list)
561
- norm = Normalize(vmin=0.1, vmax=0.9)
562
- norm_percentiles = norm(percentiles / 100)
563
- colors = [cmap(p) for p in norm_percentiles]
564
-
565
- # Figure setup
566
- num_stats = len(stats)
567
- bar_height = 4.4
568
- spacing = 0.7
569
- fig_height = (bar_height + spacing) * num_stats
570
- fig = plt.figure(figsize=(12, 12))
571
- gs = GridSpec(7, 5, height_ratios=[0.05, 1.5, 0.75, 0.75,0.75, 7.7, 0.1], width_ratios=[0.2, 1.5, 7, 1.5, 0.2])
572
-
573
- # Define subplots
574
- ax_title = fig.add_subplot(gs[1, 2])
575
- ax_table = fig.add_subplot(gs[2, :])
576
- ax_fv_table = fig.add_subplot(gs[3, :])
577
- ax_fv_table.axis('off')
578
- ax_stuff = fig.add_subplot(gs[4, :])
579
- ax = fig.add_subplot(gs[5, :])
580
- ax_logo = fig.add_subplot(gs[1, 3])
581
-
582
- ax.set_xlim(-1, 99)
583
- ax.set_ylim(-1, 99)
584
- ax.set_aspect("equal")
585
- ax.axis("off")
586
-
587
- # Draw each bar
588
- for i, (stat, percentile, value, color) in enumerate(zip(stats, percentiles, values, colors)):
589
- y = fig_height - (i + 1) * (bar_height + spacing)
590
- ax.add_patch(patches.Rectangle((0, y + bar_height / 4), 100, bar_height / 2, color="#C7DCDC", lw=0))
591
- ax.add_patch(patches.Rectangle((0, y), percentile, bar_height, color=color, lw=0))
592
- circle_y = y + bar_height - bar_height / 2
593
- circle = plt.Circle((percentile, circle_y), bar_height / 2, color=color, ec='white', lw=1.5, zorder=10)
594
- ax.add_patch(circle)
595
- fs = 14
596
- ax.text(percentile, circle_y, f"{percentile}", ha="center", va="center", fontsize=10, color='white', zorder=10, fontweight='bold')
597
- ax.text(-5, y + bar_height / 2, stat, ha="right", va="center", fontsize=fs)
598
- ax.text(115, y + bar_height / 2, str(value), ha="right", va="center", fontsize=fs, zorder=5)
599
- if i < len(stats) and i > 0:
600
- ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=-33, xmax=0)
601
- ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=100, xmax=115)
602
-
603
- # Draw vertical lines for 10%, 50%, and 90% with labels
604
- for x, label, align, color in zip([10, 50, 90], ["Poor", "Average", "Great"], ['center', 'center', 'center'], color_list):
605
- ax.axvline(x=x, ymin=0, ymax=1, color='#FFF', linestyle='-', lw=1, zorder=1, alpha=0.5)
606
- ax.text(x, fig_height + 4, label, ha=align, va='center', fontsize=12, fontweight='bold', color=color)
607
- triangle = patches.RegularPolygon((x, fig_height + 1), 3, radius=1, orientation=0, color=color, zorder=2)
608
- ax.add_patch(triangle)
609
-
610
- # # Title
611
- # ax_title.set_ylim(0, 1)
612
- # ax_title.text(0.5, 0.5, f"{player_name} - {player_position_dict[pitcher_id]}\nPercentile Rankings - 2024 AAA", ha="center", va="center", fontsize=24)
613
- # ax_title.axis("off")
614
- player_bio(pitcher_id, ax=ax_title, sport_id=sport_id, year_input=year_input)
615
-
616
- # Add team logo
617
- #response = requests.get(logo_url)
618
- #######if input.switch():
619
- ######## response = requests.get(input.logo_select())
620
- ######else:
621
- response = requests.get(logo_url)
622
- img = Image.open(BytesIO(response.content))
623
- ax_logo.imshow(img)
624
- ax_logo.axis("off")
625
- ax.axis('equal')
626
- lg_dict = {
627
- 11:'all',
628
- 14:10
629
- }
630
- levelt = {
631
- 11:1,
632
- 14:4
633
- }
634
-
635
-
636
- fg_api = f'https://www.fangraphs.com/api/leaders/minor-league/data?pos=all&level={levelt[sport_id]}&lg={lg_dict[sport_id]}&stats=pit&qual=0&type=2&team=&season=2024&seasonEnd=2024&org=&ind=0&splitTeam=false'
637
- response = requests.get(fg_api)
638
- data = response.json()
639
- df_fg = pl.DataFrame(data)
640
- if pitcher_id not in dict_mlb_fg.keys():
641
- #ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))
642
- metrics_data = {
643
- "Pitches": new_player_metrics['pitches'][0],
644
- "PA": new_player_metrics['pa'][0],
645
- "BIP": new_player_metrics['bip'][0],
646
- "HR": f"{new_player_metrics['home_run'][0]:.0f}",
647
- "K": f"{new_player_metrics['k'][0]:.0f}",
648
- "BB": f"{new_player_metrics['bb'][0]:.0f}",
649
- }
650
- else:
651
- df_fg_filter = df_fg.filter(pl.col('minormasterid') == dict_mlb_fg[pitcher_id])
652
- # Metrics data table
653
- metrics_data = {
654
- "G": f"{df_fg_filter['G'][0]:.0f}",
655
- "IP": f"{df_fg_filter['IP'][0]:.1f}",
656
- "Pitches": f"{new_player_metrics['pitches'][0]:.0f}",
657
- "PA": f"{df_fg_filter['TBF'][0]:.0f}",
658
- "BIP": new_player_metrics['bip'][0],
659
- "ERA": f"{df_fg_filter['ERA'][0]:.2f}",
660
- "FIP": f"{df_fg_filter['FIP'][0]:.2f}",
661
- "WHIP": f"{df_fg_filter['WHIP'][0]:.2f}",
662
- }
663
- df_table = pd.DataFrame(metrics_data, index=[0])
664
- ax_table.axis('off')
665
- table = ax_table.table(cellText=df_table.values, colLabels=df_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])
666
- for key, cell in table.get_celld().items():
667
- if key[0] == 0:
668
- cell.set_text_props(fontweight='bold')
669
- table.auto_set_font_size(False)
670
- table.set_fontsize(12)
671
- table.scale(1, 1.5)
672
-
673
- # Additional subplots for spacing
674
- ax_top = fig.add_subplot(gs[0, :])
675
- ax_bot = fig.add_subplot(gs[-1, :])
676
- ax_top.axis('off')
677
- ax_bot.axis('off')
678
- ax_bot.text(0.05, 2, "By: Thomas Nestico (@TJStats)", ha="left", va="center", fontsize=14)
679
- ax_bot.text(0.95, 2, "Data: MLB, Fangraphs", ha="right", va="center", fontsize=14)
680
-
681
-
682
- # Player headshot
683
- ax_headshot = fig.add_subplot(gs[1, 1])
684
- try:
685
- url = f'https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto/w_640/v1/people/{pitcher_id}/headshot/milb/current.png'
686
- response = requests.get(url)
687
- img = Image.open(BytesIO(response.content))
688
- ax_headshot.set_xlim(0, 1)
689
- ax_headshot.set_ylim(0, 1)
690
- ax_headshot.imshow(img, extent=[1/6, 5/6, 0, 1], origin='upper')
691
- except PIL.UnidentifiedImageError:
692
- ax_headshot.axis('off')
693
- #return
694
- ax_headshot.axis('off')
695
- ax_table.set_title('Season Summary', style='italic')
696
-
697
- # Fangraphs scouting grades table
698
- print(pitcher_id)
699
-
700
- if pitcher_id not in dict_mlb_fg.keys():
701
- ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))
702
- #return
703
- df_fv_table = df_prospects[(df_prospects['minorMasterId'] == dict_mlb_fg[pitcher_id])][['cFV','FB', 'SL', 'CB', 'CH', 'SPL', 'CT','CMD']].dropna(axis=1).reset_index(drop=True)
704
- ax_fv_table.axis('off')
705
- if df_fv_table.empty:
706
- ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))
707
- #return
708
- else:
709
- df_fv_table.columns = ['FV']+[x.upper() for x in df_fv_table.columns[1:]]
710
- table_fv = ax_fv_table.table(cellText=df_fv_table.values, colLabels=df_fv_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])
711
- for key, cell in table_fv.get_celld().items():
712
- if key[0] == 0:
713
- cell.set_text_props(fontweight='bold')
714
- table_fv.auto_set_font_size(False)
715
- table_fv.set_fontsize(12)
716
- table_fv.scale(1, 1.5)
717
- ax_fv_table.set_title('Fangraphs Scouting Grades', style='italic')
718
-
719
-
720
- # df_stuff_filter = df_stuff.filter(pl.col('pitcher_id')==pitcher_id)
721
-
722
- stuff_table = ax_stuff.table(cellText=[df_stuff_filter['tj_stuff_plus']],
723
- colLabels=df_stuff_filter['pitch_type'],
724
- cellLoc='center',
725
- loc='center', bbox=[0.07, 0, 0.86, 1])
726
- stuff_table.auto_set_font_size(False)
727
- stuff_table.set_fontsize(12)
728
- stuff_table.scale(1, 1.5)
729
- ax_stuff.axis('off')
730
- ax_stuff.set_title('tjStuff+', style='italic')
731
- for key, cell in stuff_table.get_celld().items():
732
- if key[0] == 0:
733
- cell.set_text_props(fontweight='bold')
734
-
735
- # Color the stuff_table values based on the cmap defined
736
- for (i, j), cell in stuff_table.get_celld().items():
737
- if i == 0:
738
- cell.set_text_props(fontweight='bold')
739
- else:
740
- norm = Normalize(vmin=90, vmax=110)
741
- value = float(cell.get_text().get_text())
742
- color = cmap(norm(value))
743
- cell.set_facecolor(color)
744
- #cell.set_text_props(color='white' if value < 100 else 'black')
745
-
746
-
747
-
748
-
749
-
750
- fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
751
-
752
-
753
-
754
-
755
-
756
- def calculate_new_player_percentiles(player_id, new_player_metrics, player_summary_filtered):
757
- """
758
- Calculate percentiles for a new player's metrics.
759
-
760
- :param player_id: ID of the player.
761
- :param new_player_metrics: DataFrame containing new player metrics.
762
- :param player_summary_filtered: Filtered player summary DataFrame.
763
- :return: DataFrame containing new player percentiles.
764
- """
765
- filtered_summary_clone = player_summary_filtered[['pitcher_id'] + stat_list].filter(pl.col('pitcher_id') != player_id).clone()
766
- combined_data = pl.concat([filtered_summary_clone, new_player_metrics], how="vertical").to_pandas()
767
- combined_percentiles = pl.DataFrame(pd.concat([combined_data['pitcher_id'], combined_data[stat_list].rank(pct=True)], axis=1))
768
- new_player_percentiles = combined_percentiles.filter(pl.col('pitcher_id') == player_id)
769
- return new_player_percentiles
770
-
771
- p.set(message="Generating plot", detail="This may take a while...")
772
-
773
-
774
- p.set(0.3, "Gathering data...")
775
-
776
-
777
- df_teams = scrape.get_teams()
778
- team_dict = dict(zip(df_teams['team_id'],df_teams['parent_org_abbreviation']))
779
-
780
- # Example: New player's metrics
781
- # Example: New player's metrics
782
- year = int(input.year_input())
783
- sport_id = int(input.level_input())
784
- pitcher_id = int(input.pitcher_id())
785
-
786
- df_player = scrape.get_players(sport_id=sport_id,season=2024)
787
- pitcher_name_id = dict(zip(df_player['player_id'],df_player['name']))
788
- player_team_dict = dict(zip(df_player['player_id'],df_player['team']))
789
- player_position_dict = dict(zip(df_player['player_id'],df_player['position']))
790
- player_position_dict = dict(zip(df_player['player_id'],df_player['position']))
791
-
792
-
793
-
794
-
795
- pitcher_summary = pl.read_csv(f'data/statcast/pitcher_summary_{level_dict_file[str(sport_id)]}_{year}.csv')
796
- df_prospects = pd.read_csv(f'data/prospects/prospects_{year}.csv')
797
- df_rosters = pd.read_csv(f'data/rosters/fangraphs_rosters_{year}.csv')
798
- df_small = df_rosters[['minorbamid','minormasterid']].dropna()
799
- dict_mlb_fg=dict(zip(df_small['minorbamid'].astype(int),df_small['minormasterid']))
800
-
801
- df_stuff = pl.read_csv(f'data/stuff/stuff_{level_dict_file[str(sport_id)]}_{year}.csv')
802
- # Filter out the "All" row
803
- filtered_df = df_stuff.filter(pl.col("pitch_type") != "All")
804
-
805
- filtered_all_df = df_stuff.filter(pl.col("pitch_type") == "All")
806
- # Calculate total pitches for each pitcher and proportion of each pitch type
807
- result_df = (
808
- filtered_df
809
- .with_columns([
810
- # Total pitches for each pitcher
811
- pl.col("pitches").sum().over("pitcher_id").alias("total_pitches"),
812
- # Proportion of pitches
813
- (pl.col("pitches") / pl.col("pitches").sum().over("pitcher_id")).alias("pitch_proportion"),
814
- ])
815
- ).filter(pl.col("pitch_proportion") > 0.05)
816
-
817
- df_stuff = pl.concat([filtered_all_df.with_columns(
818
- [pl.col("pitches").sum().over("pitcher_id").alias("total_pitches"),
819
- (pl.col("pitches") / pl.col("pitches").sum().over("pitcher_id")).alias("pitch_proportion")]
820
- ), result_df])
821
-
822
-
823
-
824
-
825
- df_stuff_filter = df_stuff.filter(pl.col('pitcher_id')==pitcher_id)
826
-
827
- pitcher_summary_filter = pitcher_summary.filter((pl.col('pa') >= 300) & (pl.col('launch_speed') >= 0))
828
- stat_list = pitcher_summary.columns[2:]
829
- pitcher_summary_filter_pd = pitcher_summary_filter.to_pandas()
830
- new_player_metrics = pitcher_summary.filter(pl.col('pitcher_id') == pitcher_id)[['pitcher_id'] + stat_list]
831
-
832
- # Get percentiles for the new player
833
- new_player_percentiles = calculate_new_player_percentiles(pitcher_id, new_player_metrics, pitcher_summary_filter)
834
-
835
- p.set(0.6, "Creating plot...")
836
- # Draw Baseball Savant-style percentile bars
837
- draw_baseball_savant_percentiles(new_player_metrics=new_player_metrics,
838
- new_player_percentiles=new_player_percentiles,
839
- sport_id=sport_id,
840
- year_input=year)
841
-
842
-
843
  app = App(app_ui, server)
 
1
+ import polars as pl
2
+ import api_scraper
3
+ import pandas as pd
4
+ scrape = api_scraper.MLB_Scrape()
5
+
6
+ # import df_update
7
+ # update = df_update.df_update()
8
+ from matplotlib.colors import LinearSegmentedColormap, Normalize
9
+ import numpy as np
10
+ import requests
11
+ from io import BytesIO
12
+ from PIL import Image
13
+ from matplotlib.gridspec import GridSpec
14
+
15
+ import matplotlib.pyplot as plt
16
+ import matplotlib.patches as patches
17
+ import PIL
18
+
19
+ level_dict = {'1':'MLB',
20
+ '11':'AAA',
21
+ '14':'A',}
22
+
23
+
24
+
25
+ def player_bio(pitcher_id: str, ax: plt.Axes, sport_id: int, year_input: int):
26
+ """
27
+ Display the player's bio information on the given axis.
28
+ Parameters
29
+ ----------
30
+ pitcher_id : str
31
+ The player's ID.
32
+ ax : plt.Axes
33
+ The axis to display the bio information on.
34
+ sport_id : int
35
+ The sport ID (1 for MLB, other for minor leagues).
36
+ year_input : int
37
+ The season year.
38
+ """
39
+ # Construct the URL to fetch player data
40
+ url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam"
41
+
42
+ # Send a GET request to the URL and parse the JSON response
43
+ data = requests.get(url).json()
44
+
45
+ # Extract player information from the JSON data
46
+ player_name = data['people'][0]['fullName']
47
+ position = data['people'][0]['primaryPosition']['abbreviation']
48
+ bat_side = data['people'][0]['batSide']['code']
49
+ pitcher_hand = data['people'][0]['pitchHand']['code']
50
+ age = data['people'][0]['currentAge']
51
+ height = data['people'][0]['height']
52
+ weight = data['people'][0]['weight']
53
+
54
+ # Display the player's name, handedness, age, height, and weight on the axis
55
+ ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=30)
56
+ ax.text(0.5, 0.65, f'{position}, B/T: {bat_side}/{pitcher_hand}, Age: {age}, {height}/{weight}', va='top', ha='center', fontsize=20)
57
+ if position == 'P':
58
+ ax.text(0.5, 0.38, f'Season Pitching Percentiles', va='top', ha='center', fontsize=16)
59
+ else:
60
+ ax.text(0.5, 0.41, f'Season Batting Percentiles', va='top', ha='center', fontsize=16)
61
+
62
+ # Make API call to retrieve sports information
63
+ response = requests.get(url='https://statsapi.mlb.com/api/v1/sports').json()
64
+
65
+ # Convert the JSON response into a Polars DataFrame
66
+ df_sport_id = pl.DataFrame(response['sports'])
67
+ abb = df_sport_id.filter(pl.col('id') == sport_id)['abbreviation'][0]
68
+
69
+ # Display the season and sport abbreviation
70
+ ax.text(0.5, 0.20, f'{year_input} {abb} Season', va='top', ha='center', fontsize=14, fontstyle='italic')
71
+
72
+ # Turn off the axis
73
+ ax.axis('off')
74
+
75
+
76
+ df_teams = scrape.get_teams()
77
+ team_dict = dict(zip(df_teams['team_id'],df_teams['parent_org_abbreviation']))
78
+
79
+
80
+ # List of MLB teams and their corresponding ESPN logo URLs
81
+ mlb_teams = [
82
+ {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
83
+ {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
84
+ {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
85
+ {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
86
+ {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
87
+ {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
88
+ {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
89
+ {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
90
+ {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
91
+ {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
92
+ {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
93
+ {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
94
+ {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
95
+ {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
96
+ {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
97
+ {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
98
+ {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
99
+ {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
100
+ {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
101
+ {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
102
+ {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
103
+ {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
104
+ {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
105
+ {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
106
+ {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
107
+ {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
108
+ {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
109
+ {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
110
+ {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
111
+ {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"},
112
+ {"team": "ZZZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png&w=500&h=500"}
113
+ ]
114
+
115
+ df_image = pd.DataFrame(mlb_teams)
116
+ image_dict = df_image.set_index('team')['logo_url'].to_dict()
117
+ image_dict_flip = df_image.set_index('logo_url')['team'].to_dict()
118
+
119
+ # level_dict = {'1':'MLB',
120
+ # '11':'AAA'}
121
+
122
+ level_dict = {'1':'MLB',
123
+ '11':'AAA',
124
+ '14':'A (FSL)',}
125
+
126
+
127
+ level_dict_file = {'1':'mlb',
128
+ '11':'aaa',
129
+ '14':'a',}
130
+
131
+
132
+
133
+ year_list = [2024]
134
+
135
+
136
+ from shiny import App, reactive, ui, render
137
+ from shiny.ui import h2, tags
138
+
139
+ # Define the UI layout for the app
140
+ app_ui = ui.page_fluid(
141
+
142
+
143
+ ui.tags.div(
144
+ {"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
145
+ ui.tags.style(
146
+ """
147
+ h4 {
148
+ margin-top: 1em;font-size:35px;
149
+ }
150
+ h2{
151
+ font-size:25px;
152
+ }
153
+ """
154
+ ),
155
+
156
+ ui.tags.h4("TJStats"),
157
+ ui.tags.i("Baseball Analytics and Visualizations"),
158
+ ui.markdown("""<a href='https://x.com/TJStats'>Follow me on Twitter</a><sup>1</sup>"""),
159
+ ui.markdown("""<a href='https://www.patreon.com/tj_stats'>Support me on Patreon for Access to 2024 Apps</a><sup>1</sup>"""),
160
+
161
+ ui.markdown("### MiLB Statcast Batting Summaries"),
162
+ ui.markdown("""This Shiny App allows you to generate Baseball Savant-style percentile bars for MiLB players in the 2024 Season.
163
+ Currently, MiLB Statcast is only available for AAA and A (Florida State League) levels."""),
164
+
165
+ ui.layout_sidebar(
166
+ ui.panel_sidebar(
167
+ # Row for selecting season and level
168
+ ui.row(
169
+ ui.column(6, ui.input_select('year_input', 'Select Season', year_list, selected=2024)),
170
+ ui.column(6, ui.input_select('level_input', 'Select Level', level_dict)),
171
+ ),
172
+ # Row for the action button to get player list
173
+ ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
174
+ # Row for selecting the player
175
+ ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
176
+
177
+ ui.row(
178
+ ui.column(6, ui.input_switch("switch", "Custom Team?", False)),
179
+ ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False))
180
+ ),
181
+
182
+ # Row for the action button to generate plot
183
+ ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
184
+ width=3,
185
+ ),
186
+
187
+ ui.panel_main(
188
+ ui.navset_tab(
189
+ # Tab for game summary plot
190
+ ui.nav("Batter Summary",
191
+ ui.output_text("status_batter"),
192
+ ui.output_plot('batter_plot', width='1200px', height='1200px')
193
+ ),
194
+ ui.nav("Pitcher Summary",
195
+ ui.output_text("status_pitcher"),
196
+ ui.output_plot('pitcher_plot', width='1200px', height='1200px')
197
+ )
198
+ ,id="tabset"
199
+ )
200
+ )
201
+ )
202
+ )
203
+ )
204
+
205
+ def server(input, output, session):
206
+ @render.ui
207
+ @reactive.event(input.player_button,input.tabset, ignore_none=False)
208
+ def player_select_ui():
209
+ if input.tabset() == "Batter Summary":
210
+ #Get the list of pitchers for the selected level and season
211
+ df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input())).filter(
212
+ ~pl.col("position").is_in(['P','TWP'])).sort("name")
213
+
214
+
215
+
216
+ # Create a dictionary of pitcher IDs and names
217
+ batter_dict_pos = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['position']))
218
+
219
+ year = int(input.year_input())
220
+ sport_id = int(input.level_input())
221
+ batter_summary = pl.read_csv(f'data/statcast/batter_summary_{level_dict_file[str(sport_id)]}_{year}.csv').sort('batter_name',descending=False)
222
+ # Map elements in Polars DataFrame from a dictionary
223
+ batter_summary = batter_summary.with_columns(
224
+ pl.col("batter_id").map_elements(lambda x: batter_dict_pos.get(x, x)).alias("position")
225
+ )
226
+
227
+
228
+ batter_dict_pos = dict(zip(batter_summary['batter_id'], batter_summary['batter_name']))
229
+ # Create a dictionary of pitcher IDs and names
230
+ batter_dict = dict(zip(batter_summary['batter_id'], batter_summary['batter_name'] + ' - ' + batter_summary['position']))
231
+
232
+ # Return a select input for choosing a pitcher
233
+ return ui.input_select("batter_id", "Select Batter", batter_dict, selectize=True)
234
+
235
+ if input.tabset() == "Pitcher Summary":
236
+ #Get the list of pitchers for the selected level and season
237
+ df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input())).filter(
238
+ pl.col("position").is_in(['P','TWP'])).sort("name")
239
+
240
+
241
+
242
+ # Create a dictionary of pitcher IDs and names
243
+ batter_dict_pos = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['position']))
244
+
245
+ year = int(input.year_input())
246
+ sport_id = int(input.level_input())
247
+ batter_summary = pl.read_csv(f'data/statcast/pitcher_summary_{level_dict_file[str(sport_id)]}_{year}.csv').sort('pitcher_name',descending=False)
248
+ # Map elements in Polars DataFrame from a dictionary
249
+ batter_summary = batter_summary.with_columns(
250
+ pl.col("pitcher_id").map_elements(lambda x: batter_dict_pos.get(x, x)).alias("position")
251
+ )
252
+
253
+
254
+ batter_dict_pos = dict(zip(batter_summary['pitcher_id'], batter_summary['pitcher_name']))
255
+ # Create a dictionary of pitcher IDs and names
256
+ batter_dict = dict(zip(batter_summary['pitcher_id'], batter_summary['pitcher_name'] + ' - ' + batter_summary['position']))
257
+
258
+ # Return a select input for choosing a pitcher
259
+ return ui.input_select("pitcher_id", "Select Batter", batter_dict, selectize=True)
260
+
261
+
262
+
263
+ @output
264
+ @render.plot
265
+ @reactive.event(input.generate_plot, ignore_none=False)
266
+ def batter_plot():
267
+
268
+
269
+ merged_dict = {
270
+ "woba_percent": { "format": '.3f', "percentile_flip": False, "stat_title": "wOBA" },
271
+ "xwoba_percent": { "format": '.3f', "percentile_flip": False, "stat_title": "xwOBA" },
272
+ "launch_speed": { "format": '.1f', "percentile_flip": False, "stat_title": "Average EV"},
273
+ "launch_speed_90": { "format": '.1f', "percentile_flip": False, "stat_title": "90th% EV"},
274
+ "max_launch_speed": { "format": '.1f', "percentile_flip": False, "stat_title": "Max EV"},
275
+ "barrel_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Barrel%" },
276
+ "hard_hit_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Hard-Hit%" },
277
+ "sweet_spot_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "LA Sweet-Spot%" },
278
+ "zone_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Zone%" },
279
+ "zone_swing_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Z-Swing%" },
280
+ "chase_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "O-Swing%" },
281
+ "whiff_rate": { "format": '.1%', "percentile_flip": True, "stat_title": "Whiff%" },
282
+ "k_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "K%" },
283
+ "bb_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "BB%" },
284
+ "pull_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Pull%" },
285
+ "pulled_fly_ball_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Pull FB%" },
286
+ }
287
+ # Show progress/loading notification
288
+ with ui.Progress(min=0, max=1) as p:
289
+
290
+ def draw_baseball_savant_percentiles(new_player_metrics, new_player_percentiles, colors=None,
291
+ sport_id=None,
292
+ year_input=None):
293
+ """
294
+ Draw Baseball Savant-style percentile bars with proper alignment and scaling.
295
+
296
+ :param new_player_metrics: DataFrame containing new player metrics.
297
+ :param new_player_percentiles: DataFrame containing new player percentiles.
298
+ :param colors: List of colors for bars (optional, red/blue default).
299
+ """
300
+ # Extract player information
301
+ batter_id = new_player_metrics['batter_id'][0]
302
+ player_name = batter_name_id[batter_id]
303
+ stats = [merged_dict[x]['stat_title'] for x in merged_dict.keys()]
304
+
305
+ # Calculate percentiles and values
306
+ percentiles = [int((1 - x) * 100) if merged_dict[stat]["percentile_flip"] else int(x * 100) for x, stat in zip(new_player_percentiles.select(merged_dict.keys()).to_numpy()[0], merged_dict.keys())]
307
+ percentiles = np.clip(percentiles, 1, 100)
308
+ values = [str(f'{x:{merged_dict[stat]["format"]}}').strip('%') for x, stat in zip(new_player_metrics.select(merged_dict.keys()).to_numpy()[0], merged_dict.keys())]
309
+
310
+ # Get team logo URL
311
+ logo_url = image_dict[team_dict[player_team_dict[batter_id]]]
312
+
313
+ # Create a custom colormap
314
+ color_list = ['#3661AD', '#B4CFD1', '#D82129']
315
+ cmap = LinearSegmentedColormap.from_list("custom_cmap", color_list)
316
+ norm = Normalize(vmin=0.1, vmax=0.9)
317
+ norm_percentiles = norm(percentiles / 100)
318
+ colors = [cmap(p) for p in norm_percentiles]
319
+
320
+ # Figure setup
321
+ num_stats = len(stats)
322
+ bar_height = 4.5
323
+ spacing = 1
324
+ fig_height = (bar_height + spacing) * num_stats
325
+ fig = plt.figure(figsize=(12, 12))
326
+ gs = GridSpec(6, 5, height_ratios=[0.1, 1.5, 0.9, 0.9, 7.6, 0.1], width_ratios=[0.2, 1.5, 7, 1.5, 0.2])
327
+
328
+ # Define subplots
329
+ ax_title = fig.add_subplot(gs[1, 2])
330
+ ax_table = fig.add_subplot(gs[2, :])
331
+ ax_fv_table = fig.add_subplot(gs[3, :])
332
+ ax_fv_table.axis('off')
333
+ ax = fig.add_subplot(gs[4, :])
334
+ ax_logo = fig.add_subplot(gs[1, 3])
335
+
336
+ ax.set_xlim(-1, 99)
337
+ ax.set_ylim(-1, 99)
338
+ ax.set_aspect("equal")
339
+ ax.axis("off")
340
+
341
+ # Draw each bar
342
+ for i, (stat, percentile, value, color) in enumerate(zip(stats, percentiles, values, colors)):
343
+ y = fig_height - (i + 1) * (bar_height + spacing)
344
+ ax.add_patch(patches.Rectangle((0, y + bar_height / 4), 100, bar_height / 2, color="#C7DCDC", lw=0))
345
+ ax.add_patch(patches.Rectangle((0, y), percentile, bar_height, color=color, lw=0))
346
+ circle_y = y + bar_height - bar_height / 2
347
+ circle = plt.Circle((percentile, circle_y), bar_height / 2, color=color, ec='white', lw=1.5, zorder=10)
348
+ ax.add_patch(circle)
349
+ fs = 14
350
+ ax.text(percentile, circle_y, f"{percentile}", ha="center", va="center", fontsize=10, color='white', zorder=10, fontweight='bold')
351
+ ax.text(-5, y + bar_height / 2, stat, ha="right", va="center", fontsize=fs)
352
+ ax.text(115, y + bar_height / 2, str(value), ha="right", va="center", fontsize=fs, zorder=5)
353
+ if i < len(stats) and i > 0:
354
+ ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=-33, xmax=0)
355
+ ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=100, xmax=115)
356
+
357
+ # Draw vertical lines for 10%, 50%, and 90% with labels
358
+ for x, label, align, color in zip([10, 50, 90], ["Poor", "Average", "Great"], ['center', 'center', 'center'], color_list):
359
+ ax.axvline(x=x, ymin=0, ymax=1, color='#FFF', linestyle='-', lw=1, zorder=1, alpha=0.5)
360
+ ax.text(x, fig_height + 4, label, ha=align, va='center', fontsize=12, fontweight='bold', color=color)
361
+ triangle = patches.RegularPolygon((x, fig_height + 1), 3, radius=1, orientation=0, color=color, zorder=2)
362
+ ax.add_patch(triangle)
363
+
364
+ # # Title
365
+ # ax_title.set_ylim(0, 1)
366
+ # ax_title.text(0.5, 0.5, f"{player_name} - {player_position_dict[batter_id]}\nPercentile Rankings - 2024 AAA", ha="center", va="center", fontsize=24)
367
+ # ax_title.axis("off")
368
+ player_bio(batter_id, ax=ax_title, sport_id=sport_id, year_input=year_input)
369
+
370
+ # Add team logo
371
+ #response = requests.get(logo_url)
372
+ if input.switch():
373
+ response = requests.get(input.logo_select())
374
+ else:
375
+ response = requests.get(logo_url)
376
+ img = Image.open(BytesIO(response.content))
377
+ ax_logo.imshow(img)
378
+ ax_logo.axis("off")
379
+ ax.axis('equal')
380
+
381
+ # Metrics data table
382
+ metrics_data = {
383
+ "Pitches": new_player_metrics['pitches'][0],
384
+ "PA": new_player_metrics['pa'][0],
385
+ "BIP": new_player_metrics['bip'][0],
386
+ "HR": f"{new_player_metrics['home_run'][0]:.0f}",
387
+ "AVG": f"{new_player_metrics['avg'][0]:.3f}",
388
+ "OBP": f"{new_player_metrics['obp'][0]:.3f}",
389
+ "SLG": f"{new_player_metrics['slg'][0]:.3f}",
390
+ "OPS": f"{new_player_metrics['obp'][0] + new_player_metrics['slg'][0]:.3f}",
391
+ }
392
+ df_table = pd.DataFrame(metrics_data, index=[0])
393
+ ax_table.axis('off')
394
+ table = ax_table.table(cellText=df_table.values, colLabels=df_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])
395
+ for key, cell in table.get_celld().items():
396
+ if key[0] == 0:
397
+ cell.set_text_props(fontweight='bold')
398
+ table.auto_set_font_size(False)
399
+ table.set_fontsize(12)
400
+ table.scale(1, 1.5)
401
+
402
+ # Additional subplots for spacing
403
+ ax_top = fig.add_subplot(gs[0, :])
404
+ ax_bot = fig.add_subplot(gs[-1, :])
405
+ ax_top.axis('off')
406
+ ax_bot.axis('off')
407
+ ax_bot.text(0.05, 2, "By: Thomas Nestico (@TJStats)", ha="left", va="center", fontsize=14)
408
+ ax_bot.text(0.95, 2, "Data: MLB, Fangraphs", ha="right", va="center", fontsize=14)
409
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
410
+
411
+ # Player headshot
412
+ ax_headshot = fig.add_subplot(gs[1, 1])
413
+ try:
414
+ url = f'https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto/w_640/v1/people/{batter_id}/headshot/milb/current.png'
415
+ response = requests.get(url)
416
+ img = Image.open(BytesIO(response.content))
417
+ ax_headshot.set_xlim(0, 1)
418
+ ax_headshot.set_ylim(0, 1)
419
+ ax_headshot.imshow(img, extent=[1/6, 5/6, 0, 1], origin='upper')
420
+ except PIL.UnidentifiedImageError:
421
+ ax_headshot.axis('off')
422
+ #return
423
+ ax_headshot.axis('off')
424
+ ax_table.set_title('Season Summary', style='italic')
425
+
426
+ # Fangraphs scouting grades table
427
+ print(batter_id)
428
+
429
+ if batter_id not in dict_mlb_fg.keys():
430
+ ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))
431
+ return
432
+ df_fv_table = df_prospects[(df_prospects['minorMasterId'] == dict_mlb_fg[batter_id])][['cFV', 'Hit', 'Game', 'Raw', 'Spd', 'Fld']].reset_index(drop=True)
433
+ ax_fv_table.axis('off')
434
+ if df_fv_table.empty:
435
+ ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))
436
+ return
437
+ df_fv_table.columns = ['FV', 'Hit', 'Game', 'Raw', 'Spd', 'Fld']
438
+ table_fv = ax_fv_table.table(cellText=df_fv_table.values, colLabels=df_fv_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])
439
+ for key, cell in table_fv.get_celld().items():
440
+ if key[0] == 0:
441
+ cell.set_text_props(fontweight='bold')
442
+ table_fv.auto_set_font_size(False)
443
+ table_fv.set_fontsize(12)
444
+ table_fv.scale(1, 1.5)
445
+ ax_fv_table.set_title('Fangraphs Scouting Grades', style='italic')
446
+
447
+
448
+
449
+ #plt.show()
450
+
451
+
452
+ def calculate_new_player_percentiles(player_id, new_player_metrics, player_summary_filtered):
453
+ """
454
+ Calculate percentiles for a new player's metrics.
455
+
456
+ :param player_id: ID of the player.
457
+ :param new_player_metrics: DataFrame containing new player metrics.
458
+ :param player_summary_filtered: Filtered player summary DataFrame.
459
+ :return: DataFrame containing new player percentiles.
460
+ """
461
+ filtered_summary_clone = player_summary_filtered[['batter_id'] + stat_list].filter(pl.col('batter_id') != player_id).clone()
462
+ combined_data = pl.concat([filtered_summary_clone, new_player_metrics], how="vertical").to_pandas()
463
+ combined_percentiles = pl.DataFrame(pd.concat([combined_data['batter_id'], combined_data[stat_list].rank(pct=True)], axis=1))
464
+ new_player_percentiles = combined_percentiles.filter(pl.col('batter_id') == player_id)
465
+ return new_player_percentiles
466
+
467
+
468
+
469
+ p.set(message="Generating plot", detail="This may take a while...")
470
+
471
+
472
+ p.set(0.3, "Gathering data...")
473
+
474
+ # Example: New player's metrics
475
+ year = int(input.year_input())
476
+ sport_id = int(input.level_input())
477
+ batter_id = int(input.batter_id())
478
+
479
+
480
+ df_player = scrape.get_players(sport_id=sport_id,season=year)
481
+ batter_name_id = dict(zip(df_player['player_id'],df_player['name']))
482
+ player_team_dict = dict(zip(df_player['player_id'],df_player['team']))
483
+ player_position_dict = dict(zip(df_player['player_id'],df_player['position']))
484
+
485
+
486
+ batter_summary = pl.read_csv(f'data/statcast/batter_summary_{level_dict_file[str(sport_id)]}_{year}.csv')
487
+ df_prospects = pd.read_csv(f'data/prospects/prospects_{year}.csv')
488
+ df_rosters = pd.read_csv(f'data/rosters/fangraphs_rosters_{year}.csv')
489
+ df_small = df_rosters[['minorbamid','minormasterid']].dropna()
490
+ dict_mlb_fg=dict(zip(df_small['minorbamid'].astype(int),df_small['minormasterid']))
491
+
492
+
493
+
494
+
495
+ batter_summary_filter = batter_summary.filter((pl.col('pa') >= 300) & (pl.col('launch_speed') >= 0))
496
+ stat_list = batter_summary.columns[2:]
497
+ batter_summary_filter_pd = batter_summary_filter.to_pandas()
498
+ new_player_metrics = batter_summary.filter(pl.col('batter_id') == batter_id)[['batter_id'] + stat_list]
499
+
500
+ # Get percentiles for the new player
501
+ new_player_percentiles = calculate_new_player_percentiles(batter_id, new_player_metrics, batter_summary_filter)
502
+
503
+ p.set(0.6, "Creating plot...")
504
+ # Draw Baseball Savant-style percentile bars
505
+ draw_baseball_savant_percentiles(new_player_metrics=new_player_metrics,
506
+ new_player_percentiles=new_player_percentiles,
507
+ sport_id=sport_id,
508
+ year_input=year)
509
+
510
+ @output
511
+ @render.plot
512
+ @reactive.event(input.generate_plot, ignore_none=False)
513
+ def pitcher_plot():
514
+ merged_dict = {
515
+ "avg_start_speed_ff": { "format": '.1f', "percentile_flip": False, "stat_title": "Fastball Velocity" },
516
+ "extension": { "format": '.1f', "percentile_flip": False, "stat_title": "Extension" },
517
+ "woba_percent": { "format": '.3f', "percentile_flip": True, "stat_title": "wOBA" },
518
+ "xwoba_percent": { "format": '.3f', "percentile_flip": True, "stat_title": "xwOBA" },
519
+ "launch_speed": { "format": '.1f', "percentile_flip": True, "stat_title": "Average EV"},
520
+ "barrel_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "Barrel%" },
521
+ "hard_hit_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "Hard-Hit%" },
522
+ "whiff_rate": { "format": '.1%', "percentile_flip": False, "stat_title": "Whiff%" },
523
+ "zone_contact_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "Z-Contact%" },
524
+ "zone_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Zone%" },
525
+ "chase_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "O-Swing%" },
526
+ "csw_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "CSW%" },
527
+ "k_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "K%" },
528
+ "bb_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "BB%" },
529
+ "k_minus_bb_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "K - BB%" },
530
+ "ground_ball_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "GB%" },
531
+ }
532
+
533
+ with ui.Progress(min=0, max=1) as p:
534
+
535
+ def draw_baseball_savant_percentiles(new_player_metrics, new_player_percentiles, colors=None,
536
+ sport_id=None,
537
+ year_input=None):
538
+ """
539
+ Draw Baseball Savant-style percentile bars with proper alignment and scaling.
540
+
541
+ :param new_player_metrics: DataFrame containing new player metrics.
542
+ :param new_player_percentiles: DataFrame containing new player percentiles.
543
+ :param colors: List of colors for bars (optional, red/blue default).
544
+ """
545
+ # Extract player information
546
+ pitcher_id = new_player_metrics['pitcher_id'][0]
547
+ player_name = pitcher_name_id[pitcher_id]
548
+ stats = [merged_dict[x]['stat_title'] for x in merged_dict.keys()]
549
+
550
+ # Calculate percentiles and values
551
+ percentiles = [int((1 - x) * 100) if merged_dict[stat]["percentile_flip"] else int(x * 100) for x, stat in zip(new_player_percentiles.select(merged_dict.keys()).to_numpy()[0], merged_dict.keys())]
552
+ percentiles = np.clip(percentiles, 1, 100)
553
+ values = [str(f'{x:{merged_dict[stat]["format"]}}').strip('%') for x, stat in zip(new_player_metrics.select(merged_dict.keys()).to_numpy()[0], merged_dict.keys())]
554
+
555
+ # Get team logo URL
556
+ logo_url = image_dict[team_dict[player_team_dict[pitcher_id]]]
557
+
558
+ # Create a custom colormap
559
+ color_list = ['#3661AD', '#B4CFD1', '#D82129']
560
+ cmap = LinearSegmentedColormap.from_list("custom_cmap", color_list)
561
+ norm = Normalize(vmin=0.1, vmax=0.9)
562
+ norm_percentiles = norm(percentiles / 100)
563
+ colors = [cmap(p) for p in norm_percentiles]
564
+
565
+ # Figure setup
566
+ num_stats = len(stats)
567
+ bar_height = 4.4
568
+ spacing = 0.7
569
+ fig_height = (bar_height + spacing) * num_stats
570
+ fig = plt.figure(figsize=(12, 12))
571
+ gs = GridSpec(7, 5, height_ratios=[0.05, 1.5, 0.75, 0.75,0.75, 7.7, 0.1], width_ratios=[0.2, 1.5, 7, 1.5, 0.2])
572
+
573
+ # Define subplots
574
+ ax_title = fig.add_subplot(gs[1, 2])
575
+ ax_table = fig.add_subplot(gs[2, :])
576
+ ax_fv_table = fig.add_subplot(gs[3, :])
577
+ ax_fv_table.axis('off')
578
+ ax_stuff = fig.add_subplot(gs[4, :])
579
+ ax = fig.add_subplot(gs[5, :])
580
+ ax_logo = fig.add_subplot(gs[1, 3])
581
+
582
+ ax.set_xlim(-1, 99)
583
+ ax.set_ylim(-1, 99)
584
+ ax.set_aspect("equal")
585
+ ax.axis("off")
586
+
587
+ # Draw each bar
588
+ for i, (stat, percentile, value, color) in enumerate(zip(stats, percentiles, values, colors)):
589
+ y = fig_height - (i + 1) * (bar_height + spacing)
590
+ ax.add_patch(patches.Rectangle((0, y + bar_height / 4), 100, bar_height / 2, color="#C7DCDC", lw=0))
591
+ ax.add_patch(patches.Rectangle((0, y), percentile, bar_height, color=color, lw=0))
592
+ circle_y = y + bar_height - bar_height / 2
593
+ circle = plt.Circle((percentile, circle_y), bar_height / 2, color=color, ec='white', lw=1.5, zorder=10)
594
+ ax.add_patch(circle)
595
+ fs = 14
596
+ ax.text(percentile, circle_y, f"{percentile}", ha="center", va="center", fontsize=10, color='white', zorder=10, fontweight='bold')
597
+ ax.text(-5, y + bar_height / 2, stat, ha="right", va="center", fontsize=fs)
598
+ ax.text(115, y + bar_height / 2, str(value), ha="right", va="center", fontsize=fs, zorder=5)
599
+ if i < len(stats) and i > 0:
600
+ ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=-33, xmax=0)
601
+ ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=100, xmax=115)
602
+
603
+ # Draw vertical lines for 10%, 50%, and 90% with labels
604
+ for x, label, align, color in zip([10, 50, 90], ["Poor", "Average", "Great"], ['center', 'center', 'center'], color_list):
605
+ ax.axvline(x=x, ymin=0, ymax=1, color='#FFF', linestyle='-', lw=1, zorder=1, alpha=0.5)
606
+ ax.text(x, fig_height + 4, label, ha=align, va='center', fontsize=12, fontweight='bold', color=color)
607
+ triangle = patches.RegularPolygon((x, fig_height + 1), 3, radius=1, orientation=0, color=color, zorder=2)
608
+ ax.add_patch(triangle)
609
+
610
+ # # Title
611
+ # ax_title.set_ylim(0, 1)
612
+ # ax_title.text(0.5, 0.5, f"{player_name} - {player_position_dict[pitcher_id]}\nPercentile Rankings - 2024 AAA", ha="center", va="center", fontsize=24)
613
+ # ax_title.axis("off")
614
+ player_bio(pitcher_id, ax=ax_title, sport_id=sport_id, year_input=year_input)
615
+
616
+ # Add team logo
617
+ #response = requests.get(logo_url)
618
+ #######if input.switch():
619
+ ######## response = requests.get(input.logo_select())
620
+ ######else:
621
+ response = requests.get(logo_url)
622
+ img = Image.open(BytesIO(response.content))
623
+ ax_logo.imshow(img)
624
+ ax_logo.axis("off")
625
+ ax.axis('equal')
626
+ lg_dict = {
627
+ 11:'all',
628
+ 14:10
629
+ }
630
+ levelt = {
631
+ 11:1,
632
+ 14:4
633
+ }
634
+
635
+
636
+ fg_api = f'https://www.fangraphs.com/api/leaders/minor-league/data?pos=all&level={levelt[sport_id]}&lg={lg_dict[sport_id]}&stats=pit&qual=0&type=2&team=&season=2024&seasonEnd=2024&org=&ind=0&splitTeam=false'
637
+ response = requests.get(fg_api)
638
+ data = response.json()
639
+ df_fg = pl.DataFrame(data)
640
+ if pitcher_id not in dict_mlb_fg.keys():
641
+ #ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))
642
+ metrics_data = {
643
+ "Pitches": new_player_metrics['pitches'][0],
644
+ "PA": new_player_metrics['pa'][0],
645
+ "BIP": new_player_metrics['bip'][0],
646
+ "HR": f"{new_player_metrics['home_run'][0]:.0f}",
647
+ "K": f"{new_player_metrics['k'][0]:.0f}",
648
+ "BB": f"{new_player_metrics['bb'][0]:.0f}",
649
+ }
650
+ else:
651
+ df_fg_filter = df_fg.filter(pl.col('minormasterid') == dict_mlb_fg[pitcher_id])
652
+ # Metrics data table
653
+ metrics_data = {
654
+ "G": f"{df_fg_filter['G'][0]:.0f}",
655
+ "IP": f"{df_fg_filter['IP'][0]:.1f}",
656
+ "Pitches": f"{new_player_metrics['pitches'][0]:.0f}",
657
+ "PA": f"{df_fg_filter['TBF'][0]:.0f}",
658
+ "BIP": new_player_metrics['bip'][0],
659
+ "ERA": f"{df_fg_filter['ERA'][0]:.2f}",
660
+ "FIP": f"{df_fg_filter['FIP'][0]:.2f}",
661
+ "WHIP": f"{df_fg_filter['WHIP'][0]:.2f}",
662
+ }
663
+ df_table = pd.DataFrame(metrics_data, index=[0])
664
+ ax_table.axis('off')
665
+ table = ax_table.table(cellText=df_table.values, colLabels=df_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])
666
+ for key, cell in table.get_celld().items():
667
+ if key[0] == 0:
668
+ cell.set_text_props(fontweight='bold')
669
+ table.auto_set_font_size(False)
670
+ table.set_fontsize(12)
671
+ table.scale(1, 1.5)
672
+
673
+ # Additional subplots for spacing
674
+ ax_top = fig.add_subplot(gs[0, :])
675
+ ax_bot = fig.add_subplot(gs[-1, :])
676
+ ax_top.axis('off')
677
+ ax_bot.axis('off')
678
+ ax_bot.text(0.05, 2, "By: Thomas Nestico (@TJStats)", ha="left", va="center", fontsize=14)
679
+ ax_bot.text(0.95, 2, "Data: MLB, Fangraphs", ha="right", va="center", fontsize=14)
680
+
681
+
682
+ # Player headshot
683
+ ax_headshot = fig.add_subplot(gs[1, 1])
684
+ try:
685
+ url = f'https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto/w_640/v1/people/{pitcher_id}/headshot/milb/current.png'
686
+ response = requests.get(url)
687
+ img = Image.open(BytesIO(response.content))
688
+ ax_headshot.set_xlim(0, 1)
689
+ ax_headshot.set_ylim(0, 1)
690
+ ax_headshot.imshow(img, extent=[1/6, 5/6, 0, 1], origin='upper')
691
+ except PIL.UnidentifiedImageError:
692
+ ax_headshot.axis('off')
693
+ #return
694
+ ax_headshot.axis('off')
695
+ ax_table.set_title('Season Summary', style='italic')
696
+
697
+ # Fangraphs scouting grades table
698
+ print(pitcher_id)
699
+
700
+ if pitcher_id not in dict_mlb_fg.keys():
701
+ ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))
702
+ #return
703
+ df_fv_table = df_prospects[(df_prospects['minorMasterId'] == dict_mlb_fg[pitcher_id])][['cFV','FB', 'SL', 'CB', 'CH', 'SPL', 'CT','CMD']].dropna(axis=1).reset_index(drop=True)
704
+ ax_fv_table.axis('off')
705
+ if df_fv_table.empty:
706
+ ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))
707
+ #return
708
+ else:
709
+ df_fv_table.columns = ['FV']+[x.upper() for x in df_fv_table.columns[1:]]
710
+ table_fv = ax_fv_table.table(cellText=df_fv_table.values, colLabels=df_fv_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])
711
+ for key, cell in table_fv.get_celld().items():
712
+ if key[0] == 0:
713
+ cell.set_text_props(fontweight='bold')
714
+ table_fv.auto_set_font_size(False)
715
+ table_fv.set_fontsize(12)
716
+ table_fv.scale(1, 1.5)
717
+ ax_fv_table.set_title('Fangraphs Scouting Grades', style='italic')
718
+
719
+
720
+ # df_stuff_filter = df_stuff.filter(pl.col('pitcher_id')==pitcher_id)
721
+
722
+ stuff_table = ax_stuff.table(cellText=[df_stuff_filter['tj_stuff_plus']],
723
+ colLabels=df_stuff_filter['pitch_type'],
724
+ cellLoc='center',
725
+ loc='center', bbox=[0.07, 0, 0.86, 1])
726
+ stuff_table.auto_set_font_size(False)
727
+ stuff_table.set_fontsize(12)
728
+ stuff_table.scale(1, 1.5)
729
+ ax_stuff.axis('off')
730
+ ax_stuff.set_title('tjStuff+', style='italic')
731
+ for key, cell in stuff_table.get_celld().items():
732
+ if key[0] == 0:
733
+ cell.set_text_props(fontweight='bold')
734
+
735
+ # Color the stuff_table values based on the cmap defined
736
+ for (i, j), cell in stuff_table.get_celld().items():
737
+ if i == 0:
738
+ cell.set_text_props(fontweight='bold')
739
+ else:
740
+ norm = Normalize(vmin=90, vmax=110)
741
+ value = float(cell.get_text().get_text())
742
+ color = cmap(norm(value))
743
+ cell.set_facecolor(color)
744
+ #cell.set_text_props(color='white' if value < 100 else 'black')
745
+
746
+
747
+
748
+
749
+
750
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
751
+
752
+
753
+
754
+
755
+
756
+ def calculate_new_player_percentiles(player_id, new_player_metrics, player_summary_filtered):
757
+ """
758
+ Calculate percentiles for a new player's metrics.
759
+
760
+ :param player_id: ID of the player.
761
+ :param new_player_metrics: DataFrame containing new player metrics.
762
+ :param player_summary_filtered: Filtered player summary DataFrame.
763
+ :return: DataFrame containing new player percentiles.
764
+ """
765
+ filtered_summary_clone = player_summary_filtered[['pitcher_id'] + stat_list].filter(pl.col('pitcher_id') != player_id).clone()
766
+ combined_data = pl.concat([filtered_summary_clone, new_player_metrics], how="vertical").to_pandas()
767
+ combined_percentiles = pl.DataFrame(pd.concat([combined_data['pitcher_id'], combined_data[stat_list].rank(pct=True)], axis=1))
768
+ new_player_percentiles = combined_percentiles.filter(pl.col('pitcher_id') == player_id)
769
+ return new_player_percentiles
770
+
771
+ p.set(message="Generating plot", detail="This may take a while...")
772
+
773
+
774
+ p.set(0.3, "Gathering data...")
775
+
776
+
777
+ df_teams = scrape.get_teams()
778
+ team_dict = dict(zip(df_teams['team_id'],df_teams['parent_org_abbreviation']))
779
+
780
+ # Example: New player's metrics
781
+ # Example: New player's metrics
782
+ year = int(input.year_input())
783
+ sport_id = int(input.level_input())
784
+ pitcher_id = int(input.pitcher_id())
785
+
786
+ df_player = scrape.get_players(sport_id=sport_id,season=2024)
787
+ pitcher_name_id = dict(zip(df_player['player_id'],df_player['name']))
788
+ player_team_dict = dict(zip(df_player['player_id'],df_player['team']))
789
+ player_position_dict = dict(zip(df_player['player_id'],df_player['position']))
790
+ player_position_dict = dict(zip(df_player['player_id'],df_player['position']))
791
+
792
+
793
+
794
+
795
+ pitcher_summary = pl.read_csv(f'data/statcast/pitcher_summary_{level_dict_file[str(sport_id)]}_{year}.csv')
796
+ df_prospects = pd.read_csv(f'data/prospects/prospects_{year}.csv')
797
+ df_rosters = pd.read_csv(f'data/rosters/fangraphs_rosters_{year}.csv')
798
+ df_small = df_rosters[['minorbamid','minormasterid']].dropna()
799
+ dict_mlb_fg=dict(zip(df_small['minorbamid'].astype(int),df_small['minormasterid']))
800
+
801
+ df_stuff = pl.read_csv(f'data/stuff/stuff_{level_dict_file[str(sport_id)]}_{year}.csv')
802
+ # Filter out the "All" row
803
+ filtered_df = df_stuff.filter(pl.col("pitch_type") != "All")
804
+
805
+ filtered_all_df = df_stuff.filter(pl.col("pitch_type") == "All")
806
+ # Calculate total pitches for each pitcher and proportion of each pitch type
807
+ result_df = (
808
+ filtered_df
809
+ .with_columns([
810
+ # Total pitches for each pitcher
811
+ pl.col("pitches").sum().over("pitcher_id").alias("total_pitches"),
812
+ # Proportion of pitches
813
+ (pl.col("pitches") / pl.col("pitches").sum().over("pitcher_id")).alias("pitch_proportion"),
814
+ ])
815
+ ).filter(pl.col("pitch_proportion") > 0.05)
816
+
817
+ df_stuff = pl.concat([filtered_all_df.with_columns(
818
+ [pl.col("pitches").sum().over("pitcher_id").alias("total_pitches"),
819
+ (pl.col("pitches") / pl.col("pitches").sum().over("pitcher_id")).alias("pitch_proportion")]
820
+ ), result_df])
821
+
822
+
823
+
824
+
825
+ df_stuff_filter = df_stuff.filter(pl.col('pitcher_id')==pitcher_id)
826
+
827
+ pitcher_summary_filter = pitcher_summary.filter((pl.col('pa') >= 300) & (pl.col('launch_speed') >= 0))
828
+ stat_list = pitcher_summary.columns[2:]
829
+ pitcher_summary_filter_pd = pitcher_summary_filter.to_pandas()
830
+ new_player_metrics = pitcher_summary.filter(pl.col('pitcher_id') == pitcher_id)[['pitcher_id'] + stat_list]
831
+
832
+ # Get percentiles for the new player
833
+ new_player_percentiles = calculate_new_player_percentiles(pitcher_id, new_player_metrics, pitcher_summary_filter)
834
+
835
+ p.set(0.6, "Creating plot...")
836
+ # Draw Baseball Savant-style percentile bars
837
+ draw_baseball_savant_percentiles(new_player_metrics=new_player_metrics,
838
+ new_player_percentiles=new_player_percentiles,
839
+ sport_id=sport_id,
840
+ year_input=year)
841
+
842
+
843
  app = App(app_ui, server)