##### games.,py ##### # Import modules from shiny import * import shinyswatch #import plotly.express as px from shinywidgets import output_widget, render_widget import pandas as pd from configure import base_url import math import datetime import datasets from datasets import load_dataset import numpy as np import matplotlib from matplotlib.ticker import MaxNLocator from matplotlib.gridspec import GridSpec import matplotlib.pyplot as plt from scipy.stats import gaussian_kde ### Import Datasets dataset = load_dataset('nesticot/mlb_data', data_files=['mlb_pitch_data_2023.csv', 'mlb_pitch_data_2022.csv']) dataset_train = dataset['train'] df_2023 = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True) # Paths to data ### Normalize Hit Locations df_2023['hit_x'] = df_2023['hit_x'] - 126#df_2023['hit_x'].median() df_2023['hit_y'] = -df_2023['hit_y']+204.5#df_2023['hit_y'].quantile(0.9999) df_2023['hit_x_og'] = df_2023['hit_x'] df_2023.loc[df_2023['batter_hand'] == 'R','hit_x'] = -1*df_2023.loc[df_2023['batter_hand'] == 'R','hit_x'] ### Calculate Horizontal Launch Angles df_2023['h_la'] = np.arctan(df_2023['hit_x'] / df_2023['hit_y'])*180/np.pi conditions_ss = [ (df_2023['h_la']<-16+5/6), (df_2023['h_la']<16+5/6)&(df_2023['h_la']>=-16+5/6), (df_2023['h_la']>=16+5/6) ] choices_ss = ['Oppo','Straight','Pull'] df_2023['traj'] = np.select(conditions_ss, choices_ss, default=np.nan) df_2023['bip'] = [1 if x > 0 else np.nan for x in df_2023['launch_speed']] conditions_woba = [ (df_2023['event_type']=='walk'), (df_2023['event_type']=='hit_by_pitch'), (df_2023['event_type']=='single'), (df_2023['event_type']=='double'), (df_2023['event_type']=='triple'), (df_2023['event_type']=='home_run'), ] choices_woba = [0.698, 0.728, 0.887, 1.253, 1.583, 2.027] df_2023['woba'] = np.select(conditions_woba, choices_woba, default=0) df_2023_bip = df_2023[~df_2023['bip'].isnull()].dropna(subset=['h_la','launch_angle']) df_2023_bip['h_la'] = df_2023_bip['h_la'].round(0) df_2023_bip['season'] = df_2023_bip['game_date'].str[0:4].astype(int) df_2023_bip = df_2023_bip[df_2023_bip['season'] == 2023] df_2022_bip = df_2023_bip[df_2023_bip['season'] == 2022] batter_dict = df_2023_bip.sort_values('batter_name').set_index('batter_id')['batter_name'].to_dict() def server(input,output,session): @output @render.plot(alt="plot") @reactive.event(input.go, ignore_none=False) def plot(): batter_id_select = int(input.batter_id()) df_batter_2023 = df_2023_bip.loc[(df_2023_bip['batter_id'] == batter_id_select)&(df_2023_bip['season']==2023)] df_batter_2022 = df_2023_bip.loc[(df_2023_bip['batter_id'] == batter_id_select)&(df_2023_bip['season']==2022)] df_non_batter_2023 = df_2023_bip.loc[(df_2023_bip['batter_id'] != batter_id_select)&(df_2023_bip['season']==2023)] df_non_batter_2022 = df_2023_bip.loc[(df_2023_bip['batter_id'] != batter_id_select)&(df_2023_bip['season']==2022)] traj_df = df_batter_2023.groupby(['traj'])['launch_speed'].count() / len(df_batter_2023) trajectory_df = df_batter_2023.groupby(['trajectory'])['launch_speed'].count() / len(df_batter_2023)#.loc['Oppo'] colour_palette = ['#FFB000','#648FFF','#785EF0', '#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED'] fig = plt.figure(figsize=(10, 10)) # Create a 2x2 grid of subplots using GridSpec gs = GridSpec(3, 3, width_ratios=[0.1,0.8,0.1], height_ratios=[0.1,0.8,0.1]) # ax00 = fig.add_subplot(gs[0, 0]) ax01 = fig.add_subplot(gs[0, :]) # Subplot at the top-right position # ax02 = fig.add_subplot(gs[0, 2]) # Subplot spanning the entire bottom row ax10 = fig.add_subplot(gs[1, 0]) ax11 = fig.add_subplot(gs[1, 1]) # Subplot at the top-right position ax12 = fig.add_subplot(gs[1, 2]) # ax20 = fig.add_subplot(gs[2, 0]) ax21 = fig.add_subplot(gs[2, :]) # Subplot at the top-right position # ax22 = fig.add_subplot(gs[2, 2]) initial_position = ax12.get_position() # Change the size of the axis # new_width = 0.06 # Set your desired width # new_height = 0.4 # Set your desired height # new_position = [initial_position.x0-0.01, initial_position.y0+0.065, new_width, new_height] # ax12.set_position(new_position) cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],'#ffffff',colour_palette[0]]) # Generate two sets of two-dimensional data # data1 = np.random.multivariate_normal([0, 0], [[1, 0.5], [0.5, 1]], 1000) # data2 = np.random.multivariate_normal([3, 3], [[1, -0.5], [-0.5, 1]], 1000) bat_hand = df_batter_2023.groupby('batter_hand')['launch_speed'].count().sort_values(ascending=False).index[0] bat_hand_value = 1 if bat_hand == 'R': bat_hand_value = -1 kde1_df = df_batter_2023[['h_la','launch_angle']] kde1_df['h_la'] = kde1_df['h_la'] * bat_hand_value kde2_df = df_non_batter_2023[['h_la','launch_angle']].sample(n=50000, random_state=42) kde2_df['h_la'] = kde2_df['h_la'] * bat_hand_value # Calculate 2D KDE for each dataset kde1 = gaussian_kde(kde1_df.values.T) kde2 = gaussian_kde(kde2_df.values.T) # Generate a grid of points for evaluation x, y = np.meshgrid(np.arange(-45, 46,1 ), np.arange(-30, 61,1 )) positions = np.vstack([x.ravel(), y.ravel()]) # Evaluate the KDEs on the grid kde1_values = np.reshape(kde1(positions).T, x.shape) kde2_values = np.reshape(kde2(positions).T, x.shape) # Subtract one KDE from the other result_kde_values = kde1_values - kde2_values # Normalize the array to the range [0, 1] # result_kde_values = (result_kde_values - np.min(result_kde_values)) / (np.max(result_kde_values) - np.min(result_kde_values)) result_kde_values = (result_kde_values - np.mean(result_kde_values)) / (np.std(result_kde_values)) result_kde_values = np.clip(result_kde_values, -3, 3) # # Plot the original KDEs # plt.contourf(x, y, kde1_values, cmap='Blues', alpha=0.5, levels=20) # plt.contourf(x, y, kde2_values, cmap='Reds', alpha=0.5, levels=20) # Plot the subtracted KDE # Set the number of levels and midrange value # Set the number of levels and midrange value num_levels = 14 midrange_value = 0 # Create a filled contour plot with specified levels levels = np.linspace(-3, 3, num_levels) batter_plot = ax11.contourf(x, y, result_kde_values, cmap=cmap_hue, levels=levels, vmin=-3, vmax=3) ax11.hlines(y=10,xmin=45,xmax=-45,color=colour_palette[3],linewidth=1) ax11.hlines(y=25,xmin=45,xmax=-45,color=colour_palette[3],linewidth=1) ax11.hlines(y=50,xmin=45,xmax=-45,color=colour_palette[3],linewidth=1) ax11.vlines(x=-15,ymin=-30,ymax=60,color=colour_palette[3],linewidth=1) ax11.vlines(x=15,ymin=-30,ymax=60,color=colour_palette[3],linewidth=1) #ax11.axis('square') #ax11.axis('off') #ax.hlines(y=10,xmin=-45,xmax=-45) # Add labels and legend #plt.xlabel('X-axis') #plt.ylabel('Y-axis') #ax.plot('equal') #plt.gca().set_aspect('equal') #Choose a mappable (can be any plot or image) ax12.set_ylim(0,1) cbar = plt.colorbar(batter_plot, cax=ax12, orientation='vertical',shrink=1) cbar.set_ticks([]) # Set the colorbar to have 13 levels cbar_locator = MaxNLocator(nbins=13) cbar.locator = cbar_locator cbar.update_ticks() #cbar.set_clim(vmin=-3, vmax=) # Set ticks and tick labels # cbar.set_ticks(np.linspace(-3, 3, 13)) # cbar.set_ticklabels(np.linspace(0, 3, 13)) cbar.set_ticks([]) ax10.text(s=f"Pop Up\n({trajectory_df.loc['popup']:.1%})", x=1, y=0.95,va='center',ha='right',fontsize=16) # Choose a mappable (can be any plot or image) ax10.text(s=f"Fly Ball\n({trajectory_df.loc['fly_ball']:.1%})", x=1, y=0.75,va='center',ha='right',fontsize=16) ax10.text(s=f"Line\nDrive\n({trajectory_df.loc['line_drive']:.1%})", x=1, y=0.53,va='center',ha='right',fontsize=16) ax10.text(s=f"Ground\nBall\n({trajectory_df.loc['ground_ball']:.1%})", x=1, y=0.23,va='center',ha='right',fontsize=16) #ax12.axis(True) # Set equal aspect ratio for the contour plot if bat_hand == 'R': ax21.text(s=f"Pull\n({traj_df.loc['Pull']:.1%})", x=0.2+1/16*0.8, y=1,va='top',ha='center',fontsize=16) ax21.text(s=f"Straight\n({traj_df.loc['Straight']:.1%})", x=0.5, y=1,va='top',ha='center',fontsize=16) ax21.text(s=f"Oppo\n({traj_df.loc['Oppo']:.1%})", x=0.8-1/16*0.8, y=1,va='top',ha='center',fontsize=16) else: ax21.text(s=f"Pull\n({traj_df.loc['Pull']:.1%})", x=0.8-1/16*0.8, y=1,va='top',ha='center',fontsize=16) ax21.text(s=f"Straight\n({traj_df.loc['Straight']:.1%})", x=0.5, y=1,va='top',ha='center',fontsize=16) ax21.text(s=f"Oppo\n({traj_df.loc['Oppo']:.1%})", x=0.2+1/16*0.8, y=1,va='top',ha='center',fontsize=16) # Define the initial position of the axis # Customize colorbar properties # cbar = fig.colorbar(orientation='vertical', pad=0.1,ax=ax12) #cbar.set_label('Difference', rotation=270, labelpad=15) # Show the plot # ax21.text(0.0, 0., "By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12) # ax21.text(1, 0., "Data: MLB",ha='right', va='bottom',fontsize=12) # ax21.text(0.5, 0., "Inspired by @blandalytics",ha='center', va='bottom',fontsize=12) # ax00.axis('off') ax01.axis('off') # ax02.axis('off') ax10.axis('off') #ax11.axis('off') #ax12.axis('off') # ax20.axis('off') ax21.axis('off') # ax22.axis('off') ax21.text(0.0, 0., "By: Thomas Nestico\n @TJStats",ha='left', va='bottom',fontsize=12) ax21.text(0.98, 0., "Data: MLB",ha='right', va='bottom',fontsize=12) ax21.text(0.5, 0., "Inspired by @blandalytics",ha='center', va='bottom',fontsize=12) ax11.set_xticks([]) ax11.set_yticks([]) # ax12.text(s='Same',x=np.mean([x for x in ax12.get_xlim()]),y=np.median([x for x in ax12.get_ylim()]), # va='center',ha='center',fontsize=12) # ax12.text(s='More\nOften',x=0.5,y=0.74, # va='top',ha='center',fontsize=12) ax12.text(s='+3σ',x=0.5,y=3-1/14*3, va='center',ha='center',fontsize=12) ax12.text(s='+2σ',x=0.5,y=2-1/14*2, va='center',ha='center',fontsize=12) ax12.text(s='+1σ',x=0.5,y=1-1/14*1, va='center',ha='center',fontsize=12) ax12.text(s='±0σ',x=0.5,y=0, va='center',ha='center',fontsize=12) ax12.text(s='-1σ',x=0.5,y=-1-1/14*-1, va='center',ha='center',fontsize=12) ax12.text(s='-2σ',x=0.5,y=-2-1/14*-2, va='center',ha='center',fontsize=12) ax12.text(s='-3σ',x=0.5,y=-3-1/14*-3, va='center',ha='center',fontsize=12) # # ax12.text(s='Less\nOften',x=0.5,y=0.26, # # va='bottom',ha='center',fontsize=12) ax01.text(s=f"{df_batter_2023['batter_name'].values[0]}'s 2023 Batted Ball Tendencies", x=0.5, y=0.8,va='top',ha='center',fontsize=20) ax01.text(s=f"(Compared to rest of MLB)", x=0.5, y=0.3,va='top',ha='center',fontsize=16) #plt.show() spray = App(ui.page_fluid( ui.tags.base(href=base_url), 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.markdown("""Support me on Patreon for Access to 2024 Apps1"""), ui.navset_tab( ui.nav_control( ui.a( "Home", href="home/" ), ), ui.nav_menu( "Batter Charts", ui.nav_control( ui.a( "Batting Rolling", href="rolling_batter/" ), ui.a( "Spray", href="spray/" ), ui.a( "Decision Value", href="decision_value/" ), ui.a( "Damage Model", href="damage_model/" ), ui.a( "Batter Scatter", href="batter_scatter/" ), # ui.a( # "EV vs LA Plot", # href="ev_angle/" # ), ui.a( "Statcast Compare", href="statcast_compare/" ) ), ), ui.nav_menu( "Pitcher Charts", ui.nav_control( ui.a( "Pitcher Rolling", href="rolling_pitcher/" ), ui.a( "Pitcher Summary", href="pitching_summary_graphic_new/" ), ui.a( "Pitcher Scatter", href="pitcher_scatter/" ) ), )),ui.row( ui.layout_sidebar( ui.panel_sidebar( ui.input_select("batter_id", "Select Batter", batter_dict, width=1, size=1, selectize=True), ui.input_action_button("go", "Generate",class_="btn-primary", )), ui.panel_main( ui.navset_tab( ui.nav("2023 vs MLB", ui.output_plot('plot', width='1000px', height='1000px')), )) )),)),server)