Spaces:
Running
Running
import pandas as pd | |
import numpy as np | |
import matplotlib.pyplot as plt | |
import seaborn as sns | |
import pitch_summary_functions as psf | |
import requests | |
import matplotlib | |
from api_scraper import MLB_Scrape | |
from shinywidgets import output_widget, render_widget | |
import shinyswatch | |
colour_palette = ['#FFB000','#648FFF','#785EF0', | |
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED'] | |
import datasets | |
from datasets import load_dataset | |
### Import Datasets | |
dataset = load_dataset('nesticot/mlb_data', data_files=['mlb_pitch_data_2024.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') | |
# ### Import Datasets | |
# import datasets | |
# from datasets import load_dataset | |
# dataset = load_dataset('nesticot/mlb_data', data_files=['mlb_pitch_data_2020.csv' ]) | |
# dataset_train = dataset['train'] | |
# df_2024 = dataset_train.to_pandas().set_index(list(dataset_train.features.keys())[0]).reset_index(drop=True) | |
### PITCH COLOURS ### | |
pitch_colours = { | |
'Four-Seam Fastball':'#FF007D',#BC136F | |
'Fastball':'#FF007D',#BC136F | |
'Sinker':'#98165D',#DC267F | |
'Cutter':'#BE5FA0', | |
'Changeup':'#F79E70',#F75233 | |
'Splitter':'#FE6100',#F75233 | |
'Screwball':'#F08223', | |
'Forkball':'#FFB000', | |
'Slider':'#67E18D',#1BB999#785EF0 | |
'Sweeper':'#1BB999',#37CD85#904039 | |
'Slurve':'#376748',#785EF0#549C07#BEABD8 | |
'Knuckle Curve':'#311D8B', | |
'Curveball':'#3025CE', | |
'Slow Curve':'#274BFC', | |
'Eephus':'#648FFF', | |
'Knuckle Ball':'#867A08', | |
'Pitch Out':'#472C30', | |
'Other':'#9C8975', | |
} | |
spring_teams = df_2024.groupby(['pitcher_id']).tail(1)[['pitcher_id','pitcher_team']].set_index(['pitcher_id'])['pitcher_team'].to_dict() | |
season_start = '2024-03-20' | |
season_end = '2024-09-29' | |
season_fg=2024 | |
chad_fg = requests.get(f'https://www.fangraphs.com/api/leaders/major-league/data?age=&pos=all&stats=pit&lg=all&qual=0&season={season_fg}&season={season_fg}&month=1000&season1={season_fg}&ind=0&pageitems=2000000000&pagenum=1&ind=0&rost=0&players=&type=36&postseason=&sortdir=default&sortstat=sp_pitching').json() | |
chadwick_df_small = pd.DataFrame(data={ | |
'key_mlbam':[x['xMLBAMID'] for x in chad_fg['data']], | |
'key_fangraphs':[x['playerid'] for x in chad_fg['data']], | |
'Name':[x['PlayerName'] for x in chad_fg['data']], | |
}) | |
pitcher_dicts = chadwick_df_small.set_index('key_mlbam')['Name'].sort_values().to_dict() | |
mlb_fg_dicts = chadwick_df_small.set_index('key_mlbam')['key_fangraphs'].sort_values().to_dict() | |
statcast_pitch_summary = pd.read_csv('statcast_pitch_summary.csv') | |
cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFFFFF','#FFB000',]) | |
df_2024_codes = psf.df_update_code(df_2024) | |
df_2024_update = psf.df_clean(df_2024_codes) | |
import joblib | |
model = joblib.load('joblib_model/tjstuff_model_20240428.joblib') | |
# y_pred_mean = 0.0011434511 | |
# y_pred_std = 0.006554768 | |
from stuff_values_20240428 import my_dict | |
y_pred_mean = my_dict['y_pred_mean'] | |
y_pred_std = my_dict['y_pred_std'] | |
xwoba_model = joblib.load('joblib_model/xwoba_model.joblib') | |
features = ['start_speed','spin_rate','log_extension','ivb','hb','x0','z0','primary_mean_velo_diff','primary_mean_ivb_diff','primary_mean_hb_diff'] | |
targets = ['delta_run_exp_mean'] | |
df_2024_update['y_pred'] = model.predict(df_2024_update[features]) | |
df_2024_update['tj_stuff_plus'] = 100 + 10*((-df_2024_update.y_pred +y_pred_mean) / y_pred_std) | |
df_2024_update['woba_pred'] = np.nan | |
df_2024_update.loc[df_2024_update[['launch_angle','launch_speed']].isnull().sum(axis=1)==0,'woba_pred'] = [sum(x) for x in xwoba_model.predict_proba(df_2024_update.loc[df_2024_update[['launch_angle','launch_speed']].isnull().sum(axis=1)==0][['launch_angle','launch_speed']]) * ([0, 0.883,1.244,1.569,2.004])] | |
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() | |
font_properties = {'family': 'calibi', 'size': 12} | |
font_properties_titles = {'family': 'calibi', 'size': 20} | |
font_properties_axes = {'family': 'calibi', 'size': 16} | |
df_plot = [] | |
ax2_loc = [] | |
gs = [] | |
fig = [] | |
function_dict={ | |
'velocity_kde':'Velocity Distributions', | |
'break_plot':'Pitch Movement', | |
'rolling_tj_stuff':'Rolling tjStuff+', | |
'location_lhb':'Locations vs LHB', | |
'location_rhb':'Locations vs RHB', | |
} | |
split_dict = {'all':'All', | |
'left':'LHB', | |
'right':'RHB'} | |
split_dict_hand = {'all':['L','R'], | |
'left':['L'], | |
'right':['R']} | |
ball_dict = {'0':'0', | |
'1':'1', | |
'2':'2', | |
'3':'3'} | |
strike_dict = {'0':'0', | |
'1':'1', | |
'2':'2'} | |
# count_dict = {'0_0':'Through 0-0', | |
# '0_1':'Through 0-1', | |
# '0_2':'Through 0-2', | |
# '1_0':'Through 1-0', | |
# '1_1':'Through 1-1', | |
# '1_2':'Through 1-2', | |
# '2_1':'Through 2-1', | |
# '2_0':'Through 2-0', | |
# '3_0':'Through 3-0', | |
# '3_1':'Through 3-1', | |
# '2_2':'Through 2-2', | |
# '3_2':'Through 3-2'} | |
# count_dict_fg = {'0_0':'', | |
# '0_1':'61', | |
# '0_2':'62', | |
# '1_0':'63', | |
# '1_1':'64', | |
# '1_2':'65', | |
# '2_1':'66', | |
# '2_0':'67', | |
# '3_0':'68', | |
# '3_1':'69', | |
# '2_2':'70', | |
# '3_2':'71'} | |
from urllib.request import Request, urlopen | |
from shiny import App, reactive, ui, render | |
from shiny.ui import h2, tags | |
# importing OpenCV(cv2) module | |
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.row( | |
ui.layout_sidebar( | |
ui.panel_sidebar( | |
ui.row( | |
ui.column(6, | |
ui.input_select('player_id','Select Player',pitcher_dicts,selectize=True,multiple=False)), | |
ui.column(6, ui.output_ui('test','Select Game'))), | |
ui.row( | |
ui.column(4, | |
ui.input_select('plot_id_1','Plot Left',function_dict,multiple=False,selected='velocity_kde')), | |
ui.column(4, | |
ui.input_select('plot_id_2','Plot Middle',function_dict,multiple=False,selected='rolling_tj_stuff')), | |
ui.column(4, | |
ui.input_select('plot_id_3','Plot Right',function_dict,multiple=False,selected='break_plot'))), | |
# ui.input_select('count_id','Count',count_dict,multiple=True,selectize=True,selected='0_0'), | |
ui.row( | |
ui.column(6, | |
ui.input_select('ball_id','Balls',ball_dict,multiple=False,selected='0'), | |
ui.input_radio_buttons( | |
"count_id_balls", | |
"Count Filter Balls", | |
{ | |
"exact": "Exact Balls", | |
"greater": ">= Balls", | |
"lesser": "<= Balls", | |
},selected='greater')), | |
ui.column(6, | |
ui.input_select('strike_id','Strikes',strike_dict,multiple=False,selected='0'), | |
ui.input_radio_buttons( | |
"count_id_strikes", | |
"Count Filter Strikes", | |
{ | |
"exact": "Exact Strikes", | |
"greater": ">= Strikes", | |
"lesser": "<= Strikes", | |
},selected='greater'))), | |
ui.row( | |
ui.column(6, | |
ui.input_select('split_id','Select Split',split_dict,multiple=False)), | |
ui.column(6, | |
ui.input_numeric('rolling_window','Rolling Window (for tjStuff+ Plot)',min=1,value=10))), | |
ui.input_action_button("go", "Generate",class_="btn-primary"), | |
width=4) | |
, | |
ui.panel_main( | |
ui.navset_tab( | |
# ui.nav("Raw Data", | |
# ui.output_data_frame("raw_table")), | |
ui.nav("Season Summary", | |
ui.output_plot('plot', | |
width='2000px', | |
height='2000px')), | |
ui.nav("Game Summary", | |
ui.output_plot('plot_game', | |
width='2000px', | |
height='2000px')) | |
,id="my_tabs")))))) | |
#print(app_ui) | |
def server(input, output, session): | |
def test(): | |
# @reactive.Effect | |
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()), | |
# @reactive.Effect | |
if input.my_tabs() == 'Game Summary': | |
pitcher_id_select = int(input.player_id()) | |
df_plot = df_2024_update[(df_2024_update['pitcher_id']==pitcher_id_select)] | |
# ax0.text(x=0.5,y=0.30,s=f'2024 Spring Training',fontname='Calibri',ha='center',fontsize=30,va='top') | |
df_plot['game_opp'] = df_plot['game_date'].astype(str) + ' vs ' + df_plot['batter_team'].astype(str) | |
#print(df_plot['game_opp']) | |
date_dict = pd.concat([df_plot.drop_duplicates(subset=['pitcher_id','game_id','game_opp'])[['game_id','game_opp']]]).set_index('game_id').to_dict() | |
return ui.input_select("game_id", "Select Game",date_dict,selectize=True) | |
def plot(): | |
#fig, ax = plt.subplots(3, 2, figsize=(9, 9)) | |
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_id_select = int(input.player_id()) | |
df_plot = df_2024_update[(df_2024_update['pitcher_id']==pitcher_id_select)] | |
df_plot = df_plot[(pd.to_datetime(df_plot['game_date']).dt.date>=input.date_range_id()[0])& | |
(pd.to_datetime(df_plot['game_date']).dt.date<=input.date_range_id()[1])] | |
df_plot = df_plot[df_plot['batter_hand'].isin(split_dict_hand[input.split_id()])] | |
if input.count_id_balls()=='greater' and input.count_id_strikes()=='greater' and int(input.ball_id())==0 and int(input.strike_id())==0: | |
ball_title = '' | |
strike_title = '' | |
else: | |
if input.count_id_balls()=='exact': | |
df_plot = df_plot[df_plot['balls']==int(input.ball_id())] | |
ball_title = str(f'{(input.ball_id())} Ball Count; ') | |
elif input.count_id_balls()=='greater': | |
df_plot = df_plot[df_plot['balls']>=int(input.ball_id())] | |
ball_title = str(f'At Least {(input.ball_id())} Ball Count; ') | |
elif input.count_id_balls()=='lesser': | |
df_plot = df_plot[df_plot['balls']<=int(input.ball_id())] | |
ball_title = str(f'At Most {(input.ball_id())} Ball Count; ') | |
if input.count_id_strikes()=='exact': | |
df_plot = df_plot[df_plot['strikes']==int(input.strike_id())] | |
strike_title = str(f'{(input.strike_id())} Strike Count; ') | |
elif input.count_id_strikes()=='greater': | |
df_plot = df_plot[df_plot['strikes']>=int(input.strike_id())] | |
strike_title = str(f'At Least {(input.strike_id())} Strike Count; ') | |
elif input.count_id_strikes()=='lesser': | |
df_plot = df_plot[df_plot['strikes']<=int(input.strike_id())] | |
strike_title = str(f'At Most {(input.strike_id())} Strike Count; ') | |
if input.split_id() == 'all': | |
split_title = '' | |
elif input.split_id() == 'left': | |
split_title = 'vs. LHH' | |
elif input.split_id() == 'right': | |
split_title = 'vs. RHH' | |
if len(df_plot)<1: | |
fig, ax = plt.subplots(1, 1, figsize=(9, 9)) | |
ax.text(x=0.5,y=0.5,s='Please Select\nOther Parameters',fontsize=150,ha='center') | |
ax.grid('off') | |
return | |
df_plot['pitch_type_count'] = df_plot.groupby(['pitcher_id'])['pitch_type'].cumcount()+1 | |
df_plot['pitch_type_count_each'] = df_plot.groupby(['pitch_type'])['pitch_type'].cumcount()+1 | |
#df_plot = df_plot.merge(df_2024_update[['tj_stuff_plus','play_id']],left_on=['play_id'],right_on=['play_id'],how='left') | |
df_plot = df_plot.sort_values(by=['pitch_description']) | |
df_plot = df_plot.sort_values(by=['start_time']) | |
grouped_ivb = psf.group_ivb_update(df=df_plot,agg_list=['pitcher_id','pitcher_name','pitcher_hand','pitch_type','pitch_description']) | |
grouped_ivb_all = psf.group_ivb_update(df=df_plot,agg_list=['pitcher_id','pitcher_name','pitcher_hand']) | |
from matplotlib.gridspec import GridSpec | |
plt.rcParams['font.family'] = 'Calibri' | |
df_plot['prop'] = df_plot.groupby("pitch_type")["is_pitch"].transform("sum") | |
label_labels = df_plot.sort_values(by=['prop','pitch_type'],ascending=[False,True]).pitch_description.unique() | |
#plt.rcParams["figure.figsize"] = [10,10] | |
fig = plt.figure(figsize=(20, 20)) | |
plt.rcParams.update({'figure.autolayout': True}) | |
fig.set_facecolor('white') | |
sns.set_theme(style="whitegrid", palette=colour_palette) | |
print('this is the one plot') | |
# gs = GridSpec(7, 2, width_ratios=[1,1], height_ratios=[1.5,1,1,1,1,1,2.5]) | |
gs = GridSpec(5, 5, height_ratios=[150,75,225,325,50],width_ratios=[1,100,100,100,1]) | |
#### NO FG | |
####gs = GridSpec(5, 5, height_ratios=[225,0,225,325,50],width_ratios=[1,100,100,100,1]) | |
#gs = GridSpec(4, 1, width_ratios=[1], height_ratios=[1,0.75,7-len(label_labels)/4,1+len(label_labels)/4]) | |
gs.update(hspace=0.2, wspace=0.3) | |
# Add subplots to the grid | |
ax0 = fig.add_subplot(gs[0, :]) | |
ax1_table = fig.add_subplot(gs[1, :]) | |
ax2_left = fig.add_subplot(gs[2, 1]) | |
ax2_middle = fig.add_subplot(gs[2, 2]) | |
ax2_right = fig.add_subplot(gs[2, 3]) | |
ax3 = fig.add_subplot(gs[-2, :]) | |
#axfooter = fig.add_subplot(gs[-1, :]) | |
ax1_table.axis('off') | |
sns.set_theme(style="whitegrid", palette=colour_palette) | |
fig.set_facecolor('white') | |
font_properties = {'family': 'calibi', 'size': 12} | |
font_properties_titles = {'family': 'calibi', 'size': 20} | |
font_properties_axes = {'family': 'calibi', 'size': 16} | |
## FANGRAPHS TABLE ### | |
data_pull = psf.fangraphs_scrape(pitcher_id=pitcher_id_select, | |
split=input.split_id(), | |
start_date=input.date_range_id()[0], | |
end_date=input.date_range_id()[1]) | |
psf.fangraphs_table(data=data_pull, | |
stats=['IP','WHIP','ERA','FIP','TBF','K%','BB%','K-BB%'], | |
ax=ax1_table) | |
for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax2_left,ax2_middle,ax2_right],[1,2,3]): | |
if x == 'velocity_kde': | |
psf.velocity_kdes(df=df_plot,ax=y,gs=gs,gs_list=z,fig=fig) | |
if x == 'rolling_tj_stuff': | |
psf.tj_stuff_roling(df = df_plot,window = int(input.rolling_window()),ax=y) | |
if x == 'break_plot': | |
psf.break_plot(df=df_plot,ax=y) | |
if x == 'location_lhb': | |
psf.location_plot(df=df_plot,ax=y,hand='L') | |
if x == 'location_rhb': | |
psf.location_plot(df=df_plot,ax=y,hand='R') | |
pitches_list = df_plot['pitch_description'].unique() | |
colour_pitches = [pitch_colours[x] for x in pitches_list] | |
# handles, labels = ax2_right.get_legend_handles_labels() | |
# # Manually create handles and labels for each pitch-color pair | |
handles = [plt.scatter([], [], color=color, marker='o', s=100) for color in colour_pitches] | |
labels = pitches_list | |
### FANGRAPHS TABLE ### | |
psf.table_summary(df=df_plot.copy(), | |
pitcher_id=pitcher_id_select, | |
ax=ax3, | |
df_group=grouped_ivb.copy(), | |
df_group_all=grouped_ivb_all.copy(), | |
statcast_pitch_summary=statcast_pitch_summary.copy()) | |
# ############ FOOTER ################ | |
# #fig.text(x=0.5,y=0.05,s='Note: Colour Coding Compares to League Average By Pitch',ha='center',fontname='Calibri',fontsize=10) | |
# axfooter.text(x=0.05,y=1,s='By: Thomas Nestico\n @TJStats',fontname='Calibri',ha='left',fontsize=24,va='top') | |
# axfooter.text(x=1-0.05,y=1,s='Data: MLB, Fangraphs',ha='right',fontname='Calibri',fontsize=24,va='top') | |
# axfooter.text(x=0.5,y=0.8,s='Colour Coding Compares to League Average By Pitch\ntjStuff+ calculates the Expected Run Value (xRV) of a pitch regardless of type\ntjStuff+ is normally distributed, where 100 is the mean and Standard Deviation is 10', | |
# ha='center',va='center',fontname='Calibri',fontsize=16) | |
# axfooter.axis('off') | |
# #fig.tight_layout() | |
# Get value counts of the column and sort in descending order | |
sorted_value_counts = df_plot['pitch_description'].value_counts().sort_values(ascending=False) | |
# Get the list of items ordered from most to least frequent | |
items_in_order = sorted_value_counts.index.tolist() | |
# Create a dictionary to map names to colors | |
name_to_color = dict(zip(labels, handles)) | |
# Order the colors based on the correct order of names | |
ordered_colors = [name_to_color[name] for name in items_in_order] | |
ax3.legend(ordered_colors, items_in_order, bbox_to_anchor=(0.1, 0.81, 0.8, 0.2), ncol=5, | |
fancybox=True,loc='lower center',fontsize=20,framealpha=1.0, markerscale=2,prop={'family': 'calibi', 'size': 20}) | |
################## Title ########## | |
title_spot = f'{df_plot.pitcher_name.values[0]}' | |
ax0.text(x=0.5,y=1.0,s=title_spot,fontname='Calibri',ha='center',fontsize=56,va='top') | |
ax0.text(x=0.5,y=0.5,s='Season Pitching Summary',fontname='Calibri',ha='center',fontsize=40,va='top',fontstyle='italic') | |
#ax0.text(x=0.5,y=0.25,s=f'2024 Spring Training',fontname='Calibri',ha='center',fontsize=30,va='top') | |
# ax0.text(x=0.5,y=0.25,s=f'{season_fg} MLB Season',fontname='Calibri',ha='center',fontsize=30,va='top') | |
# ax0.axis('off') | |
ax0.text(x=0.5,y=0.25,s=f'{input.date_range_id()[0]} to {input.date_range_id()[1]}',fontname='Calibri',ha='center',fontsize=30,va='top',fontstyle='italic') | |
ax0.text(x=0.5,y=0.05,s=f'{ball_title}{strike_title}{split_title}',fontname='Calibri',ha='center',fontsize=20,va='top') | |
ax0.axis('off') | |
from matplotlib.offsetbox import (OffsetImage, AnnotationBbox) | |
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/{pitcher_id_select}/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.6) | |
ab = AnnotationBbox(imagebox, (0.125, 0.5), frameon = False) | |
ax0.add_artist(ab) | |
player_bio = requests.get(url=f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id_select}&hydrate=currentTeam").json() | |
ax0.text(x=0.5,y=0.7,s=f"{ player_bio['people'][0]['pitchHand']['code']}HP, Age: {player_bio['people'][0]['currentAge']}, {player_bio['people'][0]['height']}/{player_bio['people'][0]['weight']}",fontname='Calibri',ha='center',fontsize=24,va='top') | |
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) | |
# response = requests.get(url) | |
# im = Image.open(BytesIO(response.content)) | |
# im = plt.imread(team_logos[team_logos['id'] == player_bio['people'][0]['currentTeam']['parentOrgId']]['imageLink'].values[0]) | |
# ax = fig.add_axes([0,0,1,0.85], anchor='C', zorder=1) | |
imagebox = OffsetImage(im, zoom = 0.48) | |
ab = AnnotationBbox(imagebox, (0.875, 0.5), frameon = False) | |
ax0.add_artist(ab) | |
except IndexError: | |
print() | |
############ FOOTER ################ | |
#fig.text(x=0.5,y=0.05,s='Note: Colour Coding Compares to League Average By Pitch',ha='center',fontname='Calibri',fontsize=10) | |
axfooter = fig.add_subplot(gs[-1, :]) | |
axfooter.text(x=0.05,y=1,s='By: Thomas Nestico\n @TJStats',fontname='Calibri',ha='left',fontsize=24,va='top') | |
axfooter.text(x=1-0.05,y=1,s='Data: MLB, Fangraphs',ha='right',fontname='Calibri',fontsize=24,va='top') | |
axfooter.text(x=0.5,y=0.8,s='Colour Coding Compares to League Average By Pitch\ntjStuff+ calculates the Expected Run Value (xRV) of a pitch regardless of type\ntjStuff+ is normally distributed, where 100 is the mean and Standard Deviation is 10', | |
ha='center',va='center',fontname='Calibri',fontsize=16) | |
axfooter.axis('off') | |
#fig.tight_layout() | |
fig.subplots_adjust(left=0.03, right=0.97, top=0.97, bottom=0.03) | |
def plot_game(): | |
#fig, ax = plt.subplots(3, 2, figsize=(9, 9)) | |
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_id_select = int(input.player_id()) | |
df_plot = df_2024_update[(df_2024_update['pitcher_id']==pitcher_id_select)&(df_2024_update['game_id']==int(input.game_id()))] | |
df_plot = df_plot[df_plot['batter_hand'].isin(split_dict_hand[input.split_id()])] | |
if input.count_id_balls()=='greater' and input.count_id_strikes()=='greater' and int(input.ball_id())==0 and int(input.strike_id())==0: | |
ball_title = '' | |
strike_title = '' | |
else: | |
if input.count_id_balls()=='exact': | |
df_plot = df_plot[df_plot['balls']==int(input.ball_id())] | |
ball_title = str(f'{(input.ball_id())} Ball Count; ') | |
elif input.count_id_balls()=='greater': | |
df_plot = df_plot[df_plot['balls']>=int(input.ball_id())] | |
ball_title = str(f'At Least {(input.ball_id())} Ball Count; ') | |
elif input.count_id_balls()=='lesser': | |
df_plot = df_plot[df_plot['balls']<=int(input.ball_id())] | |
ball_title = str(f'At Most {(input.ball_id())} Ball Count; ') | |
if input.count_id_strikes()=='exact': | |
df_plot = df_plot[df_plot['strikes']==int(input.strike_id())] | |
strike_title = str(f'{(input.strike_id())} Strike Count; ') | |
elif input.count_id_strikes()=='greater': | |
df_plot = df_plot[df_plot['strikes']>=int(input.strike_id())] | |
strike_title = str(f'At Least {(input.strike_id())} Strike Count; ') | |
elif input.count_id_strikes()=='lesser': | |
df_plot = df_plot[df_plot['strikes']<=int(input.strike_id())] | |
strike_title = str(f'At Most {(input.strike_id())} Strike Count; ') | |
if input.split_id() == 'all': | |
split_title = '' | |
elif input.split_id() == 'left': | |
split_title = 'vs. LHH' | |
elif input.split_id() == 'right': | |
split_title = 'vs. RHH' | |
if len(df_plot)<1: | |
fig, ax = plt.subplots(1, 1, figsize=(9, 9)) | |
ax.text(x=0.5,y=0.5,s='Please Select\nOther Parameters',fontsize=150,ha='center') | |
ax.grid('off') | |
return | |
df_plot['pitch_type_count'] = df_plot.groupby(['pitcher_id'])['pitch_type'].cumcount()+1 | |
df_plot['pitch_type_count_each'] = df_plot.groupby(['pitch_type'])['pitch_type'].cumcount()+1 | |
#df_plot = df_plot.merge(df_2024_update[['tj_stuff_plus','play_id']],left_on=['play_id'],right_on=['play_id'],how='left') | |
df_plot = df_plot.sort_values(by=['pitch_description']) | |
df_plot = df_plot.sort_values(by=['start_time']) | |
# ax0.text(x=0.5,y=0.30,s=f'2024 Spring Training',fontname='Calibri',ha='center',fontsize=30,va='top') | |
df_plot['game_opp'] = df_plot['game_date'].astype(str) + ' vs ' + df_plot['batter_team'].astype(str) | |
#print(df_plot['game_opp']) | |
#date_dict = pd.concat([df_plot.drop_duplicates(subset=['pitcher_id','game_id','game_opp'])[['game_id','game_opp']]]).set_index('game_id').to_dict() | |
grouped_ivb = psf.group_ivb_update(df=df_plot,agg_list=['pitcher_id','pitcher_name','pitcher_hand','pitch_type','pitch_description']) | |
grouped_ivb_all = psf.group_ivb_update(df=df_plot,agg_list=['pitcher_id','pitcher_name','pitcher_hand']) | |
from matplotlib.gridspec import GridSpec | |
plt.rcParams['font.family'] = 'Calibri' | |
df_plot['prop'] = df_plot.groupby("pitch_type")["is_pitch"].transform("sum") | |
label_labels = df_plot.sort_values(by=['prop','pitch_type'],ascending=[False,True]).pitch_description.unique() | |
#plt.rcParams["figure.figsize"] = [10,10] | |
fig = plt.figure(figsize=(20, 20)) | |
plt.rcParams.update({'figure.autolayout': True}) | |
fig.set_facecolor('white') | |
sns.set_theme(style="whitegrid", palette=colour_palette) | |
print('this is the one plot') | |
# gs = GridSpec(7, 2, width_ratios=[1,1], height_ratios=[1.5,1,1,1,1,1,2.5]) | |
gs = GridSpec(5, 5, height_ratios=[150,75,225,325,50],width_ratios=[1,100,100,100,1]) | |
#### NO FG | |
####gs = GridSpec(5, 5, height_ratios=[225,0,225,325,50],width_ratios=[1,100,100,100,1]) | |
#gs = GridSpec(4, 1, width_ratios=[1], height_ratios=[1,0.75,7-len(label_labels)/4,1+len(label_labels)/4]) | |
gs.update(hspace=0.2, wspace=0.3) | |
# Add subplots to the grid | |
ax0 = fig.add_subplot(gs[0, :]) | |
ax1_table = fig.add_subplot(gs[1, :]) | |
ax2_left = fig.add_subplot(gs[2, 1]) | |
ax2_middle = fig.add_subplot(gs[2, 2]) | |
ax2_right = fig.add_subplot(gs[2, 3]) | |
ax3 = fig.add_subplot(gs[-2, :]) | |
# axfooter = fig.add_subplot(gs[-1, :]) | |
ax1_table.axis('off') | |
sns.set_theme(style="whitegrid", palette=colour_palette) | |
fig.set_facecolor('white') | |
font_properties = {'family': 'calibi', 'size': 12} | |
font_properties_titles = {'family': 'calibi', 'size': 20} | |
font_properties_axes = {'family': 'calibi', 'size': 16} | |
print(df_2024_update['game_date'].values[0]) | |
## FANGRAPHS TABLE ### | |
data_pull = psf.fangraphs_scrape(pitcher_id=pitcher_id_select, | |
split=input.split_id(), | |
start_date=df_plot['game_date'].values[0], | |
end_date=df_plot['game_date'].values[0]) | |
psf.fangraphs_table(data=data_pull, | |
stats=['IP','WHIP','ERA','FIP','TBF','K%','BB%','K-BB%'], | |
ax=ax1_table) | |
# psf.velocity_kdes(df=df_plot, | |
# ax=ax2_loc, | |
# gs=gs, | |
# fig=fig) | |
# # psf.tj_stuff_roling(df = df_plot, | |
# # window = 5, | |
# # ax=ax2_velo) | |
# psf.location_plot(df=df_plot,ax=ax2_velo,hand='L') | |
# psf.location_plot(df=df_plot,ax=ax2_loc,hand='R') | |
# # # ## Break Plot | |
# psf.break_plot(df=df_plot,ax=ax2) | |
for x,y,z in zip([input.plot_id_1(),input.plot_id_2(),input.plot_id_3()],[ax2_left,ax2_middle,ax2_right],[1,2,3]): | |
if x == 'velocity_kde': | |
psf.velocity_kdes(df=df_plot,ax=y,gs=gs,gs_list=z,fig=fig) | |
if x == 'rolling_tj_stuff': | |
psf.tj_stuff_roling(df = df_plot,window = int(input.rolling_window()),ax=y) | |
if x == 'break_plot': | |
psf.break_plot(df=df_plot,ax=y) | |
if x == 'location_lhb': | |
psf.location_plot(df=df_plot,ax=y,hand='L') | |
if x == 'location_rhb': | |
psf.location_plot(df=df_plot,ax=y,hand='R') | |
pitches_list = df_plot['pitch_description'].unique() | |
colour_pitches = [pitch_colours[x] for x in pitches_list] | |
# handles, labels = ax2_right.get_legend_handles_labels() | |
# # Manually create handles and labels for each pitch-color pair | |
handles = [plt.scatter([], [], color=color, marker='o', s=100) for color in colour_pitches] | |
labels = pitches_list | |
### FANGRAPHS TABLE ### | |
psf.table_summary(df=df_plot.copy(), | |
pitcher_id=pitcher_id_select, | |
ax=ax3, | |
df_group=grouped_ivb.copy(), | |
df_group_all=grouped_ivb_all.copy(), | |
statcast_pitch_summary=statcast_pitch_summary.copy()) | |
# Get value counts of the column and sort in descending order | |
sorted_value_counts = df_plot['pitch_description'].value_counts().sort_values(ascending=False) | |
# Get the list of items ordered from most to least frequent | |
items_in_order = sorted_value_counts.index.tolist() | |
# Create a dictionary to map names to colors | |
name_to_color = dict(zip(labels, handles)) | |
# Order the colors based on the correct order of names | |
ordered_colors = [name_to_color[name] for name in items_in_order] | |
ax3.legend(ordered_colors, items_in_order, bbox_to_anchor=(0.1, 0.81, 0.8, 0.2), ncol=5, | |
fancybox=True,loc='lower center',fontsize=20,framealpha=1.0, markerscale=2,prop={'family': 'calibi', 'size': 20}) | |
################## Title ########## | |
title_spot = f'{df_plot.pitcher_name.values[0]}' | |
ax0.text(x=0.5,y=1.0,s=title_spot,fontname='Calibri',ha='center',fontsize=56,va='top') | |
ax0.text(x=0.5,y=0.5,s='Game Pitching Summary',fontname='Calibri',ha='center',fontsize=40,va='top',fontstyle='italic') | |
#ax0.text(x=0.5,y=0.25,s=f'2024 Spring Training',fontname='Calibri',ha='center',fontsize=30,va='top') | |
#ax0.text(x=0.5,y=0.25,s=f'{season_fg} MLB Season',fontname='Calibri',ha='center',fontsize=30,va='top') | |
#ax0.text(x=0.5,y=0.25,s=f'2024 Spring Training',fontname='Calibri',ha='center',fontsize=30,va='top') | |
# ax0.text(x=0.5,y=0.25,s=f'{season_fg} MLB Season',fontname='Calibri',ha='center',fontsize=30,va='top') | |
ax0.text(x=0.5,y=0.25,s= df_plot['game_opp'].values[0],fontname='Calibri',ha='center',fontstyle='italic',fontsize=30,va='top') | |
ax0.text(x=0.5,y=0.05,s=f'{ball_title}{strike_title}{split_title}',fontname='Calibri',ha='center',fontsize=20,va='top') | |
ax0.axis('off') | |
from matplotlib.offsetbox import (OffsetImage, AnnotationBbox) | |
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/{pitcher_id_select}/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.6) | |
ab = AnnotationBbox(imagebox, (0.125, 0.5), frameon = False) | |
ax0.add_artist(ab) | |
player_bio = requests.get(url=f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id_select}&hydrate=currentTeam").json() | |
ax0.text(x=0.5,y=0.7,s=f"{ player_bio['people'][0]['pitchHand']['code']}HP, Age: {player_bio['people'][0]['currentAge']}, {player_bio['people'][0]['height']}/{player_bio['people'][0]['weight']}",fontname='Calibri',ha='center',fontsize=24,va='top') | |
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) | |
# response = requests.get(url) | |
# im = Image.open(BytesIO(response.content)) | |
# im = plt.imread(team_logos[team_logos['id'] == player_bio['people'][0]['currentTeam']['parentOrgId']]['imageLink'].values[0]) | |
# ax = fig.add_axes([0,0,1,0.85], anchor='C', zorder=1) | |
imagebox = OffsetImage(im, zoom = 0.48) | |
ab = AnnotationBbox(imagebox, (0.875, 0.5), frameon = False) | |
ax0.add_artist(ab) | |
except IndexError: | |
print() | |
############ FOOTER ################ | |
#fig.text(x=0.5,y=0.05,s='Note: Colour Coding Compares to League Average By Pitch',ha='center',fontname='Calibri',fontsize=10) | |
axfooter = fig.add_subplot(gs[-1, :]) | |
axfooter.text(x=0.05,y=1,s='By: Thomas Nestico\n @TJStats',fontname='Calibri',ha='left',fontsize=24,va='top') | |
axfooter.text(x=1-0.05,y=1,s='Data: MLB, Fangraphs',ha='right',fontname='Calibri',fontsize=24,va='top') | |
axfooter.text(x=0.5,y=0.8,s='Colour Coding Compares to League Average By Pitch\ntjStuff+ calculates the Expected Run Value (xRV) of a pitch regardless of type\ntjStuff+ is normally distributed, where 100 is the mean and Standard Deviation is 10', | |
ha='center',va='center',fontname='Calibri',fontsize=16) | |
axfooter.axis('off') | |
#fig.tight_layout() | |
fig.subplots_adjust(left=0.03, right=0.97, top=0.97, bottom=0.03) | |
app = App(app_ui, server) |