import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns #import pitch_summary_functions as psf import requests import matplotlib from api_scraper import MLB_Scrape from shinywidgets import output_widget, render_widget import shinyswatch season = 2024 level = 'mlb' colour_palette = ['#FFB000','#648FFF','#785EF0', '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED'] import datasets from datasets import load_dataset ### Import Datasets dataset = load_dataset('nesticot/mlb_data', data_files=[f'{level}_pitch_data_{season}.csv' ]) dataset_train = dataset['train'] df_2024 = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True).drop_duplicates(subset=['play_id'],keep='last') # df_2024 = pd.read_csv('C:/Users/thoma/Google Drive/Python/Baseball/season_stats/2024/2024_regular_data.csv',index_col=[0]) # ### Import Datasets # import datasets # from datasets import load_dataset # dataset = load_dataset('nesticot/mlb_data', data_files=['mlb_pitch_data_2020.csv' ]) # dataset_train = dataset['train'] # df_2024 = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True) ### PITCH COLOURS ### pitch_colours = { 'Four-Seam Fastball':'#FF007D',#BC136F 'Sinker':'#98165D',#DC267F 'Cutter':'#BE5FA0', 'Changeup':'#F79E70',#F75233 'Splitter':'#FE6100',#F75233 'Screwball':'#F08223', 'Forkball':'#FFB000', 'Slider':'#67E18D',#1BB999#785EF0 'Sweeper':'#1BB999',#37CD85#904039 'Slurve':'#376748',#785EF0#549C07#BEABD8 'Knuckle Curve':'#311D8B', 'Curveball':'#3025CE', 'Slow Curve':'#274BFC', 'Eephus':'#648FFF', 'Knuckleball':'#867A08', 'Pitch Out':'#472C30', 'Other':'#9C8975', } import pitcher_update as pu df_2024 = pu.df_update(df_2024) # DEFINE STRIKE ZONE strike_zone = pd.DataFrame({ 'PlateLocSide': [-0.9, -0.9, 0.9, 0.9, -0.9], 'PlateLocHeight': [1.5, 3.5, 3.5, 1.5, 1.5] }) ### STRIKE ZONE ### def draw_line(axis,alpha_spot=1,catcher_p = True): axis.plot(strike_zone['PlateLocSide'], strike_zone['PlateLocHeight'], color='black', linewidth=1.3,zorder=3,alpha=alpha_spot,) # ax.plot([-0.2833333, -0.2833333], [1.6, 3.5], color='black', linestyle='dashed',alpha=alpha_spot,zorder=3) # ax.plot([0.2833333, 0.2833333], [1.6, 3.5], color='black', linestyle='dashed',alpha=alpha_spot,zorder=3) # ax.plot([-0.85, 0.85], [2.2, 2.2], color='black', linestyle='dashed',alpha=alpha_spot,zorder=3) # ax.plot([-0.85, 0.85], [2.9, 2.9], color='black', linestyle='dashed',alpha=alpha_spot,zorder=3) if catcher_p: # Add dashed line # Add home plate axis.plot([-0.708, 0.708], [0.15, 0.15], color='black', linewidth=1,alpha=alpha_spot,zorder=1) axis.plot([-0.708, -0.708], [0.15, 0.3], color='black', linewidth=1,alpha=alpha_spot,zorder=1) axis.plot([-0.708, 0], [0.3, 0.5], color='black', linewidth=1,alpha=alpha_spot,zorder=1) axis.plot([0, 0.708], [0.5, 0.3], color='black', linewidth=1,alpha=alpha_spot,zorder=1) axis.plot([0.708, 0.708], [0.3, 0.15], color='black', linewidth=1,alpha=alpha_spot,zorder=1) else: axis.plot([-0.708, 0.708], [0.4, 0.4], color='black', linewidth=1,alpha=alpha_spot,zorder=1) axis.plot([-0.708, -0.9], [0.4, -0.1], color='black', linewidth=1,alpha=alpha_spot,zorder=1) axis.plot([-0.9, 0], [-0.1, -0.35], color='black', linewidth=1,alpha=alpha_spot,zorder=1) axis.plot([0, 0.9], [-.35, -0.1], color='black', linewidth=1,alpha=alpha_spot,zorder=1) axis.plot([0.9, 0.708], [-0.1,0.4], color='black', linewidth=1,alpha=alpha_spot,zorder=1) pitcher_dicts = df_2024.set_index('pitcher_id')['pitcher_name'].sort_values().to_dict() team_logos = pd.read_csv('team_logos.csv') cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#FFFFFF','#FFB000','#FE6100']) cmap_sum2 = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#FFFFFF','#FFB000','#FE6100']) from urllib.request import Request, urlopen from shiny import App, reactive, ui, render from shiny.ui import h2, tags # importing OpenCV(cv2) module app_ui = ui.page_fluid( ui.tags.div( {"style": "width:90%;margin: 0 auto;max-width: 1600px;"}, ui.tags.style( """ h4 { margin-top: 1em;font-size:35px; } h2{ font-size:25px; } """ ), shinyswatch.theme.simplex(), ui.tags.h4("TJStats"), ui.tags.i("Baseball Analytics and Visualizations"), ui.tags.h5("Pitcher Heat Maps"), ui.row( ui.layout_sidebar( ui.panel_sidebar( ui.input_select('player_id','Select Player',pitcher_dicts,selectize=True,multiple=False), ui.output_ui('game_id_select','Date Range'), ui.output_ui('pitch_type_select','Select Pitch Type'), ui.input_action_button("go", "Generate",class_="btn-primary"),width=2 ), ui.panel_main( ui.navset_tab( # ui.nav("Raw Data", # ui.output_data_frame("raw_table")), ui.nav("Season Summary", ui.output_plot('plot', width='1600px', height='900px')),id="my_tabs")))))) #print(app_ui) def server(input, output, session): @render.ui def game_id_select(): # @reactive.Effect if input.my_tabs() == 'Season Summary': return ui.input_date_range("date_range_id", "Date range input",start = df_2024.game_date.min(), end = df_2024.game_date.max(),width=2,min=df_2024.game_date.min(), max=df_2024.game_date.max()), @render.ui def pitch_type_select(): if input.player_id() == '': return ui.input_select('pitch_type','Select a Pitcher',{'pitch':''},selectize=True,multiple=False) else: pitch_dicts = df_2024[(df_2024['pitcher_id']==int(input.player_id()))].set_index('pitch_type')['pitch_description'].sort_values().to_dict() # @reactive.Effect return ui.input_select('pitch_type','Select Pitch Type',pitch_dicts,selectize=True,multiple=False) @output @render.plot @reactive.event(input.go, ignore_none=False) def plot(): #fig, ax = plt.subplots(3, 2, figsize=(9, 9)) font_properties = {'family': 'calibi', 'size': 12} font_properties_titles = {'family': 'calibi', 'size': 20} font_properties_axes = {'family': 'calibi', 'size': 16} if len((input.player_id()))<1: fig, ax = plt.subplots(1, 1, figsize=(9, 9)) ax.text(x=0.5,y=0.5,s='Please Select\nA Player',fontsize=150,ha='center') ax.grid('off') return pitcher_input = int(input.player_id()) pitch_input = input.pitch_type() df_plot_full = df_2024[(df_2024['pitcher_id']==pitcher_input)& (pd.to_datetime(df_2024['game_date']).dt.date>=input.date_range_id()[0])& (pd.to_datetime(df_2024['game_date']).dt.date<=input.date_range_id()[1])] df_plot_full['pitch_count_hand'] = df_plot_full.groupby(['pitcher_id','batter_hand'])['start_speed'].transform('count') df_plot_full['h_s_b'] = df_plot_full.groupby(['batter_hand','strikes', 'balls']).transform('count')['pitcher_id'] df_plot_full['h_s_b_pitch'] = df_plot_full.groupby(['batter_hand','strikes', 'balls','pitch_type']).transform('count')['pitcher_id'] df_plot_full['h_s_b_pitch_percent'] = df_plot_full['h_s_b_pitch']/df_plot_full['h_s_b'] df_plot = df_plot_full[(df_plot_full['pitch_type']==pitch_input)] print("THIS IS HERE") print(df_plot) pivot_table_l = df_plot[df_plot['batter_hand'].isin(['L'])].groupby(['batter_hand','strikes', 'balls'])[['h_s_b_pitch_percent']].mean().reset_index().pivot('strikes','balls','h_s_b_pitch_percent')#.fillna(0).style.background_gradient(cmap=cmap_sum2, axis=None).format("{:.0%}") # Create a new index and columns range new_index = range(3) new_columns = range(4) # Reindex the pivot table pivot_table_l = pivot_table_l.reindex(index=new_index, columns=new_columns) # Fill any missing values with 0 pivot_table_l = pivot_table_l.fillna(0) pivot_table_l = df_plot[df_plot['batter_hand']=='L'].groupby(['batter_hand','strikes', 'balls'])[['h_s_b_pitch_percent']].mean().reset_index().pivot('strikes','balls','h_s_b_pitch_percent')#.fillna(0).style.background_gradient(cmap=cmap_sum2, axis=None).format("{:.0%}") # Create a new index and columns range new_index = range(3) new_columns = range(4) # Reindex the pivot table pivot_table_l = pivot_table_l.reindex(index=new_index, columns=new_columns) # Fill any missing values with 0 pivot_table_l = pivot_table_l.fillna(0) pivot_table_r = df_plot[df_plot['batter_hand']=='R'].groupby(['batter_hand','strikes', 'balls'])[['h_s_b_pitch_percent']].mean().reset_index().pivot('strikes','balls','h_s_b_pitch_percent')#.fillna(0).style.background_gradient(cmap=cmap_sum2, axis=None).format("{:.0%}") # Create a new index and columns range new_index = range(3) new_columns = range(4) # Reindex the pivot table pivot_table_r = pivot_table_r.reindex(index=new_index, columns=new_columns) # Fill any missing values with 0 pivot_table_r = pivot_table_r.fillna(0) # Assuming you have a DataFrame called 'df_plot_full' with columns 'pitch_type', 'strikes', and 'balls' # Filter the dataset to include only slider pitches # slider_pitches = df_plot_full[df_plot_full['pitch_type'] == 'SL'] # Group the filtered dataset by strike and ball counts # grouped_counts = slider_pitches.groupby(['pitcher_hand','strikes', 'balls']).size().reset_index(name='total_pitches') # Calculate the proportion of slider pitches for each strike and ball count # grouped_counts['proportion'] = grouped_counts['total_pitches'] / grouped_counts['total_pitches'].sum() # Print the resulting DataFrame df_summ = df_plot.groupby(['batter_hand']).agg( pitch_count = ('pitch_count_hand','max'), pa = ('pa','sum'), ab = ('ab','sum'), obp_pa = ('obp','sum'), hits = ('hits','sum'), on_base = ('on_base','sum'), k = ('k','sum'), bb = ('bb','sum'), bb_minus_k = ('bb_minus_k','sum'), csw = ('csw','sum'), bip = ('bip','sum'), bip_div = ('bip_div','sum'), tb = ('tb','sum'), woba = ('woba','sum'), woba_contact = ('woba_contact','sum'), xwoba = ('woba_pred','sum'), xwoba_contact = ('woba_pred_contact','sum'), woba_codes = ('woba_codes','sum'), hard_hit = ('hard_hit','sum'), barrel = ('barrel','sum'), sweet_spot = ('sweet_spot','sum'), max_launch_speed = ('launch_speed','max'), launch_speed = ('launch_speed','mean'), launch_angle = ('launch_angle','mean'), pitches = ('is_pitch','sum'), swings = ('swings','sum'), in_zone = ('in_zone','sum'), out_zone = ('out_zone','sum'), whiffs = ('whiffs','sum'), zone_swing = ('zone_swing','sum'), zone_contact = ('zone_contact','sum'), ozone_swing = ('ozone_swing','sum'), ozone_contact = ('ozone_contact','sum'), ground_ball = ('trajectory_ground_ball','sum'), line_drive = ('trajectory_line_drive','sum'), fly_ball =('trajectory_fly_ball','sum'), pop_up = ('trajectory_popup','sum'), attack_zone = ('attack_zone','count'), heart = ('heart','sum'), shadow = ('shadow','sum'), chase = ('chase','sum'), waste = ('waste','sum'), heart_swing = ('heart_swing','sum'), shadow_swing = ('shadow_swing','sum'), chase_swing = ('chase_swing','sum'), waste_swing = ('waste_swing','sum'), heart_whiff = ('heart_whiff','sum'), shadow_whiff = ('shadow_whiff','sum'), chase_whiff = ('chase_whiff','sum'), waste_whiff = ('waste_whiff','sum'), ).reset_index() df_summ['avg'] = [df_summ.hits[x]/df_summ.ab[x] if df_summ.ab[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['obp'] = [df_summ.on_base[x]/df_summ.obp_pa[x] if df_summ.obp_pa[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['slg'] = [df_summ.tb[x]/df_summ.ab[x] if df_summ.ab[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['ops'] = df_summ['obp']+df_summ['slg'] df_summ['k_percent'] = [df_summ.k[x]/df_summ.pa[x] if df_summ.pa[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['bb_percent'] =[df_summ.bb[x]/df_summ.pa[x] if df_summ.pa[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['bb_minus_k_percent'] =[(df_summ.bb_minus_k[x])/df_summ.pa[x] if df_summ.pa[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['bb_over_k_percent'] =[df_summ.bb[x]/df_summ.k[x] if df_summ.k[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['csw_percent'] =[df_summ.csw[x]/df_summ.pitches[x] if df_summ.pitches[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['sweet_spot_percent'] = [df_summ.sweet_spot[x]/df_summ.bip_div[x] if df_summ.bip_div[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['woba_percent'] = [df_summ.woba[x]/df_summ.woba_codes[x] if df_summ.woba_codes[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['woba_percent_contact'] = [df_summ.woba_contact[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] #df_summ['hard_hit_percent'] = [df_summ.sweet_spot[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['hard_hit_percent'] = [df_summ.hard_hit[x]/df_summ.bip_div[x] if df_summ.bip_div[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['barrel_percent'] = [df_summ.barrel[x]/df_summ.bip_div[x] if df_summ.bip_div[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['zone_contact_percent'] = [df_summ.zone_contact[x]/df_summ.zone_swing[x] if df_summ.zone_swing[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['zone_swing_percent'] = [df_summ.zone_swing[x]/df_summ.in_zone[x] if df_summ.in_zone[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['zone_percent'] = [df_summ.in_zone[x]/df_summ.pitches[x] if df_summ.pitches[x] > 0 else np.nan for x in range(len(df_summ))] df_summ['chase_percent'] = [df_summ.ozone_swing[x]/(df_summ.pitches[x] - df_summ.in_zone[x]) if (df_summ.pitches[x]- df_summ.in_zone[x]) != 0 else np.nan for x in range(len(df_summ))] df_summ['chase_contact'] = [df_summ.ozone_contact[x]/df_summ.ozone_swing[x] if df_summ.ozone_swing[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['swing_percent'] = [df_summ.swings[x]/df_summ.pitches[x] if df_summ.pitches[x] > 0 else np.nan for x in range(len(df_summ))] df_summ['whiff_rate'] = [df_summ.whiffs[x]/df_summ.swings[x] if df_summ.swings[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['swstr_rate'] = [df_summ.whiffs[x]/df_summ.pitches[x] if df_summ.pitches[x] > 0 else np.nan for x in range(len(df_summ))] df_summ['ground_ball_percent'] = [df_summ.ground_ball[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['line_drive_percent'] = [df_summ.line_drive[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['fly_ball_percent'] = [df_summ.fly_ball[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['pop_up_percent'] = [df_summ.pop_up[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['heart_zone_percent'] = [df_summ.heart[x]/df_summ.attack_zone[x] if df_summ.attack_zone[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['shadow_zone_percent'] = [df_summ.shadow[x]/df_summ.attack_zone[x] if df_summ.attack_zone[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['chase_zone_percent'] = [df_summ.chase[x]/df_summ.attack_zone[x] if df_summ.attack_zone[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['waste_zone_percent'] = [df_summ.waste[x]/df_summ.attack_zone[x] if df_summ.attack_zone[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['heart_zone_swing_percent'] = [df_summ.heart_swing[x]/df_summ.heart[x] if df_summ.heart[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['shadow_zone_swing_percent'] = [df_summ.shadow_swing[x]/df_summ.shadow[x] if df_summ.shadow[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['chase_zone_swing_percent'] = [df_summ.chase_swing[x]/df_summ.chase[x] if df_summ.chase[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['waste_zone_swing_percent'] = [df_summ.waste_swing[x]/df_summ.waste[x] if df_summ.waste[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['heart_zone_whiff_percent'] = [df_summ.heart_whiff[x]/df_summ.heart_swing[x] if df_summ.heart_swing[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['shadow_zone_whiff_percent'] = [df_summ.shadow_whiff[x]/df_summ.shadow_swing[x] if df_summ.shadow_swing[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['chase_zone_whiff_percent'] = [df_summ.chase_whiff[x]/df_summ.chase_swing[x] if df_summ.chase_swing[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['waste_zone_whiff_percent'] = [df_summ.waste_whiff[x]/df_summ.waste_swing[x] if df_summ.waste_swing[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['xwoba_percent'] = [df_summ.xwoba[x]/df_summ.woba_codes[x] if df_summ.woba_codes[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['xwoba_percent_contact'] = [df_summ.xwoba_contact[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] df_summ['pitch_percent'] = [df_summ.pitches[x]/df_summ.pitch_count[x] if df_summ.pitch_count[x] != 0 else np.nan for x in range(len(df_summ))] table_left = df_summ[df_summ['batter_hand']=='L'][['pitch_percent', 'pitches', 'heart_zone_percent', 'shadow_zone_percent', 'chase_zone_percent', 'waste_zone_percent', 'csw_percent', 'whiff_rate', 'chase_percent', 'bip', 'xwoba_percent_contact' ]] ### GET COLOURS## import matplotlib.colors import matplotlib.colors as mcolors def get_color(value,normalize): color = cmap_sum(normalize(value)) return mcolors.to_hex(color) try: normalize = mcolors.Normalize(vmin=table_left['pitch_percent']*0.5, vmax=table_left['pitch_percent']*1.5) # Define the range of values except ValueError: normalize = mcolors.Normalize(vmin=0, vmax=1) # Define the range of values df_colour_left = pd.DataFrame(data=[[get_color(x,normalize) for x in pivot_table_l.loc[0]], [get_color(x,normalize) for x in pivot_table_l.loc[1]], [get_color(x,normalize) for x in pivot_table_l.loc[2]]],) table_left['pitch_percent'] = table_left['pitch_percent'].map('{:.1%}'.format) table_left['pitches'] = table_left['pitches'].astype(int).astype(str) # table_left['pa'] = table_left['pa'].astype(int).astype(str) # table_left['k_percent'] = table_left['k_percent'].map('{:.1%}'.format) # table_left['bb_percent'] = table_left['bb_percent'].map('{:.1%}'.format) table_left['heart_zone_percent'] = table_left['heart_zone_percent'].map('{:.1%}'.format) table_left['shadow_zone_percent'] = table_left['shadow_zone_percent'].map('{:.1%}'.format) table_left['chase_zone_percent'] = table_left['chase_zone_percent'].map('{:.1%}'.format) table_left['waste_zone_percent'] = table_left['waste_zone_percent'].map('{:.1%}'.format) table_left['csw_percent'] = table_left['csw_percent'].map('{:.1%}'.format) table_left['whiff_rate'] = table_left['whiff_rate'].map('{:.1%}'.format) table_left['chase_percent'] = table_left['chase_percent'].map('{:.1%}'.format) table_left['bip'] = table_left['bip'].astype(int).astype(str) table_left['xwoba_percent_contact'] = table_left['xwoba_percent_contact'].map('{:.3f}'.format) table_left.columns = ['Usage%','Pitches','Heart%','Shadow%','Chase%','Waste%','CSW%','Whiff%','O-Swing%','BBE','xwOBACON'] table_left = table_left.replace({'nan%':'—'}) table_left = table_left.replace({'nan':'—'}) table_left = table_left.T table_right = df_summ[df_summ['batter_hand']=='R'][['pitch_percent', 'pitches', 'heart_zone_percent', 'shadow_zone_percent', 'chase_zone_percent', 'waste_zone_percent', 'csw_percent', 'whiff_rate', 'chase_percent', 'bip', 'xwoba_percent_contact' ]] try: normalize = mcolors.Normalize(vmin=table_right['pitch_percent']*0.5, vmax=table_right['pitch_percent']*1.5) # Define the range of values except ValueError: normalize = mcolors.Normalize(vmin=0, vmax=1) # Define the range of values df_colour_right = pd.DataFrame(data=[[get_color(x,normalize) for x in pivot_table_r.loc[0]], [get_color(x,normalize) for x in pivot_table_r.loc[1]], [get_color(x,normalize) for x in pivot_table_r.loc[2]]],) table_right['pitch_percent'] = table_right['pitch_percent'].map('{:.1%}'.format) table_right['pitches'] = table_right['pitches'].astype(int).astype(str) # table_right['pa'] = table_right['pa'].astype(int).astype(str) # table_right['k_percent'] = table_right['k_percent'].map('{:.1%}'.format) # table_right['bb_percent'] = table_right['bb_percent'].map('{:.1%}'.format) table_right['heart_zone_percent'] = table_right['heart_zone_percent'].map('{:.1%}'.format) table_right['shadow_zone_percent'] = table_right['shadow_zone_percent'].map('{:.1%}'.format) table_right['chase_zone_percent'] = table_right['chase_zone_percent'].map('{:.1%}'.format) table_right['waste_zone_percent'] = table_right['waste_zone_percent'].map('{:.1%}'.format) table_right['csw_percent'] = table_right['csw_percent'].map('{:.1%}'.format) table_right['whiff_rate'] = table_right['whiff_rate'].map('{:.1%}'.format) table_right['chase_percent'] = table_right['chase_percent'].map('{:.1%}'.format) table_right['bip'] = table_right['bip'].astype(int).astype(str) table_right['xwoba_percent_contact'] = table_right['xwoba_percent_contact'].map('{:.3f}'.format) table_right.columns = ['Usage%','Pitches','Heart%','Shadow%','Chase%','Waste%','CSW%','Whiff%','O-Swing%','BBE','xwOBACON'] table_right = table_right.replace({'nan%':'—'}) table_right = table_right.replace({'nan':'—'}) table_right = table_right.T import matplotlib.pyplot as plt import seaborn as sns import matplotlib.gridspec as gridspec from matplotlib.gridspec import GridSpec # Assuming you have a list of pitch locations called 'pitch_locations' # where each location is a tuple of (x, y) coordinates fig = plt.figure(figsize=(16, 9)) fig.set_facecolor('white') sns.set_theme(style="whitegrid", palette=colour_palette) gs = GridSpec(3, 5, height_ratios=[2,9,1],width_ratios=[2,9,0.5,9,2]) gs.update(hspace=0.2, wspace=0.2) # Add subplots to the grid axheader = fig.add_subplot(gs[0, :]) ax_left = fig.add_subplot(gs[1, 1]) ax_right = fig.add_subplot(gs[1, 3]) axfooter = fig.add_subplot(gs[-1, :]) if df_plot[df_plot['batter_hand']=='L'].shape[0] > 3: sns.kdeplot(data=df_plot[df_plot['batter_hand']=='L'], x='px', y='pz', cmap=cmap_sum2, shade=True, ax=ax_left, thresh=0.3, bw_adjust=1) else: sns.scatterplot(data=df_plot[df_plot['batter_hand']=='L'], x='px', y='pz', cmap=cmap_sum2, ax=ax_left, s=125) if df_plot[df_plot['batter_hand']=='R'].shape[0] > 3: sns.kdeplot(data=df_plot[df_plot['batter_hand']=='R'], x='px', y='pz', cmap=cmap_sum2, shade=True, ax=ax_right, thresh=0.3, bw_adjust=1) else: sns.scatterplot(data=df_plot[df_plot['batter_hand']=='R'], x='px', y='pz', cmap=cmap_sum2, ax=ax_right, s=125) draw_line(ax_left,alpha_spot=1,catcher_p = False) draw_line(ax_right,alpha_spot=1,catcher_p = False) ax_left.axis('off') ax_right.axis('off') ax_left.axis('square') ax_right.axis('square') ax_left.set_xlim(-2.75,2.75) ax_right.set_xlim(-2.75,2.75) ax_left.set_ylim(-0.5,5) ax_right.set_ylim(-0.5,5) import matplotlib.pyplot as plt import matplotlib.image as mpimg from matplotlib.offsetbox import OffsetImage, AnnotationBbox # Load the image img = mpimg.imread('left.png') imagebox = OffsetImage(img, zoom=0.7) # adjust zoom as needed ab = AnnotationBbox(imagebox, (1.25, -0.5), box_alignment=(0, 0), frameon=False) ax_left.add_artist(ab) # Load the image img = mpimg.imread('right.png') imagebox = OffsetImage(img, zoom=0.7) # adjust zoom as needed # Create an AnnotationBbox ab = AnnotationBbox(imagebox, (-1.25, -0.5), box_alignment=(1, 0), frameon=False) ax_right.add_artist(ab) from matplotlib.transforms import Bbox # Create a transformation that converts from data coordinates to axes coordinates trans = ax_left.transData + ax_left.transAxes.inverted() # Calculate the bbox in axes coordinates bbox_data = Bbox.from_bounds(-4.2, -0.5, 2.5, 5) # replace width and height with the desired values bbox_axes = trans.transform_bbox(bbox_data) table_left_plot = ax_left.table(cellText=table_left.reset_index().values, loc='right', cellLoc='center', colWidths=[0.52,0.3], bbox=bbox_axes.bounds,zorder=100) min_font_size = 14 # Set table properties table_left_plot.auto_set_font_size(False) #table.set_fontsize(min(min_font_size,max(min_font_size/((len(label_labels)/4)),10))) table_left_plot.set_fontsize(min_font_size) #table_left_plot.scale(1,3) # Calculate the bbox in axes coordinates bbox_data = Bbox.from_bounds(-0.75, 5, 2.5, 1) # replace width and height with the desired values bbox_axes = trans.transform_bbox(bbox_data) def format_as_percentage(val): return f'{val * 100:.0f}%' table_left_plot_pivot = ax_left.table(cellText=[[format_as_percentage(val) for val in row] for row in pivot_table_l.values], colLabels =pivot_table_l.columns, rowLabels =[' 0 ',' 1 ',' 2 '], loc='center', cellLoc='center', colWidths=[0.3,0.3,0.30,0.3], bbox=bbox_axes.bounds,zorder=100,cellColours =df_colour_left.values) min_font_size = 11 # Set table properties table_left_plot_pivot.auto_set_font_size(False) #table.set_fontsize(min(min_font_size,max(min_font_size/((len(label_labels)/4)),10))) table_left_plot_pivot.set_fontsize(min_font_size) # Create a transformation that converts from data coordinates to axes coordinates trans = ax_right.transData + ax_right.transAxes.inverted() # Calculate the bbox in axes coordinates bbox_data = Bbox.from_bounds(1.7, -0.5, 2.5, 5) # replace width and height with the desired values bbox_axes = trans.transform_bbox(bbox_data) table_right_plot = ax_right.table(cellText=table_right.reset_index().values, loc='right', cellLoc='center', colWidths=[0.52,0.3], bbox=bbox_axes.bounds,zorder=100) min_font_size = 14 # Set table properties table_right_plot.auto_set_font_size(False) #table.set_fontsize(min(min_font_size,max(min_font_size/((len(label_labels)/4)),10))) table_right_plot.set_fontsize(min_font_size) table_right_plot.scale(0.5,3) # Calculate the bbox in axes coordinates # Create a transformation that converts from data coordinates to axes coordinates trans = ax_right.transData + ax_right.transAxes.inverted() bbox_data = Bbox.from_bounds(-0.75, 5, 2.5, 1) # replace width and height with the desired values bbox_axes = trans.transform_bbox(bbox_data) table_right_plot_pivot = ax_right.table(cellText=[[format_as_percentage(val) for val in row] for row in pivot_table_r.values], colLabels =pivot_table_r.columns, rowLabels =[' 0 ',' 1 ',' 2 '], loc='center', cellLoc='center', colWidths=[0.3,0.3,0.30,0.3], bbox=bbox_axes.bounds,zorder=100,cellColours =df_colour_right.values) min_font_size = 11 # Set table properties table_right_plot_pivot.auto_set_font_size(False) #table.set_fontsize(min(min_font_size,max(min_font_size/((len(label_labels)/4)),10))) table_right_plot_pivot.set_fontsize(min_font_size) from matplotlib.cm import ScalarMappable from matplotlib.colors import Normalize # Create a ScalarMappable with the same colormap and normalization sm = ScalarMappable(cmap=cmap_sum2, norm=Normalize(vmin=0, vmax=1)) #from mpl_toolkits.axes_grid1.inset_locator import inset_axes ####################### # Create a new Subplot object for the colorbar # Create a new Axes object for the colorbar at the bottom middle of the figure cbar = fig.colorbar(sm, ax=axfooter, orientation='horizontal',aspect=100) # cbar.ax.set_aspect(20) # cbar = plt.colorbar(batter_plot, cax=ax12, orientation='vertical',shrink=1, cmap=cmap_hue) # cbar = plt.colorbar(batter_plot, cax=ax12, orientation='vertical',shrink=1) cbar.set_ticks([]) # # Create an inset axes for the colorbar # cax = inset_axes(axfooter, # width="50%", # width = 50% of parent_bbox width # height="100%", # height : 5% # loc='center') # # Add the colorbar to the inset axes # cbar = fig.colorbar(sm, cax=cax, orientation='horizontal') # # Set the labels on the low and high ends of the colorbar # # Set the xticks to only include the low and high ends of the colorbar cbar.set_ticks([sm.norm.vmin, sm.norm.vmax]) # # Set the labels on the low and high ends of the colorbar cbar.ax.set_xticklabels(['Least', 'Most']) # # Place the xticks on top of the colorbar cbar.ax.tick_params(labeltop=True, labelbottom=False, labelsize=14) # # Get the labels labels = cbar.ax.get_xticklabels() # # Set the alignment of the labels labels[0].set_horizontalalignment('left') labels[-1].set_horizontalalignment('right') # # Get the labels labels = cbar.ax.get_xticklabels() # # Set the font size of the labels # for label in labels: # label.set_fontsize(16) # # Set the labels cbar.ax.set_xticklabels(labels) # # Remove the tick lines on the colorbar cbar.ax.tick_params(length=0) axfooter.text(x=0.02,y=0.5,s='By: Thomas Nestico\n @TJStats',fontname='Calibri',ha='left',fontsize=18,va='top') axfooter.text(x=1-0.02,y=0.5,s='Data: MLB',ha='right',fontname='Calibri',fontsize=18,va='top') axfooter.axis('off') axheader.text(x=0.5,y=1.2,s=f"{df_plot['pitcher_name'].values[0]} - {df_plot['pitcher_hand'].values[0]}HP\n{season} {df_plot['pitch_description'].values[0]} Pitch Frequency",ha='center',fontsize=24,va='top') axheader.text(x=0.5,y=0.5,s=f"{input.date_range_id()[0]} to {input.date_range_id()[1]}",ha='center',fontsize=16,va='top') axheader.axis('off') import urllib import urllib.request import urllib.error from urllib.error import HTTPError try: url = f'https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:67:current.png/w_213,q_auto:best/v1/people/{df_plot["pitcher_id"].values[0]}/headshot/67/current.png' test_mage = plt.imread(url) except urllib.error.HTTPError as err: url = f'https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:67:current.png/w_213,q_auto:best/v1/people/1/headshot/67/current.png' imagebox = OffsetImage(test_mage, zoom = 0.4) ab = AnnotationBbox(imagebox, (0.075, 0.4), frameon = False) axheader.add_artist(ab) player_bio = requests.get(url=f"https://statsapi.mlb.com/api/v1/people?personIds={df_plot['pitcher_id'].values[0]}&hydrate=currentTeam").json() team_logos = pd.read_csv('team_logos.csv') mlb_stats = MLB_Scrape() teams_df = mlb_stats.get_teams() team_logo_dict = teams_df.set_index(['team_id'])['parent_org_id'].to_dict() if 'currentTeam' in player_bio['people'][0]: try: url = team_logos[team_logos['id'] == team_logo_dict[player_bio['people'][0]['currentTeam']['id']]]['imageLink'].values[0] im = plt.imread(url) # response = requests.get(url) # im = Image.open(BytesIO(response.content)) # im = plt.imread(team_logos[team_logos['id'] == player_bio['people'][0]['currentTeam']['parentOrgId']]['imageLink'].values[0]) # ax = fig.add_axes([0,0,1,0.85], anchor='C', zorder=1) imagebox = OffsetImage(im, zoom = 0.3) ab = AnnotationBbox(imagebox, (0.925, 0.40), frameon = False) axheader.add_artist(ab) except IndexError: print() ax_left.text(s='Against LHH',x=-2.95,y=4.65,fontsize=18,fontweight='bold',ha='center') ax_right.text(s='Against RHH',x=2.95,y=4.65,fontsize=18,fontweight='bold',ha='center') # Center the labels ax_left.text(x=-1.76, y=5.08, s='Strikes', rotation=90,fontweight='bold') ax_right.text(x=-1.76, y=5.08, s='Strikes', rotation=90,fontweight='bold') ax_left.text(x=0, y=6.03, s='Balls',ha='center',fontweight='bold') ax_right.text(x=0, y=6.03, s='Balls',ha='center',fontweight='bold') #cbar.ax.set_xticklabels(cbar.ax.get_xticklabels(), ha='center') fig.subplots_adjust(left=0.01, right=0.99, top=0.95, bottom=0.05) return app = App(app_ui, server)