|
import pandas as pd |
|
import numpy as np |
|
import matplotlib.pyplot as plt |
|
import seaborn as sns |
|
|
|
import requests |
|
import matplotlib |
|
from api_scraper import MLB_Scrape |
|
from shinywidgets import output_widget, render_widget |
|
import shinyswatch |
|
|
|
|
|
season = 2024 |
|
level = 'mlb' |
|
colour_palette = ['#FFB000','#648FFF','#785EF0', |
|
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED'] |
|
|
|
import datasets |
|
from datasets import load_dataset |
|
|
|
dataset = load_dataset('nesticot/mlb_data', data_files=[f'{level}_pitch_data_{season}.csv' ]) |
|
dataset_train = dataset['train'] |
|
df_2024 = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True).drop_duplicates(subset=['play_id'],keep='last') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pitch_colours = { |
|
'Four-Seam Fastball':'#FF007D', |
|
'Sinker':'#98165D', |
|
'Cutter':'#BE5FA0', |
|
|
|
'Changeup':'#F79E70', |
|
'Splitter':'#FE6100', |
|
'Screwball':'#F08223', |
|
'Forkball':'#FFB000', |
|
|
|
'Slider':'#67E18D', |
|
'Sweeper':'#1BB999', |
|
'Slurve':'#376748', |
|
|
|
'Knuckle Curve':'#311D8B', |
|
'Curveball':'#3025CE', |
|
'Slow Curve':'#274BFC', |
|
'Eephus':'#648FFF', |
|
|
|
'Knuckleball':'#867A08', |
|
|
|
'Pitch Out':'#472C30', |
|
'Other':'#9C8975', |
|
} |
|
|
|
import pitcher_update as pu |
|
df_2024 = pu.df_update(df_2024) |
|
df_2024['pitch_count_hand'] = df_2024.groupby(['pitcher_id','batter_hand'])['start_speed'].transform('count') |
|
|
|
|
|
|
|
|
|
strike_zone = pd.DataFrame({ |
|
'PlateLocSide': [-0.9, -0.9, 0.9, 0.9, -0.9], |
|
'PlateLocHeight': [1.5, 3.5, 3.5, 1.5, 1.5] |
|
}) |
|
|
|
|
|
def draw_line(axis,alpha_spot=1,catcher_p = True): |
|
|
|
axis.plot(strike_zone['PlateLocSide'], strike_zone['PlateLocHeight'], color='black', linewidth=1.3,zorder=3,alpha=alpha_spot,) |
|
|
|
|
|
|
|
|
|
|
|
if catcher_p: |
|
|
|
|
|
axis.plot([-0.708, 0.708], [0.15, 0.15], color='black', linewidth=1,alpha=alpha_spot,zorder=1) |
|
axis.plot([-0.708, -0.708], [0.15, 0.3], color='black', linewidth=1,alpha=alpha_spot,zorder=1) |
|
axis.plot([-0.708, 0], [0.3, 0.5], color='black', linewidth=1,alpha=alpha_spot,zorder=1) |
|
axis.plot([0, 0.708], [0.5, 0.3], color='black', linewidth=1,alpha=alpha_spot,zorder=1) |
|
axis.plot([0.708, 0.708], [0.3, 0.15], color='black', linewidth=1,alpha=alpha_spot,zorder=1) |
|
else: |
|
axis.plot([-0.708, 0.708], [0.4, 0.4], color='black', linewidth=1,alpha=alpha_spot,zorder=1) |
|
axis.plot([-0.708, -0.9], [0.4, -0.1], color='black', linewidth=1,alpha=alpha_spot,zorder=1) |
|
axis.plot([-0.9, 0], [-0.1, -0.35], color='black', linewidth=1,alpha=alpha_spot,zorder=1) |
|
axis.plot([0, 0.9], [-.35, -0.1], color='black', linewidth=1,alpha=alpha_spot,zorder=1) |
|
axis.plot([0.9, 0.708], [-0.1,0.4], color='black', linewidth=1,alpha=alpha_spot,zorder=1) |
|
|
|
pitcher_dicts = df_2024.set_index('pitcher_id')['pitcher_name'].sort_values().to_dict() |
|
|
|
team_logos = pd.read_csv('team_logos.csv') |
|
cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFFFFF','#FFB000',]) |
|
cmap_sum2 = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#FFFFFF','#FFB000',]) |
|
|
|
|
|
from urllib.request import Request, urlopen |
|
from shiny import App, reactive, ui, render |
|
from shiny.ui import h2, tags |
|
|
|
app_ui = ui.page_fluid( |
|
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.tags.h5("Pitcher Heat Maps"), |
|
ui.row( |
|
|
|
|
|
ui.layout_sidebar( |
|
|
|
ui.panel_sidebar( |
|
|
|
|
|
ui.input_select('player_id','Select Player',pitcher_dicts,selectize=True,multiple=False), |
|
ui.output_ui('game_id_select','Date Range'), |
|
|
|
|
|
ui.output_ui('pitch_type_select','Select Pitch Type'), |
|
ui.input_action_button("go", "Generate",class_="btn-primary"),width=2 |
|
|
|
|
|
), |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ui.panel_main( |
|
ui.navset_tab( |
|
|
|
|
|
ui.nav("Season Summary", |
|
ui.output_plot('plot', |
|
width='1600px', |
|
height='900px')),id="my_tabs")))))) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def server(input, output, session): |
|
|
|
@render.ui |
|
def game_id_select(): |
|
|
|
|
|
if input.my_tabs() == 'Season Summary': |
|
|
|
return ui.input_date_range("date_range_id", "Date range input",start = df_2024.game_date.min(), |
|
end = df_2024.game_date.max(),width=2,min=df_2024.game_date.min(), |
|
max=df_2024.game_date.max()), |
|
|
|
@render.ui |
|
def pitch_type_select(): |
|
if input.player_id() == '': |
|
return ui.input_select('pitch_type','Select a Pitcher',{'pitch':''},selectize=True,multiple=False) |
|
else: |
|
pitch_dicts = df_2024[(df_2024['pitcher_id']==int(input.player_id()))].set_index('pitch_type')['pitch_description'].sort_values().to_dict() |
|
|
|
|
|
return ui.input_select('pitch_type','Select Pitch Type',pitch_dicts,selectize=True,multiple=False) |
|
|
|
@output |
|
@render.plot |
|
@reactive.event(input.go, ignore_none=False) |
|
def plot(): |
|
|
|
|
|
font_properties = {'family': 'calibi', 'size': 12} |
|
font_properties_titles = {'family': 'calibi', 'size': 20} |
|
font_properties_axes = {'family': 'calibi', 'size': 16} |
|
|
|
if len((input.player_id()))<1: |
|
fig, ax = plt.subplots(1, 1, figsize=(9, 9)) |
|
ax.text(x=0.5,y=0.5,s='Please Select\nA Player',fontsize=150,ha='center') |
|
ax.grid('off') |
|
return |
|
|
|
|
|
pitcher_input = int(input.player_id()) |
|
pitch_input = input.pitch_type() |
|
|
|
df_plot_full = df_2024[(df_2024['pitcher_id']==pitcher_input)& |
|
(pd.to_datetime(df_plot_full['game_date']).dt.date>=input.date_range_id()[0])& |
|
(pd.to_datetime(df_plot_full['game_date']).dt.date<=input.date_range_id()[1])] |
|
|
|
df_plot_full['h_s_b'] = df_plot_full.groupby(['batter_hand','strikes', 'balls']).transform('count')['pitcher_id'] |
|
df_plot_full['h_s_b_pitch'] = df_plot_full.groupby(['batter_hand','strikes', 'balls','pitch_type']).transform('count')['pitcher_id'] |
|
df_plot_full['h_s_b_pitch_percent'] = df_plot_full['h_s_b_pitch']/df_plot_full['h_s_b'] |
|
|
|
|
|
df_plot = df_plot_full[(df_plot_full['pitch_type']==pitch_input)] |
|
|
|
|
|
|
|
print("THIS IS HERE") |
|
print(df_plot) |
|
pivot_table_l = df_plot[df_plot['batter_hand'].isin(['L'])].groupby(['batter_hand','strikes', 'balls'])[['h_s_b_pitch_percent']].mean().reset_index().pivot('strikes','balls','h_s_b_pitch_percent') |
|
|
|
new_index = range(3) |
|
new_columns = range(4) |
|
|
|
|
|
pivot_table_l = pivot_table_l.reindex(index=new_index, columns=new_columns) |
|
|
|
|
|
pivot_table_l = pivot_table_l.fillna(0) |
|
|
|
pivot_table_l = df_plot[df_plot['batter_hand']=='L'].groupby(['batter_hand','strikes', 'balls'])[['h_s_b_pitch_percent']].mean().reset_index().pivot('strikes','balls','h_s_b_pitch_percent') |
|
|
|
new_index = range(3) |
|
new_columns = range(4) |
|
|
|
|
|
pivot_table_l = pivot_table_l.reindex(index=new_index, columns=new_columns) |
|
|
|
|
|
pivot_table_l = pivot_table_l.fillna(0) |
|
|
|
pivot_table_r = df_plot[df_plot['batter_hand']=='R'].groupby(['batter_hand','strikes', 'balls'])[['h_s_b_pitch_percent']].mean().reset_index().pivot('strikes','balls','h_s_b_pitch_percent') |
|
|
|
new_index = range(3) |
|
new_columns = range(4) |
|
|
|
|
|
pivot_table_r = pivot_table_r.reindex(index=new_index, columns=new_columns) |
|
|
|
|
|
pivot_table_r = pivot_table_r.fillna(0) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
df_summ = df_plot.groupby(['batter_hand']).agg( |
|
pitch_count = ('pitch_count_hand','max'), |
|
pa = ('pa','sum'), |
|
ab = ('ab','sum'), |
|
obp_pa = ('obp','sum'), |
|
hits = ('hits','sum'), |
|
on_base = ('on_base','sum'), |
|
k = ('k','sum'), |
|
bb = ('bb','sum'), |
|
bb_minus_k = ('bb_minus_k','sum'), |
|
csw = ('csw','sum'), |
|
bip = ('bip','sum'), |
|
bip_div = ('bip_div','sum'), |
|
tb = ('tb','sum'), |
|
woba = ('woba','sum'), |
|
woba_contact = ('woba_contact','sum'), |
|
xwoba = ('woba_pred','sum'), |
|
xwoba_contact = ('woba_pred_contact','sum'), |
|
woba_codes = ('woba_codes','sum'), |
|
hard_hit = ('hard_hit','sum'), |
|
barrel = ('barrel','sum'), |
|
sweet_spot = ('sweet_spot','sum'), |
|
max_launch_speed = ('launch_speed','max'), |
|
launch_speed = ('launch_speed','mean'), |
|
launch_angle = ('launch_angle','mean'), |
|
pitches = ('is_pitch','sum'), |
|
swings = ('swings','sum'), |
|
in_zone = ('in_zone','sum'), |
|
out_zone = ('out_zone','sum'), |
|
whiffs = ('whiffs','sum'), |
|
zone_swing = ('zone_swing','sum'), |
|
zone_contact = ('zone_contact','sum'), |
|
ozone_swing = ('ozone_swing','sum'), |
|
ozone_contact = ('ozone_contact','sum'), |
|
ground_ball = ('trajectory_ground_ball','sum'), |
|
line_drive = ('trajectory_line_drive','sum'), |
|
fly_ball =('trajectory_fly_ball','sum'), |
|
pop_up = ('trajectory_popup','sum'), |
|
attack_zone = ('attack_zone','count'), |
|
heart = ('heart','sum'), |
|
shadow = ('shadow','sum'), |
|
chase = ('chase','sum'), |
|
waste = ('waste','sum'), |
|
heart_swing = ('heart_swing','sum'), |
|
shadow_swing = ('shadow_swing','sum'), |
|
chase_swing = ('chase_swing','sum'), |
|
waste_swing = ('waste_swing','sum'), |
|
heart_whiff = ('heart_whiff','sum'), |
|
shadow_whiff = ('shadow_whiff','sum'), |
|
chase_whiff = ('chase_whiff','sum'), |
|
waste_whiff = ('waste_whiff','sum'), |
|
).reset_index() |
|
|
|
|
|
df_summ['avg'] = [df_summ.hits[x]/df_summ.ab[x] if df_summ.ab[x] != 0 else np.nan for x in range(len(df_summ))] |
|
df_summ['obp'] = [df_summ.on_base[x]/df_summ.obp_pa[x] if df_summ.obp_pa[x] != 0 else np.nan for x in range(len(df_summ))] |
|
df_summ['slg'] = [df_summ.tb[x]/df_summ.ab[x] if df_summ.ab[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['ops'] = df_summ['obp']+df_summ['slg'] |
|
|
|
df_summ['k_percent'] = [df_summ.k[x]/df_summ.pa[x] if df_summ.pa[x] != 0 else np.nan for x in range(len(df_summ))] |
|
df_summ['bb_percent'] =[df_summ.bb[x]/df_summ.pa[x] if df_summ.pa[x] != 0 else np.nan for x in range(len(df_summ))] |
|
df_summ['bb_minus_k_percent'] =[(df_summ.bb_minus_k[x])/df_summ.pa[x] if df_summ.pa[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['bb_over_k_percent'] =[df_summ.bb[x]/df_summ.k[x] if df_summ.k[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
|
|
|
|
|
|
df_summ['csw_percent'] =[df_summ.csw[x]/df_summ.pitches[x] if df_summ.pitches[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
|
|
df_summ['sweet_spot_percent'] = [df_summ.sweet_spot[x]/df_summ.bip_div[x] if df_summ.bip_div[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['woba_percent'] = [df_summ.woba[x]/df_summ.woba_codes[x] if df_summ.woba_codes[x] != 0 else np.nan for x in range(len(df_summ))] |
|
df_summ['woba_percent_contact'] = [df_summ.woba_contact[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['hard_hit_percent'] = [df_summ.hard_hit[x]/df_summ.bip_div[x] if df_summ.bip_div[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
|
|
df_summ['barrel_percent'] = [df_summ.barrel[x]/df_summ.bip_div[x] if df_summ.bip_div[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['zone_contact_percent'] = [df_summ.zone_contact[x]/df_summ.zone_swing[x] if df_summ.zone_swing[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['zone_swing_percent'] = [df_summ.zone_swing[x]/df_summ.in_zone[x] if df_summ.in_zone[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['zone_percent'] = [df_summ.in_zone[x]/df_summ.pitches[x] if df_summ.pitches[x] > 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['chase_percent'] = [df_summ.ozone_swing[x]/(df_summ.pitches[x] - df_summ.in_zone[x]) if (df_summ.pitches[x]- df_summ.in_zone[x]) != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['chase_contact'] = [df_summ.ozone_contact[x]/df_summ.ozone_swing[x] if df_summ.ozone_swing[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['swing_percent'] = [df_summ.swings[x]/df_summ.pitches[x] if df_summ.pitches[x] > 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['whiff_rate'] = [df_summ.whiffs[x]/df_summ.swings[x] if df_summ.swings[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['swstr_rate'] = [df_summ.whiffs[x]/df_summ.pitches[x] if df_summ.pitches[x] > 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['ground_ball_percent'] = [df_summ.ground_ball[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['line_drive_percent'] = [df_summ.line_drive[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['fly_ball_percent'] = [df_summ.fly_ball[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['pop_up_percent'] = [df_summ.pop_up[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
|
|
|
|
df_summ['heart_zone_percent'] = [df_summ.heart[x]/df_summ.attack_zone[x] if df_summ.attack_zone[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['shadow_zone_percent'] = [df_summ.shadow[x]/df_summ.attack_zone[x] if df_summ.attack_zone[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['chase_zone_percent'] = [df_summ.chase[x]/df_summ.attack_zone[x] if df_summ.attack_zone[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['waste_zone_percent'] = [df_summ.waste[x]/df_summ.attack_zone[x] if df_summ.attack_zone[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
|
|
df_summ['heart_zone_swing_percent'] = [df_summ.heart_swing[x]/df_summ.heart[x] if df_summ.heart[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['shadow_zone_swing_percent'] = [df_summ.shadow_swing[x]/df_summ.shadow[x] if df_summ.shadow[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['chase_zone_swing_percent'] = [df_summ.chase_swing[x]/df_summ.chase[x] if df_summ.chase[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['waste_zone_swing_percent'] = [df_summ.waste_swing[x]/df_summ.waste[x] if df_summ.waste[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['heart_zone_whiff_percent'] = [df_summ.heart_whiff[x]/df_summ.heart_swing[x] if df_summ.heart_swing[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['shadow_zone_whiff_percent'] = [df_summ.shadow_whiff[x]/df_summ.shadow_swing[x] if df_summ.shadow_swing[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['chase_zone_whiff_percent'] = [df_summ.chase_whiff[x]/df_summ.chase_swing[x] if df_summ.chase_swing[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['waste_zone_whiff_percent'] = [df_summ.waste_whiff[x]/df_summ.waste_swing[x] if df_summ.waste_swing[x] != 0 else np.nan for x in range(len(df_summ))] |
|
df_summ['xwoba_percent'] = [df_summ.xwoba[x]/df_summ.woba_codes[x] if df_summ.woba_codes[x] != 0 else np.nan for x in range(len(df_summ))] |
|
df_summ['xwoba_percent_contact'] = [df_summ.xwoba_contact[x]/df_summ.bip[x] if df_summ.bip[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
df_summ['pitch_percent'] = [df_summ.pitches[x]/df_summ.pitch_count[x] if df_summ.pitch_count[x] != 0 else np.nan for x in range(len(df_summ))] |
|
|
|
table_left = df_summ[df_summ['batter_hand']=='L'][['pitch_percent', |
|
'pitches', |
|
'heart_zone_percent', |
|
'shadow_zone_percent', |
|
'chase_zone_percent', |
|
'waste_zone_percent', |
|
'csw_percent', |
|
'whiff_rate', |
|
'chase_percent', |
|
'bip', |
|
'xwoba_percent_contact' |
|
]] |
|
|
|
|
|
import matplotlib.colors |
|
import matplotlib.colors as mcolors |
|
def get_color(value,normalize): |
|
color = cmap_sum(normalize(value)) |
|
return mcolors.to_hex(color) |
|
try: |
|
normalize = mcolors.Normalize(vmin=table_left['pitch_percent']*0.5, |
|
vmax=table_left['pitch_percent']*1.5) |
|
|
|
except ValueError: |
|
normalize = mcolors.Normalize(vmin=0, |
|
vmax=1) |
|
|
|
|
|
|
|
df_colour_left = pd.DataFrame(data=[[get_color(x,normalize) for x in pivot_table_l.loc[0]], |
|
[get_color(x,normalize) for x in pivot_table_l.loc[1]], |
|
[get_color(x,normalize) for x in pivot_table_l.loc[2]]],) |
|
|
|
|
|
table_left['pitch_percent'] = table_left['pitch_percent'].map('{:.1%}'.format) |
|
table_left['pitches'] = table_left['pitches'].astype(int).astype(str) |
|
|
|
|
|
|
|
table_left['heart_zone_percent'] = table_left['heart_zone_percent'].map('{:.1%}'.format) |
|
table_left['shadow_zone_percent'] = table_left['shadow_zone_percent'].map('{:.1%}'.format) |
|
table_left['chase_zone_percent'] = table_left['chase_zone_percent'].map('{:.1%}'.format) |
|
table_left['waste_zone_percent'] = table_left['waste_zone_percent'].map('{:.1%}'.format) |
|
table_left['csw_percent'] = table_left['csw_percent'].map('{:.1%}'.format) |
|
table_left['whiff_rate'] = table_left['whiff_rate'].map('{:.1%}'.format) |
|
table_left['chase_percent'] = table_left['chase_percent'].map('{:.1%}'.format) |
|
table_left['bip'] = table_left['bip'].astype(int).astype(str) |
|
table_left['xwoba_percent_contact'] = table_left['xwoba_percent_contact'].map('{:.3f}'.format) |
|
table_left.columns = ['Usage%','Pitches','Heart%','Shadow%','Chase%','Waste%','CSW%','Whiff%','O-Swing%','BBE','xwOBACON'] |
|
|
|
|
|
table_left = table_left.replace({'nan%':'—'}) |
|
table_left = table_left.replace({'nan':'—'}) |
|
table_left = table_left.T |
|
|
|
|
|
table_right = df_summ[df_summ['batter_hand']=='R'][['pitch_percent', |
|
'pitches', |
|
'heart_zone_percent', |
|
'shadow_zone_percent', |
|
'chase_zone_percent', |
|
'waste_zone_percent', |
|
'csw_percent', |
|
'whiff_rate', |
|
'chase_percent', |
|
'bip', |
|
'xwoba_percent_contact' |
|
]] |
|
try: |
|
normalize = mcolors.Normalize(vmin=table_right['pitch_percent']*0.5, |
|
vmax=table_right['pitch_percent']*1.5) |
|
|
|
except ValueError: |
|
normalize = mcolors.Normalize(vmin=0, |
|
vmax=1) |
|
|
|
|
|
|
|
|
|
df_colour_right = pd.DataFrame(data=[[get_color(x,normalize) for x in pivot_table_r.loc[0]], |
|
[get_color(x,normalize) for x in pivot_table_r.loc[1]], |
|
[get_color(x,normalize) for x in pivot_table_r.loc[2]]],) |
|
|
|
|
|
|
|
table_right['pitch_percent'] = table_right['pitch_percent'].map('{:.1%}'.format) |
|
table_right['pitches'] = table_right['pitches'].astype(int).astype(str) |
|
|
|
|
|
|
|
table_right['heart_zone_percent'] = table_right['heart_zone_percent'].map('{:.1%}'.format) |
|
table_right['shadow_zone_percent'] = table_right['shadow_zone_percent'].map('{:.1%}'.format) |
|
table_right['chase_zone_percent'] = table_right['chase_zone_percent'].map('{:.1%}'.format) |
|
table_right['waste_zone_percent'] = table_right['waste_zone_percent'].map('{:.1%}'.format) |
|
table_right['csw_percent'] = table_right['csw_percent'].map('{:.1%}'.format) |
|
table_right['whiff_rate'] = table_right['whiff_rate'].map('{:.1%}'.format) |
|
table_right['chase_percent'] = table_right['chase_percent'].map('{:.1%}'.format) |
|
table_right['bip'] = table_right['bip'].astype(int).astype(str) |
|
table_right['xwoba_percent_contact'] = table_right['xwoba_percent_contact'].map('{:.3f}'.format) |
|
table_right.columns = ['Usage%','Pitches','Heart%','Shadow%','Chase%','Waste%','CSW%','Whiff%','O-Swing%','BBE','xwOBACON'] |
|
|
|
|
|
table_right = table_right.replace({'nan%':'—'}) |
|
table_right = table_right.replace({'nan':'—'}) |
|
table_right = table_right.T |
|
|
|
|
|
import matplotlib.pyplot as plt |
|
import seaborn as sns |
|
import matplotlib.gridspec as gridspec |
|
from matplotlib.gridspec import GridSpec |
|
|
|
|
|
|
|
|
|
fig = plt.figure(figsize=(16, 9)) |
|
fig.set_facecolor('white') |
|
sns.set_theme(style="whitegrid", palette=colour_palette) |
|
gs = GridSpec(3, 5, height_ratios=[2,9,1],width_ratios=[2,9,0.5,9,2]) |
|
gs.update(hspace=0.2, wspace=0.2) |
|
|
|
|
|
axheader = fig.add_subplot(gs[0, :]) |
|
ax_left = fig.add_subplot(gs[1, 1]) |
|
ax_right = fig.add_subplot(gs[1, 3]) |
|
|
|
axfooter = fig.add_subplot(gs[-1, :]) |
|
|
|
|
|
|
|
if df_plot[df_plot['batter_hand']=='L'].shape[0] > 3: |
|
sns.kdeplot(data=df_plot[df_plot['batter_hand']=='L'], |
|
x='px', |
|
y='pz', |
|
cmap=cmap_sum, |
|
shade=True, |
|
ax=ax_left, |
|
thresh=0.3, |
|
bw_adjust=0.5) |
|
else: |
|
sns.scatterplot(data=df_plot[df_plot['batter_hand']=='L'], |
|
x='px', |
|
y='pz', |
|
cmap=cmap_sum, |
|
ax=ax_left, |
|
s=125) |
|
|
|
if df_plot[df_plot['batter_hand']=='R'].shape[0] > 3: |
|
sns.kdeplot(data=df_plot[df_plot['batter_hand']=='R'], |
|
x='px', |
|
y='pz', |
|
cmap=cmap_sum, |
|
shade=True, |
|
ax=ax_right, |
|
thresh=0.3, |
|
bw_adjust=0.5) |
|
else: |
|
sns.scatterplot(data=df_plot[df_plot['batter_hand']=='R'], |
|
x='px', |
|
y='pz', |
|
cmap=cmap_sum, |
|
ax=ax_right, |
|
s=125) |
|
|
|
draw_line(ax_left,alpha_spot=1,catcher_p = False) |
|
draw_line(ax_right,alpha_spot=1,catcher_p = False) |
|
|
|
ax_left.axis('off') |
|
ax_right.axis('off') |
|
|
|
ax_left.axis('square') |
|
ax_right.axis('square') |
|
|
|
ax_left.set_xlim(-2.75,2.75) |
|
ax_right.set_xlim(-2.75,2.75) |
|
|
|
ax_left.set_ylim(-0.5,5) |
|
ax_right.set_ylim(-0.5,5) |
|
|
|
|
|
import matplotlib.pyplot as plt |
|
|
|
import matplotlib.image as mpimg |
|
from matplotlib.offsetbox import OffsetImage, AnnotationBbox |
|
|
|
|
|
img = mpimg.imread('left.png') |
|
imagebox = OffsetImage(img, zoom=0.7) |
|
ab = AnnotationBbox(imagebox, (1.25, -0.5), box_alignment=(0, 0), frameon=False) |
|
ax_left.add_artist(ab) |
|
|
|
|
|
|
|
img = mpimg.imread('right.png') |
|
imagebox = OffsetImage(img, zoom=0.7) |
|
|
|
ab = AnnotationBbox(imagebox, (-1.25, -0.5), box_alignment=(1, 0), frameon=False) |
|
|
|
ax_right.add_artist(ab) |
|
|
|
|
|
from matplotlib.transforms import Bbox |
|
|
|
trans = ax_left.transData + ax_left.transAxes.inverted() |
|
|
|
|
|
bbox_data = Bbox.from_bounds(-4.2, -0.5, 2.5, 5) |
|
bbox_axes = trans.transform_bbox(bbox_data) |
|
|
|
|
|
table_left_plot = ax_left.table(cellText=table_left.reset_index().values, |
|
loc='right', |
|
cellLoc='center', |
|
colWidths=[0.52,0.3], |
|
bbox=bbox_axes.bounds,zorder=100) |
|
|
|
|
|
min_font_size = 14 |
|
|
|
table_left_plot.auto_set_font_size(False) |
|
|
|
table_left_plot.set_fontsize(min_font_size) |
|
|
|
|
|
bbox_data = Bbox.from_bounds(-0.75, 5, 2.5, 1) |
|
bbox_axes = trans.transform_bbox(bbox_data) |
|
|
|
def format_as_percentage(val): |
|
return f'{val * 100:.0f}%' |
|
|
|
table_left_plot_pivot = ax_left.table(cellText=[[format_as_percentage(val) for val in row] for row in pivot_table_l.values], |
|
colLabels =pivot_table_l.columns, |
|
rowLabels =[' 0 ',' 1 ',' 2 '], |
|
loc='center', |
|
cellLoc='center', |
|
colWidths=[0.3,0.3,0.30,0.3], |
|
bbox=bbox_axes.bounds,zorder=100,cellColours =df_colour_left.values) |
|
|
|
|
|
min_font_size = 11 |
|
|
|
table_left_plot_pivot.auto_set_font_size(False) |
|
|
|
table_left_plot_pivot.set_fontsize(min_font_size) |
|
|
|
|
|
|
|
|
|
|
|
trans = ax_right.transData + ax_right.transAxes.inverted() |
|
|
|
|
|
bbox_data = Bbox.from_bounds(1.7, -0.5, 2.5, 5) |
|
bbox_axes = trans.transform_bbox(bbox_data) |
|
|
|
|
|
table_right_plot = ax_right.table(cellText=table_right.reset_index().values, |
|
loc='right', |
|
cellLoc='center', |
|
colWidths=[0.52,0.3], |
|
bbox=bbox_axes.bounds,zorder=100) |
|
|
|
|
|
|
|
min_font_size = 14 |
|
|
|
table_right_plot.auto_set_font_size(False) |
|
|
|
table_right_plot.set_fontsize(min_font_size) |
|
table_right_plot.scale(0.5,3) |
|
|
|
|
|
|
|
trans = ax_right.transData + ax_right.transAxes.inverted() |
|
bbox_data = Bbox.from_bounds(-0.75, 5, 2.5, 1) |
|
bbox_axes = trans.transform_bbox(bbox_data) |
|
|
|
table_right_plot_pivot = ax_right.table(cellText=[[format_as_percentage(val) for val in row] for row in pivot_table_r.values], |
|
colLabels =pivot_table_r.columns, |
|
rowLabels =[' 0 ',' 1 ',' 2 '], |
|
loc='center', |
|
cellLoc='center', |
|
colWidths=[0.3,0.3,0.30,0.3], |
|
bbox=bbox_axes.bounds,zorder=100,cellColours =df_colour_right.values) |
|
|
|
|
|
min_font_size = 11 |
|
|
|
table_right_plot_pivot.auto_set_font_size(False) |
|
|
|
table_right_plot_pivot.set_fontsize(min_font_size) |
|
|
|
from matplotlib.cm import ScalarMappable |
|
from matplotlib.colors import Normalize |
|
|
|
sm = ScalarMappable(cmap=cmap_sum, norm=Normalize(vmin=0, vmax=1)) |
|
|
|
|
|
|
|
|
|
|
|
cbar = fig.colorbar(sm, ax=axfooter, orientation='horizontal',aspect=100) |
|
|
|
|
|
|
|
|
|
|
|
cbar.set_ticks([]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cbar.set_ticks([sm.norm.vmin, sm.norm.vmax]) |
|
|
|
|
|
cbar.ax.set_xticklabels(['Least', 'Most']) |
|
|
|
cbar.ax.tick_params(labeltop=True, labelbottom=False, labelsize=14) |
|
|
|
|
|
labels = cbar.ax.get_xticklabels() |
|
|
|
|
|
labels[0].set_horizontalalignment('left') |
|
labels[-1].set_horizontalalignment('right') |
|
|
|
labels = cbar.ax.get_xticklabels() |
|
|
|
|
|
|
|
|
|
|
|
|
|
cbar.ax.set_xticklabels(labels) |
|
|
|
cbar.ax.tick_params(length=0) |
|
|
|
|
|
|
|
axfooter.text(x=0.02,y=0.5,s='By: Thomas Nestico\n @TJStats',fontname='Calibri',ha='left',fontsize=18,va='top') |
|
axfooter.text(x=1-0.02,y=0.5,s='Data: MLB',ha='right',fontname='Calibri',fontsize=18,va='top') |
|
|
|
axfooter.axis('off') |
|
|
|
|
|
axheader.text(x=0.5,y=1.2,s=f"{df_plot['pitcher_name'].values[0]} - {df_plot['pitcher_hand'].values[0]}HP\n{season} {df_plot['pitch_description'].values[0]} Pitch Frequency",ha='center',fontsize=24,va='top') |
|
axheader.text(x=0.5,y=0.5,s=f"{input.date_range_id()[0]} to {input.date_range_id()[1]}",ha='center',fontsize=16,va='top') |
|
|
|
axheader.axis('off') |
|
|
|
|
|
import urllib |
|
import urllib.request |
|
import urllib.error |
|
from urllib.error import HTTPError |
|
|
|
try: |
|
url = f'https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:67:current.png/w_213,q_auto:best/v1/people/{df_plot["pitcher_id"].values[0]}/headshot/67/current.png' |
|
test_mage = plt.imread(url) |
|
except urllib.error.HTTPError as err: |
|
url = f'https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:67:current.png/w_213,q_auto:best/v1/people/1/headshot/67/current.png' |
|
imagebox = OffsetImage(test_mage, zoom = 0.4) |
|
ab = AnnotationBbox(imagebox, (0.075, 0.4), frameon = False) |
|
axheader.add_artist(ab) |
|
|
|
player_bio = requests.get(url=f"https://statsapi.mlb.com/api/v1/people?personIds={df_plot['pitcher_id'].values[0]}&hydrate=currentTeam").json() |
|
|
|
|
|
team_logos = pd.read_csv('team_logos.csv') |
|
|
|
|
|
mlb_stats = MLB_Scrape() |
|
teams_df = mlb_stats.get_teams() |
|
team_logo_dict = teams_df.set_index(['team_id'])['parent_org_id'].to_dict() |
|
|
|
|
|
if 'currentTeam' in player_bio['people'][0]: |
|
try: |
|
url = team_logos[team_logos['id'] == team_logo_dict[player_bio['people'][0]['currentTeam']['id']]]['imageLink'].values[0] |
|
|
|
im = plt.imread(url) |
|
|
|
|
|
|
|
|
|
imagebox = OffsetImage(im, zoom = 0.3) |
|
ab = AnnotationBbox(imagebox, (0.925, 0.40), frameon = False) |
|
axheader.add_artist(ab) |
|
except IndexError: |
|
print() |
|
|
|
|
|
ax_left.text(s='Against LHH',x=-2.95,y=4.65,fontsize=18,fontweight='bold',ha='center') |
|
ax_right.text(s='Against RHH',x=2.95,y=4.65,fontsize=18,fontweight='bold',ha='center') |
|
|
|
|
|
ax_left.text(x=-1.76, y=5.08, s='Strikes', rotation=90,fontweight='bold') |
|
ax_right.text(x=-1.76, y=5.08, s='Strikes', rotation=90,fontweight='bold') |
|
|
|
ax_left.text(x=0, y=6.03, s='Balls',ha='center',fontweight='bold') |
|
ax_right.text(x=0, y=6.03, s='Balls',ha='center',fontweight='bold') |
|
|
|
fig.subplots_adjust(left=0.01, right=0.99, top=0.95, bottom=0.05) |
|
return |
|
|
|
app = App(app_ui, server) |