|
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 |
|
import inflect |
|
from matplotlib.pyplot import text |
|
|
|
|
|
exit_velo_df_codes_summ_batter = pd.read_csv('summary_batter.csv',index_col=[0]) |
|
|
|
|
|
exit_velo_df_codes_summ_non_level = pd.read_csv('summary_batter_level.csv',index_col=[0]).reset_index(drop=True) |
|
|
|
exit_velo_df_codes_summ_non_level['levels'] = exit_velo_df_codes_summ_non_level.levels.str.split(', ') |
|
|
|
exit_velo_df_codes_summ_non_level = exit_velo_df_codes_summ_non_level.rename(columns={'levels':'level'}) |
|
|
|
|
|
|
|
print(exit_velo_df_codes_summ_batter.bb_minus_k_percent) |
|
|
|
batter_dict_stat = { 'sweet_spot_percent':{'x_axis':'SweetSpot%','title':'SweetSpot%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100}, |
|
'max_launch_speed':{'x_axis':'Max Exit Velocity','title':'Max Exit Velocity','flip_p':False,'decimal_format':'string_0','percent_adjust':1}, |
|
'launch_speed_90':{'x_axis':'90th Percentile EV','title':'90th Percentile EV','flip_p':False,'decimal_format':'string_0','percent_adjust':1}, |
|
'launch_speed':{'x_axis':'Exit Velocity','title':'Exit Velocity','flip_p':False,'decimal_format':'string_0','percent_adjust':1}, |
|
'launch_angle':{'x_axis':'Launch Angle','title':'Launch Angle','flip_p':False,'decimal_format':'string_0','percent_adjust':100}, |
|
'avg':{'x_axis':'AVG','title':'AVG','flip_p':False,'decimal_format':'string_3','percent_adjust':100}, |
|
'obp':{'x_axis':'OBP','title':'OBP','flip_p':False,'decimal_format':'string_3','percent_adjust':100}, |
|
'slg':{'x_axis':'SLG','title':'SLG','flip_p':False,'decimal_format':'string_3','percent_adjust':100}, |
|
'ops':{'x_axis':'OPS','title':'OPS','flip_p':False,'decimal_format':'string_3','percent_adjust':100}, |
|
'k_percent':{'x_axis':'K%','title':'K%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100}, |
|
'bb_percent':{'x_axis':'BB%','title':'BB%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100}, |
|
'bb_over_k_percent':{'x_axis':'BB/K','title':'BB/K','flip_p':False,'decimal_format':'string_1','percent_adjust':100}, |
|
'bb_minus_k_percent':{'x_axis':'BB%-K%','title':'BB%-K%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100}, |
|
'csw_percent':{'x_axis':'CSW%','title':'CSW%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100}, |
|
'woba_percent':{'x_axis':'wOBA','title':'wOBA','flip_p':False,'decimal_format':'string_3','percent_adjust':100}, |
|
'hard_hit_percent':{'x_axis':'HardHit%','title':'HardHit%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100}, |
|
'barrel_percent':{'x_axis':'Barrel%','title':'Barrel%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100}, |
|
'zone_contact_percent':{'x_axis':'Z-Contact%','title':'Z-Contact%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100}, |
|
'zone_swing_percent':{'x_axis':'Z-Swing%','title':'Z-Swing%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100}, |
|
'zone_percent':{'x_axis':'Zone%','title':'Zone%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100}, |
|
'chase_percent':{'x_axis':'O-Swing%','title':'O-Swing%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100}, |
|
'chase_contact':{'x_axis':'O-Contact%','title':'O-Contact%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100}, |
|
'swing_percent':{'x_axis':'Swing%','title':'Swing%','flip_p':False,'decimal_format':'percent_1','percent_adjust':100}, |
|
'whiff_rate':{'x_axis':'Whiff%','title':'Whiff%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100}, |
|
'swstr_rate':{'x_axis':'SwStr%','title':'SwStr%','flip_p':True,'decimal_format':'percent_1','percent_adjust':100}, |
|
} |
|
|
|
batter_dict_stat_small = { 'sweet_spot_percent':'SweetSpot%', |
|
'max_launch_speed':'Max Exit Velocity', |
|
'launch_speed_90':'90th Percentile EV', |
|
'launch_speed':'Exit Velocity', |
|
'launch_angle':'Launch Angle', |
|
'avg':'AVG', |
|
'obp':'OBP', |
|
'slg':'SLG', |
|
'ops':'OPS', |
|
'k_percent':'K%', |
|
'bb_percent':'BB%', |
|
'bb_over_k_percent':'BB/K', |
|
'bb_minus_k_percent':'BB%-K%', |
|
'csw_percent':'CSW%', |
|
'woba_percent':'wOBA', |
|
'hard_hit_percent':'HardHit%', |
|
'barrel_percent':'Barrel%', |
|
'zone_contact_percent':'Z-Contact%', |
|
'zone_swing_percent':'Z-Swing%', |
|
'zone_percent':'Zone%', |
|
'chase_percent':'O-Swing%', |
|
'chase_contact':'O-Contact%', |
|
'swing_percent':'Swing%', |
|
'whiff_rate':'Whiff%', |
|
'swstr_rate':'SwStr%', |
|
} |
|
|
|
|
|
colour_palette = ['#FFB000','#648FFF','#785EF0', |
|
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED'] |
|
|
|
level_dict = {'MLB':'MLB','AAA':'AAA','AA':'AA','A+':'A+','A':'A','ROK':'ROK'} |
|
|
|
batter_test_df = exit_velo_df_codes_summ_batter.sort_values(by='batter').drop_duplicates(subset='batter_id').reset_index(drop=True)[['batter_id','batter']] |
|
batter_test_df = batter_test_df.set_index('batter_id') |
|
|
|
|
|
def decimal_format_assign(x): |
|
if x['decimal_format'] == 'percent_1': |
|
return mtick.PercentFormatter(1,decimals=1) |
|
if x['decimal_format'] == 'string_3': |
|
return mtick.FormatStrFormatter('%.3f') |
|
if x['decimal_format'] == 'string_0': |
|
return mtick.FormatStrFormatter('%.0f') |
|
if x['decimal_format'] == 'string_1': |
|
return mtick.FormatStrFormatter('%.1f') |
|
|
|
|
|
|
|
|
|
batter_dict = batter_test_df['batter'].to_dict() |
|
|
|
exit_velo_df_codes_summ_batter.position = exit_velo_df_codes_summ_batter.position.replace(['LF','RF','CF','TWP'],['OF','OF','OF','DH']) |
|
exit_velo_df_codes_summ_non_level.position = exit_velo_df_codes_summ_non_level.position.replace(['LF','RF','CF','TWP'],['OF','OF','OF','DH']) |
|
|
|
position_list = ['All'] + list(exit_velo_df_codes_summ_batter.position.unique()) |
|
team_list = ['All'] + sorted(list(exit_velo_df_codes_summ_batter.parent_org_abb.unique())) |
|
|
|
|
|
|
|
def server(input,output,session): |
|
|
|
|
|
@output |
|
@render.plot(alt="A histogram") |
|
@reactive.event(input.go, ignore_none=False) |
|
def plot(): |
|
sns.set_theme(style="whitegrid", palette="pastel") |
|
print(input.level_id()) |
|
print(input.n()) |
|
print('we made it here',input.team_id(),input.position_id()) |
|
if input.group_level(): |
|
data_df = exit_velo_df_codes_summ_non_level.copy() |
|
|
|
turth_list = [] |
|
|
|
for x in range(0,len(data_df.level)): |
|
turth_list_2 = [] |
|
for y in range(0,len(data_df.level[x])): |
|
|
|
turth_list_2.append(data_df.level[x][y] in input.level_id()) |
|
turth_list.append(turth_list_2) |
|
|
|
final_check_list = [True if True in x else False for x in turth_list] |
|
|
|
|
|
data_df = data_df[(data_df.pa >= input.n())&(data_df.age <= input.n_age())&(final_check_list)] |
|
|
|
|
|
else: |
|
|
|
|
|
data_df = exit_velo_df_codes_summ_batter.copy() |
|
data_df = data_df[(data_df.pa >= input.n())&(data_df.age <= input.n_age())&(data_df.level.isin(input.level_id()))] |
|
print(data_df) |
|
if 'All' in input.team_id(): |
|
print('nice') |
|
|
|
else: |
|
data_df = data_df[(data_df.parent_org_abb.isin(input.team_id()))].reset_index(drop=True) |
|
|
|
if 'All' in input.position_id(): |
|
print('nice') |
|
|
|
else: |
|
data_df = data_df[(data_df.position.isin(input.position_id()))].reset_index(drop=True) |
|
|
|
|
|
|
|
print(data_df) |
|
data_df = data_df.sort_values(by='level').reset_index(drop=True) |
|
print(batter_dict_stat[input.stat_x()]['flip_p']) |
|
|
|
|
|
|
|
x_flip = batter_dict_stat[input.stat_x()]['flip_p'] |
|
y_flip = batter_dict_stat[input.stat_y()]['flip_p'] |
|
cbr_flip = batter_dict_stat[input.stat_z()]['flip_p'] |
|
|
|
|
|
|
|
data_df[input.stat_x()+'_percent'] = data_df[input.stat_x()].rank(pct=True,ascending=abs(x_flip-1)) |
|
|
|
data_df[input.stat_y()+'_percent'] = data_df[input.stat_y()].rank(pct=True,ascending=abs(y_flip-1)) |
|
|
|
data_df[input.stat_z()+'_percent'] = data_df[input.stat_z()].rank(pct=True,ascending=abs(cbr_flip-1)) |
|
|
|
|
|
|
|
fig, ax = plt.subplots(1, 1, figsize=(9, 9)) |
|
|
|
|
|
|
|
|
|
|
|
|
|
if cbr_flip: |
|
cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[0],colour_palette[3],colour_palette[1]]) |
|
norm = plt.Normalize(data_df[input.stat_z()].min(), data_df[input.stat_z()].max()) |
|
|
|
else: |
|
cmap_hue = matplotlib.colors.LinearSegmentedColormap.from_list("", [colour_palette[1],colour_palette[3],colour_palette[0]]) |
|
norm = plt.Normalize(data_df[input.stat_z()].min(), data_df[input.stat_z()].max()) |
|
|
|
sm = plt.cm.ScalarMappable(cmap=cmap_hue, norm=norm) |
|
print('we made it here') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if input.group_level(): |
|
scatter = sns.scatterplot(x = input.stat_x(), y = input.stat_y(), data=data_df, color = '#b3b3b3') |
|
|
|
scatter = sns.scatterplot(x = input.stat_x(), y = input.stat_y(), data=data_df, color = colour_palette[0],ax=ax,hue=input.stat_z(),palette=cmap_hue) |
|
else: |
|
scatter = sns.scatterplot(x = input.stat_x(), y = input.stat_y(), data=data_df, color = '#b3b3b3',style='level') |
|
|
|
scatter = sns.scatterplot(x = input.stat_x(), y = input.stat_y(), data=data_df, color = colour_palette[0],ax=ax,hue=input.stat_z(),palette=cmap_hue,style='level') |
|
sns.set_theme(style="whitegrid", palette="pastel") |
|
|
|
fig.set_facecolor('#F0F0F0') |
|
ax.set_facecolor('white') |
|
|
|
print('we made it here') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ts=[] |
|
print(input.player_id()) |
|
|
|
print(len(data_df)) |
|
if input.names(): |
|
for i in range(len(data_df)): |
|
if (data_df[input.stat_x()+'_percent'].values[i] < input.n_percent_bot_x() or data_df[input.stat_x()+'_percent'].values[i] > 1 - input.n_percent_top_x() ) \ |
|
or (data_df[input.stat_y()+'_percent'].values[i] < input.n_percent_bot_y() or data_df[input.stat_y()+'_percent'].values[i] > 1 -input.n_percent_top_y()) \ |
|
or (data_df[input.stat_z()+'_percent'].values[i] < input.n_percent_bot_z() or data_df[input.stat_z()+'_percent'].values[i] > 1 -input.n_percent_top_z() )\ |
|
or (str(data_df.batter_id[i]) in (input.player_id())): |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ts.append(ax.text(data_df[input.stat_x()][i], data_df[input.stat_y()][i], data_df.batter[i],fontsize=8)) |
|
|
|
|
|
|
|
ax.hlines(xmin=(math.floor((data_df[input.stat_x()].min()*batter_dict_stat[input.stat_x()]['percent_adjust']-0.01)/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'], |
|
xmax= (math.ceil((data_df[input.stat_x()].max()*batter_dict_stat[input.stat_x()]['percent_adjust']+0.01)/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'], |
|
y=data_df[input.stat_y()].mean(),color='gray',linewidth=3,linestyle='dotted',alpha=0.4) |
|
|
|
print('we made it here') |
|
|
|
ax.vlines(ymin=(math.floor((data_df[input.stat_y()].min()*batter_dict_stat[input.stat_y()]['percent_adjust']-0.01)/5))*5/batter_dict_stat[input.stat_y()]['percent_adjust'], |
|
ymax= (math.ceil((data_df[input.stat_y()].max()*batter_dict_stat[input.stat_y()]['percent_adjust']+0.01)/5))*5/batter_dict_stat[input.stat_y()]['percent_adjust'], |
|
x=data_df[input.stat_x()].mean(),color='gray',linewidth=3,linestyle='dotted',alpha=0.4) |
|
|
|
print(data_df[input.stat_x()].min()) |
|
print(batter_dict_stat[input.stat_x()]['percent_adjust']) |
|
print((math.floor((data_df[input.stat_x()].min()*batter_dict_stat[input.stat_x()]['percent_adjust']-0.01)/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust']) |
|
|
|
|
|
ax.set_xlim((math.floor((data_df[input.stat_x()].min()*batter_dict_stat[input.stat_x()]['percent_adjust'])/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust'], |
|
(math.ceil((data_df[input.stat_x()].max()*batter_dict_stat[input.stat_x()]['percent_adjust'])/5))*5/batter_dict_stat[input.stat_x()]['percent_adjust']) |
|
|
|
|
|
ax.set_ylim((math.floor((data_df[input.stat_y()].min()*batter_dict_stat[input.stat_y()]['percent_adjust'])/5))*5/batter_dict_stat[input.stat_y()]['percent_adjust'], |
|
(math.ceil((data_df[input.stat_y()].max()*batter_dict_stat[input.stat_y()]['percent_adjust'])/5))*5/batter_dict_stat[input.stat_y()]['percent_adjust']) |
|
|
|
|
|
|
|
title_level = str([x .strip("\'")for x in input.level_id()]).strip('[').strip(']').replace("'",'') |
|
|
|
if title_level == 'AAA, AA, A+, A': |
|
title_level='MiLB' |
|
|
|
if input.n_age() >= 50: |
|
title_spot = f'{title_level} Batter {batter_dict_stat[input.stat_y()]["title"]} vs {batter_dict_stat[input.stat_x()]["title"]} (min. {input.n()} PA)' |
|
|
|
else: |
|
title_spot = f'{title_level} Batter {batter_dict_stat[input.stat_y()]["title"]} vs {batter_dict_stat[input.stat_x()]["title"]} (min. {input.n()} PA, Max Age {input.n_age()})' |
|
|
|
ax.set_title(title_spot, fontsize=24/(len(title_spot)*0.03),fontname='Century Gothic') |
|
|
|
ax.set_xlabel(batter_dict_stat[input.stat_x()]['x_axis'], fontsize=16,fontname='Century Gothic') |
|
ax.set_ylabel(batter_dict_stat[input.stat_y()]['x_axis'], fontsize=16,fontname='Century Gothic') |
|
|
|
|
|
if input.group_level(): |
|
ax.get_legend().remove() |
|
|
|
if not input.group_level(): |
|
if len(input.level_id()) > 1: |
|
h,l = scatter.get_legend_handles_labels() |
|
l[-(len(input.level_id())+1)] = 'Level' |
|
ax.legend(h[-(len(input.level_id())+1):],l[-(len(input.level_id())+1):], borderaxespad=0.1,loc=0) |
|
|
|
else: |
|
ax.get_legend().remove() |
|
|
|
|
|
|
|
|
|
cbar = ax.figure.colorbar(sm, ax=ax,format=decimal_format_assign(x=batter_dict_stat[input.stat_z()]),orientation='vertical',aspect=30) |
|
cbar.set_label(batter_dict_stat[input.stat_z()]['x_axis']) |
|
|
|
print('we made it here5') |
|
fig.subplots_adjust(wspace=.02, hspace=.02) |
|
|
|
|
|
|
|
|
|
|
|
if batter_dict_stat[input.stat_x()]['flip_p']: |
|
fig.axes[0].invert_xaxis() |
|
|
|
if batter_dict_stat[input.stat_y()]['flip_p']: |
|
fig.axes[0].invert_yaxis() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print('we made it here6') |
|
|
|
ax.xaxis.set_major_formatter(decimal_format_assign(x=batter_dict_stat[input.stat_x()])) |
|
ax.yaxis.set_major_formatter(decimal_format_assign(x=batter_dict_stat[input.stat_y()])) |
|
|
|
|
|
print('we made it here7') |
|
|
|
|
|
|
|
|
|
print(ts) |
|
if len(ts) > 0: |
|
adjust_text(ts, |
|
arrowprops=dict(arrowstyle="-", color=colour_palette[4], lw=1),ax=ax) |
|
|
|
|
|
fig.text(x=0.03,y=0.02,s='By: @TJStats',fontname='Century Gothic') |
|
fig.text(x=1-0.03,y=0.02,s='Data: MLB',ha='right',fontname='Century Gothic') |
|
fig.tight_layout() |
|
|
|
|
|
|
|
|
|
batter_scatter = 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.row( |
|
ui.column(4,ui.input_select("level_id", "Select Level",level_dict,width=1,size=1,multiple=True,selected='MLB',selectize=True),), |
|
ui.column(4,ui.input_select("team_id", "Select Team",team_list,width=1,size=1,multiple=True,selected='All',selectize=True),), |
|
ui.column(4,ui.input_select("position_id", "Select Position",position_list,width=1,size=1,selected='All',multiple=True,selectize=True))), |
|
ui.row( |
|
ui.column(6,ui.input_numeric("n", "Minimum PA", value=100)), |
|
ui.column(6,ui.input_numeric("n_age", "Maximum Age", value=50))), |
|
ui.row( |
|
ui.column(4,ui.input_select("stat_x", "X-Axis",batter_dict_stat_small,selected='k_percent',width=1,size=1)), |
|
ui.column(4,ui.input_select("stat_y", "Y-Axis",batter_dict_stat_small,selected='bb_percent',width=1,size=1)), |
|
ui.column(4,ui.input_select("stat_z", "Colour-Bar Axis",batter_dict_stat_small,selected='bb_over_k_percent',width=1,size=1))), |
|
|
|
ui.row( |
|
ui.column(6,ui.input_numeric("n_percent_top_x", "Top 'n' Percentile X-Labels", value=0.01)), |
|
ui.column(6,ui.input_numeric("n_percent_bot_x", "Bottom 'n' Percentile X-Labels", value=0.01))), |
|
ui.row( |
|
ui.column(6,ui.input_numeric("n_percent_top_y", "Top 'n' Percentile Y-Labels", value=0.01)), |
|
ui.column(6,ui.input_numeric("n_percent_bot_y", "Bottom 'n' Percentile Y-Labels", value=0.01))), |
|
ui.row( |
|
ui.column(6,ui.input_numeric("n_percent_top_z", "Top 'n' Percentile Z-Labels", value=0.01)), |
|
ui.column(6,ui.input_numeric("n_percent_bot_z", "Bottom 'n' Percentile Z-Labels", value=0.01))), |
|
|
|
ui.input_select("player_id", "Label Player",batter_dict,width=1,size=1,multiple=True,selectize=True), |
|
ui.row( |
|
ui.input_switch("names", "Toggle Names"), |
|
ui.input_switch("group_level", "Group Levels")), |
|
ui.input_action_button("go", "Generate",class_="btn-primary"), |
|
), |
|
|
|
ui.panel_main( |
|
ui.output_plot("plot",height = "1000px",width="1000px") |
|
, |
|
), |
|
)),)),server) |