nesticot commited on
Commit
7aedbde
·
verified ·
1 Parent(s): 298ba6a

Upload 28 files

Browse files
api_scraper.py CHANGED
The diff for this file is too large to render. See raw diff
 
app.py CHANGED
@@ -1,957 +1,948 @@
1
- import polars as pl
2
- import numpy as np
3
- import pandas as pd
4
- import api_scraper
5
- scrape = api_scraper.MLB_Scrape()
6
- from functions import df_update
7
- from functions import pitch_summary_functions
8
- update = df_update.df_update()
9
- from stuff_model import feature_engineering as fe
10
- from stuff_model import stuff_apply
11
- import requests
12
- import joblib
13
- from matplotlib.gridspec import GridSpec
14
- from shiny import App, reactive, ui, render
15
- from shiny.ui import h2, tags
16
- import matplotlib.pyplot as plt
17
- import matplotlib.gridspec as gridspec
18
- import seaborn as sns
19
- from functions.pitch_summary_functions import *
20
- from shiny import App, reactive, ui, render
21
- from shiny.ui import h2, tags
22
- import datetime
23
- import matplotlib.colors
24
- import pandas as pd
25
- cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ["#648FFF","#FFFFFF","#FFB000"])
26
-
27
-
28
- colour_palette = ['#FFB000','#648FFF','#785EF0',
29
- '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
30
-
31
-
32
- year_list = [2017,2018,2019,2020,2021,2022,2023,2024]
33
-
34
-
35
-
36
- level_dict = {'1':'MLB',
37
- '11':'AAA',
38
- '12':'AA',
39
- '13':'A+',
40
- '14':'A',
41
- '17':'AFL',
42
- '22':'College',
43
- '21':'Prospects',
44
- '51':'International' }
45
-
46
- function_dict={
47
- 'velocity_kdes':'Velocity Distributions',
48
- 'break_plot':'Pitch Movement',
49
- 'break_plot_rhh':'Pitch Movement LHH',
50
- 'break_plot_lhh':'Pitch Movement RHH',
51
- 'tj_stuff_roling':'Rolling tjStuff+ by Pitch',
52
- 'tj_stuff_roling_game':'Rolling tjStuff+ by Game',
53
- 'location_plot_lhb':'Locations vs LHB',
54
- 'location_plot_rhb':'Locations vs RHB',
55
- }
56
-
57
-
58
- split_dict = {'all':'All',
59
- 'left':'LHH',
60
- 'right':'RHH'}
61
-
62
- split_dict_hand = {'all':['L','R'],
63
- 'left':['L'],
64
- 'right':['R']}
65
-
66
- # List of MLB teams and their corresponding ESPN logo URLs
67
- mlb_teams = [
68
- {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
69
- {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
70
- {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
71
- {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
72
- {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
73
- {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
74
- {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
75
- {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
76
- {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
77
- {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
78
- {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
79
- {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
80
- {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
81
- {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
82
- {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
83
- {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
84
- {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
85
- {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
86
- {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
87
- {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
88
- {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
89
- {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
90
- {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
91
- {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
92
- {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
93
- {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
94
- {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
95
- {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
96
- {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
97
- {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"},
98
- {"team": "ZZZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png&w=500&h=500"}
99
- ]
100
-
101
-
102
- df_image = pd.DataFrame(mlb_teams)
103
- image_dict = df_image.set_index('team')['logo_url'].to_dict()
104
- image_dict_flip = df_image.set_index('logo_url')['team'].to_dict()
105
-
106
-
107
- import requests
108
-
109
- import requests
110
-
111
- import os
112
- CAMPAIGN_ID = os.getenv("CAMPAIGN_ID")
113
- ACCESS_TOKEN = os.getenv("ACCESS_TOKEN")
114
- BACKUP_PW = os.getenv("BACKUP_PW")
115
- ADMIN_PW = os.getenv("ADMIN_PW")
116
-
117
- url = f"https://www.patreon.com/api/oauth2/v2/campaigns/{CAMPAIGN_ID}/members"
118
-
119
- headers = {
120
- "Authorization": f"Bearer {ACCESS_TOKEN}"
121
- }
122
-
123
- # Simple parameters, requesting the member's email and currently entitled tiers
124
- params = {
125
- "fields[member]": "full_name,email", # Request the member's email
126
- "include": "currently_entitled_tiers", # Include the currently entitled tiers
127
- "page[size]": 1000 # Fetch up to 1000 patrons per request
128
- }
129
-
130
- response = requests.get(url, headers=headers, params=params)
131
-
132
-
133
- VALID_PASSWORDS = []
134
- if response.status_code == 200:
135
- data = response.json()
136
- for patron in data['data']:
137
- try:
138
- tiers = patron['relationships']['currently_entitled_tiers']['data']
139
- if any(tier['id'] == '9078921' for tier in tiers):
140
- full_name = patron['attributes']['email']
141
- VALID_PASSWORDS.append(full_name)
142
- except KeyError:
143
- continue
144
- VALID_PASSWORDS.append(BACKUP_PW)
145
- VALID_PASSWORDS.append(ADMIN_PW)
146
-
147
-
148
- from shiny import App, reactive, ui, render
149
- from shiny.ui import h2, tags
150
-
151
- # Define the login UI
152
- login_ui = ui.page_fluid(
153
- ui.card(
154
- ui.h2([
155
- "TJStats Daily Pitching Summary App ",
156
- ui.tags.a("(@TJStats)", href="https://twitter.com/TJStats", target="_blank")
157
- ]),
158
- ui.p(
159
- "This App is available to Superstar Patrons. Please enter your Patreon email address in the box below. If you're having trouble, please refer to the ",
160
- ui.tags.a("Patreon post", href="https://www.patreon.com/posts/122860440", target="_blank"),
161
- "."
162
- ),
163
- ui.input_password("password", "Enter Patreon Email (or Password from Link):", width="25%"),
164
- ui.tags.input(
165
- type="checkbox",
166
- id="authenticated",
167
- value=False,
168
- disabled=True
169
- ),
170
- ui.input_action_button("login", "Login", class_="btn-primary"),
171
- ui.output_text("login_message"),
172
- )
173
- )
174
-
175
-
176
-
177
-
178
- # Define the UI layout for the app
179
- main_ui = ui.page_fluid(
180
- ui.layout_sidebar(
181
- ui.panel_sidebar(
182
- # Row for selecting season and level
183
- ui.row(
184
-
185
- ui.column(6, ui.input_date('date_input', 'Select Date')),
186
- ui.column(6, ui.input_select('level_input', 'Select Level', level_dict))
187
- ),
188
- ui.row(ui.input_action_button("game_button", "Get Games", class_="btn-primary")),
189
- ui.row(
190
-
191
- ui.row(ui.column(12, ui.output_ui('game_select_ui', 'Select Game'))),
192
- ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
193
- ),
194
-
195
- # Rows for selecting plots and split options
196
- ui.row(
197
- ui.column(4, ui.input_select('plot_id_1', 'Plot Left', function_dict, multiple=False, selected='location_plot_lhb')),
198
- ui.column(4, ui.input_select('plot_id_2', 'Plot Middle', function_dict, multiple=False, selected='break_plot')),
199
- ui.column(4, ui.input_select('plot_id_3', 'Plot Right', function_dict, multiple=False, selected='location_plot_rhb'))
200
- ),
201
- ui.row(
202
- ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
203
- ui.column(6, ui.input_numeric('rolling_window', 'Rolling Window (for tjStuff+ Plot)', min=1, value=50))
204
- ),
205
- ui.row(
206
- ui.column(6, ui.input_switch("switch", "Custom Team?", False)),
207
- ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False))
208
- ),
209
-
210
- # Row for the action button to generate plot
211
- ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary"))
212
- ),
213
-
214
- ui.panel_main(
215
- # Main content area with tabs (placed directly in page_sidebar)
216
- ui.navset_tab(
217
- ui.nav_panel("Pitching Summary",
218
- ui.output_text("status"),
219
- ui.output_plot('plot', width='2100px', height='2100px')
220
- ),
221
- ui.nav_panel("Table Summary",
222
- ui.output_data_frame("grid_summary")),
223
-
224
- ui.nav_panel("Daily Table",
225
- ui.output_data_frame("grid")),
226
-
227
- ui.nav_panel("Daily Table Style",
228
- ui.input_numeric('head', 'Table Limit', min=0, value=10),
229
- ui.input_numeric('pitch_min', 'Pitch Min.', min=0, value=10),
230
- ui.card(
231
- {"style": "width: 1560px;"},
232
- ui.head_content(
233
- ui.tags.script(src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"),
234
- ui.tags.script("""
235
- async function downloadPNG() {
236
- const content = document.getElementById('capture-section');
237
-
238
- try {
239
- // Create a wrapper div with right margin only
240
- const wrapper = document.createElement('div');
241
- wrapper.style.paddingRight = '20px';
242
- wrapper.style.paddingLeft = '20px';
243
- wrapper.style.paddingTop = '20px';
244
- wrapper.style.backgroundColor = 'white';
245
-
246
- // Clone the content
247
- const clonedContent = content.cloneNode(true);
248
- wrapper.appendChild(clonedContent);
249
-
250
- // Add wrapper to document temporarily
251
- document.body.appendChild(wrapper);
252
-
253
- const canvas = await html2canvas(wrapper, {
254
- backgroundColor: 'white',
255
- scale: 2,
256
- useCORS: true,
257
- logging: false,
258
- width: content.offsetWidth + 20,
259
- height: content.offsetHeight + 50
260
- });
261
-
262
- // Remove temporary wrapper
263
- document.body.removeChild(wrapper);
264
-
265
- // Convert canvas to blob
266
- canvas.toBlob(function(blob) {
267
- const url = URL.createObjectURL(blob);
268
- const link = document.createElement('a');
269
- link.href = url;
270
- link.download = 'stats_card.png';
271
- document.body.appendChild(link);
272
- link.click();
273
- document.body.removeChild(link);
274
- URL.revokeObjectURL(url);
275
- }, 'image/png');
276
- } catch (error) {
277
- console.error('Error generating PNG:', error);
278
- }
279
- }
280
-
281
- $(document).on('click', '#capture_png_btn', function() {
282
- downloadPNG();
283
- });
284
- """)
285
- ),
286
- ui.div(
287
- {
288
- "id": "capture-section",
289
- "style": "background-color: white; padding: 0; margin-left: 20px; margin-right: 20px; margin-top: 20px; margin-bottom: 20px;"
290
- },
291
- ui.div({"style": "font-size:3em;"}, ui.output_text("style_title")),
292
- ui.div({"style": "font-size:1.5em;"}, ui.output_text("min_title")),
293
- ui.br(),
294
- ui.output_table("grid_style"),
295
- ui.br(),
296
- ui.row(
297
- ui.column(8,
298
- ui.div(
299
- {"style": "text-align: left;"},
300
- ui.markdown("### By: @TJStats"),
301
- ui.markdown("### Data: MLB"),
302
- )
303
- ),
304
- ui.column(4,
305
- ui.div(
306
- {"style": "text-align: left; height: 86px; display: flex; justify-content: flex-end;"},
307
- ui.output_image("logo", height="86px")
308
- )
309
- )
310
- ),
311
- ui.div({"style": "height: 20px;"})
312
- ),
313
- ui.div(
314
- {"style": "display: flex; gap: 10px;"},
315
- ui.input_action_button("capture_png_btn", "Save as PNG", class_="btn-success"),
316
- ),
317
- )
318
- )
319
- )
320
- )
321
- )
322
- )
323
-
324
- # Combined UI with conditional panel
325
- app_ui = ui.page_fluid(
326
- ui.tags.head(
327
- ui.tags.script(src="script.js")
328
- ),
329
-
330
- ui.panel_conditional(
331
- "!input.authenticated",
332
- login_ui
333
- ),
334
- ui.panel_conditional(
335
- "input.authenticated",
336
- main_ui
337
- )
338
- )
339
-
340
-
341
-
342
- def server(input, output, session):
343
-
344
-
345
- @reactive.Effect
346
- @reactive.event(input.login)
347
- def check_password():
348
- if input.password() in VALID_PASSWORDS:
349
- ui.update_checkbox("authenticated", value=True)
350
- ui.update_text("login_message", value="")
351
- else:
352
- ui.update_text("login_message", value="Invalid password!")
353
- ui.update_text("password", value="")
354
-
355
- @output
356
- @render.text
357
- def login_message():
358
- return ""
359
-
360
-
361
- @render.image
362
- def logo():
363
- # You'll need to provide the actual image path or URL here
364
- return {"src": "tjstats_logo.jpg"}
365
- @render.text
366
- def style_title():
367
-
368
- return f"Daily {level_dict[input.level_input()]} tjStuff+ Leaders - {str(input.date_input())}"
369
-
370
- @render.text
371
- def min_title():
372
-
373
- return f"(Min. {int(input.pitch_min())} Pitches)"
374
-
375
- @render.ui
376
- @reactive.event(input.game_button,input.date_input,input.level_input, ignore_none=False)
377
- def game_select_ui():
378
- df = (scrape.get_schedule(year_input=[int(str(input.date_input())[:4])],
379
- sport_id=[int(input.level_input())],
380
- game_type=['S','R','P','E','A','I','W','F','L'])
381
- .filter(pl.col('gameday_type').is_in(['P','E']))
382
- .filter(pl.col('state').is_in(['I','M','N','O','F','T','U','Q','R']))
383
- .with_columns(pl.col('date').cast(pl.Utf8))
384
- .filter(pl.col('date') == str(input.date_input()))).with_columns(
385
- (pl.col('away')+' @ '+pl.col('home')+' - '+pl.col('state')).alias('matchup')).sort('time')
386
- game_dict = dict(zip(df['game_id'], df['matchup']))
387
- print('GAMES')
388
- print(game_dict)
389
-
390
- return ui.input_select("game_id", "Select Game", game_dict)
391
-
392
-
393
- @render.ui
394
- @reactive.event(input.game_id)
395
- def player_select_ui():
396
- try:
397
- # Get the list of pitchers for the selected level and season
398
- print('GAMEDATA')
399
- print(input.game_id())
400
- print(type(input.game_id()))
401
-
402
- data_list = scrape.get_data(game_list_input = [int(input.game_id())])
403
- print('GAME')
404
- print(input.game_id())
405
-
406
-
407
- df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
408
- (pl.col("is_pitch") == True)&
409
- (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
410
-
411
- )))).with_columns(
412
- pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
413
- ).with_columns(
414
- (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name'))
415
- )
416
-
417
- print('DATAFRAME')
418
-
419
- # df = scrape.get_data_df(data_list = data_list)
420
- print(df)
421
- pitcher_dict = dict(zip(df['pitcher_id'], df['pitcher_name']))
422
- print('PITCHERS')
423
- print(pitcher_dict)
424
- return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict)
425
- except Exception as e:
426
- print(e)
427
- return ui.output_text('pitcher_id',"No pitchers available for this game")
428
-
429
- @output
430
- @render.text
431
- def status():
432
- # Only show status when generating
433
- if input.generate == 0:
434
- return ""
435
- return ""
436
-
437
- @output
438
- @render.plot
439
- @reactive.event(input.generate_plot, ignore_none=False)
440
- def plot():
441
- # Show progress/loading notification
442
- with ui.Progress(min=0, max=1) as p:
443
- p.set(message="Generating plot", detail="This may take a while...")
444
-
445
-
446
- p.set(0.3, "Gathering data...")
447
-
448
- data_list = scrape.get_data(game_list_input = [int(input.game_id())])
449
- # df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
450
- # (pl.col("pitcher_id") == int(input.pitcher_id()))&
451
- # (pl.col("is_pitch") == True)&
452
- # (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
453
-
454
- # )))).with_columns(
455
- # pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
456
- # ))
457
-
458
- df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
459
- (pl.col("pitcher_id") == int(input.pitcher_id()))&
460
- (pl.col("is_pitch") == True)&
461
- (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
462
-
463
- ))).with_columns(
464
- pl.col("extension").fill_null(6.2)
465
- )).with_columns(
466
- pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
467
- ))
468
-
469
-
470
- df = df.clone()
471
- # df = df.with_columns(
472
- # pl.lit(6.2).alias("extension")
473
- # )
474
-
475
-
476
- p.set(0.6, "Creating plot...")
477
-
478
-
479
- #plt.rcParams["figure.figsize"] = [10,10]
480
- fig = plt.figure(figsize=(26,26))
481
- plt.rcParams.update({'figure.autolayout': True})
482
- fig.set_facecolor('white')
483
- sns.set_theme(style="whitegrid", palette=colour_palette)
484
- print('this is the one plot')
485
-
486
- gs = gridspec.GridSpec(6, 8,
487
- height_ratios=[6,20,12,36,36,6],
488
- width_ratios=[4,18,18,18,18,18,18,4])
489
-
490
-
491
- gs.update(hspace=0.2, wspace=0.5)
492
-
493
- # Define the positions of each subplot in the grid
494
- ax_headshot = fig.add_subplot(gs[1,1:3])
495
- ax_bio = fig.add_subplot(gs[1,3:5])
496
- ax_logo = fig.add_subplot(gs[1,5:7])
497
-
498
- ax_season_table = fig.add_subplot(gs[2,1:7])
499
-
500
- ax_plot_1 = fig.add_subplot(gs[3,1:3])
501
- ax_plot_2 = fig.add_subplot(gs[3,3:5])
502
- ax_plot_3 = fig.add_subplot(gs[3,5:7])
503
-
504
- ax_table = fig.add_subplot(gs[4,1:7])
505
-
506
-
507
- ax_footer = fig.add_subplot(gs[-1,1:7])
508
- ax_header = fig.add_subplot(gs[0,1:7])
509
- ax_left = fig.add_subplot(gs[:,0])
510
- ax_right = fig.add_subplot(gs[:,-1])
511
-
512
- # Hide axes for footer, header, left, and right
513
- ax_footer.axis('off')
514
- ax_header.axis('off')
515
- ax_left.axis('off')
516
- ax_right.axis('off')
517
-
518
- sns.set_theme(style="whitegrid", palette=colour_palette)
519
- fig.set_facecolor('white')
520
-
521
- df_teams = scrape.get_teams()
522
-
523
- year_input = int(str(input.date_input())[:4])
524
- sport_id = int(input.level_input())
525
- player_input = int(input.pitcher_id())
526
- team_id = df['pitcher_team_id'][0]
527
- player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
528
- player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
529
- # plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
530
-
531
- if input.switch():
532
-
533
- # Get the logo URL from the image dictionary using the team abbreviation
534
- logo_url = input.logo_select()
535
-
536
- # Send a GET request to the logo URL
537
- response = requests.get(logo_url)
538
-
539
- # Open the image from the response content
540
- img = Image.open(BytesIO(response.content))
541
-
542
- # Display the image on the axis
543
- ax_logo.set_xlim(0, 1.3)
544
- ax_logo.set_ylim(0, 1)
545
- ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')
546
-
547
- # Turn off the axis
548
- ax_logo.axis('off')
549
-
550
- else:
551
- plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input),team_id=team_id)
552
-
553
-
554
- # stat_summary_table(df=df,
555
- # ax=ax_season_table,
556
- # player_input=player_input,
557
- # split=input.split_id(),
558
- # sport_id=sport_id)
559
-
560
- stat_daily_summary(df=df,
561
- data=data_list,
562
- player_input=int(input.pitcher_id()),
563
- sport_id=int(input.level_input()),
564
- ax=ax_season_table)
565
-
566
-
567
- # break_plot(df=df_plot,ax=ax2)
568
- for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax_plot_1,ax_plot_2,ax_plot_3],[1,3,5]):
569
- if x == 'velocity_kdes':
570
- velocity_kdes(df,
571
- ax=y,
572
- gs=gs,
573
- gs_x=[3,4],
574
- gs_y=[z,z+2],
575
- fig=fig)
576
- if x == 'tj_stuff_roling':
577
- tj_stuff_roling(df=df,
578
- window=int(input.rolling_window()),
579
- ax=y)
580
-
581
- if x == 'tj_stuff_roling_game':
582
- tj_stuff_roling_game(df=df,
583
- window=int(input.rolling_window()),
584
- ax=y)
585
-
586
- if x == 'break_plot':
587
- break_plot(df = df,ax=y)
588
-
589
- if x == 'location_plot_lhb':
590
- location_plot(df = df,ax=y,hand='L')
591
-
592
- if x == 'location_plot_rhb':
593
- location_plot(df = df,ax=y,hand='R')
594
-
595
- if x == 'break_plot_rhh':
596
- break_plot(df = df.filter(pl.col('batter_hand')=='R'),ax=y)
597
-
598
- if x == 'break_plot_lhh':
599
- break_plot(df = df.filter(pl.col('batter_hand')=='L'),ax=y)
600
-
601
-
602
-
603
-
604
-
605
- summary_table(df=df,
606
- ax=ax_table)
607
-
608
- plot_footer(ax_footer)
609
-
610
- ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1)
611
- # Hide axes ticks and labels
612
- ax_watermark.set_xticks([])
613
- ax_watermark.set_yticks([])
614
- ax_watermark.set_frame_on(False) # Optional: Hide border
615
-
616
- img = Image.open('tj stats circle-01_new.jpg')
617
- # Display the image
618
- ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.06)
619
-
620
-
621
- ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1)
622
- ax_watermark2.set_xlim(0,1)
623
- ax_watermark2.set_ylim(0,1)
624
- # Hide axes ticks and labels
625
- ax_watermark2.set_xticks([])
626
- ax_watermark2.set_yticks([])
627
- ax_watermark2.set_frame_on(False) # Optional: Hide border
628
-
629
- # Open the image
630
- img = Image.open('tj stats circle-01_new.jpg')
631
- # Get the original size
632
- width, height = img.size
633
- # Calculate the new size (50% larger)
634
- new_width = int(width * 0.5)
635
- new_height = int(height * 0.5)
636
- # Resize the image
637
- img_resized = img.resize((new_width, new_height))
638
- # Display the image
639
- ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1)
640
-
641
-
642
-
643
-
644
- fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
645
-
646
-
647
-
648
- @output
649
- @render.data_frame
650
- @reactive.event(input.generate_plot, ignore_none=False)
651
- def grid_summary():
652
-
653
- data_list = scrape.get_data(game_list_input = [int(input.game_id())])
654
- df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
655
- (pl.col("pitcher_id") == int(input.pitcher_id()))&
656
- (pl.col("is_pitch") == True)&
657
- (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
658
-
659
- ))).with_columns(
660
- pl.col("extension").fill_null(6.2)
661
- )).with_columns(
662
- pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
663
- ))
664
-
665
-
666
- df = df.clone()
667
- features_table = ['start_speed',
668
- 'spin_rate',
669
- 'extension',
670
- 'ivb',
671
- 'hb',
672
- 'x0',
673
- 'z0']
674
-
675
-
676
-
677
- selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand',
678
- 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus']
679
-
680
-
681
-
682
- return render.DataGrid(
683
- df.select(selection).to_pandas().round(1),
684
- row_selection_mode='multiple',
685
- height='700px',
686
- width='fit-content',
687
- filters=True,
688
- )
689
-
690
-
691
- @output
692
- @render.data_frame
693
- @reactive.event(input.generate_plot, ignore_none=False)
694
- def grid():
695
-
696
-
697
- df_games = (scrape.get_schedule(year_input=[int(str(input.date_input())[:4])],
698
- sport_id=[int(input.level_input())],
699
- game_type=['S','R','P','E','A','I','W','F','L']).with_columns(pl.col('date').cast(pl.Utf8)).
700
- filter(pl.col('date') == str(input.date_input()))).with_columns(
701
- (pl.col('away')+' @ '+pl.col('home')).alias('matchup'))
702
-
703
-
704
-
705
- game_list = df_games['game_id'].unique().to_list()
706
-
707
- # Get the list of pitchers for the selected level and season
708
- data_list = scrape.get_data(game_list)
709
- df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
710
- (pl.col("is_pitch") == True)&
711
- (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
712
-
713
- )))).with_columns(
714
- pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
715
- ).with_columns(
716
- (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name'))
717
- )
718
-
719
- # game_list = game_list_df['game_id'].unique().to_list()
720
- data = scrape.get_data(game_list[:])
721
- df = scrape.get_data_df(data)
722
-
723
- pitcher_team_dict = dict(zip(df['pitcher_id'], df['pitcher_team']))
724
-
725
-
726
- df_test = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(df).filter(
727
- (pl.col("is_pitch") == True)))))
728
-
729
- df_test = df_test.with_columns(
730
- (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name')
731
- )
732
-
733
-
734
-
735
-
736
- # Aggregate tj_stuff_plus by pitcher_id and year
737
- df_agg_2024_pitch = df_test.group_by(['pitcher_id','pitcher_name','pitch_type']).agg(
738
- pl.col('tj_stuff_plus').len().alias('count'),
739
- pl.col('tj_stuff_plus').mean()
740
- )
741
-
742
- # Calculate the weighted average of 'tj_stuff_plus' for each pitcher
743
- df_weighted_avg = df_agg_2024_pitch.with_columns(
744
- (pl.col('tj_stuff_plus') * pl.col('count')).alias('weighted_tj_stuff_plus')
745
- ).group_by(['pitcher_id', 'pitcher_name']).agg(
746
- pl.col('count').sum().alias('total_count'),
747
- pl.col('weighted_tj_stuff_plus').sum().alias('total_weighted_tj_stuff_plus')
748
- ).with_columns(
749
- (pl.col('total_weighted_tj_stuff_plus') / pl.col('total_count')).alias('tj_stuff_plus')
750
- ).select(['pitcher_id', 'pitcher_name', 'tj_stuff_plus', 'total_count'])
751
-
752
- # Add the 'pitch_type' column with value "All"
753
- df_weighted_avg = df_weighted_avg.with_columns(
754
- pl.lit("All").alias('pitch_type')
755
- )
756
-
757
- # Select and rename columns to match the original DataFrame
758
- df_weighted_avg = df_weighted_avg.select([
759
- 'pitcher_id',
760
- 'pitcher_name',
761
-
762
- 'pitch_type',
763
- pl.col('total_count').alias('count'),
764
- 'tj_stuff_plus'
765
- ])
766
-
767
- # Concatenate the new rows with the original DataFrame
768
- df_agg_2024_pitch = pl.concat([df_agg_2024_pitch, df_weighted_avg])
769
-
770
-
771
- df_small = df_agg_2024_pitch.select(['pitcher_id','pitcher_name','pitch_type','count','tj_stuff_plus'])
772
- count_dict = dict(zip(df_small.filter(pl.col('pitch_type')=='All')['pitcher_id'],
773
- df_small.filter(pl.col('pitch_type')=='All')['count']))
774
- # Check if 'FS' column exists, if not create it and fill with None
775
-
776
- df_small_pivot = (df_small.pivot(index=['pitcher_id','pitcher_name'],
777
- columns='pitch_type',
778
- values='tj_stuff_plus').with_columns(
779
- pl.col("pitcher_id").replace_strict(count_dict, default=None).alias("count")))
780
-
781
- # Check if 'FS' column exists, if not create it and fill with None
782
- for col in ['CH', 'CU', 'FC', 'FF', 'FS', 'SI', 'SL', 'ST', 'All']:
783
- if col not in df_small_pivot.columns:
784
- df_small_pivot = df_small_pivot.with_columns(pl.lit(None).alias(col))
785
-
786
-
787
- df_small_pivot = df_small_pivot.with_columns(
788
- pl.col("pitcher_id").replace_strict(pitcher_team_dict, default=None).alias("pitcher_team"))
789
-
790
- df_small_pivot = df_small_pivot.select(['pitcher_id','pitcher_name','pitcher_team','count','CH','CU','FC','FF','FS','SI','SL','ST','All']).sort('All',descending=True)#.head(10)#.write_clipboard()
791
-
792
- df_small_pivot = df_small_pivot.with_columns(
793
- pl.col(col).cast(pl.Int32, strict=False) for col in ['CH', 'CU', 'FC', 'FF', 'FS', 'SI', 'SL', 'ST', 'All']
794
- )
795
-
796
-
797
- return render.DataGrid(
798
- df_small_pivot,
799
- row_selection_mode='multiple',
800
- height='700px',
801
- width='fit-content',
802
- filters=True,
803
- )
804
-
805
-
806
- @output
807
- @render.table
808
- @reactive.event(input.generate_plot, input.pitch_min,input.head,ignore_none=False)
809
- def grid_style():
810
-
811
- row_limit = int(input.head())
812
- pitch_limit = int(input.pitch_min())
813
- df_games = (scrape.get_schedule(year_input=[int(str(input.date_input())[:4])],
814
- sport_id=[int(input.level_input())],
815
- game_type=['S','R','P','E','A','I','W','F','L']).with_columns(pl.col('date').cast(pl.Utf8)).
816
- filter(pl.col('date') == str(input.date_input()))).with_columns(
817
- (pl.col('away')+' @ '+pl.col('home')).alias('matchup'))
818
-
819
-
820
-
821
- game_list = df_games['game_id'].unique().to_list()
822
-
823
- # Get the list of pitchers for the selected level and season
824
- data_list = scrape.get_data(game_list)
825
- df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
826
- (pl.col("is_pitch") == True)&
827
- (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
828
-
829
- )))).with_columns(
830
- pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
831
- ).with_columns(
832
- (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name'))
833
- )
834
-
835
- # game_list = game_list_df['game_id'].unique().to_list()
836
- data = scrape.get_data(game_list[:])
837
- df = scrape.get_data_df(data)
838
-
839
- pitcher_team_dict = dict(zip(df['pitcher_id'], df['pitcher_team']))
840
-
841
-
842
- df_test = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(df).filter(
843
- (pl.col("is_pitch") == True)))))
844
-
845
- # df_test = df_test.with_columns(
846
- # (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name')
847
- # )
848
-
849
-
850
-
851
-
852
- # Aggregate tj_stuff_plus by pitcher_id and year
853
- df_agg_2024_pitch = df_test.group_by(['pitcher_id','pitcher_name','pitch_type']).agg(
854
- pl.col('tj_stuff_plus').len().alias('count'),
855
- pl.col('tj_stuff_plus').mean()
856
- )
857
-
858
- # Calculate the weighted average of 'tj_stuff_plus' for each pitcher
859
- df_weighted_avg = df_agg_2024_pitch.with_columns(
860
- (pl.col('tj_stuff_plus') * pl.col('count')).alias('weighted_tj_stuff_plus')
861
- ).group_by(['pitcher_id', 'pitcher_name']).agg(
862
- pl.col('count').sum().alias('total_count'),
863
- pl.col('weighted_tj_stuff_plus').sum().alias('total_weighted_tj_stuff_plus')
864
- ).with_columns(
865
- (pl.col('total_weighted_tj_stuff_plus') / pl.col('total_count')).alias('tj_stuff_plus')
866
- ).select(['pitcher_id', 'pitcher_name', 'tj_stuff_plus', 'total_count'])
867
-
868
- # Add the 'pitch_type' column with value "All"
869
- df_weighted_avg = df_weighted_avg.with_columns(
870
- pl.lit("All").alias('pitch_type')
871
- )
872
-
873
- # Select and rename columns to match the original DataFrame
874
- df_weighted_avg = df_weighted_avg.select([
875
- 'pitcher_id',
876
- 'pitcher_name',
877
-
878
- 'pitch_type',
879
- pl.col('total_count').alias('count'),
880
- 'tj_stuff_plus'
881
- ])
882
-
883
- # Concatenate the new rows with the original DataFrame
884
- df_agg_2024_pitch = pl.concat([df_agg_2024_pitch, df_weighted_avg])
885
-
886
-
887
- df_small = df_agg_2024_pitch.select(['pitcher_id','pitcher_name','pitch_type','count','tj_stuff_plus'])
888
- count_dict = dict(zip(df_small.filter(pl.col('pitch_type')=='All')['pitcher_id'],
889
- df_small.filter(pl.col('pitch_type')=='All')['count']))
890
- # Check if 'FS' column exists, if not create it and fill with None
891
-
892
- df_small_pivot = (df_small.pivot(index=['pitcher_id','pitcher_name'],
893
- columns='pitch_type',
894
- values='tj_stuff_plus').with_columns(
895
- pl.col("pitcher_id").replace_strict(count_dict, default=None).alias("count")))
896
-
897
- # Check if 'FS' column exists, if not create it and fill with None
898
- for col in ['CH', 'CU', 'FC', 'FF', 'FS', 'SI', 'SL', 'ST', 'All']:
899
- if col not in df_small_pivot.columns:
900
- df_small_pivot = df_small_pivot.with_columns(pl.lit(None).alias(col))
901
-
902
-
903
- df_small_pivot = df_small_pivot.with_columns(
904
- pl.col("pitcher_id").replace_strict(pitcher_team_dict, default=None).alias("pitcher_team"))
905
-
906
- df_small_pivot = df_small_pivot.select(['pitcher_name','pitcher_team','count','CH','CU','FC','FF','FS','SI','SL','ST','All']).sort('All',descending=True)#.head(10)#.write_clipboard()
907
-
908
- df_small_pivot = df_small_pivot.with_columns(
909
- pl.col(col).cast(pl.Int32, strict=False) for col in ['CH', 'CU', 'FC', 'FF', 'FS', 'SI', 'SL', 'ST', 'All']
910
- )
911
-
912
- df_export = df_small_pivot.filter(pl.col('count')>=pitch_limit).to_pandas().head(row_limit)
913
- df_export.columns = ['Name', 'Team', 'Pitches', 'CH', 'CU', 'FC',
914
- 'FF', 'FS', 'SI', 'SL', 'ST', 'All']
915
- df_style = df_export.style
916
-
917
- df_style = df_style.set_properties(**{'border': '1.0 px'},overwrite=False).set_table_styles([{'selector' :'th',
918
- 'props':[('text-align', 'center'),('font-size', '22px'),('Height','30px'),('border', '1px black solid !important')]},
919
- {'selector' :'td', 'props':[('text-align', 'center'),('font-size', '22px')]}],overwrite=False).set_table_styles(
920
- [{'selector': 'tr', 'props': [('line-height', '1px')]}],overwrite=False).set_properties(
921
- **{'Height': '60px'},**{'text-align': 'center'},overwrite=False).hide_index()
922
-
923
-
924
-
925
-
926
- #cmap_sum_2 = matplotlib.colors.LinearSegmentedColormap.from_list("", ["#FFFFFF","#F0E442"])
927
-
928
- df_style = df_style.format('{:.0f}',subset=df_export.columns[3:], na_rep='')
929
-
930
-
931
- # df_style
932
- df_style = df_style.background_gradient(cmap=cmap_sum,subset = ((list(df_export.index[:]),df_export.columns[3:])),vmin=80,vmax=120)#.applymap(lambda x: 'color: white' if pd.isnull(x) else '')
933
-
934
- #df_style = df_style.applymap(background_gradient_ignore_nan)
935
- #df_style = df_style
936
- df_style = df_style.applymap(lambda x: 'color: transparent; background-color: transparent' if pd.isnull(x) else '')
937
-
938
-
939
-
940
- df_style = df_style.set_properties(
941
- **{'border': '1px black solid !important'},subset = ((list(df_style.index[:-1]),df_style.columns[:]))).set_properties(
942
- **{'min-width':'325px'},subset = ((list(df_style.index[:-1]),df_style.columns[0])),overwrite=False).set_properties(
943
- **{'min-width':'100px'},subset = ((list(df_style.index[:-1]),df_style.columns[1:3])),overwrite=False).set_properties(
944
- **{'min-width':'100px'},subset = ((list(df_style.index[:-1]),df_style.columns[3:])),overwrite=False).set_properties(
945
- # **{'min-width':'125px'},subset = ((list(df_style.index[:-1]),df_style.columns[-1])),overwrite=False).set_properties(
946
- **{'border': '1px black solid !important'},subset = ((list(df_style.index[:]),df_style.columns[:])))
947
-
948
- # df_style = df_style.set_table_styles([{'selector' :'th',
949
- # 'props':[('text-align', 'center'),('font-size', '22px'),('Height','30px'),('border', '1px black solid !important')]},
950
- # {'selector' :'td', 'props':[('text-align', 'center'),('font-size', '22px')]}], overwrite=False)
951
-
952
- return df_style
953
-
954
-
955
-
956
-
957
  app = App(app_ui, server)
 
1
+ import polars as pl
2
+ import numpy as np
3
+ import pandas as pd
4
+ import api_scraper
5
+ scrape = api_scraper.MLB_Scrape()
6
+ from functions import df_update
7
+ from functions import pitch_summary_functions
8
+ update = df_update.df_update()
9
+ from stuff_model import feature_engineering as fe
10
+ from stuff_model import stuff_apply
11
+ import requests
12
+ import joblib
13
+ from matplotlib.gridspec import GridSpec
14
+ from shiny import App, reactive, ui, render
15
+ from shiny.ui import h2, tags
16
+ import matplotlib.pyplot as plt
17
+ import matplotlib.gridspec as gridspec
18
+ import seaborn as sns
19
+ from functions.pitch_summary_functions import *
20
+ from shiny import App, reactive, ui, render
21
+ from shiny.ui import h2, tags
22
+ import datetime
23
+ import matplotlib.colors
24
+ import pandas as pd
25
+ cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ["#648FFF","#FFFFFF","#FFB000"])
26
+
27
+
28
+ colour_palette = ['#FFB000','#648FFF','#785EF0',
29
+ '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
30
+
31
+
32
+ year_list = [2017,2018,2019,2020,2021,2022,2023,2024]
33
+
34
+
35
+
36
+ level_dict = {'1':'MLB',
37
+ '11':'AAA',
38
+ '12':'AA',
39
+ '13':'A+',
40
+ '14':'A',
41
+ '17':'AFL',
42
+ '22':'College',
43
+ '21':'Prospects',
44
+ '51':'International' }
45
+
46
+ function_dict={
47
+ 'velocity_kdes':'Velocity Distributions',
48
+ 'break_plot':'Pitch Movement',
49
+ 'break_plot_rhh':'Pitch Movement LHH',
50
+ 'break_plot_lhh':'Pitch Movement RHH',
51
+ 'tj_stuff_roling':'Rolling tjStuff+ by Pitch',
52
+ 'tj_stuff_roling_game':'Rolling tjStuff+ by Game',
53
+ 'location_plot_lhb':'Locations vs LHB',
54
+ 'location_plot_rhb':'Locations vs RHB',
55
+ }
56
+
57
+
58
+ split_dict = {'all':'All',
59
+ 'left':'LHH',
60
+ 'right':'RHH'}
61
+
62
+ split_dict_hand = {'all':['L','R'],
63
+ 'left':['L'],
64
+ 'right':['R']}
65
+
66
+ # List of MLB teams and their corresponding ESPN logo URLs
67
+ mlb_teams = [
68
+ {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
69
+ {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
70
+ {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
71
+ {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
72
+ {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
73
+ {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
74
+ {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
75
+ {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
76
+ {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
77
+ {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
78
+ {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
79
+ {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
80
+ {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
81
+ {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
82
+ {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
83
+ {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
84
+ {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
85
+ {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
86
+ {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
87
+ {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
88
+ {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
89
+ {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
90
+ {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
91
+ {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
92
+ {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
93
+ {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
94
+ {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
95
+ {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
96
+ {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
97
+ {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"},
98
+ {"team": "ZZZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png&w=500&h=500"}
99
+ ]
100
+
101
+
102
+ df_image = pd.DataFrame(mlb_teams)
103
+ image_dict = df_image.set_index('team')['logo_url'].to_dict()
104
+ image_dict_flip = df_image.set_index('logo_url')['team'].to_dict()
105
+
106
+
107
+ import requests
108
+
109
+ import requests
110
+
111
+ import os
112
+ CAMPAIGN_ID = os.getenv("CAMPAIGN_ID")
113
+ ACCESS_TOKEN = os.getenv("ACCESS_TOKEN")
114
+ BACKUP_PW = os.getenv("BACKUP_PW")
115
+ ADMIN_PW = os.getenv("ADMIN_PW")
116
+
117
+ url = f"https://www.patreon.com/api/oauth2/v2/campaigns/{CAMPAIGN_ID}/members"
118
+
119
+ headers = {
120
+ "Authorization": f"Bearer {ACCESS_TOKEN}"
121
+ }
122
+
123
+ # Simple parameters, requesting the member's email and currently entitled tiers
124
+ params = {
125
+ "fields[member]": "full_name,email", # Request the member's email
126
+ "include": "currently_entitled_tiers", # Include the currently entitled tiers
127
+ "page[size]": 1000 # Fetch up to 1000 patrons per request
128
+ }
129
+
130
+ response = requests.get(url, headers=headers, params=params)
131
+
132
+
133
+ VALID_PASSWORDS = []
134
+ if response.status_code == 200:
135
+ data = response.json()
136
+ for patron in data['data']:
137
+ try:
138
+ tiers = patron['relationships']['currently_entitled_tiers']['data']
139
+ if any(tier['id'] == '9078921' for tier in tiers):
140
+ full_name = patron['attributes']['email']
141
+ VALID_PASSWORDS.append(full_name)
142
+ except KeyError:
143
+ continue
144
+ VALID_PASSWORDS.append(BACKUP_PW)
145
+ VALID_PASSWORDS.append(ADMIN_PW)
146
+ VALID_PASSWORDS.append('')
147
+
148
+ from shiny import App, reactive, ui, render
149
+ from shiny.ui import h2, tags
150
+
151
+ # Define the login UI
152
+ login_ui = ui.page_fluid(
153
+ ui.card(
154
+ ui.h2([
155
+ "TJStats Daily Pitching Summary App ",
156
+ ui.tags.a("(@TJStats)", href="https://twitter.com/TJStats", target="_blank")
157
+ ]),
158
+ ui.p(
159
+ "This App is available to Superstar Patrons. Please enter your Patreon email address in the box below. If you're having trouble, please refer to the ",
160
+ ui.tags.a("Patreon post", href="https://www.patreon.com/posts/122860440", target="_blank"),
161
+ "."
162
+ ),
163
+ ui.input_password("password", "Enter Patreon Email (or Password from Link):", width="25%"),
164
+ ui.tags.input(
165
+ type="checkbox",
166
+ id="authenticated",
167
+ value=False,
168
+ disabled=True
169
+ ),
170
+ ui.input_action_button("login", "Login", class_="btn-primary"),
171
+ ui.output_text("login_message"),
172
+ )
173
+ )
174
+
175
+
176
+
177
+
178
+ # Define the UI layout for the app
179
+ main_ui = ui.page_fluid(
180
+ ui.layout_sidebar(
181
+ ui.panel_sidebar(
182
+ # Row for selecting season and level
183
+ ui.row(
184
+
185
+ ui.column(6, ui.input_date('date_input', 'Select Date')),
186
+ ui.column(6, ui.input_select('level_input', 'Select Level', level_dict))
187
+ ),
188
+ ui.row(ui.input_action_button("game_button", "Get Games", class_="btn-primary")),
189
+ ui.row(
190
+
191
+ ui.row(ui.column(12, ui.output_ui('game_select_ui', 'Select Game'))),
192
+ ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),
193
+ ),
194
+
195
+ # Rows for selecting plots and split options
196
+ ui.row(
197
+ ui.column(4, ui.input_select('plot_id_1', 'Plot Left', function_dict, multiple=False, selected='location_plot_lhb')),
198
+ ui.column(4, ui.input_select('plot_id_2', 'Plot Middle', function_dict, multiple=False, selected='break_plot')),
199
+ ui.column(4, ui.input_select('plot_id_3', 'Plot Right', function_dict, multiple=False, selected='location_plot_rhb'))
200
+ ),
201
+ ui.row(
202
+ ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)),
203
+ ui.column(6, ui.input_numeric('rolling_window', 'Rolling Window (for tjStuff+ Plot)', min=1, value=50))
204
+ ),
205
+ ui.row(
206
+ ui.column(6, ui.input_switch("switch", "Custom Team?", False)),
207
+ ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False))
208
+ ),
209
+
210
+ # Row for the action button to generate plot
211
+ ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary"))
212
+ ),
213
+
214
+ ui.panel_main(
215
+ # Main content area with tabs (placed directly in page_sidebar)
216
+ ui.navset_tab(
217
+ ui.nav_panel("Pitching Summary",
218
+ ui.output_text("status"),
219
+ ui.output_plot('plot', width='2100px', height='2100px')
220
+ ),
221
+ ui.nav_panel("Table Summary",
222
+ ui.output_data_frame("grid_summary")),
223
+
224
+ ui.nav_panel("Daily Table",
225
+ ui.output_data_frame("grid")),
226
+
227
+ ui.nav_panel("Daily Table Style",
228
+ ui.input_numeric('head', 'Table Limit', min=0, value=10),
229
+ ui.input_numeric('pitch_min', 'Pitch Min.', min=0, value=10),
230
+ ui.card(
231
+ {"style": "width: 1560px;"},
232
+ ui.head_content(
233
+ ui.tags.script(src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"),
234
+ ui.tags.script("""
235
+ async function downloadPNG() {
236
+ const content = document.getElementById('capture-section');
237
+
238
+ try {
239
+ // Create a wrapper div with right margin only
240
+ const wrapper = document.createElement('div');
241
+ wrapper.style.paddingRight = '20px';
242
+ wrapper.style.paddingLeft = '20px';
243
+ wrapper.style.paddingTop = '20px';
244
+ wrapper.style.backgroundColor = 'white';
245
+
246
+ // Clone the content
247
+ const clonedContent = content.cloneNode(true);
248
+ wrapper.appendChild(clonedContent);
249
+
250
+ // Add wrapper to document temporarily
251
+ document.body.appendChild(wrapper);
252
+
253
+ const canvas = await html2canvas(wrapper, {
254
+ backgroundColor: 'white',
255
+ scale: 2,
256
+ useCORS: true,
257
+ logging: false,
258
+ width: content.offsetWidth + 20,
259
+ height: content.offsetHeight + 50
260
+ });
261
+
262
+ // Remove temporary wrapper
263
+ document.body.removeChild(wrapper);
264
+
265
+ // Convert canvas to blob
266
+ canvas.toBlob(function(blob) {
267
+ const url = URL.createObjectURL(blob);
268
+ const link = document.createElement('a');
269
+ link.href = url;
270
+ link.download = 'stats_card.png';
271
+ document.body.appendChild(link);
272
+ link.click();
273
+ document.body.removeChild(link);
274
+ URL.revokeObjectURL(url);
275
+ }, 'image/png');
276
+ } catch (error) {
277
+ console.error('Error generating PNG:', error);
278
+ }
279
+ }
280
+
281
+ $(document).on('click', '#capture_png_btn', function() {
282
+ downloadPNG();
283
+ });
284
+ """)
285
+ ),
286
+ ui.div(
287
+ {
288
+ "id": "capture-section",
289
+ "style": "background-color: white; padding: 0; margin-left: 20px; margin-right: 20px; margin-top: 20px; margin-bottom: 20px;"
290
+ },
291
+ ui.div({"style": "font-size:3em;"}, ui.output_text("style_title")),
292
+ ui.div({"style": "font-size:1.5em;"}, ui.output_text("min_title")),
293
+ ui.br(),
294
+ ui.output_table("grid_style"),
295
+ ui.br(),
296
+ ui.row(
297
+ ui.column(8,
298
+ ui.div(
299
+ {"style": "text-align: left;"},
300
+ ui.markdown("### By: @TJStats"),
301
+ ui.markdown("### Data: MLB"),
302
+ )
303
+ ),
304
+ ui.column(4,
305
+ ui.div(
306
+ {"style": "text-align: left; height: 86px; display: flex; justify-content: flex-end;"},
307
+ ui.output_image("logo", height="86px")
308
+ )
309
+ )
310
+ ),
311
+ ui.div({"style": "height: 20px;"})
312
+ ),
313
+ ui.div(
314
+ {"style": "display: flex; gap: 10px;"},
315
+ ui.input_action_button("capture_png_btn", "Save as PNG", class_="btn-success"),
316
+ ),
317
+ )
318
+ )
319
+ )
320
+ )
321
+ )
322
+ )
323
+
324
+ # Combined UI with conditional panel
325
+ app_ui = ui.page_fluid(
326
+ ui.tags.head(
327
+ ui.tags.script(src="script.js")
328
+ ),
329
+
330
+ ui.panel_conditional(
331
+ "!input.authenticated",
332
+ login_ui
333
+ ),
334
+ ui.panel_conditional(
335
+ "input.authenticated",
336
+ main_ui
337
+ )
338
+ )
339
+
340
+
341
+
342
+ def server(input, output, session):
343
+
344
+
345
+ @reactive.Effect
346
+ @reactive.event(input.login)
347
+ def check_password():
348
+ if input.password() in VALID_PASSWORDS:
349
+ ui.update_checkbox("authenticated", value=True)
350
+ ui.update_text("login_message", value="")
351
+ else:
352
+ ui.update_text("login_message", value="Invalid password!")
353
+ ui.update_text("password", value="")
354
+
355
+ @output
356
+ @render.text
357
+ def login_message():
358
+ return ""
359
+
360
+
361
+ @render.image
362
+ def logo():
363
+ # You'll need to provide the actual image path or URL here
364
+ return {"src": "tjstats_logo.jpg"}
365
+ @render.text
366
+ def style_title():
367
+
368
+ return f"Daily {level_dict[input.level_input()]} tjStuff+ Leaders - {str(input.date_input())}"
369
+
370
+ @render.text
371
+ def min_title():
372
+
373
+ return f"(Min. {int(input.pitch_min())} Pitches)"
374
+
375
+ @render.ui
376
+ @reactive.event(input.game_button,input.date_input,input.level_input, ignore_none=False)
377
+ def game_select_ui():
378
+ df = (scrape.get_schedule(year_input=[int(str(input.date_input())[:4])],
379
+ sport_id=[int(input.level_input())],
380
+ game_type=['S','R','P','E','A','I','W','F','L'])
381
+ .filter(pl.col('gameday_type').is_in(['P','E']))
382
+ .filter(pl.col('state').is_in(['I','M','N','O','F','T','U','Q','R']))
383
+ .with_columns(pl.col('date').cast(pl.Utf8))
384
+ .filter(pl.col('date') == str(input.date_input()))).with_columns(
385
+ (pl.col('away')+' @ '+pl.col('home')+' - '+pl.col('state')).alias('matchup')).sort('time')
386
+ game_dict = dict(zip(df['game_id'], df['matchup']))
387
+ print('GAMES')
388
+ print(game_dict)
389
+
390
+ return ui.input_select("game_id", "Select Game", game_dict)
391
+
392
+
393
+ @render.ui
394
+ @reactive.event(input.game_id)
395
+ def player_select_ui():
396
+ try:
397
+ # Get the list of pitchers for the selected level and season
398
+ data_list = scrape.get_data(game_list_input = [int(input.game_id())])
399
+ print('DATALIST')
400
+ print(input.game_id())
401
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
402
+ (pl.col("is_pitch") == True)&
403
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
404
+
405
+ )))).with_columns(
406
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
407
+ ).with_columns(
408
+ (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name'))
409
+ )
410
+ # print('DATAFRAME')
411
+ # print(df)
412
+ pitcher_dict = dict(zip(df['pitcher_id'], df['pitcher_name']))
413
+ print('PITCHERS')
414
+ print(pitcher_dict)
415
+ return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict)
416
+ except Exception as e:
417
+ print(e)
418
+ return ui.output_text('pitcher_id',"No pitchers available for this game")
419
+
420
+ @output
421
+ @render.text
422
+ def status():
423
+ # Only show status when generating
424
+ if input.generate == 0:
425
+ return ""
426
+ return ""
427
+
428
+ @output
429
+ @render.plot
430
+ @reactive.event(input.generate_plot, ignore_none=False)
431
+ def plot():
432
+ # Show progress/loading notification
433
+ with ui.Progress(min=0, max=1) as p:
434
+ p.set(message="Generating plot", detail="This may take a while...")
435
+
436
+
437
+ p.set(0.3, "Gathering data...")
438
+
439
+ data_list = scrape.get_data(game_list_input = [int(input.game_id())])
440
+ # df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
441
+ # (pl.col("pitcher_id") == int(input.pitcher_id()))&
442
+ # (pl.col("is_pitch") == True)&
443
+ # (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
444
+
445
+ # )))).with_columns(
446
+ # pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
447
+ # ))
448
+
449
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
450
+ (pl.col("pitcher_id") == int(input.pitcher_id()))&
451
+ (pl.col("is_pitch") == True)&
452
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
453
+
454
+ ))).with_columns(
455
+ pl.col("extension").fill_null(6.2)
456
+ )).with_columns(
457
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
458
+ ))
459
+
460
+
461
+ df = df.clone()
462
+ # df = df.with_columns(
463
+ # pl.lit(6.2).alias("extension")
464
+ # )
465
+
466
+
467
+ p.set(0.6, "Creating plot...")
468
+
469
+
470
+ #plt.rcParams["figure.figsize"] = [10,10]
471
+ fig = plt.figure(figsize=(26,26))
472
+ plt.rcParams.update({'figure.autolayout': True})
473
+ fig.set_facecolor('white')
474
+ sns.set_theme(style="whitegrid", palette=colour_palette)
475
+ print('this is the one plot')
476
+
477
+ gs = gridspec.GridSpec(6, 8,
478
+ height_ratios=[6,20,12,36,36,6],
479
+ width_ratios=[4,18,18,18,18,18,18,4])
480
+
481
+
482
+ gs.update(hspace=0.2, wspace=0.5)
483
+
484
+ # Define the positions of each subplot in the grid
485
+ ax_headshot = fig.add_subplot(gs[1,1:3])
486
+ ax_bio = fig.add_subplot(gs[1,3:5])
487
+ ax_logo = fig.add_subplot(gs[1,5:7])
488
+
489
+ ax_season_table = fig.add_subplot(gs[2,1:7])
490
+
491
+ ax_plot_1 = fig.add_subplot(gs[3,1:3])
492
+ ax_plot_2 = fig.add_subplot(gs[3,3:5])
493
+ ax_plot_3 = fig.add_subplot(gs[3,5:7])
494
+
495
+ ax_table = fig.add_subplot(gs[4,1:7])
496
+
497
+
498
+ ax_footer = fig.add_subplot(gs[-1,1:7])
499
+ ax_header = fig.add_subplot(gs[0,1:7])
500
+ ax_left = fig.add_subplot(gs[:,0])
501
+ ax_right = fig.add_subplot(gs[:,-1])
502
+
503
+ # Hide axes for footer, header, left, and right
504
+ ax_footer.axis('off')
505
+ ax_header.axis('off')
506
+ ax_left.axis('off')
507
+ ax_right.axis('off')
508
+
509
+ sns.set_theme(style="whitegrid", palette=colour_palette)
510
+ fig.set_facecolor('white')
511
+
512
+ df_teams = scrape.get_teams()
513
+
514
+ year_input = int(str(input.date_input())[:4])
515
+ sport_id = int(input.level_input())
516
+ player_input = int(input.pitcher_id())
517
+ team_id = df['pitcher_team_id'][0]
518
+ player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
519
+ player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
520
+ # plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
521
+
522
+ if input.switch():
523
+
524
+ # Get the logo URL from the image dictionary using the team abbreviation
525
+ logo_url = input.logo_select()
526
+
527
+ # Send a GET request to the logo URL
528
+ response = requests.get(logo_url)
529
+
530
+ # Open the image from the response content
531
+ img = Image.open(BytesIO(response.content))
532
+
533
+ # Display the image on the axis
534
+ ax_logo.set_xlim(0, 1.3)
535
+ ax_logo.set_ylim(0, 1)
536
+ ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')
537
+
538
+ # Turn off the axis
539
+ ax_logo.axis('off')
540
+
541
+ else:
542
+ plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input),team_id=team_id)
543
+
544
+
545
+ # stat_summary_table(df=df,
546
+ # ax=ax_season_table,
547
+ # player_input=player_input,
548
+ # split=input.split_id(),
549
+ # sport_id=sport_id)
550
+
551
+ stat_daily_summary(df=df,
552
+ data=data_list,
553
+ player_input=int(input.pitcher_id()),
554
+ sport_id=int(input.level_input()),
555
+ ax=ax_season_table)
556
+
557
+
558
+ # break_plot(df=df_plot,ax=ax2)
559
+ for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax_plot_1,ax_plot_2,ax_plot_3],[1,3,5]):
560
+ if x == 'velocity_kdes':
561
+ velocity_kdes(df,
562
+ ax=y,
563
+ gs=gs,
564
+ gs_x=[3,4],
565
+ gs_y=[z,z+2],
566
+ fig=fig)
567
+ if x == 'tj_stuff_roling':
568
+ tj_stuff_roling(df=df,
569
+ window=int(input.rolling_window()),
570
+ ax=y)
571
+
572
+ if x == 'tj_stuff_roling_game':
573
+ tj_stuff_roling_game(df=df,
574
+ window=int(input.rolling_window()),
575
+ ax=y)
576
+
577
+ if x == 'break_plot':
578
+ break_plot(df = df,ax=y)
579
+
580
+ if x == 'location_plot_lhb':
581
+ location_plot(df = df,ax=y,hand='L')
582
+
583
+ if x == 'location_plot_rhb':
584
+ location_plot(df = df,ax=y,hand='R')
585
+
586
+ if x == 'break_plot_rhh':
587
+ break_plot(df = df.filter(pl.col('batter_hand')=='R'),ax=y)
588
+
589
+ if x == 'break_plot_lhh':
590
+ break_plot(df = df.filter(pl.col('batter_hand')=='L'),ax=y)
591
+
592
+
593
+
594
+
595
+
596
+ summary_table(df=df,
597
+ ax=ax_table)
598
+
599
+ plot_footer(ax_footer)
600
+
601
+ ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1)
602
+ # Hide axes ticks and labels
603
+ ax_watermark.set_xticks([])
604
+ ax_watermark.set_yticks([])
605
+ ax_watermark.set_frame_on(False) # Optional: Hide border
606
+
607
+ img = Image.open('tj stats circle-01_new.jpg')
608
+ # Display the image
609
+ ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.06)
610
+
611
+
612
+ ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1)
613
+ ax_watermark2.set_xlim(0,1)
614
+ ax_watermark2.set_ylim(0,1)
615
+ # Hide axes ticks and labels
616
+ ax_watermark2.set_xticks([])
617
+ ax_watermark2.set_yticks([])
618
+ ax_watermark2.set_frame_on(False) # Optional: Hide border
619
+
620
+ # Open the image
621
+ img = Image.open('tj stats circle-01_new.jpg')
622
+ # Get the original size
623
+ width, height = img.size
624
+ # Calculate the new size (50% larger)
625
+ new_width = int(width * 0.5)
626
+ new_height = int(height * 0.5)
627
+ # Resize the image
628
+ img_resized = img.resize((new_width, new_height))
629
+ # Display the image
630
+ ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1)
631
+
632
+
633
+
634
+
635
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
636
+
637
+
638
+
639
+ @output
640
+ @render.data_frame
641
+ @reactive.event(input.generate_plot, ignore_none=False)
642
+ def grid_summary():
643
+
644
+ data_list = scrape.get_data(game_list_input = [int(input.game_id())])
645
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
646
+ (pl.col("pitcher_id") == int(input.pitcher_id()))&
647
+ (pl.col("is_pitch") == True)&
648
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
649
+
650
+ ))).with_columns(
651
+ pl.col("extension").fill_null(6.2)
652
+ )).with_columns(
653
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
654
+ ))
655
+
656
+
657
+ df = df.clone()
658
+ features_table = ['start_speed',
659
+ 'spin_rate',
660
+ 'extension',
661
+ 'ivb',
662
+ 'hb',
663
+ 'x0',
664
+ 'z0']
665
+
666
+
667
+
668
+ selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand',
669
+ 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus']
670
+
671
+
672
+
673
+ return render.DataGrid(
674
+ df.select(selection).to_pandas().round(1),
675
+ row_selection_mode='multiple',
676
+ height='700px',
677
+ width='fit-content',
678
+ filters=True,
679
+ )
680
+
681
+
682
+ @output
683
+ @render.data_frame
684
+ @reactive.event(input.generate_plot, ignore_none=False)
685
+ def grid():
686
+
687
+
688
+ df_games = (scrape.get_schedule(year_input=[int(str(input.date_input())[:4])],
689
+ sport_id=[int(input.level_input())],
690
+ game_type=['S','R','P','E','A','I','W','F','L']).with_columns(pl.col('date').cast(pl.Utf8)).
691
+ filter(pl.col('date') == str(input.date_input()))).with_columns(
692
+ (pl.col('away')+' @ '+pl.col('home')).alias('matchup'))
693
+
694
+
695
+
696
+ game_list = df_games['game_id'].unique().to_list()
697
+
698
+ # Get the list of pitchers for the selected level and season
699
+ data_list = scrape.get_data(game_list)
700
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
701
+ (pl.col("is_pitch") == True)&
702
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
703
+
704
+ )))).with_columns(
705
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
706
+ ).with_columns(
707
+ (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name'))
708
+ )
709
+
710
+ # game_list = game_list_df['game_id'].unique().to_list()
711
+ data = scrape.get_data(game_list[:])
712
+ df = scrape.get_data_df(data)
713
+
714
+ pitcher_team_dict = dict(zip(df['pitcher_id'], df['pitcher_team']))
715
+
716
+
717
+ df_test = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(df).filter(
718
+ (pl.col("is_pitch") == True)))))
719
+
720
+ df_test = df_test.with_columns(
721
+ (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name')
722
+ )
723
+
724
+
725
+
726
+
727
+ # Aggregate tj_stuff_plus by pitcher_id and year
728
+ df_agg_2024_pitch = df_test.group_by(['pitcher_id','pitcher_name','pitch_type']).agg(
729
+ pl.col('tj_stuff_plus').len().alias('count'),
730
+ pl.col('tj_stuff_plus').mean()
731
+ )
732
+
733
+ # Calculate the weighted average of 'tj_stuff_plus' for each pitcher
734
+ df_weighted_avg = df_agg_2024_pitch.with_columns(
735
+ (pl.col('tj_stuff_plus') * pl.col('count')).alias('weighted_tj_stuff_plus')
736
+ ).group_by(['pitcher_id', 'pitcher_name']).agg(
737
+ pl.col('count').sum().alias('total_count'),
738
+ pl.col('weighted_tj_stuff_plus').sum().alias('total_weighted_tj_stuff_plus')
739
+ ).with_columns(
740
+ (pl.col('total_weighted_tj_stuff_plus') / pl.col('total_count')).alias('tj_stuff_plus')
741
+ ).select(['pitcher_id', 'pitcher_name', 'tj_stuff_plus', 'total_count'])
742
+
743
+ # Add the 'pitch_type' column with value "All"
744
+ df_weighted_avg = df_weighted_avg.with_columns(
745
+ pl.lit("All").alias('pitch_type')
746
+ )
747
+
748
+ # Select and rename columns to match the original DataFrame
749
+ df_weighted_avg = df_weighted_avg.select([
750
+ 'pitcher_id',
751
+ 'pitcher_name',
752
+
753
+ 'pitch_type',
754
+ pl.col('total_count').alias('count'),
755
+ 'tj_stuff_plus'
756
+ ])
757
+
758
+ # Concatenate the new rows with the original DataFrame
759
+ df_agg_2024_pitch = pl.concat([df_agg_2024_pitch, df_weighted_avg])
760
+
761
+
762
+ df_small = df_agg_2024_pitch.select(['pitcher_id','pitcher_name','pitch_type','count','tj_stuff_plus'])
763
+ count_dict = dict(zip(df_small.filter(pl.col('pitch_type')=='All')['pitcher_id'],
764
+ df_small.filter(pl.col('pitch_type')=='All')['count']))
765
+ # Check if 'FS' column exists, if not create it and fill with None
766
+
767
+ df_small_pivot = (df_small.pivot(index=['pitcher_id','pitcher_name'],
768
+ columns='pitch_type',
769
+ values='tj_stuff_plus').with_columns(
770
+ pl.col("pitcher_id").replace_strict(count_dict, default=None).alias("count")))
771
+
772
+ # Check if 'FS' column exists, if not create it and fill with None
773
+ for col in ['CH', 'CU', 'FC', 'FF', 'FS', 'SI', 'SL', 'ST', 'All']:
774
+ if col not in df_small_pivot.columns:
775
+ df_small_pivot = df_small_pivot.with_columns(pl.lit(None).alias(col))
776
+
777
+
778
+ df_small_pivot = df_small_pivot.with_columns(
779
+ pl.col("pitcher_id").replace_strict(pitcher_team_dict, default=None).alias("pitcher_team"))
780
+
781
+ df_small_pivot = df_small_pivot.select(['pitcher_id','pitcher_name','pitcher_team','count','CH','CU','FC','FF','FS','SI','SL','ST','All']).sort('All',descending=True)#.head(10)#.write_clipboard()
782
+
783
+ df_small_pivot = df_small_pivot.with_columns(
784
+ pl.col(col).cast(pl.Int32, strict=False) for col in ['CH', 'CU', 'FC', 'FF', 'FS', 'SI', 'SL', 'ST', 'All']
785
+ )
786
+
787
+
788
+ return render.DataGrid(
789
+ df_small_pivot,
790
+ row_selection_mode='multiple',
791
+ height='700px',
792
+ width='fit-content',
793
+ filters=True,
794
+ )
795
+
796
+
797
+ @output
798
+ @render.table
799
+ @reactive.event(input.generate_plot, input.pitch_min,input.head,ignore_none=False)
800
+ def grid_style():
801
+
802
+ row_limit = int(input.head())
803
+ pitch_limit = int(input.pitch_min())
804
+ df_games = (scrape.get_schedule(year_input=[int(str(input.date_input())[:4])],
805
+ sport_id=[int(input.level_input())],
806
+ game_type=['S','R','P','E','A','I','W','F','L']).with_columns(pl.col('date').cast(pl.Utf8)).
807
+ filter(pl.col('date') == str(input.date_input()))).with_columns(
808
+ (pl.col('away')+' @ '+pl.col('home')).alias('matchup'))
809
+
810
+
811
+
812
+ game_list = df_games['game_id'].unique().to_list()
813
+
814
+ # Get the list of pitchers for the selected level and season
815
+ data_list = scrape.get_data(game_list)
816
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
817
+ (pl.col("is_pitch") == True)&
818
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
819
+
820
+ )))).with_columns(
821
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
822
+ ).with_columns(
823
+ (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name'))
824
+ )
825
+
826
+ # game_list = game_list_df['game_id'].unique().to_list()
827
+ data = scrape.get_data(game_list[:])
828
+ df = scrape.get_data_df(data)
829
+
830
+ pitcher_team_dict = dict(zip(df['pitcher_id'], df['pitcher_team']))
831
+
832
+
833
+ df_test = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(df).filter(
834
+ (pl.col("is_pitch") == True)))))
835
+
836
+ # df_test = df_test.with_columns(
837
+ # (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name')
838
+ # )
839
+
840
+
841
+
842
+
843
+ # Aggregate tj_stuff_plus by pitcher_id and year
844
+ df_agg_2024_pitch = df_test.group_by(['pitcher_id','pitcher_name','pitch_type']).agg(
845
+ pl.col('tj_stuff_plus').len().alias('count'),
846
+ pl.col('tj_stuff_plus').mean()
847
+ )
848
+
849
+ # Calculate the weighted average of 'tj_stuff_plus' for each pitcher
850
+ df_weighted_avg = df_agg_2024_pitch.with_columns(
851
+ (pl.col('tj_stuff_plus') * pl.col('count')).alias('weighted_tj_stuff_plus')
852
+ ).group_by(['pitcher_id', 'pitcher_name']).agg(
853
+ pl.col('count').sum().alias('total_count'),
854
+ pl.col('weighted_tj_stuff_plus').sum().alias('total_weighted_tj_stuff_plus')
855
+ ).with_columns(
856
+ (pl.col('total_weighted_tj_stuff_plus') / pl.col('total_count')).alias('tj_stuff_plus')
857
+ ).select(['pitcher_id', 'pitcher_name', 'tj_stuff_plus', 'total_count'])
858
+
859
+ # Add the 'pitch_type' column with value "All"
860
+ df_weighted_avg = df_weighted_avg.with_columns(
861
+ pl.lit("All").alias('pitch_type')
862
+ )
863
+
864
+ # Select and rename columns to match the original DataFrame
865
+ df_weighted_avg = df_weighted_avg.select([
866
+ 'pitcher_id',
867
+ 'pitcher_name',
868
+
869
+ 'pitch_type',
870
+ pl.col('total_count').alias('count'),
871
+ 'tj_stuff_plus'
872
+ ])
873
+
874
+ # Concatenate the new rows with the original DataFrame
875
+ df_agg_2024_pitch = pl.concat([df_agg_2024_pitch, df_weighted_avg])
876
+
877
+
878
+ df_small = df_agg_2024_pitch.select(['pitcher_id','pitcher_name','pitch_type','count','tj_stuff_plus'])
879
+ count_dict = dict(zip(df_small.filter(pl.col('pitch_type')=='All')['pitcher_id'],
880
+ df_small.filter(pl.col('pitch_type')=='All')['count']))
881
+ # Check if 'FS' column exists, if not create it and fill with None
882
+
883
+ df_small_pivot = (df_small.pivot(index=['pitcher_id','pitcher_name'],
884
+ columns='pitch_type',
885
+ values='tj_stuff_plus').with_columns(
886
+ pl.col("pitcher_id").replace_strict(count_dict, default=None).alias("count")))
887
+
888
+ # Check if 'FS' column exists, if not create it and fill with None
889
+ for col in ['CH', 'CU', 'FC', 'FF', 'FS', 'SI', 'SL', 'ST', 'All']:
890
+ if col not in df_small_pivot.columns:
891
+ df_small_pivot = df_small_pivot.with_columns(pl.lit(None).alias(col))
892
+
893
+
894
+ df_small_pivot = df_small_pivot.with_columns(
895
+ pl.col("pitcher_id").replace_strict(pitcher_team_dict, default=None).alias("pitcher_team"))
896
+
897
+ df_small_pivot = df_small_pivot.select(['pitcher_name','pitcher_team','count','CH','CU','FC','FF','FS','SI','SL','ST','All']).sort('All',descending=True)#.head(10)#.write_clipboard()
898
+
899
+ df_small_pivot = df_small_pivot.with_columns(
900
+ pl.col(col).cast(pl.Int32, strict=False) for col in ['CH', 'CU', 'FC', 'FF', 'FS', 'SI', 'SL', 'ST', 'All']
901
+ )
902
+
903
+ df_export = df_small_pivot.filter(pl.col('count')>=pitch_limit).to_pandas().head(row_limit)
904
+ df_export.columns = ['Name', 'Team', 'Pitches', 'CH', 'CU', 'FC',
905
+ 'FF', 'FS', 'SI', 'SL', 'ST', 'All']
906
+ df_style = df_export.style
907
+
908
+ df_style = df_style.set_properties(**{'border': '1.0 px'},overwrite=False).set_table_styles([{'selector' :'th',
909
+ 'props':[('text-align', 'center'),('font-size', '22px'),('Height','30px'),('border', '1px black solid !important')]},
910
+ {'selector' :'td', 'props':[('text-align', 'center'),('font-size', '22px')]}],overwrite=False).set_table_styles(
911
+ [{'selector': 'tr', 'props': [('line-height', '1px')]}],overwrite=False).set_properties(
912
+ **{'Height': '60px'},**{'text-align': 'center'},overwrite=False).hide_index()
913
+
914
+
915
+
916
+
917
+ #cmap_sum_2 = matplotlib.colors.LinearSegmentedColormap.from_list("", ["#FFFFFF","#F0E442"])
918
+
919
+ df_style = df_style.format('{:.0f}',subset=df_export.columns[3:], na_rep='')
920
+
921
+
922
+ # df_style
923
+ df_style = df_style.background_gradient(cmap=cmap_sum,subset = ((list(df_export.index[:]),df_export.columns[3:])),vmin=80,vmax=120)#.applymap(lambda x: 'color: white' if pd.isnull(x) else '')
924
+
925
+ #df_style = df_style.applymap(background_gradient_ignore_nan)
926
+ #df_style = df_style
927
+ df_style = df_style.applymap(lambda x: 'color: transparent; background-color: transparent' if pd.isnull(x) else '')
928
+
929
+
930
+
931
+ df_style = df_style.set_properties(
932
+ **{'border': '1px black solid !important'},subset = ((list(df_style.index[:-1]),df_style.columns[:]))).set_properties(
933
+ **{'min-width':'325px'},subset = ((list(df_style.index[:-1]),df_style.columns[0])),overwrite=False).set_properties(
934
+ **{'min-width':'100px'},subset = ((list(df_style.index[:-1]),df_style.columns[1:3])),overwrite=False).set_properties(
935
+ **{'min-width':'100px'},subset = ((list(df_style.index[:-1]),df_style.columns[3:])),overwrite=False).set_properties(
936
+ # **{'min-width':'125px'},subset = ((list(df_style.index[:-1]),df_style.columns[-1])),overwrite=False).set_properties(
937
+ **{'border': '1px black solid !important'},subset = ((list(df_style.index[:]),df_style.columns[:])))
938
+
939
+ # df_style = df_style.set_table_styles([{'selector' :'th',
940
+ # 'props':[('text-align', 'center'),('font-size', '22px'),('Height','30px'),('border', '1px black solid !important')]},
941
+ # {'selector' :'td', 'props':[('text-align', 'center'),('font-size', '22px')]}], overwrite=False)
942
+
943
+ return df_style
944
+
945
+
946
+
947
+
 
 
 
 
 
 
 
 
 
