Spaces:
Sleeping
Sleeping
| ##### 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): | |
| 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("""<a href='https://www.patreon.com/tj_stats'>Support me on Patreon for Access to 2024 Apps</a><sup>1</sup>"""), | |
| 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) |