|
from shiny import App, Inputs, Outputs, Session, reactive, render, req, ui |
|
import datasets |
|
from datasets import load_dataset |
|
import pandas as pd |
|
import numpy as np |
|
import matplotlib.pyplot as plt |
|
import seaborn as sns |
|
import numpy as np |
|
from scipy.stats import gaussian_kde |
|
import matplotlib |
|
from matplotlib.ticker import MaxNLocator |
|
from matplotlib.gridspec import GridSpec |
|
from scipy.stats import zscore |
|
import math |
|
import matplotlib |
|
from adjustText import adjust_text |
|
import matplotlib.ticker as mtick |
|
from shinywidgets import output_widget, render_widget |
|
import pandas as pd |
|
from configure import base_url |
|
import shinyswatch |
|
from matplotlib.pyplot import text |
|
|
|
|
|
|
|
dataset = load_dataset('nesticot/mlb_data', data_files=['mlb_pitch_data_2023.csv' ]) |
|
dataset_train = dataset['train'] |
|
exit_velo_df = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True) |
|
|
|
colour_palette = ['#FFB000','#648FFF','#785EF0', |
|
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED'] |
|
|
|
|
|
|
|
|
|
conditions = [ |
|
(exit_velo_df['launch_speed'].isna()), |
|
(exit_velo_df['launch_speed']*1.5 - exit_velo_df['launch_angle'] >= 117 ) & (exit_velo_df['launch_speed'] + exit_velo_df['launch_angle'] >= 124) & (exit_velo_df['launch_speed'] > 98) & (exit_velo_df['launch_angle'] >= 8) & (exit_velo_df['launch_angle'] <= 50) |
|
] |
|
|
|
choices = [False,True] |
|
exit_velo_df['barrel'] = np.select(conditions, choices, default=np.nan) |
|
|
|
test_df = exit_velo_df.sort_values(by='batter_name').drop_duplicates(subset='batter_id').reset_index(drop=True)[['batter_id','batter_name']] |
|
test_df = test_df.set_index('batter_id') |
|
|
|
|
|
|
|
batter_dict = test_df['batter_name'].to_dict() |
|
|
|
colour_palette = ['#FFB000','#648FFF','#785EF0', |
|
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED'] |
|
|
|
angle_ev_list_df = pd.read_csv('angle_ev_list_df.csv') |
|
ev_ranges = list(np.arange(97.5,130,0.1)) |
|
angle_ranges = list(range(8,51)) |
|
|
|
|
|
|
|
def server(input,output,session): |
|
@output |
|
@render.plot(alt="A histogram") |
|
@reactive.event(input.go, ignore_none=False) |
|
def plot(): |
|
data_df = exit_velo_df[exit_velo_df.batter_id==int(input.id())] |
|
|
|
sns.set_theme(style="whitegrid", palette="pastel") |
|
fig, ax = plt.subplots(1, 1, figsize=(10, 10)) |
|
|
|
|
|
|
|
if input.plot_id() == 'dist': |
|
sns.histplot(x=data_df.launch_angle,y=data_df.launch_speed,cbar=colour_palette,binwidth=(5,2.5),ax=ax,cbar_kws=dict(shrink=.75,label='Count'),binrange=( |
|
(math.floor((min(data_df.launch_angle.dropna())/5))*5,math.ceil((max(data_df.launch_angle.dropna())/5))*5),(math.floor((min(data_df.launch_speed.dropna())/5))*5,math.ceil((max(data_df.launch_speed.dropna())/5))*5))) |
|
if input.plot_id() == 'scatter': |
|
sns.scatterplot(x=data_df.launch_angle,y=data_df.launch_speed,color=colour_palette[1]) |
|
ax.set_xlim(math.floor((min(data_df.launch_angle.dropna())/10))*10,math.ceil((max(data_df.launch_angle.dropna())/10))*10) |
|
|
|
sns.lineplot(x=angle_ev_list_df.launch_angle,y=angle_ev_list_df.launch_speed,color=colour_palette[0]) |
|
ax.vlines(x=angle_ev_list_df.launch_angle[0],ymin=angle_ev_list_df.launch_speed[0],ymax=ev_ranges[-1],color=colour_palette[0]) |
|
ax.vlines(x=angle_ev_list_df.launch_angle[len(angle_ev_list_df)-1],ymin=angle_ev_list_df.launch_speed[len(angle_ev_list_df)-1],ymax=ev_ranges[-1],color=colour_palette[0]) |
|
|
|
groundball = f'{sum(data_df.launch_angle.dropna()<=10)/len(data_df.launch_angle.dropna()):.1%}' |
|
linedrive = f'{sum((data_df.launch_angle.dropna()<=25) & (data_df.launch_angle.dropna()>10))/len(data_df.launch_angle.dropna()):.1%}' |
|
flyball = f'{sum((data_df.launch_angle.dropna()<=50) & (data_df.launch_angle.dropna()>25))/len(data_df.launch_angle.dropna()):.1%}' |
|
popup = f'{sum(data_df.launch_angle.dropna()>50)/len(data_df.launch_angle.dropna()):.1%}' |
|
percentages_list = [groundball,linedrive,flyball,popup] |
|
|
|
hard_hit_percent = f'{sum(data_df.launch_speed.dropna()>=95)/len(data_df.launch_speed.dropna()):.1%}' |
|
|
|
barrel_percentage = f'{data_df.barrel.dropna().sum()/len(data_df.launch_angle.dropna()):.1%}' |
|
|
|
plt.text(x=27, y=math.ceil((max(data_df.launch_speed.dropna())/5))*5+5-3, s=f'Barrel% {barrel_percentage}',ha='left',bbox=dict(facecolor='white',alpha=0.8, edgecolor=colour_palette[4], pad=5)) |
|
|
|
|
|
sample_dates = np.array([math.floor((min(data_df.launch_angle.dropna())/10))*10,10,25,50]) |
|
sample_text = [f'Groundball ({groundball})',f'Line Drive ({linedrive})',f'Fly Ball ({flyball})',f'Pop-up ({popup})'] |
|
|
|
hard_hit_dates = [95] |
|
hard_hit_text = [f'Hard Hit% ({hard_hit_percent})'] |
|
|
|
|
|
|
|
|
|
plt.hlines(y=hard_hit_dates,xmin=math.floor((min(data_df.launch_angle.dropna())/10))*10, xmax=math.ceil((max(data_df.launch_angle.dropna())/10))*10, color = colour_palette[4],linestyles='--') |
|
plt.vlines(x=sample_dates, ymin=0, ymax=130, color = colour_palette[3],linestyles='--') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for i, x in enumerate(hard_hit_dates): |
|
text(math.ceil((max(data_df.launch_angle.dropna())/10))*10-2.5, x+1.25,hard_hit_text[i], rotation=0, ha='right', |
|
bbox=dict(facecolor='white',alpha=0.5, edgecolor=colour_palette[4], pad=5)) |
|
|
|
|
|
for i, x in enumerate(sample_dates): |
|
text(x+0.75, (math.floor((min(data_df.launch_speed.dropna())/5))*5)+1,sample_text[i], rotation=90, verticalalignment='bottom', |
|
bbox=dict(facecolor='white',alpha=0.5, edgecolor=colour_palette[3], pad=5)) |
|
|
|
|
|
ax.set_xlim((math.floor((min(data_df.launch_angle.dropna())/10))*10,math.ceil((max(data_df.launch_angle.dropna())/10))*10)) |
|
ax.set_ylim((math.floor((min(data_df.launch_speed.dropna())/5))*5,math.ceil((max(data_df.launch_speed.dropna())/5))*5+5)) |
|
|
|
|
|
ax.set_title(f'MLB - {data_df.batter_name.unique()[0]} Launch Angle vs EV Plot', fontsize=18,fontname='Century Gothic',) |
|
|
|
ax.set_xlabel('Launch Angle', fontsize=16,fontname='Century Gothic') |
|
ax.set_ylabel('Exit Velocity', fontsize=16,fontname='Century Gothic') |
|
ax.fill_between(angle_ev_list_df.launch_angle, 130, angle_ev_list_df.launch_speed, interpolate=True, color=colour_palette[3],alpha=0.1,label='Barrel') |
|
|
|
|
|
|
|
ax.legend(fontsize='16',loc='upper left') |
|
fig.text(x=0.03,y=0.02,s='By: @TJStats') |
|
fig.text(x=1-0.03,y=0.02,s='Data: MLB',ha='right') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
fig.set_facecolor('white') |
|
fig.tight_layout() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ev_angle = 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 & Damage", |
|
href="spray/" |
|
), |
|
ui.a( |
|
"Decision Value", |
|
href="decision_value/" |
|
), |
|
|
|
|
|
|
|
|
|
ui.a( |
|
"Batter Scatter", |
|
href="batter_scatter/" |
|
), |
|
|
|
|
|
|
|
|
|
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("id", "Select Batter",batter_dict,width=1), |
|
ui.input_select("plot_id", "Select Plot",{'scatter':'Scatter Plot','dist':'Distribution Plot'},width=1), |
|
|
|
ui.input_action_button("go", "Generate",class_="btn-primary", |
|
)), |
|
|
|
|
|
ui.panel_main( |
|
ui.output_plot("plot",height = "1000px",width="1000px") |
|
), |
|
)),)),server) |