948
  app = App(app_ui, server)
functions/__pycache__/df_update.cpython-39.pyc ADDED
Binary file (14 kB). View file
 
functions/__pycache__/pitch_summary_functions.cpython-39.pyc ADDED
Binary file (38.7 kB). View file
 
functions/pitch_summary_functions.py CHANGED
The diff for this file is too large to render. See raw diff
 
requirements.txt CHANGED
@@ -1,16 +1,16 @@
1
- joblib==1.3.2
2
- lightgbm
3
- matplotlib==3.5.1
4
- numpy==1.23.5
5
- pandas==1.5.2
6
- polars==1.12.0
7
- Requests==2.31.0
8
- scipy==1.11.1
9
- seaborn==0.11.1
10
- scikit-learn==1.0.1
11
- shiny==0.6.1
12
- Jinja2==3.1.4
13
- tqdm==4.62.3
14
- pyarrow
15
- cairosvg
16
-
 
1
+ joblib==1.3.2
2
+ lightgbm
3
+ matplotlib==3.5.1
4
+ numpy==1.23.5
5
+ pandas==1.5.2
6
+ polars==1.12.0
7
+ Requests==2.31.0
8
+ scipy==1.11.1
9
+ seaborn==0.11.1
10
+ scikit-learn==1.0.1
11
+ shiny==0.6.1
12
+ Jinja2==3.1.4
13
+ tqdm==4.62.3
14
+ pyarrow
15
+ cairosvg
16
+
stuff_model/__pycache__/calculate_arm_angles.cpython-39.pyc ADDED
Binary file (2.01 kB). View file
 
stuff_model/__pycache__/feature_engineering.cpython-39.pyc ADDED
Binary file (2.49 kB). View file
 
stuff_model/__pycache__/stuff_apply.cpython-39.pyc ADDED
Binary file (1.31 kB). View file