import polars as pl import numpy as np import pandas as pd import api_scraper scrape = api_scraper.MLB_Scrape() from functions import df_update from functions import pitch_summary_functions update = df_update.df_update() from stuff_model import feature_engineering as fe from stuff_model import stuff_apply import requests import joblib from matplotlib.gridspec import GridSpec from shiny import App, reactive, ui, render from shiny.ui import h2, tags import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec import seaborn as sns from functions.pitch_summary_functions import * from shiny import App, reactive, ui, render from shiny.ui import h2, tags colour_palette = ['#FFB000','#648FFF','#785EF0', '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED'] year_list = [2017,2018,2019,2020,2021,2022,2023,2024] level_dict = {'1':'MLB', '11':'AAA', # '12':'AA', #'13':'A+', '14':'A', '17':'AFL', '22':'College', '21':'Prospects', '51':'International' } function_dict={ 'velocity_kdes':'Velocity Distributions', 'break_plot':'Pitch Movement', 'tj_stuff_roling':'Rolling tjStuff+ by Pitch', 'tj_stuff_roling_game':'Rolling tjStuff+ by Game', 'location_plot_lhb':'Locations vs LHB', 'location_plot_rhb':'Locations vs RHB', } split_dict = {'all':'All', 'left':'LHH', 'right':'RHH'} split_dict_hand = {'all':['L','R'], 'left':['L'], 'right':['R']} type_dict = {'R':'Regular Season', 'S':'Spring', 'P':'Playoffs' } # List of MLB teams and their corresponding ESPN logo URLs mlb_teams = [ {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"}, {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"}, {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"}, {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"}, {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"}, {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"}, {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"}, {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"}, {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"}, {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"}, {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"}, {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"}, {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"}, {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"}, {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"}, {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"}, {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"}, {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"}, {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"}, {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"}, {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"}, {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"}, {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"}, {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"}, {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"}, {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"}, {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"}, {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"}, {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"}, {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"}, {"team": "ZZZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png&w=500&h=500"} ] df_image = pd.DataFrame(mlb_teams) image_dict = df_image.set_index('team')['logo_url'].to_dict() image_dict_flip = df_image.set_index('logo_url')['team'].to_dict() # # Define the features to be used for training # features_table = ['start_speed', # 'spin_rate', # 'extension', # 'ivb', # 'hb', # 'x0', # 'z0', # 'tj_stuff_plus'] from shiny import App, reactive, ui, render from shiny.ui import h2, tags # Define the UI layout for the app app_ui = ui.page_sidebar( ui.sidebar( # Row for selecting season and level ui.row( ui.column(4, ui.input_select('year_input', 'Select Season', year_list, selected=2024)), ui.column(4, ui.input_select('level_input', 'Select Level', level_dict)), ui.column(4, ui.input_select('type_input', 'Select Type', type_dict,selected='R')) ), # Row for the action button to get player list ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")), # Row for selecting the player ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))), # Row for selecting the date range ui.row(ui.column(12, ui.output_ui('date_id', 'Select Date'))), # Rows for selecting plots and split options ui.row( ui.column(4, ui.input_select('plot_id_1', 'Plot Left', function_dict, multiple=False, selected='velocity_kdes')), ui.column(4, ui.input_select('plot_id_2', 'Plot Middle', function_dict, multiple=False, selected='tj_stuff_roling')), ui.column(4, ui.input_select('plot_id_3', 'Plot Right', function_dict, multiple=False, selected='break_plot')) ), ui.row( ui.column(6, ui.input_select('split_id', 'Select Split', split_dict, multiple=False)), ui.column(6, ui.input_numeric('rolling_window', 'Rolling Window (for tjStuff+ Plot)', min=1, value=50)) ), ui.row( ui.column(6, ui.input_switch("switch", "Custom Team?", False)), ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False)) ), # Row for the action button to generate plot ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")), width="400px" # Added this parameter to control sidebar width ), # Main content area with tabs (placed directly in page_sidebar) ui.navset_tab( ui.nav_panel("Pitching Summary", ui.output_text("status"), ui.output_plot('plot', width='2100px', height='2100px') ), ui.nav_panel("Summary Table", ui.output_data_frame("grid")) ) ) def server(input, output, session): @reactive.calc @reactive.event(input.pitcher_id, input.date_id,input.split_id) def cached_data(): year_input = int(input.year_input()) sport_id = int(input.level_input()) player_input = int(input.pitcher_id()) start_date = str(input.date_id()[0]) end_date = str(input.date_id()[1]) # Simulate an expensive data operation game_list = scrape.get_player_games_list(sport_id = sport_id, season = year_input, player_id = player_input, start_date = start_date, end_date = end_date, game_type = [input.type_input()]) data_list = scrape.get_data(game_list_input = game_list[:]) try: df = (stuff_apply.stuff_apply(fe.feature_engineering(update.update(scrape.get_data_df(data_list = data_list).filter( (pl.col("pitcher_id") == player_input)& (pl.col("is_pitch") == True)& (pl.col("start_speed") >= 50)& (pl.col('batter_hand').is_in(split_dict_hand[input.split_id()])) )))).with_columns( pl.col('pitch_type').count().over('pitch_type').alias('pitch_count') )) return df except TypeError: print("NONE") return None @render.ui @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,ignore_none=False) def player_select_ui(): # Get the list of pitchers for the selected level and season df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input()), game_type = [input.type_input()]).filter( (pl.col("position").is_in(['P','TWP']))| (pl.col("player_id").is_in([686846])) ).sort("name") # Create a dictionary of pitcher IDs and names pitcher_dict = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['name'])) # Return a select input for choosing a pitcher return ui.input_select("pitcher_id", "Select Pitcher", pitcher_dict, selectize=True) @render.ui @reactive.event(input.player_button, input.year_input, input.level_input, input.type_input,ignore_none=False) def date_id(): # Create a date range input for selecting the date range within the selected year return ui.input_date_range("date_id", "Select Date Range", start=f"{int(input.year_input())}-01-01", end=f"{int(input.year_input())}-12-31", min=f"{int(input.year_input())}-01-01", max=f"{int(input.year_input())}-12-31") @output @render.text def status(): # Only show status when generating if input.generate == 0: return "" return "" @output @render.plot @reactive.event(input.generate_plot, ignore_none=False) def plot(): # Show progress/loading notification with ui.Progress(min=0, max=1) as p: p.set(message="Generating plot", detail="This may take a while...") p.set(0.3, "Gathering data...") year_input = int(input.year_input()) sport_id = int(input.level_input()) player_input = int(input.pitcher_id()) start_date = str(input.date_id()[0]) end_date = str(input.date_id()[1]) print(year_input, sport_id, player_input, start_date, end_date) df = cached_data() if df is None: fig = plt.figure(figsize=(26,26)) fig.text(x=0.1,y=0.9,s='No Statcast Data For This Pitcher',fontsize=36,ha='left') return fig df = df.clone() p.set(0.6, "Creating plot...") #plt.rcParams["figure.figsize"] = [10,10] fig = plt.figure(figsize=(26,26)) plt.rcParams.update({'figure.autolayout': True}) fig.set_facecolor('white') sns.set_theme(style="whitegrid", palette=colour_palette) print('this is the one plot') gs = gridspec.GridSpec(6, 8, height_ratios=[5,20,12,36,36,7], width_ratios=[4,18,18,18,18,18,18,4]) gs.update(hspace=0.2, wspace=0.5) # Define the positions of each subplot in the grid ax_headshot = fig.add_subplot(gs[1,1:3]) ax_bio = fig.add_subplot(gs[1,3:5]) ax_logo = fig.add_subplot(gs[1,5:7]) ax_season_table = fig.add_subplot(gs[2,1:7]) ax_plot_1 = fig.add_subplot(gs[3,1:3]) ax_plot_2 = fig.add_subplot(gs[3,3:5]) ax_plot_3 = fig.add_subplot(gs[3,5:7]) ax_table = fig.add_subplot(gs[4,1:7]) ax_footer = fig.add_subplot(gs[-1,1:7]) ax_header = fig.add_subplot(gs[0,1:7]) ax_left = fig.add_subplot(gs[:,0]) ax_right = fig.add_subplot(gs[:,-1]) # Hide axes for footer, header, left, and right ax_footer.axis('off') ax_header.axis('off') ax_left.axis('off') ax_right.axis('off') sns.set_theme(style="whitegrid", palette=colour_palette) fig.set_facecolor('white') df_teams = scrape.get_teams() player_headshot(player_input=player_input, ax=ax_headshot,sport_id=sport_id,season=year_input) player_bio(pitcher_id=player_input, ax=ax_bio,sport_id=sport_id,year_input=year_input) if input.switch(): # Get the logo URL from the image dictionary using the team abbreviation logo_url = input.logo_select() # Send a GET request to the logo URL response = requests.get(logo_url) # Open the image from the response content img = Image.open(BytesIO(response.content)) # Display the image on the axis ax_logo.set_xlim(0, 1.3) ax_logo.set_ylim(0, 1) ax_logo.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper') # Turn off the axis ax_logo.axis('off') else: plot_logo(pitcher_id=player_input, ax=ax_logo, df_team=df_teams,df_players=scrape.get_players(sport_id,year_input)) stat_summary_table(df=df, ax=ax_season_table, player_input=player_input, split=input.split_id(), sport_id=sport_id, game_type=[input.type_input()]) # break_plot(df=df_plot,ax=ax2) 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]): if x == 'velocity_kdes': velocity_kdes(df, ax=y, gs=gs, gs_x=[3,4], gs_y=[z,z+2], fig=fig) if x == 'tj_stuff_roling': tj_stuff_roling(df=df, window=int(input.rolling_window()), ax=y) if x == 'tj_stuff_roling_game': tj_stuff_roling_game(df=df, window=int(input.rolling_window()), ax=y) if x == 'break_plot': break_plot(df = df,ax=y) if x == 'location_plot_lhb': location_plot(df = df,ax=y,hand='L') if x == 'location_plot_rhb': location_plot(df = df,ax=y,hand='R') summary_table(df=df, ax=ax_table) plot_footer(ax_footer) fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01) @output @render.data_frame @reactive.event(input.generate_plot, ignore_none=False) def grid(): df = cached_data() df = df.clone() features_table = ['start_speed', 'spin_rate', 'extension', 'ivb', 'hb', 'x0', 'z0'] selection = ['game_id','pitcher_id','pitcher_name','batter_id','batter_name','pitcher_hand', 'batter_hand','balls','strikes','play_code','event_type','pitch_type','vaa','haa']+features_table+['tj_stuff_plus'] return render.DataGrid( df.select(selection).to_pandas().round(1), row_selection_mode='multiple', height='700px', width='fit-content', filters=True, ) app = App(app_ui, server) app = App(app_ui, server)