nesticot commited on
Commit
5c3a4cc
·
verified ·
1 Parent(s): e98269d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +471 -471
app.py CHANGED
@@ -1,472 +1,472 @@
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
- pitcher_hand = data['people'][0]['pitchHand']['code']
49
- age = data['people'][0]['currentAge']
50
- height = data['people'][0]['height']
51
- weight = data['people'][0]['weight']
52
-
53
- # Display the player's name, handedness, age, height, and weight on the axis
54
- ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=30)
55
- ax.text(0.5, 0.65, f'{position}, Age:{age}, {height}/{weight}', va='top', ha='center', fontsize=20)
56
- ax.text(0.5, 0.4, f'Season Batting Percentiles', va='top', ha='center', fontsize=16)
57
-
58
- # Make API call to retrieve sports information
59
- response = requests.get(url='https://statsapi.mlb.com/api/v1/sports').json()
60
-
61
- # Convert the JSON response into a Polars DataFrame
62
- df_sport_id = pl.DataFrame(response['sports'])
63
- abb = df_sport_id.filter(pl.col('id') == sport_id)['abbreviation'][0]
64
-
65
- # Display the season and sport abbreviation
66
- ax.text(0.5, 0.20, f'{year_input} {abb} Season', va='top', ha='center', fontsize=14, fontstyle='italic')
67
-
68
- # Turn off the axis
69
- ax.axis('off')
70
-
71
-
72
- df_teams = scrape.get_teams()
73
- team_dict = dict(zip(df_teams['team_id'],df_teams['parent_org_abbreviation']))
74
-
75
-
76
- # List of MLB teams and their corresponding ESPN logo URLs
77
- mlb_teams = [
78
- {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
79
- {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
80
- {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
81
- {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
82
- {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
83
- {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
84
- {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
85
- {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
86
- {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
87
- {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
88
- {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
89
- {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
90
- {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
91
- {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
92
- {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
93
- {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
94
- {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
95
- {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
96
- {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
97
- {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
98
- {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
99
- {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
100
- {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
101
- {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
102
- {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
103
- {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
104
- {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
105
- {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
106
- {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
107
- {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"},
108
- {"team": "ZZZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png&w=500&h=500"}
109
- ]
110
-
111
- df_image = pd.DataFrame(mlb_teams)
112
- image_dict = df_image.set_index('team')['logo_url'].to_dict()
113
- image_dict_flip = df_image.set_index('logo_url')['team'].to_dict()
114
-
115
-
116
- merged_dict = {
117
- "woba_percent": { "format": '.3f', "percentile_flip": False, "stat_title": "wOBA" },
118
- "xwoba_percent": { "format": '.3f', "percentile_flip": False, "stat_title": "xwOBA" },
119
- "launch_speed": { "format": '.1f', "percentile_flip": False, "stat_title": "Average EV"},
120
- "launch_speed_90": { "format": '.1f', "percentile_flip": False, "stat_title": "90th% EV"},
121
- "max_launch_speed": { "format": '.1f', "percentile_flip": False, "stat_title": "Max EV"},
122
- "barrel_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Barrel%" },
123
- "hard_hit_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Hard-Hit%" },
124
- "sweet_spot_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "LA Sweet-Spot%" },
125
- "zone_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Zone%" },
126
- "zone_swing_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Z-Swing%" },
127
- "chase_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "O-Swing%" },
128
- "whiff_rate": { "format": '.1%', "percentile_flip": True, "stat_title": "Whiff%" },
129
- "k_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "K%" },
130
- "bb_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "BB%" },
131
- "pull_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Pull%" },
132
- "pulled_fly_ball_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Pull FB%" },
133
- }
134
-
135
-
136
- # level_dict = {'1':'MLB',
137
- # '11':'AAA'}
138
-
139
- level_dict = {
140
- '11':'AAA',
141
- '14':'A (FSL)',}
142
-
143
-
144
- level_dict_file = {
145
- '11':'aaa',
146
- '14':'a',}
147
-
148
-
149
-
150
- year_list = [2024]
151
-
152
-
153
- from shiny import App, reactive, ui, render
154
- from shiny.ui import h2, tags
155
-
156
- # Define the UI layout for the app
157
- app_ui = ui.page_fluid(
158
-
159
-
160
- ui.tags.div(
161
- {"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
162
- ui.tags.style(
163
- """
164
- h4 {
165
- margin-top: 1em;font-size:35px;
166
- }
167
- h2{
168
- font-size:25px;
169
- }
170
- """
171
- ),
172
-
173
- ui.tags.h4("TJStats"),
174
- ui.tags.i("Baseball Analytics and Visualizations"),
175
- ui.markdown("""<a href='https://x.com/TJStats'>Follow me on Twitter</a><sup>1</sup>"""),
176
- ui.markdown("""<a href='https://www.patreon.com/tj_stats'>Support me on Patreon for Access to 2024 Apps</a><sup>1</sup>"""),
177
-
178
- ui.tags.h5("Statcast Batting Summaries"),
179
- ui.layout_sidebar(
180
- ui.panel_sidebar(
181
- # Row for selecting season and level
182
- ui.row(
183
- ui.column(6, ui.input_select('year_input', 'Select Season', year_list, selected=2024)),
184
- ui.column(6, ui.input_select('level_input', 'Select Level', level_dict)),
185
- ),
186
- # Row for the action button to get player list
187
- ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
188
- # Row for selecting the player
189
- ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
190
-
191
- ui.row(
192
- ui.column(6, ui.input_switch("switch", "Custom Team?", False)),
193
- ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False))
194
- ),
195
-
196
- # Row for the action button to generate plot
197
- ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
198
- width=3,
199
- ),
200
-
201
- ui.panel_main(
202
- ui.navset_tab(
203
- # Tab for game summary plot
204
- ui.nav("Batter Summary",
205
- ui.output_text("status"),
206
- ui.output_plot('plot', width='1200px', height='1200px')
207
- ),
208
- )
209
- )
210
- )
211
- )
212
- )
213
-
214
- def server(input, output, session):
215
- @render.ui
216
- @reactive.event(input.player_button, ignore_none=False)
217
- def player_select_ui():
218
- #Get the list of pitchers for the selected level and season
219
- df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input())).filter(
220
- ~pl.col("position").is_in(['P','TWP'])).sort("name")
221
-
222
-
223
-
224
- # Create a dictionary of pitcher IDs and names
225
- batter_dict_pos = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['position']))
226
-
227
- year = int(input.year_input())
228
- sport_id = int(input.level_input())
229
- batter_summary = pl.read_csv(f'data/statcast/batter_summary_{level_dict_file[str(sport_id)]}_{year}.csv').sort('batter_name',descending=False)
230
- # Map elements in Polars DataFrame from a dictionary
231
- batter_summary = batter_summary.with_columns(
232
- pl.col("batter_id").map_elements(lambda x: batter_dict_pos.get(x, x)).alias("position")
233
- )
234
-
235
-
236
- batter_dict_pos = dict(zip(batter_summary['batter_id'], batter_summary['batter_name']))
237
- # Create a dictionary of pitcher IDs and names
238
- batter_dict = dict(zip(batter_summary['batter_id'], batter_summary['batter_name'] + ' - ' + batter_summary['position']))
239
-
240
- # Return a select input for choosing a pitcher
241
- return ui.input_select("batter_id", "Select Batter", batter_dict, selectize=True)
242
-
243
-
244
-
245
-
246
- @output
247
- @render.plot
248
- @reactive.event(input.generate_plot, ignore_none=False)
249
- def plot():
250
- # Show progress/loading notification
251
- with ui.Progress(min=0, max=1) as p:
252
-
253
- def draw_baseball_savant_percentiles(new_player_metrics, new_player_percentiles, colors=None,
254
- sport_id=None,
255
- year_input=None):
256
- """
257
- Draw Baseball Savant-style percentile bars with proper alignment and scaling.
258
-
259
- :param new_player_metrics: DataFrame containing new player metrics.
260
- :param new_player_percentiles: DataFrame containing new player percentiles.
261
- :param colors: List of colors for bars (optional, red/blue default).
262
- """
263
- # Extract player information
264
- batter_id = new_player_metrics['batter_id'][0]
265
- player_name = batter_name_id[batter_id]
266
- stats = [merged_dict[x]['stat_title'] for x in merged_dict.keys()]
267
-
268
- # Calculate percentiles and values
269
- 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())]
270
- percentiles = np.clip(percentiles, 1, 100)
271
- 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())]
272
-
273
- # Get team logo URL
274
- logo_url = image_dict[team_dict[player_team_dict[batter_id]]]
275
-
276
- # Create a custom colormap
277
- color_list = ['#3661AD', '#B4CFD1', '#D82129']
278
- cmap = LinearSegmentedColormap.from_list("custom_cmap", color_list)
279
- norm = Normalize(vmin=0.1, vmax=0.9)
280
- norm_percentiles = norm(percentiles / 100)
281
- colors = [cmap(p) for p in norm_percentiles]
282
-
283
- # Figure setup
284
- num_stats = len(stats)
285
- bar_height = 4.5
286
- spacing = 1
287
- fig_height = (bar_height + spacing) * num_stats
288
- fig = plt.figure(figsize=(12, 12))
289
- 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])
290
-
291
- # Define subplots
292
- ax_title = fig.add_subplot(gs[1, 2])
293
- ax_table = fig.add_subplot(gs[2, :])
294
- ax_fv_table = fig.add_subplot(gs[3, :])
295
- ax = fig.add_subplot(gs[4, :])
296
- ax_logo = fig.add_subplot(gs[1, 3])
297
-
298
- ax.set_xlim(-1, 99)
299
- ax.set_ylim(-1, 99)
300
- ax.set_aspect("equal")
301
- ax.axis("off")
302
-
303
- # Draw each bar
304
- for i, (stat, percentile, value, color) in enumerate(zip(stats, percentiles, values, colors)):
305
- y = fig_height - (i + 1) * (bar_height + spacing)
306
- ax.add_patch(patches.Rectangle((0, y + bar_height / 4), 100, bar_height / 2, color="#C7DCDC", lw=0))
307
- ax.add_patch(patches.Rectangle((0, y), percentile, bar_height, color=color, lw=0))
308
- circle_y = y + bar_height - bar_height / 2
309
- circle = plt.Circle((percentile, circle_y), bar_height / 2, color=color, ec='white', lw=1.5, zorder=10)
310
- ax.add_patch(circle)
311
- fs = 14
312
- ax.text(percentile, circle_y, f"{percentile}", ha="center", va="center", fontsize=10, color='white', zorder=10, fontweight='bold')
313
- ax.text(-5, y + bar_height / 2, stat, ha="right", va="center", fontsize=fs)
314
- ax.text(115, y + bar_height / 2, str(value), ha="right", va="center", fontsize=fs, zorder=5)
315
- if i < len(stats) and i > 0:
316
- ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=-33, xmax=0)
317
- ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=100, xmax=115)
318
-
319
- # Draw vertical lines for 10%, 50%, and 90% with labels
320
- for x, label, align, color in zip([10, 50, 90], ["Poor", "Average", "Great"], ['center', 'center', 'center'], color_list):
321
- ax.axvline(x=x, ymin=0, ymax=1, color='#FFF', linestyle='-', lw=1, zorder=1, alpha=0.5)
322
- ax.text(x, fig_height + 4, label, ha=align, va='center', fontsize=12, fontweight='bold', color=color)
323
- triangle = patches.RegularPolygon((x, fig_height + 1), 3, radius=1, orientation=0, color=color, zorder=2)
324
- ax.add_patch(triangle)
325
-
326
- # # Title
327
- # ax_title.set_ylim(0, 1)
328
- # 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)
329
- # ax_title.axis("off")
330
- player_bio(batter_id, ax=ax_title, sport_id=sport_id, year_input=year_input)
331
-
332
- # Add team logo
333
- #response = requests.get(logo_url)
334
- if input.switch():
335
- response = requests.get(input.logo_select())
336
- else:
337
- response = requests.get(logo_url)
338
- img = Image.open(BytesIO(response.content))
339
- ax_logo.imshow(img)
340
- ax_logo.axis("off")
341
- ax.axis('equal')
342
-
343
- # Metrics data table
344
- metrics_data = {
345
- "Pitches": new_player_metrics['pitches'][0],
346
- "PA": new_player_metrics['pa'][0],
347
- "BIP": new_player_metrics['bip'][0],
348
- "HR": f"{new_player_metrics['home_run'][0]:.0f}",
349
- "AVG": f"{new_player_metrics['avg'][0]:.3f}",
350
- "OBP": f"{new_player_metrics['obp'][0]:.3f}",
351
- "SLG": f"{new_player_metrics['slg'][0]:.3f}",
352
- "OPS": f"{new_player_metrics['obp'][0] + new_player_metrics['slg'][0]:.3f}",
353
- }
354
- df_table = pd.DataFrame(metrics_data, index=[0])
355
- ax_table.axis('off')
356
- table = ax_table.table(cellText=df_table.values, colLabels=df_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])
357
- for key, cell in table.get_celld().items():
358
- if key[0] == 0:
359
- cell.set_text_props(fontweight='bold')
360
- table.auto_set_font_size(False)
361
- table.set_fontsize(12)
362
- table.scale(1, 1.5)
363
-
364
- # Additional subplots for spacing
365
- ax_top = fig.add_subplot(gs[0, :])
366
- ax_bot = fig.add_subplot(gs[-1, :])
367
- ax_top.axis('off')
368
- ax_bot.axis('off')
369
- ax_bot.text(0.05, 2, "By: Thomas Nestico (@TJStats)", ha="left", va="center", fontsize=14)
370
- ax_bot.text(0.95, 2, "Data: MLB, Fangraphs", ha="right", va="center", fontsize=14)
371
- fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
372
-
373
- # Player headshot
374
- ax_headshot = fig.add_subplot(gs[1, 1])
375
- try:
376
- url = f'https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto/w_640/v1/people/{batter_id}/headshot/milb/current.png'
377
- response = requests.get(url)
378
- img = Image.open(BytesIO(response.content))
379
- ax_headshot.set_xlim(0, 1)
380
- ax_headshot.set_ylim(0, 1)
381
- ax_headshot.imshow(img, extent=[1/6, 5/6, 0, 1], origin='upper')
382
- except PIL.UnidentifiedImageError:
383
- ax_headshot.axis('off')
384
- return
385
- ax_headshot.axis('off')
386
- ax_table.set_title('Season Summary', style='italic')
387
-
388
- # Fangraphs scouting grades table
389
- print(batter_id)
390
- ax_fv_table.axis('off')
391
- if batter_id not in dict_mlb_fg.keys():
392
- 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))
393
- return
394
- df_fv_table = df_prospects[(df_prospects['minorMasterId'] == dict_mlb_fg[batter_id])][['cFV', 'Hit', 'Game', 'Raw', 'Spd', 'Fld']].reset_index(drop=True)
395
- ax_fv_table.axis('off')
396
- if df_fv_table.empty:
397
- 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))
398
- return
399
- df_fv_table.columns = ['FV', 'Hit', 'Game', 'Raw', 'Spd', 'Fld']
400
- 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])
401
- for key, cell in table_fv.get_celld().items():
402
- if key[0] == 0:
403
- cell.set_text_props(fontweight='bold')
404
- table_fv.auto_set_font_size(False)
405
- table_fv.set_fontsize(12)
406
- table_fv.scale(1, 1.5)
407
- ax_fv_table.set_title('Fangraphs Scouting Grades', style='italic')
408
-
409
-
410
-
411
- #plt.show()
412
-
413
-
414
- def calculate_new_player_percentiles(player_id, new_player_metrics, player_summary_filtered):
415
- """
416
- Calculate percentiles for a new player's metrics.
417
-
418
- :param player_id: ID of the player.
419
- :param new_player_metrics: DataFrame containing new player metrics.
420
- :param player_summary_filtered: Filtered player summary DataFrame.
421
- :return: DataFrame containing new player percentiles.
422
- """
423
- filtered_summary_clone = player_summary_filtered[['batter_id'] + stat_list].filter(pl.col('batter_id') != player_id).clone()
424
- combined_data = pl.concat([filtered_summary_clone, new_player_metrics], how="vertical").to_pandas()
425
- combined_percentiles = pl.DataFrame(pd.concat([combined_data['batter_id'], combined_data[stat_list].rank(pct=True)], axis=1))
426
- new_player_percentiles = combined_percentiles.filter(pl.col('batter_id') == player_id)
427
- return new_player_percentiles
428
-
429
-
430
-
431
- p.set(message="Generating plot", detail="This may take a while...")
432
-
433
-
434
- p.set(0.3, "Gathering data...")
435
-
436
- # Example: New player's metrics
437
- year = int(input.year_input())
438
- sport_id = int(input.level_input())
439
- batter_id = int(input.batter_id())
440
-
441
-
442
- df_player = scrape.get_players(sport_id=sport_id,season=year)
443
- batter_name_id = dict(zip(df_player['player_id'],df_player['name']))
444
- player_team_dict = dict(zip(df_player['player_id'],df_player['team']))
445
- player_position_dict = dict(zip(df_player['player_id'],df_player['position']))
446
-
447
-
448
- batter_summary = pl.read_csv(f'data/statcast/batter_summary_{level_dict_file[str(sport_id)]}_{year}.csv')
449
- df_prospects = pd.read_csv(f'data/prospects/prospects_{year}.csv')
450
- df_rosters = pd.read_csv(f'data/rosters/fangraphs_rosters_{year}.csv')
451
- df_small = df_rosters[['minorbamid','minormasterid']].dropna()
452
- dict_mlb_fg=dict(zip(df_small['minorbamid'].astype(int),df_small['minormasterid']))
453
-
454
-
455
-
456
-
457
- batter_summary_filter = batter_summary.filter((pl.col('pa') >= 300) & (pl.col('launch_speed') >= 0))
458
- stat_list = batter_summary.columns[2:]
459
- batter_summary_filter_pd = batter_summary_filter.to_pandas()
460
- new_player_metrics = batter_summary.filter(pl.col('batter_id') == batter_id)[['batter_id'] + stat_list]
461
-
462
- # Get percentiles for the new player
463
- new_player_percentiles = calculate_new_player_percentiles(batter_id, new_player_metrics, batter_summary_filter)
464
-
465
- p.set(0.6, "Creating plot...")
466
- # Draw Baseball Savant-style percentile bars
467
- draw_baseball_savant_percentiles(new_player_metrics=new_player_metrics,
468
- new_player_percentiles=new_player_percentiles,
469
- sport_id=sport_id,
470
- year_input=year)
471
-
472
  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 = {
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
+ pitcher_hand = data['people'][0]['pitchHand']['code']
49
+ age = data['people'][0]['currentAge']
50
+ height = data['people'][0]['height']
51
+ weight = data['people'][0]['weight']
52
+
53
+ # Display the player's name, handedness, age, height, and weight on the axis
54
+ ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=30)
55
+ ax.text(0.5, 0.65, f'{position}, Age:{age}, {height}/{weight}', va='top', ha='center', fontsize=20)
56
+ ax.text(0.5, 0.4, f'Season Batting Percentiles', va='top', ha='center', fontsize=16)
57
+
58
+ # Make API call to retrieve sports information
59
+ response = requests.get(url='https://statsapi.mlb.com/api/v1/sports').json()
60
+
61
+ # Convert the JSON response into a Polars DataFrame
62
+ df_sport_id = pl.DataFrame(response['sports'])
63
+ abb = df_sport_id.filter(pl.col('id') == sport_id)['abbreviation'][0]
64
+
65
+ # Display the season and sport abbreviation
66
+ ax.text(0.5, 0.20, f'{year_input} {abb} Season', va='top', ha='center', fontsize=14, fontstyle='italic')
67
+
68
+ # Turn off the axis
69
+ ax.axis('off')
70
+
71
+
72
+ df_teams = scrape.get_teams()
73
+ team_dict = dict(zip(df_teams['team_id'],df_teams['parent_org_abbreviation']))
74
+
75
+
76
+ # List of MLB teams and their corresponding ESPN logo URLs
77
+ mlb_teams = [
78
+ {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
79
+ {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
80
+ {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
81
+ {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
82
+ {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
83
+ {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
84
+ {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
85
+ {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
86
+ {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
87
+ {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
88
+ {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
89
+ {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
90
+ {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
91
+ {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
92
+ {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
93
+ {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
94
+ {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
95
+ {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
96
+ {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
97
+ {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
98
+ {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
99
+ {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
100
+ {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
101
+ {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
102
+ {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
103
+ {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
104
+ {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
105
+ {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
106
+ {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
107
+ {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"},
108
+ {"team": "ZZZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png&w=500&h=500"}
109
+ ]
110
+
111
+ df_image = pd.DataFrame(mlb_teams)
112
+ image_dict = df_image.set_index('team')['logo_url'].to_dict()
113
+ image_dict_flip = df_image.set_index('logo_url')['team'].to_dict()
114
+
115
+
116
+ merged_dict = {
117
+ "woba_percent": { "format": '.3f', "percentile_flip": False, "stat_title": "wOBA" },
118
+ "xwoba_percent": { "format": '.3f', "percentile_flip": False, "stat_title": "xwOBA" },
119
+ "launch_speed": { "format": '.1f', "percentile_flip": False, "stat_title": "Average EV"},
120
+ "launch_speed_90": { "format": '.1f', "percentile_flip": False, "stat_title": "90th% EV"},
121
+ "max_launch_speed": { "format": '.1f', "percentile_flip": False, "stat_title": "Max EV"},
122
+ "barrel_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Barrel%" },
123
+ "hard_hit_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Hard-Hit%" },
124
+ "sweet_spot_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "LA Sweet-Spot%" },
125
+ "zone_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Zone%" },
126
+ "zone_swing_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Z-Swing%" },
127
+ "chase_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "O-Swing%" },
128
+ "whiff_rate": { "format": '.1%', "percentile_flip": True, "stat_title": "Whiff%" },
129
+ "k_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "K%" },
130
+ "bb_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "BB%" },
131
+ "pull_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Pull%" },
132
+ "pulled_fly_ball_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Pull FB%" },
133
+ }
134
+
135
+
136
+ # level_dict = {'1':'MLB',
137
+ # '11':'AAA'}
138
+
139
+ level_dict = {
140
+ '11':'AAA',
141
+ '14':'A (FSL)',}
142
+
143
+
144
+ level_dict_file = {
145
+ '11':'aaa',
146
+ '14':'a',}
147
+
148
+
149
+
150
+ year_list = [2024]
151
+
152
+
153
+ from shiny import App, reactive, ui, render
154
+ from shiny.ui import h2, tags
155
+
156
+ # Define the UI layout for the app
157
+ app_ui = ui.page_fluid(
158
+
159
+
160
+ ui.tags.div(
161
+ {"style": "width:90%;margin: 0 auto;max-width: 1600px;"},
162
+ ui.tags.style(
163
+ """
164
+ h4 {
165
+ margin-top: 1em;font-size:35px;
166
+ }
167
+ h2{
168
+ font-size:25px;
169
+ }
170
+ """
171
+ ),
172
+
173
+ ui.tags.h4("TJStats"),
174
+ ui.tags.i("Baseball Analytics and Visualizations"),
175
+ ui.markdown("""<a href='https://x.com/TJStats'>Follow me on Twitter</a><sup>1</sup>"""),
176
+ ui.markdown("""<a href='https://www.patreon.com/tj_stats'>Support me on Patreon for Access to 2024 Apps</a><sup>1</sup>"""),
177
+
178
+ ui.tags.h5("Statcast Batting Summaries"),
179
+ ui.layout_sidebar(
180
+ ui.panel_sidebar(
181
+ # Row for selecting season and level
182
+ ui.row(
183
+ ui.column(6, ui.input_select('year_input', 'Select Season', year_list, selected=2024)),
184
+ ui.column(6, ui.input_select('level_input', 'Select Level', level_dict)),
185
+ ),
186
+ # Row for the action button to get player list
187
+ ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),
188
+ # Row for selecting the player
189
+ ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
190
+
191
+ ui.row(
192
+ ui.column(6, ui.input_switch("switch", "Custom Team?", False)),
193
+ ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False))
194
+ ),
195
+
196
+ # Row for the action button to generate plot
197
+ ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),
198
+ width=3,
199
+ ),
200
+
201
+ ui.panel_main(
202
+ ui.navset_tab(
203
+ # Tab for game summary plot
204
+ ui.nav("Batter Summary",
205
+ ui.output_text("status"),
206
+ ui.output_plot('plot', width='1200px', height='1200px')
207
+ ),
208
+ )
209
+ )
210
+ )
211
+ )
212
+ )
213
+
214
+ def server(input, output, session):
215
+ @render.ui
216
+ @reactive.event(input.player_button, ignore_none=False)
217
+ def player_select_ui():
218
+ #Get the list of pitchers for the selected level and season
219
+ df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input())).filter(
220
+ ~pl.col("position").is_in(['P','TWP'])).sort("name")
221
+
222
+
223
+
224
+ # Create a dictionary of pitcher IDs and names
225
+ batter_dict_pos = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['position']))
226
+
227
+ year = int(input.year_input())
228
+ sport_id = int(input.level_input())
229
+ batter_summary = pl.read_csv(f'data/statcast/batter_summary_{level_dict_file[str(sport_id)]}_{year}.csv').sort('batter_name',descending=False)
230
+ # Map elements in Polars DataFrame from a dictionary
231
+ batter_summary = batter_summary.with_columns(
232
+ pl.col("batter_id").map_elements(lambda x: batter_dict_pos.get(x, x)).alias("position")
233
+ )
234
+
235
+
236
+ batter_dict_pos = dict(zip(batter_summary['batter_id'], batter_summary['batter_name']))
237
+ # Create a dictionary of pitcher IDs and names
238
+ batter_dict = dict(zip(batter_summary['batter_id'], batter_summary['batter_name'] + ' - ' + batter_summary['position']))
239
+
240
+ # Return a select input for choosing a pitcher
241
+ return ui.input_select("batter_id", "Select Batter", batter_dict, selectize=True)
242
+
243
+
244
+
245
+
246
+ @output
247
+ @render.plot
248
+ @reactive.event(input.generate_plot, ignore_none=False)
249
+ def plot():
250
+ # Show progress/loading notification
251
+ with ui.Progress(min=0, max=1) as p:
252
+
253
+ def draw_baseball_savant_percentiles(new_player_metrics, new_player_percentiles, colors=None,
254
+ sport_id=None,
255
+ year_input=None):
256
+ """
257
+ Draw Baseball Savant-style percentile bars with proper alignment and scaling.
258
+
259
+ :param new_player_metrics: DataFrame containing new player metrics.
260
+ :param new_player_percentiles: DataFrame containing new player percentiles.
261
+ :param colors: List of colors for bars (optional, red/blue default).
262
+ """
263
+ # Extract player information
264
+ batter_id = new_player_metrics['batter_id'][0]
265
+ player_name = batter_name_id[batter_id]
266
+ stats = [merged_dict[x]['stat_title'] for x in merged_dict.keys()]
267
+
268
+ # Calculate percentiles and values
269
+ 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())]
270
+ percentiles = np.clip(percentiles, 1, 100)
271
+ 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())]
272
+
273
+ # Get team logo URL
274
+ logo_url = image_dict[team_dict[player_team_dict[batter_id]]]
275
+
276
+ # Create a custom colormap
277
+ color_list = ['#3661AD', '#B4CFD1', '#D82129']
278
+ cmap = LinearSegmentedColormap.from_list("custom_cmap", color_list)
279
+ norm = Normalize(vmin=0.1, vmax=0.9)
280
+ norm_percentiles = norm(percentiles / 100)
281
+ colors = [cmap(p) for p in norm_percentiles]
282
+
283
+ # Figure setup
284
+ num_stats = len(stats)
285
+ bar_height = 4.5
286
+ spacing = 1
287
+ fig_height = (bar_height + spacing) * num_stats
288
+ fig = plt.figure(figsize=(12, 12))
289
+ 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])
290
+
291
+ # Define subplots
292
+ ax_title = fig.add_subplot(gs[1, 2])
293
+ ax_table = fig.add_subplot(gs[2, :])
294
+ ax_fv_table = fig.add_subplot(gs[3, :])
295
+ ax = fig.add_subplot(gs[4, :])
296
+ ax_logo = fig.add_subplot(gs[1, 3])
297
+
298
+ ax.set_xlim(-1, 99)
299
+ ax.set_ylim(-1, 99)
300
+ ax.set_aspect("equal")
301
+ ax.axis("off")
302
+
303
+ # Draw each bar
304
+ for i, (stat, percentile, value, color) in enumerate(zip(stats, percentiles, values, colors)):
305
+ y = fig_height - (i + 1) * (bar_height + spacing)
306
+ ax.add_patch(patches.Rectangle((0, y + bar_height / 4), 100, bar_height / 2, color="#C7DCDC", lw=0))
307
+ ax.add_patch(patches.Rectangle((0, y), percentile, bar_height, color=color, lw=0))
308
+ circle_y = y + bar_height - bar_height / 2
309
+ circle = plt.Circle((percentile, circle_y), bar_height / 2, color=color, ec='white', lw=1.5, zorder=10)
310
+ ax.add_patch(circle)
311
+ fs = 14
312
+ ax.text(percentile, circle_y, f"{percentile}", ha="center", va="center", fontsize=10, color='white', zorder=10, fontweight='bold')
313
+ ax.text(-5, y + bar_height / 2, stat, ha="right", va="center", fontsize=fs)
314
+ ax.text(115, y + bar_height / 2, str(value), ha="right", va="center", fontsize=fs, zorder=5)
315
+ if i < len(stats) and i > 0:
316
+ ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=-33, xmax=0)
317
+ ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=100, xmax=115)
318
+
319
+ # Draw vertical lines for 10%, 50%, and 90% with labels
320
+ for x, label, align, color in zip([10, 50, 90], ["Poor", "Average", "Great"], ['center', 'center', 'center'], color_list):
321
+ ax.axvline(x=x, ymin=0, ymax=1, color='#FFF', linestyle='-', lw=1, zorder=1, alpha=0.5)
322
+ ax.text(x, fig_height + 4, label, ha=align, va='center', fontsize=12, fontweight='bold', color=color)
323
+ triangle = patches.RegularPolygon((x, fig_height + 1), 3, radius=1, orientation=0, color=color, zorder=2)
324
+ ax.add_patch(triangle)
325
+
326
+ # # Title
327
+ # ax_title.set_ylim(0, 1)
328
+ # 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)
329
+ # ax_title.axis("off")
330
+ player_bio(batter_id, ax=ax_title, sport_id=sport_id, year_input=year_input)
331
+
332
+ # Add team logo
333
+ #response = requests.get(logo_url)
334
+ if input.switch():
335
+ response = requests.get(input.logo_select())
336
+ else:
337
+ response = requests.get(logo_url)
338
+ img = Image.open(BytesIO(response.content))
339
+ ax_logo.imshow(img)
340
+ ax_logo.axis("off")
341
+ ax.axis('equal')
342
+
343
+ # Metrics data table
344
+ metrics_data = {
345
+ "Pitches": new_player_metrics['pitches'][0],
346
+ "PA": new_player_metrics['pa'][0],
347
+ "BIP": new_player_metrics['bip'][0],
348
+ "HR": f"{new_player_metrics['home_run'][0]:.0f}",
349
+ "AVG": f"{new_player_metrics['avg'][0]:.3f}",
350
+ "OBP": f"{new_player_metrics['obp'][0]:.3f}",
351
+ "SLG": f"{new_player_metrics['slg'][0]:.3f}",
352
+ "OPS": f"{new_player_metrics['obp'][0] + new_player_metrics['slg'][0]:.3f}",
353
+ }
354
+ df_table = pd.DataFrame(metrics_data, index=[0])
355
+ ax_table.axis('off')
356
+ table = ax_table.table(cellText=df_table.values, colLabels=df_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])
357
+ for key, cell in table.get_celld().items():
358
+ if key[0] == 0:
359
+ cell.set_text_props(fontweight='bold')
360
+ table.auto_set_font_size(False)
361
+ table.set_fontsize(12)
362
+ table.scale(1, 1.5)
363
+
364
+ # Additional subplots for spacing
365
+ ax_top = fig.add_subplot(gs[0, :])
366
+ ax_bot = fig.add_subplot(gs[-1, :])
367
+ ax_top.axis('off')
368
+ ax_bot.axis('off')
369
+ ax_bot.text(0.05, 2, "By: Thomas Nestico (@TJStats)", ha="left", va="center", fontsize=14)
370
+ ax_bot.text(0.95, 2, "Data: MLB, Fangraphs", ha="right", va="center", fontsize=14)
371
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
372
+
373
+ # Player headshot
374
+ ax_headshot = fig.add_subplot(gs[1, 1])
375
+ try:
376
+ url = f'https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto/w_640/v1/people/{batter_id}/headshot/milb/current.png'
377
+ response = requests.get(url)
378
+ img = Image.open(BytesIO(response.content))
379
+ ax_headshot.set_xlim(0, 1)
380
+ ax_headshot.set_ylim(0, 1)
381
+ ax_headshot.imshow(img, extent=[1/6, 5/6, 0, 1], origin='upper')
382
+ except PIL.UnidentifiedImageError:
383
+ ax_headshot.axis('off')
384
+ return
385
+ ax_headshot.axis('off')
386
+ ax_table.set_title('Season Summary', style='italic')
387
+
388
+ # Fangraphs scouting grades table
389
+ print(batter_id)
390
+ ax_fv_table.axis('off')
391
+ if batter_id not in dict_mlb_fg.keys():
392
+ 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))
393
+ return
394
+ df_fv_table = df_prospects[(df_prospects['minorMasterId'] == dict_mlb_fg[batter_id])][['cFV', 'Hit', 'Game', 'Raw', 'Spd', 'Fld']].reset_index(drop=True)
395
+ ax_fv_table.axis('off')
396
+ if df_fv_table.empty:
397
+ 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))
398
+ return
399
+ df_fv_table.columns = ['FV', 'Hit', 'Game', 'Raw', 'Spd', 'Fld']
400
+ 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])
401
+ for key, cell in table_fv.get_celld().items():
402
+ if key[0] == 0:
403
+ cell.set_text_props(fontweight='bold')
404
+ table_fv.auto_set_font_size(False)
405
+ table_fv.set_fontsize(12)
406
+ table_fv.scale(1, 1.5)
407
+ ax_fv_table.set_title('Fangraphs Scouting Grades', style='italic')
408
+
409
+
410
+
411
+ #plt.show()
412
+
413
+
414
+ def calculate_new_player_percentiles(player_id, new_player_metrics, player_summary_filtered):
415
+ """
416
+ Calculate percentiles for a new player's metrics.
417
+
418
+ :param player_id: ID of the player.
419
+ :param new_player_metrics: DataFrame containing new player metrics.
420
+ :param player_summary_filtered: Filtered player summary DataFrame.
421
+ :return: DataFrame containing new player percentiles.
422
+ """
423
+ filtered_summary_clone = player_summary_filtered[['batter_id'] + stat_list].filter(pl.col('batter_id') != player_id).clone()
424
+ combined_data = pl.concat([filtered_summary_clone, new_player_metrics], how="vertical").to_pandas()
425
+ combined_percentiles = pl.DataFrame(pd.concat([combined_data['batter_id'], combined_data[stat_list].rank(pct=True)], axis=1))
426
+ new_player_percentiles = combined_percentiles.filter(pl.col('batter_id') == player_id)
427
+ return new_player_percentiles
428
+
429
+
430
+
431
+ p.set(message="Generating plot", detail="This may take a while...")
432
+
433
+
434
+ p.set(0.3, "Gathering data...")
435
+
436
+ # Example: New player's metrics
437
+ year = int(input.year_input())
438
+ sport_id = int(input.level_input())
439
+ batter_id = int(input.batter_id())
440
+
441
+
442
+ df_player = scrape.get_players(sport_id=sport_id,season=year)
443
+ batter_name_id = dict(zip(df_player['player_id'],df_player['name']))
444
+ player_team_dict = dict(zip(df_player['player_id'],df_player['team']))
445
+ player_position_dict = dict(zip(df_player['player_id'],df_player['position']))
446
+
447
+
448
+ batter_summary = pl.read_csv(f'data/statcast/batter_summary_{level_dict_file[str(sport_id)]}_{year}.csv')
449
+ df_prospects = pd.read_csv(f'data/prospects/prospects_{year}.csv')
450
+ df_rosters = pd.read_csv(f'data/rosters/fangraphs_rosters_{year}.csv')
451
+ df_small = df_rosters[['minorbamid','minormasterid']].dropna()
452
+ dict_mlb_fg=dict(zip(df_small['minorbamid'].astype(int),df_small['minormasterid']))
453
+
454
+
455
+
456
+
457
+ batter_summary_filter = batter_summary.filter((pl.col('pa') >= 300) & (pl.col('launch_speed') >= 0))
458
+ stat_list = batter_summary.columns[2:]
459
+ batter_summary_filter_pd = batter_summary_filter.to_pandas()
460
+ new_player_metrics = batter_summary.filter(pl.col('batter_id') == batter_id)[['batter_id'] + stat_list]
461
+
462
+ # Get percentiles for the new player
463
+ new_player_percentiles = calculate_new_player_percentiles(batter_id, new_player_metrics, batter_summary_filter)
464
+
465
+ p.set(0.6, "Creating plot...")
466
+ # Draw Baseball Savant-style percentile bars
467
+ draw_baseball_savant_percentiles(new_player_metrics=new_player_metrics,
468
+ new_player_percentiles=new_player_percentiles,
469
+ sport_id=sport_id,
470
+ year_input=year)
471
+
472
  app = App(app_ui, server)