2024_mlb_pitch_heat_maps / pitch_summary_functions.py
nesticot's picture
Update pitch_summary_functions.py
ab9e3e9 verified
import pandas as pd
import numpy as np
import json
from matplotlib.ticker import FuncFormatter
from matplotlib.ticker import MaxNLocator
import math
from matplotlib.patches import Ellipse
import matplotlib.transforms as transforms
import matplotlib.colors
import matplotlib.colors as mcolors
import seaborn as sns
import matplotlib.pyplot as plt
import requests
font_properties = {'family': 'calibi', 'size': 12}
font_properties_titles = {'family': 'calibi', 'size': 20}
font_properties_axes = {'family': 'calibi', 'size': 16}
colour_palette = ['#FFB000','#648FFF','#785EF0',
'#DC267F','#FE6100','#3D1EB2','#894D80','#16AA02','#B5592B','#A3C1ED']
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()
cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFFFFF','#FFB000',])
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()
### DF UPDATE CODE ###
def df_update_code(df):
print('Starting')
#df = pd.read_csv('2024_spring_data.csv',index_col=[0])
print('Starting')
df['vy_f'] = -(df['vy0']**2 - (2 * df['ay'] * (df['y0'] - 17/12)))**0.5
df['t'] = (df['vy_f'] - df['vy0']) / df['ay']
df['vz_f'] = (df['vz0']) + (df['az'] * df['t'])
df['vaa'] = -np.arctan(df['vz_f'] / df['vy_f']) * (180 / np.pi)
#df['vy_f'] = -(df['vy0']**2 - (2 * df['ay'] * (df['y0'] - 17/12)))**0.5
#df['t'] = (df['vy_f'] - df['vy0']) / df['ay']
df['vx_f'] = (df['vx0']) + (df['ax'] * df['t'])
df['haa'] = -np.arctan(df['vx_f'] / df['vy_f']) * (180 / np.pi)
end_codes = ['strikeout', 'field_out', 'single', 'walk', 'hit_by_pitch',
'double', 'sac_fly', 'force_out', 'home_run',
'grounded_into_double_play', 'fielders_choice', 'field_error',
'triple', 'sac_bunt', 'double_play', 'intent_walk',
'fielders_choice_out', 'strikeout_double_play',
'sac_fly_double_play', 'catcher_interf', 'other_out']
df['pa'] = df.event_type.isin(end_codes)
#df['pa'] = 1
df['k'] = df.event_type.isin(list(filter(None, [x if 'strikeout' in x else '' for x in df.event_type.fillna('None').unique()])))
df['bb'] = df.event_type.isin(list(filter(None, [x if 'walk' in x else '' for x in df.event_type.fillna('None').unique()])))
df['k_minus_bb'] = df['k'].astype(np.float32)-df['bb'].astype(np.float32)
df = df.drop_duplicates(subset=['play_id'])
df = df.dropna(subset=['start_speed'])
swing_codes = ['Swinging Strike', 'In play, no out',
'Foul', 'In play, out(s)',
'In play, run(s)', 'Swinging Strike (Blocked)',
'Foul Bunt','Foul Tip', 'Missed Bunt','Foul Pitchout','Swinging Pitchout']
swings_in = ['Swinging Strike', 'In play, no out',
'Foul', 'In play, out(s)',
'In play, run(s)', 'Swinging Strike (Blocked)',
'Foul Bunt','Foul Tip', 'Missed Bunt','Foul Pitchout','Swinging Pitchout']
swing_strike_codes = ['Swinging Strike',
'Swinging Strike (Blocked)','Missed Bunt','Foul Tip','Swinging Pitchout']
contact_codes = ['In play, no out',
'Foul', 'In play, out(s)',
'In play, run(s)',
'Foul Bunt']
codes_in = ['In play, out(s)',
'Swinging Strike',
'Ball',
'Foul',
'In play, no out',
'Called Strike',
'Foul Tip',
'In play, run(s)',
'Hit By Pitch',
'Ball In Dirt',
'Pitchout',
'Swinging Strike (Blocked)',
'Foul Bunt',
'Missed Bunt',
'Foul Pitchout',
'Intent Ball',
'Swinging Pitchout']
df['in_zone'] = df['zone'] < 10
df = df.drop_duplicates(subset=['play_id'])
df_codes = df[df.play_description.isin(codes_in)].dropna(subset=['in_zone'])
df_codes['bip'] = ~df_codes.launch_speed.isna()
conditions = [
(df_codes['launch_speed'].isna()),
(df_codes['launch_speed']*1.5 - df_codes['launch_angle'] >= 117 ) & (df_codes['launch_speed'] + df_codes['launch_angle'] >= 124) & (df_codes['launch_speed'] > 98) & (df_codes['launch_angle'] >= 8) & (df_codes['launch_angle'] <= 50)
]
choices = [False,True]
df_codes['barrel'] = np.select(conditions, choices, default=np.nan)
conditions_ss = [
(df_codes['launch_angle'].isna()),
(df_codes['launch_angle'] >= 8 ) * (df_codes['launch_angle'] <= 32 )
]
choices_ss = [False,True]
df_codes['sweet_spot'] = np.select(conditions_ss, choices_ss, default=np.nan)
conditions_hh = [
(df_codes['launch_speed'].isna()),
(df_codes['launch_speed'] >= 94.5 )
]
choices_hh = [False,True]
df_codes['hard_hit'] = np.select(conditions_hh, choices_hh, default=np.nan)
conditions_tb = [
(df_codes['event_type']=='single'),
(df_codes['event_type']=='double'),
(df_codes['event_type']=='triple'),
(df_codes['event_type']=='home_run'),
]
choices_tb = [1,2,3,4]
df_codes['tb'] = np.select(conditions_tb, choices_tb, default=np.nan)
conditions_woba = [
(df_codes['event_type']=='walk'),
(df_codes['event_type']=='hit_by_pitch'),
(df_codes['event_type']=='single'),
(df_codes['event_type']=='double'),
(df_codes['event_type']=='triple'),
(df_codes['event_type']=='home_run'),
]
choices_woba = [0.705,
0.688,
0.897,
1.233,
1.612,
2.013]
df_codes['woba'] = np.select(conditions_woba, choices_woba, default=np.nan)
woba_codes = ['strikeout', 'field_out', 'single', 'walk', 'hit_by_pitch',
'double', 'sac_fly', 'force_out', 'home_run',
'grounded_into_double_play', 'fielders_choice', 'field_error',
'triple', 'sac_bunt', 'double_play',
'fielders_choice_out', 'strikeout_double_play',
'sac_fly_double_play', 'other_out']
conditions_woba_code = [
(df_codes['event_type'].isin(woba_codes))
]
choices_woba_code = [1]
df_codes['woba_codes'] = np.select(conditions_woba_code, choices_woba_code, default=np.nan)
#df_codes['barrel'] = (df_codes.launch_speed >= 98) & (df_codes.launch_angle >= (26 - (-98 + df_codes.launch_speed))) & (df_codes.launch_angle <= 30 + (-98 + df_codes.launch_speed)) & (df_codes.launch_angle >= 8) & (df_codes.launch_angle <= 50)
#df_codes['barrel'] = (df_codes.launch_speed >= 98) & (df_codes.launch_angle >= (26 - (-98 + df_codes.launch_speed))) & (df_codes.launch_angle <= 30 + (-98 + df_codes.launch_speed)) & (df_codes.launch_angle >= 8) & (df_codes.launch_angle <= 50)
df_codes['pitches'] = 1
df_codes['whiffs'] = [1 if ((x == 'S')|(x == 'W')|(x =='T')) else 0 for x in df_codes.play_code]
df_codes['csw'] = [1 if ((x == 'S')|(x == 'W')|(x =='T')|(x == 'C')) else 0 for x in df_codes.play_code]
df_codes['swings'] = [1 if x in swings_in else 0 for x in df_codes.play_description]
df_codes['out_zone'] = df_codes.in_zone == False
df_codes['zone_swing'] = (df_codes.in_zone == True)&(df_codes.swings == 1)
df_codes['zone_contact'] = (df_codes.in_zone == True)&(df_codes.swings == 1)&(df_codes.whiffs == 0)
df_codes['ozone_swing'] = (df_codes.in_zone==False)&(df_codes.swings == 1)
df_codes['ozone_contact'] = (df_codes.in_zone==False)&(df_codes.swings == 1)&(df_codes.whiffs == 0)
return df_codes
### GET COLOURS##
def get_color(value,normalize,cmap_sum):
color = cmap_sum(normalize(value))
return mcolors.to_hex(color)
### PERCENTILE ###
def percentile(n):
def percentile_(x):
return x.quantile(n)
percentile_.__name__ = 'percentile_{:02.0f}'.format(n*100)
return percentile_
### TJ STUFF+ DF CLEAN ###
def df_clean(df):
df_copy = df.copy()
df_copy = df_copy[(df_copy['spin_rate']>0)&(df_copy['extension']>0)]
df_copy.loc[df_copy['pitcher_hand'] == 'L','hb'] *= -1
df_copy.loc[df_copy['pitcher_hand'] == 'L','x0'] *= -1
df_copy.loc[df_copy['pitcher_hand'] == 'L','spin_direction'] = 360 - df_copy.loc[df_copy['pitcher_hand'] == 'L','spin_direction']
df_copy['pitch_l'] = [1 if x == 'L' else 0 for x in df_copy['pitcher_hand']]
df_copy['bat_l'] = [1 if x == 'L' else 0 for x in df_copy['batter_hand']]
#df_copy = df_copy[~df_copy.pitch_type.isin(["EP", "PO", "KN", "CS", "SC", "FA"])].reset_index(drop=True)
#df_copy = df_copy[~df_copy.pitch_type.isin(["EP", "PO", "CS", "SC", "FA"])].reset_index(drop=True)
df_copy['pitch_type'] = df_copy['pitch_type'].replace({'FT':'SI',
'FA':'FF',
'SV':'SL',
'FO':'FS'})
df_copy_fb_sum = df_copy[df_copy.pitch_type.isin(["FF", "FC", "SI"])].groupby(['pitcher_id']).agg(
fb_velo = ('start_speed','mean'),
fb_max_ivb = ('ivb',percentile(0.9)),
fb_max_x = ('hb',percentile(0.9)),
fb_min_x = ('hb',percentile(0.1)),
fb_max_velo = ('start_speed',percentile(0.9)),
fb_axis = ('spin_direction','mean'),
)
df_copy = df_copy.merge(df_copy_fb_sum,left_on='pitcher_id',right_index=True,how='left')
df_copy['fb_velo_diff'] = df_copy['start_speed']- df_copy['fb_velo']
df_copy['fb_max_ivb_diff'] = df_copy['ivb']- df_copy['fb_max_ivb']
df_copy['fb_max_hb_diff'] = -abs(df_copy['hb']- df_copy['fb_max_x'])
df_copy['fb_min_hb_diff'] = df_copy['hb']- df_copy['fb_min_x']
df_copy['fb_max_velo_diff'] = df_copy['start_speed']- df_copy['fb_max_velo']
df_copy['fb_axis_diff'] = df_copy['spin_direction']- df_copy['fb_axis']
# df_copy.loc[df_copy.pitch_type.isin(["FF", "FC", "SI"]),'fb_velo_diff'] = 0
# df_copy.loc[df_copy.pitch_type.isin(["FF", "FC", "SI"]),'fb_max_ivb_diff'] = 0
# df_copy.loc[df_copy.pitch_type.isin(["FF", "FC", "SI"]),'fb_max_hb_diff'] = 0
# df_copy.loc[df_copy.pitch_type.isin(["FF", "FC", "SI"]),'fb_min_hb_diff'] = 0
# df_copy.loc[df_copy.pitch_type.isin(["FF", "FC", "SI"]),'fb_max_velo_diff'] = 0
# df_copy.loc[df_copy.pitch_type.isin(["FF", "FC", "SI"]),'fb_axis_diff'] = 0
df_copy['max_speed'] = df_copy.groupby(['pitcher_id'])['start_speed'].transform('max')
df_copy['max_speed_diff'] = df_copy['start_speed'] - df_copy['max_speed']
df_copy['max_ivb'] = df_copy.groupby(['pitcher_id'])['ivb'].transform('max')
df_copy['max_ivb_diff'] = df_copy['ivb'] - df_copy['max_ivb']
df_copy['vy_f'] = -(df_copy['vy0']**2 - (2 * df_copy['ay'] * (df_copy['y0'] - 17/12)))**0.5
df_copy['t'] = (df_copy['vy_f'] - df_copy['vy0']) / df_copy['ay']
df_copy['vz_f'] = (df_copy['vz0']) + (df_copy['az'] * df_copy['t'])
df_copy['vaa'] = -np.arctan(df_copy['vz_f'] / df_copy['vy_f']) * (180 / np.pi)
#df_copy['vy_f'] = -(df_copy['vy0']**2 - (2 * df_copy['ay'] * (df_copy['y0'] - 17/12)))**0.5
#df_copy['t'] = (df_copy['vy_f'] - df_copy['vy0']) / df_copy['ay']
df_copy['vx_f'] = (df_copy['vx0']) + (df_copy['ax'] * df_copy['t'])
df_copy['haa'] = -np.arctan(df_copy['vx_f'] / df_copy['vy_f']) * (180 / np.pi)
# df_copy['x_diff'] = df_copy['x0'] - df_copy['px']
# df_copy['z_diff'] = df_copy['z0'] - df_copy['pz']
# df_copy['vaa'] = np.arctan(df_copy['z_diff'] / df_copy['release_pos_y']) * 360 / np.pi
# df_copy['haa'] = np.arctan(-df_copy['x_diff'] / df_copy['release_pos_y']) * 360 / np.pi
df_copy = df_copy.dropna(subset=['pitch_type'])#.fillna(0)
return df_copy
### PITCH COLOURS ###
pitch_colours = {
'Four-Seam Fastball':'#FF007D',#BC136F
'Fastball':'#FF007D',
'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',
}
### PITCH ELLIPSE ###
def confidence_ellipse(x, y, ax, n_std=3.0, facecolor='none', **kwargs):
"""
Create a plot of the covariance confidence ellipse of *x* and *y*.
Parameters
----------
x, y : array-like, shape (n, )
Input data.
ax : matplotlib.axes.Axes
The axes object to draw the ellipse into.
n_std : float
The number of standard deviations to determine the ellipse's radiuses.
**kwargs
Forwarded to `~matplotlib.patches.Ellipse`
Returns
-------
matplotlib.patches.Ellipse
"""
if x.size != y.size:
raise ValueError("x and y must be the same size")
try:
cov = np.cov(x, y)
pearson = cov[0, 1]/np.sqrt(cov[0, 0] * cov[1, 1])
# Using a special case to obtain the eigenvalues of this
# two-dimensional dataset.
ell_radius_x = np.sqrt(1 + pearson)
ell_radius_y = np.sqrt(1 - pearson)
ellipse = Ellipse((0, 0), width=ell_radius_x * 2, height=ell_radius_y * 2,
facecolor=facecolor,linewidth=2,linestyle='--', **kwargs)
# Calculating the standard deviation of x from
# the squareroot of the variance and multiplying
# with the given number of standard deviations.
scale_x = np.sqrt(cov[0, 0]) * n_std
mean_x = np.mean(x)
# calculating the standard deviation of y ...
scale_y = np.sqrt(cov[1, 1]) * n_std
mean_y = np.mean(y)
transf = transforms.Affine2D() \
.rotate_deg(45) \
.scale(scale_x, scale_y) \
.translate(mean_x, mean_y)
ellipse.set_transform(transf + ax.transData)
except ValueError:
return
return ax.add_patch(ellipse)
# DEFINE STRIKE ZONE
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]
})
### STRIKE ZONE ###
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,)
# ax.plot([-0.2833333, -0.2833333], [1.6, 3.5], color='black', linestyle='dashed',alpha=alpha_spot,zorder=3)
# ax.plot([0.2833333, 0.2833333], [1.6, 3.5], color='black', linestyle='dashed',alpha=alpha_spot,zorder=3)
# ax.plot([-0.85, 0.85], [2.2, 2.2], color='black', linestyle='dashed',alpha=alpha_spot,zorder=3)
# ax.plot([-0.85, 0.85], [2.9, 2.9], color='black', linestyle='dashed',alpha=alpha_spot,zorder=3)
if catcher_p:
# Add dashed line
# Add home plate
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)
### FANGRAPHS STATS DICT ###
fangraphs_stats_dict = {'IP':{'table_header':'$\\bf{IP}$','format':'.1f',} ,
'TBF':{'table_header':'$\\bf{PA}$','format':'.0f',} ,
'AVG':{'table_header':'$\\bf{AVG}$','format':'.3f',} ,
'K/9':{'table_header':'$\\bf{K\/9}$','format':'.2f',} ,
'BB/9':{'table_header':'$\\bf{BB\/9}$','format':'.2f',} ,
'K/BB':{'table_header':'$\\bf{K\/BB}$','format':'.2f',} ,
'HR/9':{'table_header':'$\\bf{HR\/9}$','format':'.2f',} ,
'K%':{'table_header':'$\\bf{K\%}$','format':'.1%',} ,
'BB%':{'table_header':'$\\bf{BB\%}$','format':'.1%',} ,
'K-BB%':{'table_header':'$\\bf{K-BB\%}$','format':'.1%',} ,
'WHIP':{'table_header':'$\\bf{WHIP}$','format':'.2f',} ,
'BABIP':{'table_header':'$\\bf{BABIP}$','format':'.3f',} ,
'LOB%':{'table_header':'$\\bf{LOB\%}$','format':'.1%',} ,
'xFIP':{'table_header':'$\\bf{xFIP}$','format':'.2f',} ,
'FIP':{'table_header':'$\\bf{FIP}$','format':'.2f',} ,
'H':{'table_header':'$\\bf{H}$','format':'.0f',} ,
'2B':{'table_header':'$\\bf{2B}$','format':'.0f',} ,
'3B':{'table_header':'$\\bf{3B}$','format':'.0f',} ,
'R':{'table_header':'$\\bf{R}$','format':'.0f',} ,
'ER':{'table_header':'$\\bf{ER}$','format':'.0f',} ,
'HR':{'table_header':'$\\bf{HR}$','format':'.0f',} ,
'BB':{'table_header':'$\\bf{BB}$','format':'.0f',} ,
'IBB':{'table_header':'$\\bf{IBB}$','format':'.0f',} ,
'HBP':{'table_header':'$\\bf{HBP}$','format':'.0f',} ,
'SO':{'table_header':'$\\bf{SO}$','format':'.0f',} ,
'OBP':{'table_header':'$\\bf{OBP}$','format':'.0f',} ,
'SLG':{'table_header':'$\\bf{SLG}$','format':'.0f',} ,
'ERA':{'table_header':'$\\bf{ERA}$','format':'.2f',} ,
'wOBA':{'table_header':'$\\bf{wOBA}$','format':'.3f',} ,
'G':{'table_header':'$\\bf{G}$','format':'.0f',} }
## Fangraphs Table
### FANGRAPHS SPLITS SCRAPE ###
split_dict = {'all':[],
'left':['5'],
'right':['6']
}
def fangraphs_scrape(pitcher_id=808967,
split='all',
start_date='2024-03-20',
end_date='2024-09-29'):
url = "https://www.fangraphs.com/api/leaders/splits/splits-leaders"
payload = {
"strPlayerId": str(mlb_fg_dicts[pitcher_id]),
"strSplitArr": split_dict[split],
"strGroup": "season",
"strPosition": "P",
"strType": "2",
"strStartDate": str(pd.to_datetime(start_date).strftime('%Y-%m-%d')),
"strEndDate": str(pd.to_datetime(end_date).strftime('%Y-%m-%d')),
"strSplitTeams": False,
"dctFilters": [],
"strStatType": "player",
"strAutoPt": False,
"arrPlayerId": [],
"strSplitArrPitch": [],
"arrWxTemperature": None,
"arrWxPressure": None,
"arrWxAirDensity": None,
"arrWxElevation": None,
"arrWxWindSpeed": None
}
json_payload = json.dumps(payload)
headers = {'Content-Type': 'application/json'}
response = requests.post(url, data=json_payload, headers=headers)
data_pull = response.json()['data'][0]
payload_advanced = {
"strPlayerId": str(mlb_fg_dicts[pitcher_id]),
"strSplitArr": split_dict[split],
"strGroup": "season",
"strPosition": "P",
"strType": "1",
"strStartDate": str(pd.to_datetime(start_date).strftime('%Y-%m-%d')),
"strEndDate": str(pd.to_datetime(end_date).strftime('%Y-%m-%d')),
"strSplitTeams": False,
"dctFilters": [],
"strStatType": "player",
"strAutoPt": False,
"arrPlayerId": [],
"strSplitArrPitch": [],
"arrWxTemperature": None,
"arrWxPressure": None,
"arrWxAirDensity": None,
"arrWxElevation": None,
"arrWxWindSpeed": None
}
json_payload_advanced = json.dumps(payload_advanced)
headers = {'Content-Type': 'application/json'}
response_advanced = requests.post(url, data=json_payload_advanced, headers=headers)
data_pull_advanced = response_advanced.json()['data'][0]
data_pull.update(data_pull_advanced)
return data_pull
### FANGRAPHS TABLE PLOT ###
def fangraphs_table(data,
stats,
ax):
fg_values = [data[x] if x in data else '---' for x in stats]
df_fg = pd.DataFrame(data=dict(zip(stats,fg_values)),index=[0])
df_fg.loc[0] = [format(df_fg[x][0],fangraphs_stats_dict[x]['format']) if df_fg[x][0] != '---' else '---' for x in df_fg]
table_fg = ax.table(cellText=df_fg.values, colLabels=df_fg.columns, cellLoc='center',
bbox=[0.04, 0.2, 0.92, 0.8])
min_font_size = 20
table_fg.set_fontsize(min_font_size)
new_column_names = [fangraphs_stats_dict[x]['table_header'] if x in data else '---' for x in stats]
# #new_column_names = ['Pitch Name', 'Pitch%', 'Velocity', 'Spin Rate','Exit Velocity', 'Whiff%', 'CSW%']
for i, col_name in enumerate(new_column_names):
table_fg.get_celld()[(0, i)].get_text().set_text(col_name)
ax.axis('off')
return table_fg
### VELOCITY KDES ###
def velocity_kdes(df,
ax,
gs,
gs_list,
fig):
sorted_value_counts = df['pitch_type'].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 the inner subplot inside the outer subplot
import matplotlib.gridspec as gridspec
ax.axis ('off')
#ax.set_ylabel('Pitch Velocity Distribution', fontdict=font_properties_axes)
ax.set_title('Pitch Velocity Distribution', fontdict={'family': 'calibi', 'size': 20})
inner_grid_1 = gridspec.GridSpecFromSubplotSpec(len(items_in_order),1, subplot_spec=gs[2,gs_list])
ax_top = []
for inner in inner_grid_1:
ax_top.append(fig.add_subplot(inner))
ax_number = 0
for i in items_in_order[0:]:
if np.unique(df[df['pitch_type']==i]['start_speed']).size == 1: # Check if all values are the same
print('just')
ax_top[ax_number].plot([np.unique(df[df['pitch_type']==i]['start_speed']),np.unique(df[df['pitch_type']==i]['start_speed'])],[0,1], linewidth=4,
color=pitch_colours[df[df['pitch_type']==i]['pitch_description'].values[0]],zorder=20)
# ax_top[ax_number].plot(np.unique(df_melt[df_melt['Player']==i]['value']), [0.5]*len(np.unique(df_melt[df_melt['Player']==i]['value'])), linewidth=4)
else:
sns.kdeplot(df[df['pitch_type']==i]['start_speed'],ax=ax_top[ax_number],fill=True,
clip=(df[df['pitch_type']==i]['start_speed'].min(),df[df['pitch_type']==i]['start_speed'].max()),
color=pitch_colours[df[df['pitch_type']==i]['pitch_description'].values[0]])
ax_top[ax_number].set_xlim(math.floor(df['start_speed'].min()/5)*5,math.ceil(df['start_speed'].max()/5)*5)
ax_top[ax_number].set_xlabel('')
ax_top[ax_number].set_ylabel('')
if ax_number < len(items_in_order)-1:
ax_top[ax_number].spines['top'].set_visible(False)
ax_top[ax_number].spines['right'].set_visible(False)
ax_top[ax_number].spines['left'].set_visible(False)
ax_top[ax_number].tick_params(axis='x', colors='none')
ax_top[ax_number].set_xticks(range(math.floor(df['start_speed'].min()/5)*5,math.ceil(df['start_speed'].max()/5)*5,5))
ax_top[ax_number].set_yticks([])
ax_top[ax_number].grid(axis='x', linestyle='--')
ax_top[ax_number].text(-0.01, 0.5, i, transform=ax_top[ax_number].transAxes,
fontsize=14, va='center', ha='right')
ax_number = ax_number + 1
ax_top[-1].spines['top'].set_visible(False)
ax_top[-1].spines['right'].set_visible(False)
ax_top[-1].spines['left'].set_visible(False)
ax_top[-1].set_xticks(list(range(math.floor(df['start_speed'].min()/5)*5,math.ceil(df['start_speed'].max()/5)*5,5)))
ax_top[-1].set_xlabel('Velocity (mph)')
### TJ STUFF+ ROLLING ###
def tj_stuff_roling(df,
window,
ax):
## Velocity Plot
sorted_value_counts = df['pitch_type'].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()
for i in items_in_order:
if max(df[df['pitch_type']==i]['pitch_type_count_each']) >= window:
sns.lineplot(x=range(1,max(df[df['pitch_type']==i]['pitch_type_count_each'])+1),
y=df[df['pitch_type']==i]['tj_stuff_plus'].rolling(window).sum()/window,
color=pitch_colours[df[df['pitch_type']==i]['pitch_description'].values[0]],
ax=ax,linewidth=3)
# Adjust x-axis limits to start from 1
ax.set_xlim(window,max(df['pitch_type_count_each']))
ax.set_ylim(70,130)
#ax.get_legend().remove()
ax.set_xlabel('Pitches', fontdict=font_properties_axes)
ax.set_ylabel('tjStuff+', fontdict=font_properties_axes)
ax.set_title(f"{window} Pitch Rolling tjStuff+",fontdict=font_properties_titles)
# ax.axis('square')
# ax.set_xlim(left=1)
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
### BREAK PLOT ###
def break_plot(df,
ax):
label_labels = df.sort_values(by=['prop','pitch_type'],ascending=[False,True]).pitch_description.unique()
j = 0
for label in label_labels:
subset = df[df['pitch_description'] == label]
print(label)
if len(subset) > 4:
if df['pitcher_hand'].values[0] == 'R':
subset['hb'] = subset['hb']*1
if df['pitcher_hand'].values[0] == 'L':
subset['hb'] = subset['hb']*1
subset['ivb'] = subset['ivb']*1
try:
confidence_ellipse(subset['hb'], subset['ivb'], ax=ax,edgecolor = pitch_colours[label],n_std=2,facecolor= pitch_colours[label],alpha=0.2)
except ValueError:
return
j=j+1
else:
j=j+1
if df['pitcher_hand'].values[0] == 'R':
sns.scatterplot(ax=ax,x=df.hb*1,y=df.ivb*1,hue=df.pitch_description,palette=pitch_colours,ec='black',alpha=1,zorder=2)
if df['pitcher_hand'].values[0] == 'L':
sns.scatterplot(ax=ax,x=df.hb*1,y=df.ivb*1,hue=df.pitch_description,palette=pitch_colours,ec='black',alpha=1,zorder=2)
ax.set_xlim((-25,25))
ax.set_ylim((-25,25))
ax.hlines(y=0,xmin=-50,xmax=50,color=colour_palette[8],alpha=0.5,linestyles='--',zorder=1)
ax.vlines(x=0,ymin=-50,ymax=50,color=colour_palette[8],alpha=0.5,linestyles='--',zorder=1)
ax.set_xlabel('Horizontal Break (in)', fontdict=font_properties_axes)
ax.set_ylabel('Induced Vertical Break (in)', fontdict=font_properties_axes)
ax.set_title("Pitch Breaks",fontdict=font_properties_titles)
ax.get_legend().remove()
# ax1.set_xticklabels(ax1.get_xticks(), fontdict=font_properties)
ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties)
# ax1.set_yticklabels(ax1.get_yticks(), fontdict=font_properties)
ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties)
#ax1.set_aspect('equal', adjustable='box')
if df['pitcher_hand'].values[0] == 'R':
ax.text(-24.5,-24.5,s='← Glove Side',fontstyle='italic',ha='left',va='bottom',
bbox=dict(facecolor='white', edgecolor='black'),fontsize=12,zorder=3)
ax.text(24.5,-24.5,s='Arm Side →',fontstyle='italic',ha='right',va='bottom',
bbox=dict(facecolor='white', edgecolor='black'),fontsize=12,zorder=3)
#ax.invert_xaxis()
if df['pitcher_hand'].values[0] == 'L':
ax.invert_xaxis()
ax.text(24.5,-24.5,s='← Arm Side',fontstyle='italic',ha='left',va='bottom',
bbox=dict(facecolor='white', edgecolor='black'),fontsize=12,zorder=3)
ax.text(-24.5,-24.5,s='Glove Side →',fontstyle='italic',ha='right',va='bottom',
bbox=dict(facecolor='white', edgecolor='black'),fontsize=12,zorder=3)
ax.set_aspect('equal', adjustable='box')
#ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x)))
### TABLE SUMMARY ###
def table_summary(df,
pitcher_id,
ax,
df_group,
df_group_all,
statcast_pitch_summary):
cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFFFFF','#FFB000',])
ax.axis('off')
df_group['spin_direction_adj'] = [(x + 180) for x in df_group['spin_direction']]
#(((df_group.groupby('pitch_description').mean()[['spin_direction_adj']] %360 % 30 / 30 /100 *60).round(2) *10).round(0)//1.5/4 )
clock_time = ((df_group.groupby('pitch_description').mean()['spin_direction_adj']) %360 // 30 )+ (((df_group.groupby('pitch_description').mean()['spin_direction_adj'] %360 % 30 / 30 /100 *60).round(2) *10).round(0)//1.5/4 )
# print('Clocks')
# print(clock_time)
clock_time = (clock_time.astype(int) + clock_time%1*60/100).round(2).astype(str).str.replace('.',':').str.replace(':0',':00').str.replace(':3',':30').str.replace('0:','12:').str.replace('112:','10:').to_frame()
df_group = df_group.merge(right=clock_time,left_on='pitch_description',right_index=True,suffixes=['','_clock'])
plot_table = df_group[df_group['pitcher_id']==pitcher_id].sort_values(
by=['pitches'],ascending=False)[['pitch_description','pitches','start_speed','ivb',
'hb', 'spin_rate','vaa', 'haa', 'vertical_release','horizontal_release',
'extension','tj_stuff_plus','spin_direction_adj_clock','zone_percent','chase_percent','whiff_rate','xwobacon']]
# if df['pitcher_hand'].values[0] == 'L':
# plot_table['hb'] = plot_table['hb']*-1
#if df['pitcher_hand'].values[0] == 'R':
plot_table['horizontal_release'] = plot_table['horizontal_release']*-1
plot_table['pitch_percent'] = plot_table['pitches'] / plot_table['pitches'].sum()
plot_table = plot_table[['pitch_description','pitches','pitch_percent','start_speed','ivb',
'hb', 'spin_rate','vaa', 'haa', 'vertical_release','horizontal_release',
'extension','spin_direction_adj_clock','tj_stuff_plus','zone_percent','chase_percent','whiff_rate','xwobacon']]
plot_table_all = pd.DataFrame(data={'pitch_description': 'All',
'pitches': plot_table['pitches'].sum(),
'pitch_percent': 1.0,
'start_speed': '—',
'ivb': '—',
'hb': '—',
'spin_rate': '—',
'vaa': '—',
'haa': '—',
'vertical_release': '—',
'horizontal_release': '—',
'extension': df['extension'].mean(),
'spin_direction_adj_clock': '—',
'tj_stuff_plus': df[df['pitcher_id']==pitcher_id]['tj_stuff_plus'].mean(),
'zone_percent': df_group_all[df_group_all['pitcher_id']==pitcher_id]['zone_percent'].values[0],
'chase_percent': df_group_all[df_group_all['pitcher_id']==pitcher_id]['chase_percent'].values[0],
'whiff_rate': df_group_all[df_group_all['pitcher_id']==pitcher_id]['whiff_rate'].values[0],
'xwobacon': df_group_all[df_group_all['pitcher_id']==pitcher_id]['xwobacon'].values[0],
},index=[0]
)
print('LOOK HERE')
print(plot_table)
plot_table = pd.concat([plot_table,plot_table_all]).fillna('—')
plt.rcParams['font.family'] = 'Calibri'
table = ax.table(cellText=plot_table.values, colLabels=plot_table.columns, cellLoc='center',
colWidths=[2.3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1], bbox=[0.04, 0, 0.92, 0.8])
min_font_size = 14
# Set table properties
table.auto_set_font_size(False)
#table.set_fontsize(min(min_font_size,max(min_font_size/((len(label_labels)/4)),10)))
table.set_fontsize(min_font_size)
table.scale(1, 0.5)
min_font_size = 18
# Set font size for values
# Adjust the font size as needed
for i in range(len(plot_table)+1):
for j in range(len(plot_table.columns)):
if i > 0: # Skip the header row
cell = table.get_celld()[i, j]
cell.set_fontsize(min_font_size)
for i in range(len(plot_table)):
if table.get_celld()[(i+1, 0)].get_text().get_text() != 'All':
table.get_celld()[(i+1, 0)].set_facecolor(pitch_colours[table.get_celld()[(i+1, 0)].get_text().get_text()]) # Header cell color
if table.get_celld()[(i+1, 0)].get_text().get_text() in ['Split-Finger','Slider','Changeup']:
table.get_celld()[(i+1, 0)].set_text_props(color='#000000',fontweight='bold')
else:
table.get_celld()[(i+1, 0)].set_text_props(color='#ffffff',fontweight='bold')
if table.get_celld()[(i+1, 0)].get_text().get_text() == 'Four-Seam Fastball':
table.get_celld()[(i+1, 0)].get_text().set_text('4-Seam')
print('LOOK HERE 3')
print(statcast_pitch_summary)
select_df = statcast_pitch_summary[statcast_pitch_summary['pitch_description'] == plot_table['pitch_description'].values[i]]
normalize = mcolors.Normalize(vmin=select_df['start_speed'].mean()-select_df.pitch_velocity_std.mean(),
vmax=select_df['start_speed'].mean()+select_df.pitch_velocity_std.mean()) # Define the range of values
if table.get_celld()[(i+1, 3)].get_text().get_text() != '—':
table.get_celld()[(i+1, 3)].set_facecolor(get_color(float(table.get_celld()[(i+1, 3)].get_text().get_text()),normalize,cmap_sum)) # Header cell color
cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFFFFF','#FFB000',])
normalize = mcolors.Normalize(vmin=select_df['extension'].mean()*0.9, vmax=select_df['extension'].mean()*1.1)
if table.get_celld()[(i+1,11)].get_text().get_text() != '—':
table.get_celld()[(i+1,11)].set_facecolor(get_color(float(table.get_celld()[(i+1, 11)].get_text().get_text()),normalize,cmap_sum)) # Header cell color
cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFFFFF','#FFB000',])
normalize = mcolors.Normalize(vmin=80, vmax=120)
print(normalize)
if table.get_celld()[(i+1,13)].get_text().get_text() != '—':
table.get_celld()[(i+1,13)].set_facecolor(get_color(float(table.get_celld()[(i+1, 13)].get_text().get_text()),normalize,cmap_sum)) # Header cell color
cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFFFFF','#FFB000',])
normalize = mcolors.Normalize(vmin=select_df['zone_percent'].mean()*0.7, vmax=select_df['zone_percent'].mean()*1.3)
if table.get_celld()[(i+1,14)].get_text().get_text() != '—':
table.get_celld()[(i+1,14)].set_facecolor(get_color(float(table.get_celld()[(i+1, 14)].get_text().get_text().strip('%')),normalize,cmap_sum)) # Header cell color
cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFFFFF','#FFB000',])
normalize = mcolors.Normalize(vmin=select_df['chase_percent'].mean()*0.7, vmax=select_df['chase_percent'].mean()*1.3)
if table.get_celld()[(i+1,15)].get_text().get_text() != '—':
table.get_celld()[(i+1,15)].set_facecolor(get_color(float(table.get_celld()[(i+1, 15)].get_text().get_text().strip('%')),normalize,cmap_sum)) # Header cell color
cmap_sum = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#648FFF','#FFFFFF','#FFB000',])
normalize = mcolors.Normalize(vmin=select_df['whiff_rate'].mean()*0.7, vmax=select_df['whiff_rate'].mean()*1.3)
if table.get_celld()[(i+1,16)].get_text().get_text() != '—':
table.get_celld()[(i+1,16)].set_facecolor(get_color(float(table.get_celld()[(i+1, 16)].get_text().get_text().strip('%')),normalize,cmap_sum)) # Header cell color
print("LOOK HERE")
print(select_df)
cmap_sum_r = matplotlib.colors.LinearSegmentedColormap.from_list("", ['#FFB000','#FFFFFF','#648FFF',])
normalize = mcolors.Normalize(vmin=select_df['xwobacon'].mean()*0.7, vmax=select_df['xwobacon'].mean()*1.3)
if table.get_celld()[(i+1,17)].get_text().get_text() != '—':
table.get_celld()[(i+1,17)].set_facecolor(get_color(float(table.get_celld()[(i+1, 17)].get_text().get_text().strip('%')),normalize,cmap_sum_r)) # Header cell color
table.get_celld()[(len(plot_table), 0)].set_text_props(color='#000000',fontweight='bold')
new_column_names = ['$\\bf{Pitch\ Name}$',
'$\\bf{Count}$',
'$\\bf{Pitch\%}$',
'$\\bf{Velocity}$',
'$\\bf{iVB}$',
'$\\bf{HB}$',
'$\\bf{Spin}$',
'$\\bf{VAA}$',
'$\\bf{HAA}$',
'$\\bf{vRel}$',
'$\\bf{hRel}$',
'$\\bf{Ext.}$',
'$\\bf{Axis}$',
'$\\bf{tjStuff+}$',
'$\\bf{Zone\%}$',
'$\\bf{Chase\%}$',
'$\\bf{Whiff\%}$',
'$\\bf{xwOBA}$\n$\\bf{Contact}$',
]
for i, col_name in enumerate(new_column_names):
table.get_celld()[(0, i)].get_text().set_text(col_name)
float_list = ['start_speed','ivb',
'hb', 'vaa', 'haa', 'vertical_release','horizontal_release', 'extension']
for fl in float_list:
# Subset of column names
subset_columns = [fl]
# Get the list of column indices
column_indices = [plot_table.columns.get_loc(col) for col in subset_columns]
# # print(column_indices)
for row_l in range(1,len(plot_table)+1):
# print(row_l)
if table.get_celld()[(row_l,column_indices[0])].get_text().get_text() != '—':
# print()
# print(fl)
table.get_celld()[(row_l,column_indices[0])].get_text().set_text('{:,.1f}'.format(float(table.get_celld()[(row_l,column_indices[0])].get_text().get_text().strip('%'))))
float_3_list = ['xwobacon']
for fl in float_3_list:
# Subset of column names
subset_columns = [fl]
# Get the list of column indices
column_indices = [plot_table.columns.get_loc(col) for col in subset_columns]
# # print(column_indices)
for row_l in range(1,len(plot_table)+1):
# print(row_l)
if table.get_celld()[(row_l,column_indices[0])].get_text().get_text() != '—':
# print()
# print(fl)
table.get_celld()[(row_l,column_indices[0])].get_text().set_text('{:,.3f}'.format(float(table.get_celld()[(row_l,column_indices[0])].get_text().get_text().strip('%'))))
percent_list = ['pitch_percent','zone_percent','chase_percent','whiff_rate']
for fl in percent_list:
# Subset of column names
subset_columns = [fl]
# Get the list of column indices
column_indices = [plot_table.columns.get_loc(col) for col in subset_columns]
# # print(column_indices)
for row_l in range(1,len(plot_table)+1):
# print(row_l)
if table.get_celld()[(row_l,column_indices[0])].get_text().get_text() != '—':
# print(fl)
table.get_celld()[(row_l,column_indices[0])].get_text().set_text('{:,.1%}'.format(float(table.get_celld()[(row_l,column_indices[0])].get_text().get_text().strip('%'))))
int_list = ['tj_stuff_plus','spin_rate']
for fl in int_list:
# Subset of column names
subset_columns = [fl]
# Get the list of column indices
column_indices = [plot_table.columns.get_loc(col) for col in subset_columns]
# # print(column_indices)
for row_l in range(1,len(plot_table)+1):
# print(row_l)
if table.get_celld()[(row_l,column_indices[0])].get_text().get_text() != '—':
# print(fl)
table.get_celld()[(row_l,column_indices[0])].get_text().set_text('{:,.0f}'.format(float(table.get_celld()[(row_l,column_indices[0])].get_text().get_text().strip('%'))))
return table
### GROUED IVB CREATION ###
def group_ivb_update(df,
agg_list=['pitcher_id','pitcher_name','pitcher_hand','pitch_type','pitch_description']):
#df.loc[df['launch_speed']==0,'launch_speed'] = np.nan
grouped_ivb = df.groupby(agg_list).agg(
pitches = ('start_speed','count'),
start_speed = ('start_speed','mean'),
ivb = ('ivb','mean'),
hb = ('hb','mean'),
spin_rate = ('spin_rate','mean'),
vaa = ('vaa','mean'),
haa = ('haa','mean'),
horizontal_release = ('x0','mean'),
vertical_release = ('z0','mean'),
extension = ('extension','mean'),
spin_direction = ('spin_direction','mean'),
tj_stuff_plus = ('tj_stuff_plus','mean'),
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'),
woba_pred = ('woba_pred','sum'),
bip = ('launch_speed','count'),
).reset_index()
grouped_ivb['zone_contact_percent'] = [grouped_ivb.zone_contact[x]/grouped_ivb.zone_swing[x] if grouped_ivb.zone_swing[x] != 0 else np.nan for x in range(len(grouped_ivb))]
grouped_ivb['zone_swing_percent'] = [grouped_ivb.zone_swing[x]/grouped_ivb.in_zone[x] if grouped_ivb.pitches[x] != 0 else np.nan for x in range(len(grouped_ivb))]
grouped_ivb['zone_percent'] = [grouped_ivb.in_zone[x]/grouped_ivb.pitches[x] if grouped_ivb.pitches[x] != 0 else np.nan for x in range(len(grouped_ivb))]
grouped_ivb['chase_percent'] = [grouped_ivb.ozone_swing[x]/(grouped_ivb.pitches[x] - grouped_ivb.in_zone[x]) if (grouped_ivb.pitches[x]- grouped_ivb.in_zone[x]) != 0 else np.nan for x in range(len(grouped_ivb))]
grouped_ivb['chase_contact'] = [grouped_ivb.ozone_contact[x]/grouped_ivb.ozone_swing[x] if grouped_ivb.ozone_swing[x] != 0 else np.nan for x in range(len(grouped_ivb))]
grouped_ivb['swing_percent'] = [grouped_ivb.swings[x]/grouped_ivb.pitches[x] if grouped_ivb.pitches[x] != 0 else np.nan for x in range(len(grouped_ivb))]
grouped_ivb['whiff_rate'] = [grouped_ivb.whiffs[x]/grouped_ivb.swings[x] if grouped_ivb.swings[x] != 0 else np.nan for x in range(len(grouped_ivb))]
grouped_ivb['swstr_rate'] = [grouped_ivb.whiffs[x]/grouped_ivb.pitches[x] if grouped_ivb.pitches[x] != 0 else np.nan for x in range(len(grouped_ivb))]
grouped_ivb['xwobacon'] = [grouped_ivb.woba_pred[x]/grouped_ivb.bip[x] if grouped_ivb.bip[x] != 0 else np.nan for x in range(len(grouped_ivb))]
return grouped_ivb
####LHH
def location_plot(df,ax,hand):
label_labels = df.sort_values(by=['prop','pitch_type'],ascending=[False,True]).pitch_description.unique()
j = 0
for label in label_labels:
subset = df[(df['pitch_description'] == label)&(df['batter_hand'] == hand)]
print(label)
if len(subset) >= 5:
confidence_ellipse(subset['px'], subset['pz'], ax=ax,edgecolor = pitch_colours[label],n_std=1.5,facecolor= pitch_colours[label],alpha=0.3)
j=j+1
else:
j=j+1
pitch_location_group = df[(df['batter_hand'] == hand)].groupby(['pitch_description']).agg(
pitches = ('start_speed','count'),
px = ('px','mean'),
pz = ('pz','mean')).reset_index()
pitch_location_group['pitch_percent'] = pitch_location_group['pitches']/pitch_location_group['pitches'].sum()
## Location Plot
sns.scatterplot(ax=ax,x=pitch_location_group['px'],
y=pitch_location_group['pz'],
hue=pitch_location_group['pitch_description'],
palette=pitch_colours,ec='black',
s=pitch_location_group['pitch_percent']*750,
linewidth=2,
zorder=2)
ax.axis('square')
draw_line(ax,alpha_spot=0.75,catcher_p=False)
ax.axis('off')
ax.set_xlim((-2.75,2.75))
ax.set_ylim((-0.5,5))
if len(pitch_location_group['px'])>0:
ax.get_legend().remove()
ax.grid(False)
ax.set_title(f"Pitch Locations vs {hand}HB\n{pitch_location_group['pitches'].sum()} Pitches",fontdict=font_properties_titles)