|
|
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
df_2023['hit_x'] = df_2023['hit_x'] - 126 |
|
df_2023['hit_y'] = -df_2023['hit_y']+204.5 |
|
|
|
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'] |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
colour_palette = ['#FFB000','#648FFF','#785EF0', |
|
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED'] |
|
|
|
fig = plt.figure(figsize=(10, 10)) |
|
|
|
|
|
|
|
|
|
gs = GridSpec(3, 3, width_ratios=[0.1,0.8,0.1], height_ratios=[0.1,0.8,0.1]) |
|
|
|
|
|
ax01 = fig.add_subplot(gs[0, :]) |
|
|
|
|
|
ax10 = fig.add_subplot(gs[1, 0]) |
|
ax11 = fig.add_subplot(gs[1, 1]) |
|
ax12 = fig.add_subplot(gs[1, 2]) |
|
|
|
ax21 = fig.add_subplot(gs[2, :]) |
|
|
|
|
|
initial_position = ax12.get_position() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],'#ffffff',colour_palette[0]]) |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
kde1 = gaussian_kde(kde1_df.values.T) |
|
kde2 = gaussian_kde(kde2_df.values.T) |
|
|
|
|
|
x, y = np.meshgrid(np.arange(-45, 46,1 ), np.arange(-30, 61,1 )) |
|
positions = np.vstack([x.ravel(), y.ravel()]) |
|
|
|
|
|
kde1_values = np.reshape(kde1(positions).T, x.shape) |
|
kde2_values = np.reshape(kde2(positions).T, x.shape) |
|
|
|
|
|
result_kde_values = kde1_values - kde2_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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
num_levels = 14 |
|
midrange_value = 0 |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ax12.set_ylim(0,1) |
|
cbar = plt.colorbar(batter_plot, cax=ax12, orientation='vertical',shrink=1) |
|
cbar.set_ticks([]) |
|
|
|
cbar_locator = MaxNLocator(nbins=13) |
|
cbar.locator = cbar_locator |
|
cbar.update_ticks() |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
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) |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ax01.axis('off') |
|
|
|
ax10.axis('off') |
|
|
|
|
|
|
|
ax21.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='+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) |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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) |