|
|
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
|
|
|
import polars as pl
|
|
|
from PIL import Image
|
|
|
import requests
|
|
|
from io import BytesIO
|
|
|
from matplotlib.offsetbox import OffsetImage, AnnotationBbox
|
|
|
import matplotlib.pyplot as plt
|
|
|
import matplotlib.gridspec as gridspec
|
|
|
import PIL
|
|
|
from matplotlib.transforms import Bbox
|
|
|
import matplotlib.image as mpimg
|
|
|
from scipy.stats import gaussian_kde
|
|
|
from statsmodels.nonparametric.kernel_regression import KernelReg
|
|
|
|
|
|
|
|
|
format_dict = {
|
|
|
'pitch_percent': '{:.1%}',
|
|
|
'pitches': '{:.0f}',
|
|
|
'heart_zone_percent': '{:.1%}',
|
|
|
'shadow_zone_percent': '{:.1%}',
|
|
|
'chase_zone_percent': '{:.1%}',
|
|
|
'waste_zone_percent': '{:.1%}',
|
|
|
'csw_percent': '{:.1%}',
|
|
|
'whiff_rate': '{:.1%}',
|
|
|
'zone_whiff_percent': '{:.1%}',
|
|
|
'chase_percent': '{:.1%}',
|
|
|
'bip': '{:.0f}',
|
|
|
'xwoba_percent_contact': '{:.3f}'
|
|
|
}
|
|
|
label_translation_dict = {
|
|
|
'pitch_percent': 'Pitch%',
|
|
|
'pitches': 'Pitches',
|
|
|
'heart_zone_percent': 'Heart%',
|
|
|
'shadow_zone_percent': 'Shadow%',
|
|
|
'chase_zone_percent': 'Chase%',
|
|
|
'waste_zone_percent': 'Waste%',
|
|
|
'csw_percent': 'CSW%',
|
|
|
'whiff_rate': 'Whiff%',
|
|
|
'zone_whiff_percent': 'Z-Whiff%',
|
|
|
'chase_percent': 'O-Swing%',
|
|
|
'bip': 'BBE',
|
|
|
'xwoba_percent_contact': 'xwOBACON'
|
|
|
}
|
|
|
|
|
|
def pitch_heat_map(pitch_input, df):
|
|
|
|
|
|
df = df.with_columns([
|
|
|
pl.col('pitcher_id').count().over(['batter_hand', 'strikes', 'balls']).alias('h_s_b'),
|
|
|
pl.col('pitcher_id').count().over(['batter_hand', 'strikes', 'balls', 'pitch_type']).alias('h_s_b_pitch')
|
|
|
])
|
|
|
|
|
|
df = df.with_columns([
|
|
|
(pl.col('h_s_b_pitch') / pl.col('h_s_b')).alias('h_s_b_pitch_percent')
|
|
|
])
|
|
|
|
|
|
df_plot = df.filter(pl.col('pitch_type') == pitch_input)
|
|
|
|
|
|
return df_plot
|
|
|
|
|
|
def pitch_prop(df: pl.DataFrame, hand: str = 'R') -> pd.DataFrame:
|
|
|
df_plot_pd = df.to_pandas()
|
|
|
pivot_table = (df_plot_pd[df_plot_pd['batter_hand'].isin([hand])]
|
|
|
.groupby(['batter_hand','strikes', 'balls'])[['h_s_b_pitch_percent']]
|
|
|
.mean()
|
|
|
.reset_index()
|
|
|
.pivot(index='strikes',columns='balls',values='h_s_b_pitch_percent'))
|
|
|
|
|
|
new_index = range(3)
|
|
|
new_columns = range(4)
|
|
|
|
|
|
|
|
|
pivot_table = pivot_table.reindex(index=new_index, columns=new_columns)
|
|
|
|
|
|
|
|
|
pivot_table = pivot_table.fillna(0)
|
|
|
df_hand = pl.DataFrame(pivot_table.reset_index())
|
|
|
return df_hand
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
|
plate_side = strike_zone['PlateLocSide'].to_numpy()
|
|
|
plate_height = strike_zone['PlateLocHeight'].to_numpy()
|
|
|
|
|
|
|
|
|
axis.plot(plate_side, plate_height, 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], [-0.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)
|
|
|
|
|
|
def heat_map_plot(df:pl.DataFrame,
|
|
|
ax:plt.Axes,
|
|
|
cmap:matplotlib.colors.LinearSegmentedColormap,
|
|
|
hand:str):
|
|
|
if df.filter(pl.col('batter_hand')==hand).shape[0] > 3:
|
|
|
sns.kdeplot(data=df.filter(pl.col('batter_hand')==hand),
|
|
|
x='px',
|
|
|
y='pz',
|
|
|
cmap=cmap,
|
|
|
shade=True,
|
|
|
ax=ax,
|
|
|
thresh=0.3,
|
|
|
bw_adjust=1)
|
|
|
elif df.filter(pl.col('batter_hand')==hand).shape[0] > 0:
|
|
|
sns.scatterplot(data=df.filter(pl.col('batter_hand')==hand),
|
|
|
x='px',
|
|
|
y='pz',
|
|
|
cmap=cmap,
|
|
|
ax=ax)
|
|
|
|
|
|
|
|
|
draw_line(ax,alpha_spot=1,catcher_p = False)
|
|
|
|
|
|
|
|
|
ax.axis('off')
|
|
|
ax.axis('square')
|
|
|
ax.set_xlim(-2.75,2.75)
|
|
|
ax.set_ylim(-0.5,5)
|
|
|
|
|
|
def format_as_percentage(val):
|
|
|
return f'{val * 100:.0f}%'
|
|
|
|
|
|
|
|
|
def table_plot(ax:plt.Axes,
|
|
|
table:pl.DataFrame,
|
|
|
hand='R'):
|
|
|
|
|
|
|
|
|
trans = ax.transData + ax.transAxes.inverted()
|
|
|
|
|
|
if hand == 'R':
|
|
|
bbox_data = Bbox.from_bounds(1.7, -0.5, 2.5, 5)
|
|
|
|
|
|
else:
|
|
|
bbox_data = Bbox.from_bounds(-4.2, -0.5, 2.5, 5)
|
|
|
|
|
|
|
|
|
|
|
|
bbox_axes = trans.transform_bbox(bbox_data)
|
|
|
|
|
|
if hand == 'R':
|
|
|
ax.text(s='Against RHH',x=2.95,y=4.65,fontsize=18,fontweight='bold',ha='center')
|
|
|
else:
|
|
|
ax.text(s='Against LHH',x=-2.95,y=4.65,fontsize=18,fontweight='bold',ha='center')
|
|
|
|
|
|
|
|
|
table = table.apply(lambda x: format_dict[x.name].format(x[0]) if x[0] != '—' else '—', axis=1)
|
|
|
table.index = [label_translation_dict[x] for x in table.index]
|
|
|
|
|
|
|
|
|
|
|
|
table_plot = ax.table(cellText=table.reset_index().values,
|
|
|
loc='right',
|
|
|
cellLoc='center',
|
|
|
colWidths=[0.52,0.3],
|
|
|
bbox=bbox_axes.bounds,zorder=100)
|
|
|
|
|
|
|
|
|
min_font_size = 14
|
|
|
|
|
|
table_plot.auto_set_font_size(False)
|
|
|
|
|
|
table_plot.set_fontsize(min_font_size)
|
|
|
|
|
|
|
|
|
bbox_data = Bbox.from_bounds(-1.25, 5, 2.5, 1)
|
|
|
bbox_axes = trans.transform_bbox(bbox_data)
|
|
|
|
|
|
|
|
|
|
|
|
def table_plot_pivot(ax:plt.Axes,
|
|
|
pivot_table:pl.DataFrame,
|
|
|
df_colour:pd.DataFrame):
|
|
|
|
|
|
|
|
|
trans = ax.transData + ax.transAxes.inverted()
|
|
|
bbox_data = Bbox.from_bounds(-0.75, 5, 2.5, 1)
|
|
|
bbox_axes = trans.transform_bbox(bbox_data)
|
|
|
|
|
|
table_plot_pivot = ax.table(cellText=[[format_as_percentage(val) for val in row] for row in pivot_table.select(pivot_table.columns[-4:]).to_numpy()],
|
|
|
colLabels =pivot_table.columns[-4:],
|
|
|
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[df_colour.columns[-4:]].values)
|
|
|
|
|
|
|
|
|
min_font_size = 11
|
|
|
|
|
|
table_plot_pivot.auto_set_font_size(False)
|
|
|
|
|
|
table_plot_pivot.set_fontsize(min_font_size)
|
|
|
|
|
|
|
|
|
ax.text(x=-2.0, y=5.08, s='Strikes', rotation=90,fontweight='bold')
|
|
|
ax.text(x=0, y=6.05, s='Balls',fontweight='bold',ha='center')
|
|
|
|
|
|
|
|
|
def plot_header(pitcher_id: str, ax: plt.Axes, df_team: pl.DataFrame, df_players: pl.DataFrame,sport_id:int):
|
|
|
"""
|
|
|
Display the team logo for the given pitcher on the specified axis.
|
|
|
Parameters
|
|
|
----------
|
|
|
pitcher_id : str
|
|
|
The ID of the pitcher.
|
|
|
ax : plt.Axes
|
|
|
The axis to display the logo on.
|
|
|
df_team : pl.DataFrame
|
|
|
The DataFrame containing team data.
|
|
|
df_players : pl.DataFrame
|
|
|
The DataFrame containing player data.
|
|
|
"""
|
|
|
|
|
|
mlb_teams = [
|
|
|
{"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},
|
|
|
{"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},
|
|
|
{"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},
|
|
|
{"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},
|
|
|
{"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},
|
|
|
{"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},
|
|
|
{"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},
|
|
|
{"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},
|
|
|
{"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},
|
|
|
{"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},
|
|
|
{"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},
|
|
|
{"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},
|
|
|
{"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},
|
|
|
{"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},
|
|
|
{"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},
|
|
|
{"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},
|
|
|
{"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},
|
|
|
{"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},
|
|
|
{"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},
|
|
|
{"team": "OAK", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
|
|
|
{"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},
|
|
|
{"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},
|
|
|
{"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},
|
|
|
{"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},
|
|
|
{"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},
|
|
|
{"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},
|
|
|
{"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},
|
|
|
{"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},
|
|
|
{"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},
|
|
|
{"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"},
|
|
|
{"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},
|
|
|
]
|
|
|
|
|
|
try:
|
|
|
|
|
|
if int(sport_id) == 1:
|
|
|
url = f'https://img.mlbstatic.com/mlb-photos/image/upload/d_people:generic:headshot:67:current.png/w_640,q_auto:best/v1/people/{pitcher_id}/headshot/silo/current.png'
|
|
|
else:
|
|
|
url = f'https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto/w_640/v1/people/{pitcher_id}/headshot/milb/current.png'
|
|
|
|
|
|
|
|
|
response = requests.get(url)
|
|
|
img = Image.open(BytesIO(response.content))
|
|
|
|
|
|
|
|
|
ax.imshow(img, extent=[-11.5, -9.5, 0, 2] if sport_id == 1 else [-11.5+2/6, -9.5-2/6, 0, 2], origin='upper')
|
|
|
except PIL.UnidentifiedImageError:
|
|
|
ax.axis('off')
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
df_image = pd.DataFrame(mlb_teams)
|
|
|
image_dict = df_image.set_index('team')['logo_url'].to_dict()
|
|
|
|
|
|
|
|
|
team_id = df_players.filter(pl.col('player_id') == pitcher_id)['team'][0]
|
|
|
|
|
|
|
|
|
url_team = f'https://statsapi.mlb.com/api/v1/teams/{team_id}'
|
|
|
|
|
|
|
|
|
data_team = requests.get(url_team).json()
|
|
|
|
|
|
|
|
|
if data_team['teams'][0]['id'] in df_team['parent_org_id']:
|
|
|
team_abb = df_team.filter(pl.col('team_id') == data_team['teams'][0]['id'])['parent_org_abbreviation'][0]
|
|
|
else:
|
|
|
team_abb = df_team.filter(pl.col('parent_org_id') == data_team['teams'][0]['parentOrgId'])['parent_org_abbreviation'][0]
|
|
|
|
|
|
|
|
|
logo_url = image_dict[team_abb]
|
|
|
|
|
|
|
|
|
response = requests.get(logo_url)
|
|
|
|
|
|
|
|
|
img = Image.open(BytesIO(response.content))
|
|
|
|
|
|
|
|
|
ax.imshow(img, extent=[9.5, 11.5, 0, 2], origin='upper')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
except (KeyError,IndexError) as e:
|
|
|
ax.axis('off')
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
|
plate_side = strike_zone['PlateLocSide'].to_numpy()
|
|
|
plate_height = strike_zone['PlateLocHeight'].to_numpy()
|
|
|
|
|
|
|
|
|
axis.plot(plate_side, plate_height, 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], [-0.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)
|
|
|
|
|
|
from matplotlib.patches import Rectangle
|
|
|
|
|
|
|
|
|
import matplotlib.pyplot as plt
|
|
|
import seaborn as sns
|
|
|
from matplotlib.patches import Rectangle
|
|
|
from matplotlib import gridspec
|
|
|
import numpy as np
|
|
|
import pandas as pd
|
|
|
from statsmodels.nonparametric.kernel_regression import KernelReg
|
|
|
|
|
|
|
|
|
|
|
|
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):
|
|
|
|
|
|
plate_side = strike_zone['PlateLocSide'].to_numpy()
|
|
|
plate_height = strike_zone['PlateLocHeight'].to_numpy()
|
|
|
|
|
|
|
|
|
axis.plot(plate_side, plate_height, 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], [-0.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)
|
|
|
|
|
|
|
|
|
|
|
|
def heat_map_plot_hex_whiff(df:pl.DataFrame,
|
|
|
ax:plt.Axes,
|
|
|
cmap:matplotlib.colors.LinearSegmentedColormap,
|
|
|
hand:str):
|
|
|
|
|
|
|
|
|
|
|
|
heatmap_df = df.filter((pl.col('batter_hand')==hand)&((pl.col('is_swing')))).to_pandas()
|
|
|
heatmap_df['is_whiff'] = heatmap_df['is_whiff'].fillna(0)
|
|
|
|
|
|
bin_size = max(0.1, min(0.1, 1 / np.sqrt(len(heatmap_df))))
|
|
|
|
|
|
zone_df = pd.DataFrame(columns=['px', 'pz'])
|
|
|
for x in np.arange(-2.75, 2.85,bin_size):
|
|
|
for y in np.arange(-0.5, 5.6,bin_size):
|
|
|
zone_df.loc[len(zone_df)] = [round(x,1), round(y,1)]
|
|
|
|
|
|
|
|
|
|
|
|
heatmap_df.loc[heatmap_df['px'].notna(),'kde_x'] = np.clip(heatmap_df.loc[heatmap_df['px'].notna(),'px'].astype('float').mul(10).astype('int').div(10),
|
|
|
-2.75,
|
|
|
2.75)
|
|
|
heatmap_df.loc[heatmap_df['pz'].notna(),'kde_z'] = np.clip(heatmap_df.loc[heatmap_df['pz'].notna(),'pz'].astype('float').mul(10).astype('int').div(10),
|
|
|
-0.5,
|
|
|
5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bandwidth = np.clip(1 / np.sqrt(len(df)), 0.3, 0.5)
|
|
|
|
|
|
|
|
|
v_center = 0.25
|
|
|
kde_df = pd.merge(zone_df,
|
|
|
heatmap_df
|
|
|
.dropna(subset=['is_whiff', 'px', 'pz'])
|
|
|
[['kde_x', 'kde_z', 'is_whiff']],
|
|
|
how='left',
|
|
|
left_on=['px', 'pz'],
|
|
|
right_on=['kde_x', 'kde_z']).fillna({'is_whiff': v_center})
|
|
|
|
|
|
|
|
|
|
|
|
kernel_regression = KernelReg(endog=kde_df['is_whiff'],
|
|
|
exog=[kde_df['px'], kde_df['pz']],
|
|
|
bw=[bandwidth,bandwidth],
|
|
|
var_type='cc')
|
|
|
|
|
|
kde_df['kernel_stat'] = kernel_regression.fit([kde_df['px'], kde_df['pz']])[0]
|
|
|
kde_df = kde_df.pivot_table(columns='px', index='pz', values='kernel_stat', aggfunc='mean')
|
|
|
kde_df = kde_df.round(3)
|
|
|
|
|
|
|
|
|
|
|
|
from matplotlib.colors import LinearSegmentedColormap
|
|
|
|
|
|
|
|
|
kde_min = '#648FFF'
|
|
|
kde_mid = '#ffffff'
|
|
|
kde_max = '#FFB000'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ax.imshow(kde_df.values, extent=[-2.25, 2.25, -0.5, 5], origin='lower', cmap=cmap, vmin=0.15, vmax=0.35,
|
|
|
interpolation='bilinear')
|
|
|
ax.axis('square')
|
|
|
ax.set(xlabel=None, ylabel=None)
|
|
|
ax.set_xlim(-2.75, 2.75)
|
|
|
ax.set_ylim(-0.5, 5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ax.axis('off')
|
|
|
|
|
|
draw_line(ax,alpha_spot=1,catcher_p = False)
|
|
|
|
|
|
|
|
|
def heat_map_plot_hex_damage(df:pl.DataFrame,
|
|
|
ax:plt.Axes,
|
|
|
cmap:matplotlib.colors.LinearSegmentedColormap,
|
|
|
hand:str):
|
|
|
|
|
|
heatmap_df = df.filter((pl.col('batter_hand')==hand)&((pl.col('launch_speed')>0))).to_pandas()
|
|
|
heatmap_df['woba_pred_contact'] = heatmap_df['woba_pred_contact'].fillna(0)
|
|
|
bin_size = max(0.2, min(0.3, 1 / np.sqrt(len(heatmap_df))))
|
|
|
|
|
|
|
|
|
zone_df = pd.DataFrame(columns=['px', 'pz'])
|
|
|
for x in np.arange(-2.75, 2.95,bin_size):
|
|
|
for y in np.arange(-0.5, 5.7,bin_size):
|
|
|
zone_df.loc[len(zone_df)] = [round(x,1), round(y,1)]
|
|
|
|
|
|
|
|
|
heatmap_df.loc[heatmap_df['px'].notna(),'kde_x'] = np.clip(heatmap_df.loc[heatmap_df['px'].notna(),'px'].astype('float').mul(10).astype('int').div(10),
|
|
|
-2.75,
|
|
|
2.75)
|
|
|
heatmap_df.loc[heatmap_df['pz'].notna(),'kde_z'] = np.clip(heatmap_df.loc[heatmap_df['pz'].notna(),'pz'].astype('float').mul(10).astype('int').div(10),
|
|
|
-0.5,
|
|
|
5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bandwidth = np.clip(1 / np.sqrt(len(df)), 0.3, 0.5)
|
|
|
|
|
|
|
|
|
v_center = 0.375
|
|
|
kde_df = pd.merge(zone_df,
|
|
|
heatmap_df
|
|
|
.dropna(subset=['woba_pred_contact', 'px', 'pz'])
|
|
|
[['kde_x', 'kde_z', 'woba_pred_contact']],
|
|
|
how='left',
|
|
|
left_on=['px', 'pz'],
|
|
|
right_on=['kde_x', 'kde_z']).fillna({'woba_pred_contact': v_center})
|
|
|
|
|
|
|
|
|
|
|
|
kernel_regression = KernelReg(endog=kde_df['woba_pred_contact'],
|
|
|
exog=[kde_df['px'], kde_df['pz']],
|
|
|
bw=[bandwidth,bandwidth],
|
|
|
var_type='cc')
|
|
|
|
|
|
kde_df['kernel_stat'] = kernel_regression.fit([kde_df['px'], kde_df['pz']])[0]
|
|
|
kde_df = kde_df.pivot_table(columns='px', index='pz', values='kernel_stat', aggfunc='mean')
|
|
|
kde_df = kde_df.round(3)
|
|
|
|
|
|
|
|
|
|
|
|
from matplotlib.colors import LinearSegmentedColormap
|
|
|
|
|
|
|
|
|
kde_min = '#648FFF'
|
|
|
kde_mid = '#ffffff'
|
|
|
kde_max = '#FFB000'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ax.imshow(kde_df.values, extent=[-2.25, 2.25, -0.5, 5], origin='lower', cmap=cmap, vmin=0.25, vmax=0.5,
|
|
|
interpolation='bilinear')
|
|
|
ax.axis('square')
|
|
|
ax.set(xlabel=None, ylabel=None)
|
|
|
ax.set_xlim(-2.75, 2.75)
|
|
|
ax.set_ylim(-0.5, 5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ax.axis('off')
|
|
|
|
|
|
draw_line(ax,alpha_spot=1,catcher_p = False)
|
|
|
|