Spaces:
Running
Running
##### 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) |