nesticot commited on
Commit
16c9849
·
verified ·
1 Parent(s): f57c07a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +948 -947
app.py CHANGED
@@ -1,948 +1,949 @@
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)
 
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
+ print(data_list)
402
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
403
+ (pl.col("is_pitch") == True)&
404
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
405
+
406
+ )))).with_columns(
407
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
408
+ ).with_columns(
409
+ (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name'))
410
+ )
411
+ # print('DATAFRAME')
412
+ # print(df)
413
+ pitcher_dict = dict(zip(df['pitcher_id'], df['pitcher_name']))
414
+ print('PITCHERS')
415
+ print(pitcher_dict)
416
+ return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict)
417
+ except Exception as e:
418
+ print(e)
419
+ return ui.output_text('pitcher_id',"No pitchers available for this game")
420
+
421
+ @output
422
+ @render.text
423
+ def status():
424
+ # Only show status when generating
425
+ if input.generate == 0:
426
+ return ""
427
+ return ""
428
+
429
+ @output
430
+ @render.plot
431
+ @reactive.event(input.generate_plot, ignore_none=False)
432
+ def plot():
433
+ # Show progress/loading notification
434
+ with ui.Progress(min=0, max=1) as p:
435
+ p.set(message="Generating plot", detail="This may take a while...")
436
+
437
+
438
+ p.set(0.3, "Gathering data...")
439
+
440
+ data_list = scrape.get_data(game_list_input = [int(input.game_id())])
441
+ # df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
442
+ # (pl.col("pitcher_id") == int(input.pitcher_id()))&
443
+ # (pl.col("is_pitch") == True)&
444
+ # (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
445
+
446
+ # )))).with_columns(
447
+ # pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
448
+ # ))
449
+
450
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
451
+ (pl.col("pitcher_id") == int(input.pitcher_id()))&
452
+ (pl.col("is_pitch") == True)&
453
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
454
+
455
+ ))).with_columns(
456
+ pl.col("extension").fill_null(6.2)
457
+ )).with_columns(
458
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
459
+ ))
460
+
461
+
462
+ df = df.clone()
463
+ # df = df.with_columns(
464
+ # pl.lit(6.2).alias("extension")
465
+ # )
466
+
467
+
468
+ p.set(0.6, "Creating plot...")
469
+
470
+
471
+ #plt.rcParams["figure.figsize"] = [10,10]
472
+ fig = plt.figure(figsize=(26,26))
473
+ plt.rcParams.update({'figure.autolayout': True})
474
+ fig.set_facecolor('white')
475
+ sns.set_theme(style="whitegrid", palette=colour_palette)
476
+ print('this is the one plot')
477
+
478
+ gs = gridspec.GridSpec(6, 8,
479
+ height_ratios=[6,20,12,36,36,6],
480
+ width_ratios=[4,18,18,18,18,18,18,4])
481
+
482
+
483
+ gs.update(hspace=0.2, wspace=0.5)
484
+
485
+ # Define the positions of each subplot in the grid
486
+ ax_headshot = fig.add_subplot(gs[1,1:3])
487
+ ax_bio = fig.add_subplot(gs[1,3:5])
488
+ ax_logo = fig.add_subplot(gs[1,5:7])
489
+
490
+ ax_season_table = fig.add_subplot(gs[2,1:7])
491
+
492
+ ax_plot_1 = fig.add_subplot(gs[3,1:3])
493
+ ax_plot_2 = fig.add_subplot(gs[3,3:5])
494
+ ax_plot_3 = fig.add_subplot(gs[3,5:7])
495
+
496
+ ax_table = fig.add_subplot(gs[4,1:7])
497
+
498
+
499
+ ax_footer = fig.add_subplot(gs[-1,1:7])
500
+ ax_header = fig.add_subplot(gs[0,1:7])
501
+ ax_left = fig.add_subplot(gs[:,0])
502
+ ax_right = fig.add_subplot(gs[:,-1])
503
+
504
+ # Hide axes for footer, header, left, and right
505
+ ax_footer.axis('off')
506
+ ax_header.axis('off')
507
+ ax_left.axis('off')
508
+ ax_right.axis('off')
509
+
510
+ sns.set_theme(style="whitegrid", palette=colour_palette)
511
+ fig.set_facecolor('white')
512
+
513
+ df_teams = scrape.get_teams()
514
+
515
+ year_input = int(str(input.date_input())[:4])
516
+ sport_id = int(input.level_input())
517
+ player_input = int(input.pitcher_id())
518
+ team_id = df['pitcher_team_id'][0]
519
+ player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input)
520
+ player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input)
521
+ # plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input))
522
+
523
+ if input.switch():
524
+
525
+ # Get the logo URL from the image dictionary using the team abbreviation
526
+ logo_url = input.logo_select()
527
+
528
+ # Send a GET request to the logo URL
529
+ response = requests.get(logo_url)
530
+
531
+ # Open the image from the response content
532
+ img = Image.open(BytesIO(response.content))
533
+
534
+ # Display the image on the axis
535
+ ax_logo.set_xlim(0, 1.3)
536
+ ax_logo.set_ylim(0, 1)
537
+ ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')
538
+
539
+ # Turn off the axis
540
+ ax_logo.axis('off')
541
+
542
+ else:
543
+ 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)
544
+
545
+
546
+ # stat_summary_table(df=df,
547
+ # ax=ax_season_table,
548
+ # player_input=player_input,
549
+ # split=input.split_id(),
550
+ # sport_id=sport_id)
551
+
552
+ stat_daily_summary(df=df,
553
+ data=data_list,
554
+ player_input=int(input.pitcher_id()),
555
+ sport_id=int(input.level_input()),
556
+ ax=ax_season_table)
557
+
558
+
559
+ # break_plot(df=df_plot,ax=ax2)
560
+ 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]):
561
+ if x == 'velocity_kdes':
562
+ velocity_kdes(df,
563
+ ax=y,
564
+ gs=gs,
565
+ gs_x=[3,4],
566
+ gs_y=[z,z+2],
567
+ fig=fig)
568
+ if x == 'tj_stuff_roling':
569
+ tj_stuff_roling(df=df,
570
+ window=int(input.rolling_window()),
571
+ ax=y)
572
+
573
+ if x == 'tj_stuff_roling_game':
574
+ tj_stuff_roling_game(df=df,
575
+ window=int(input.rolling_window()),
576
+ ax=y)
577
+
578
+ if x == 'break_plot':
579
+ break_plot(df = df,ax=y)
580
+
581
+ if x == 'location_plot_lhb':
582
+ location_plot(df = df,ax=y,hand='L')
583
+
584
+ if x == 'location_plot_rhb':
585
+ location_plot(df = df,ax=y,hand='R')
586
+
587
+ if x == 'break_plot_rhh':
588
+ break_plot(df = df.filter(pl.col('batter_hand')=='R'),ax=y)
589
+
590
+ if x == 'break_plot_lhh':
591
+ break_plot(df = df.filter(pl.col('batter_hand')=='L'),ax=y)
592
+
593
+
594
+
595
+
596
+
597
+ summary_table(df=df,
598
+ ax=ax_table)
599
+
600
+ plot_footer(ax_footer)
601
+
602
+ ax_watermark = fig.add_subplot(gs[1:-1,1:-1],zorder=-1)
603
+ # Hide axes ticks and labels
604
+ ax_watermark.set_xticks([])
605
+ ax_watermark.set_yticks([])
606
+ ax_watermark.set_frame_on(False) # Optional: Hide border
607
+
608
+ img = Image.open('tj stats circle-01_new.jpg')
609
+ # Display the image
610
+ ax_watermark.imshow(img, extent=[0, 1, 0, 1], origin='upper',zorder=-1, alpha=0.06)
611
+
612
+
613
+ ax_watermark2 = fig.add_subplot(gs[-2:,1:4],zorder=1)
614
+ ax_watermark2.set_xlim(0,1)
615
+ ax_watermark2.set_ylim(0,1)
616
+ # Hide axes ticks and labels
617
+ ax_watermark2.set_xticks([])
618
+ ax_watermark2.set_yticks([])
619
+ ax_watermark2.set_frame_on(False) # Optional: Hide border
620
+
621
+ # Open the image
622
+ img = Image.open('tj stats circle-01_new.jpg')
623
+ # Get the original size
624
+ width, height = img.size
625
+ # Calculate the new size (50% larger)
626
+ new_width = int(width * 0.5)
627
+ new_height = int(height * 0.5)
628
+ # Resize the image
629
+ img_resized = img.resize((new_width, new_height))
630
+ # Display the image
631
+ ax_watermark2.imshow(img, extent=[0.26, 0.46, 0.0,0.2], origin='upper',zorder=-1, alpha=1)
632
+
633
+
634
+
635
+
636
+ fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)
637
+
638
+
639
+
640
+ @output
641
+ @render.data_frame
642
+ @reactive.event(input.generate_plot, ignore_none=False)
643
+ def grid_summary():
644
+
645
+ data_list = scrape.get_data(game_list_input = [int(input.game_id())])
646
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
647
+ (pl.col("pitcher_id") == int(input.pitcher_id()))&
648
+ (pl.col("is_pitch") == True)&
649
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
650
+
651
+ ))).with_columns(
652
+ pl.col("extension").fill_null(6.2)
653
+ )).with_columns(
654
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
655
+ ))
656
+
657
+
658
+ df = df.clone()
659
+ features_table = ['start_speed',
660
+ 'spin_rate',
661
+ 'extension',
662
+ 'ivb',
663
+ 'hb',
664
+ 'x0',
665
+ 'z0']
666
+
667
+
668
+
669
+ selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand',
670
+ 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus']
671
+
672
+
673
+
674
+ return render.DataGrid(
675
+ df.select(selection).to_pandas().round(1),
676
+ row_selection_mode='multiple',
677
+ height='700px',
678
+ width='fit-content',
679
+ filters=True,
680
+ )
681
+
682
+
683
+ @output
684
+ @render.data_frame
685
+ @reactive.event(input.generate_plot, ignore_none=False)
686
+ def grid():
687
+
688
+
689
+ df_games = (scrape.get_schedule(year_input=[int(str(input.date_input())[:4])],
690
+ sport_id=[int(input.level_input())],
691
+ game_type=['S','R','P','E','A','I','W','F','L']).with_columns(pl.col('date').cast(pl.Utf8)).
692
+ filter(pl.col('date') == str(input.date_input()))).with_columns(
693
+ (pl.col('away')+' @ '+pl.col('home')).alias('matchup'))
694
+
695
+
696
+
697
+ game_list = df_games['game_id'].unique().to_list()
698
+
699
+ # Get the list of pitchers for the selected level and season
700
+ data_list = scrape.get_data(game_list)
701
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
702
+ (pl.col("is_pitch") == True)&
703
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
704
+
705
+ )))).with_columns(
706
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
707
+ ).with_columns(
708
+ (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name'))
709
+ )
710
+
711
+ # game_list = game_list_df['game_id'].unique().to_list()
712
+ data = scrape.get_data(game_list[:])
713
+ df = scrape.get_data_df(data)
714
+
715
+ pitcher_team_dict = dict(zip(df['pitcher_id'], df['pitcher_team']))
716
+
717
+
718
+ df_test = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(df).filter(
719
+ (pl.col("is_pitch") == True)))))
720
+
721
+ df_test = df_test.with_columns(
722
+ (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name')
723
+ )
724
+
725
+
726
+
727
+
728
+ # Aggregate tj_stuff_plus by pitcher_id and year
729
+ df_agg_2024_pitch = df_test.group_by(['pitcher_id','pitcher_name','pitch_type']).agg(
730
+ pl.col('tj_stuff_plus').len().alias('count'),
731
+ pl.col('tj_stuff_plus').mean()
732
+ )
733
+
734
+ # Calculate the weighted average of 'tj_stuff_plus' for each pitcher
735
+ df_weighted_avg = df_agg_2024_pitch.with_columns(
736
+ (pl.col('tj_stuff_plus') * pl.col('count')).alias('weighted_tj_stuff_plus')
737
+ ).group_by(['pitcher_id', 'pitcher_name']).agg(
738
+ pl.col('count').sum().alias('total_count'),
739
+ pl.col('weighted_tj_stuff_plus').sum().alias('total_weighted_tj_stuff_plus')
740
+ ).with_columns(
741
+ (pl.col('total_weighted_tj_stuff_plus') / pl.col('total_count')).alias('tj_stuff_plus')
742
+ ).select(['pitcher_id', 'pitcher_name', 'tj_stuff_plus', 'total_count'])
743
+
744
+ # Add the 'pitch_type' column with value "All"
745
+ df_weighted_avg = df_weighted_avg.with_columns(
746
+ pl.lit("All").alias('pitch_type')
747
+ )
748
+
749
+ # Select and rename columns to match the original DataFrame
750
+ df_weighted_avg = df_weighted_avg.select([
751
+ 'pitcher_id',
752
+ 'pitcher_name',
753
+
754
+ 'pitch_type',
755
+ pl.col('total_count').alias('count'),
756
+ 'tj_stuff_plus'
757
+ ])
758
+
759
+ # Concatenate the new rows with the original DataFrame
760
+ df_agg_2024_pitch = pl.concat([df_agg_2024_pitch, df_weighted_avg])
761
+
762
+
763
+ df_small = df_agg_2024_pitch.select(['pitcher_id','pitcher_name','pitch_type','count','tj_stuff_plus'])
764
+ count_dict = dict(zip(df_small.filter(pl.col('pitch_type')=='All')['pitcher_id'],
765
+ df_small.filter(pl.col('pitch_type')=='All')['count']))
766
+ # Check if 'FS' column exists, if not create it and fill with None
767
+
768
+ df_small_pivot = (df_small.pivot(index=['pitcher_id','pitcher_name'],
769
+ columns='pitch_type',
770
+ values='tj_stuff_plus').with_columns(
771
+ pl.col("pitcher_id").replace_strict(count_dict, default=None).alias("count")))
772
+
773
+ # Check if 'FS' column exists, if not create it and fill with None
774
+ for col in ['CH', 'CU', 'FC', 'FF', 'FS', 'SI', 'SL', 'ST', 'All']:
775
+ if col not in df_small_pivot.columns:
776
+ df_small_pivot = df_small_pivot.with_columns(pl.lit(None).alias(col))
777
+
778
+
779
+ df_small_pivot = df_small_pivot.with_columns(
780
+ pl.col("pitcher_id").replace_strict(pitcher_team_dict, default=None).alias("pitcher_team"))
781
+
782
+ 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()
783
+
784
+ df_small_pivot = df_small_pivot.with_columns(
785
+ pl.col(col).cast(pl.Int32, strict=False) for col in ['CH', 'CU', 'FC', 'FF', 'FS', 'SI', 'SL', 'ST', 'All']
786
+ )
787
+
788
+
789
+ return render.DataGrid(
790
+ df_small_pivot,
791
+ row_selection_mode='multiple',
792
+ height='700px',
793
+ width='fit-content',
794
+ filters=True,
795
+ )
796
+
797
+
798
+ @output
799
+ @render.table
800
+ @reactive.event(input.generate_plot, input.pitch_min,input.head,ignore_none=False)
801
+ def grid_style():
802
+
803
+ row_limit = int(input.head())
804
+ pitch_limit = int(input.pitch_min())
805
+ df_games = (scrape.get_schedule(year_input=[int(str(input.date_input())[:4])],
806
+ sport_id=[int(input.level_input())],
807
+ game_type=['S','R','P','E','A','I','W','F','L']).with_columns(pl.col('date').cast(pl.Utf8)).
808
+ filter(pl.col('date') == str(input.date_input()))).with_columns(
809
+ (pl.col('away')+' @ '+pl.col('home')).alias('matchup'))
810
+
811
+
812
+
813
+ game_list = df_games['game_id'].unique().to_list()
814
+
815
+ # Get the list of pitchers for the selected level and season
816
+ data_list = scrape.get_data(game_list)
817
+ df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter(
818
+ (pl.col("is_pitch") == True)&
819
+ (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()]))
820
+
821
+ )))).with_columns(
822
+ pl.col('pitch_type').count().over('pitch_type').alias('pitch_count')
823
+ ).with_columns(
824
+ (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name'))
825
+ )
826
+
827
+ # game_list = game_list_df['game_id'].unique().to_list()
828
+ data = scrape.get_data(game_list[:])
829
+ df = scrape.get_data_df(data)
830
+
831
+ pitcher_team_dict = dict(zip(df['pitcher_id'], df['pitcher_team']))
832
+
833
+
834
+ df_test = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(df).filter(
835
+ (pl.col("is_pitch") == True)))))
836
+
837
+ # df_test = df_test.with_columns(
838
+ # (pl.col('pitcher_name')+' - '+pl.col('pitcher_team')).alias('pitcher_name')
839
+ # )
840
+
841
+
842
+
843
+
844
+ # Aggregate tj_stuff_plus by pitcher_id and year
845
+ df_agg_2024_pitch = df_test.group_by(['pitcher_id','pitcher_name','pitch_type']).agg(
846
+ pl.col('tj_stuff_plus').len().alias('count'),
847
+ pl.col('tj_stuff_plus').mean()
848
+ )
849
+
850
+ # Calculate the weighted average of 'tj_stuff_plus' for each pitcher
851
+ df_weighted_avg = df_agg_2024_pitch.with_columns(
852
+ (pl.col('tj_stuff_plus') * pl.col('count')).alias('weighted_tj_stuff_plus')
853
+ ).group_by(['pitcher_id', 'pitcher_name']).agg(
854
+ pl.col('count').sum().alias('total_count'),
855
+ pl.col('weighted_tj_stuff_plus').sum().alias('total_weighted_tj_stuff_plus')
856
+ ).with_columns(
857
+ (pl.col('total_weighted_tj_stuff_plus') / pl.col('total_count')).alias('tj_stuff_plus')
858
+ ).select(['pitcher_id', 'pitcher_name', 'tj_stuff_plus', 'total_count'])
859
+
860
+ # Add the 'pitch_type' column with value "All"
861
+ df_weighted_avg = df_weighted_avg.with_columns(
862
+ pl.lit("All").alias('pitch_type')
863
+ )
864
+
865
+ # Select and rename columns to match the original DataFrame
866
+ df_weighted_avg = df_weighted_avg.select([
867
+ 'pitcher_id',
868
+ 'pitcher_name',
869
+
870
+ 'pitch_type',
871
+ pl.col('total_count').alias('count'),
872
+ 'tj_stuff_plus'
873
+ ])
874
+
875
+ # Concatenate the new rows with the original DataFrame
876
+ df_agg_2024_pitch = pl.concat([df_agg_2024_pitch, df_weighted_avg])
877
+
878
+
879
+ df_small = df_agg_2024_pitch.select(['pitcher_id','pitcher_name','pitch_type','count','tj_stuff_plus'])
880
+ count_dict = dict(zip(df_small.filter(pl.col('pitch_type')=='All')['pitcher_id'],
881
+ df_small.filter(pl.col('pitch_type')=='All')['count']))
882
+ # Check if 'FS' column exists, if not create it and fill with None
883
+
884
+ df_small_pivot = (df_small.pivot(index=['pitcher_id','pitcher_name'],
885
+ columns='pitch_type',
886
+ values='tj_stuff_plus').with_columns(
887
+ pl.col("pitcher_id").replace_strict(count_dict, default=None).alias("count")))
888
+
889
+ # Check if 'FS' column exists, if not create it and fill with None
890
+ for col in ['CH', 'CU', 'FC', 'FF', 'FS', 'SI', 'SL', 'ST', 'All']:
891
+ if col not in df_small_pivot.columns:
892
+ df_small_pivot = df_small_pivot.with_columns(pl.lit(None).alias(col))
893
+
894
+
895
+ df_small_pivot = df_small_pivot.with_columns(
896
+ pl.col("pitcher_id").replace_strict(pitcher_team_dict, default=None).alias("pitcher_team"))
897
+
898
+ 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()
899
+
900
+ df_small_pivot = df_small_pivot.with_columns(
901
+ pl.col(col).cast(pl.Int32, strict=False) for col in ['CH', 'CU', 'FC', 'FF', 'FS', 'SI', 'SL', 'ST', 'All']
902
+ )
903
+
904
+ df_export = df_small_pivot.filter(pl.col('count')>=pitch_limit).to_pandas().head(row_limit)
905
+ df_export.columns = ['Name', 'Team', 'Pitches', 'CH', 'CU', 'FC',
906
+ 'FF', 'FS', 'SI', 'SL', 'ST', 'All']
907
+ df_style = df_export.style
908
+
909
+ df_style = df_style.set_properties(**{'border': '1.0 px'},overwrite=False).set_table_styles([{'selector' :'th',
910
+ 'props':[('text-align', 'center'),('font-size', '22px'),('Height','30px'),('border', '1px black solid !important')]},
911
+ {'selector' :'td', 'props':[('text-align', 'center'),('font-size', '22px')]}],overwrite=False).set_table_styles(
912
+ [{'selector': 'tr', 'props': [('line-height', '1px')]}],overwrite=False).set_properties(
913
+ **{'Height': '60px'},**{'text-align': 'center'},overwrite=False).hide_index()
914
+
915
+
916
+
917
+
918
+ #cmap_sum_2 = matplotlib.colors.LinearSegmentedColormap.from_list("", ["#FFFFFF","#F0E442"])
919
+
920
+ df_style = df_style.format('{:.0f}',subset=df_export.columns[3:], na_rep='')
921
+
922
+
923
+ # df_style
924
+ 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 '')
925
+
926
+ #df_style = df_style.applymap(background_gradient_ignore_nan)
927
+ #df_style = df_style
928
+ df_style = df_style.applymap(lambda x: 'color: transparent; background-color: transparent' if pd.isnull(x) else '')
929
+
930
+
931
+
932
+ df_style = df_style.set_properties(
933
+ **{'border': '1px black solid !important'},subset = ((list(df_style.index[:-1]),df_style.columns[:]))).set_properties(
934
+ **{'min-width':'325px'},subset = ((list(df_style.index[:-1]),df_style.columns[0])),overwrite=False).set_properties(
935
+ **{'min-width':'100px'},subset = ((list(df_style.index[:-1]),df_style.columns[1:3])),overwrite=False).set_properties(
936
+ **{'min-width':'100px'},subset = ((list(df_style.index[:-1]),df_style.columns[3:])),overwrite=False).set_properties(
937
+ # **{'min-width':'125px'},subset = ((list(df_style.index[:-1]),df_style.columns[-1])),overwrite=False).set_properties(
938
+ **{'border': '1px black solid !important'},subset = ((list(df_style.index[:]),df_style.columns[:])))
939
+
940
+ # df_style = df_style.set_table_styles([{'selector' :'th',
941
+ # 'props':[('text-align', 'center'),('font-size', '22px'),('Height','30px'),('border', '1px black solid !important')]},
942
+ # {'selector' :'td', 'props':[('text-align', 'center'),('font-size', '22px')]}], overwrite=False)
943
+
944
+ return df_style
945
+
946
+
947
+
948
+
949
  app = App(app_ui, server)