##### 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
season = 2024
### Import Datasets
dataset = load_dataset('nesticot/mlb_data', data_files=[f'mlb_pitch_data_{season}.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'] == season]
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']==season)]
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']==season)]
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=min(50000,len(df_non_batter_2023)), 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 {season} 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()
app = 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="https://nesticot-tjstats-site.hf.space/home/"
),
),
ui.nav_menu(
"Batter Charts",
ui.nav_control(
ui.a(
"Batting Rolling",
href="https://nesticot-tjstats-site-rolling-batter.hf.space/"
),
ui.a(
"Spray",
href="https://nesticot-tjstats-site-spray.hf.space/"
),
ui.a(
"Decision Value",
href="https://nesticot-tjstats-site-decision-value.hf.space/"
),
ui.a(
"Damage Model",
href="https://nesticot-tjstats-site-damage.hf.space/"
),
ui.a(
"Batter Scatter",
href="https://nesticot-tjstats-site-batter-scatter.hf.space/"
),
ui.a(
"EV vs LA Plot",
href="https://nesticot-tjstats-site-ev-angle.hf.space/"
),
ui.a(
"Statcast Compare",
href="https://nesticot-tjstats-site-statcast-compare.hf.space/"
),
ui.a(
"MLB/MiLB Cards",
href="https://nesticot-tjstats-site-mlb-cards.hf.space/"
)
),
),
ui.nav_menu(
"Pitcher Charts",
ui.nav_control(
ui.a(
"Pitcher Rolling",
href="https://nesticot-tjstats-site-rolling-pitcher.hf.space/"
),
ui.a(
"Pitcher Summary",
href="https://nesticot-tjstats-site-pitching-summary-graphic-new.hf.space/"
),
ui.a(
"Pitcher Scatter",
href="https://nesticot-tjstats-site-pitcher-scatter.hf.space"
)
),
)),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)