|
|
import polars as pl |
|
|
import numpy as np |
|
|
import matplotlib.pyplot as plt |
|
|
import seaborn as sns |
|
|
import PIL |
|
|
from PIL import Image |
|
|
import requests |
|
|
from io import BytesIO |
|
|
from matplotlib.offsetbox import OffsetImage, AnnotationBbox |
|
|
from matplotlib.ticker import FuncFormatter |
|
|
import matplotlib.transforms as transforms |
|
|
from matplotlib.patches import Ellipse |
|
|
import matplotlib.gridspec as gridspec |
|
|
import matplotlib.patches as mpatches |
|
|
import matplotlib.lines as mlines |
|
|
from matplotlib.figure import Figure |
|
|
import api_scraper |
|
|
|
|
|
|
|
|
scraper = api_scraper.MLB_Scrape() |
|
|
|
|
|
class PitchPlotFunctions: |
|
|
|
|
|
def pitch_colours(self): |
|
|
|
|
|
pitch_colours = { |
|
|
'FF': {'colour': '#FF007D', 'name': '4-Seam Fastball'}, |
|
|
'FA': {'colour': '#FF007D', 'name': 'Fastball'}, |
|
|
'SI': {'colour': '#98165D', 'name': 'Sinker'}, |
|
|
'FC': {'colour': '#BE5FA0', 'name': 'Cutter'}, |
|
|
'CH': {'colour': '#F79E70', 'name': 'Changeup'}, |
|
|
'FS': {'colour': '#FE6100', 'name': 'Splitter'}, |
|
|
'SC': {'colour': '#F08223', 'name': 'Screwball'}, |
|
|
'FO': {'colour': '#FFB000', 'name': 'Forkball'}, |
|
|
'SL': {'colour': '#67E18D', 'name': 'Slider'}, |
|
|
'ST': {'colour': '#1BB999', 'name': 'Sweeper'}, |
|
|
'SV': {'colour': '#376748', 'name': 'Slurve'}, |
|
|
'KC': {'colour': '#311D8B', 'name': 'Knuckle Curve'}, |
|
|
'CU': {'colour': '#3025CE', 'name': 'Curveball'}, |
|
|
'CS': {'colour': '#274BFC', 'name': 'Slow Curve'}, |
|
|
'EP': {'colour': '#648FFF', 'name': 'Eephus'}, |
|
|
'KN': {'colour': '#867A08', 'name': 'Knuckleball'}, |
|
|
'PO': {'colour': '#472C30', 'name': 'Pitch Out'}, |
|
|
'UN': {'colour': '#9C8975', 'name': 'Unknown'}, |
|
|
} |
|
|
|
|
|
|
|
|
dict_colour = dict(zip(pitch_colours.keys(), [pitch_colours[key]['colour'] for key in pitch_colours])) |
|
|
dict_pitch = dict(zip(pitch_colours.keys(), [pitch_colours[key]['name'] for key in pitch_colours])) |
|
|
|
|
|
return dict_colour, dict_pitch |
|
|
|
|
|
|
|
|
def sns_custom_theme(self): |
|
|
|
|
|
custom_theme = { |
|
|
"axes.facecolor": "white", |
|
|
"axes.edgecolor": ".8", |
|
|
"axes.grid": True, |
|
|
"axes.axisbelow": True, |
|
|
"axes.labelcolor": ".15", |
|
|
"figure.facecolor": "#f9f9f9", |
|
|
"grid.color": ".8", |
|
|
"grid.linestyle": "-", |
|
|
"text.color": ".15", |
|
|
"xtick.color": ".15", |
|
|
"ytick.color": ".15", |
|
|
"xtick.direction": "out", |
|
|
"ytick.direction": "out", |
|
|
"lines.solid_capstyle": "round", |
|
|
"patch.edgecolor": "w", |
|
|
"patch.force_edgecolor": True, |
|
|
"image.cmap": "rocket", |
|
|
"font.family": ["sans-serif"], |
|
|
"font.sans-serif": ["Arial", "DejaVu Sans", "Liberation Sans", "Bitstream Vera Sans", "sans-serif"], |
|
|
"xtick.bottom": False, |
|
|
"xtick.top": False, |
|
|
"ytick.left": False, |
|
|
"ytick.right": False, |
|
|
"axes.spines.left": True, |
|
|
"axes.spines.bottom": True, |
|
|
"axes.spines.right": True, |
|
|
"axes.spines.top": True |
|
|
} |
|
|
|
|
|
|
|
|
colour_palette = ['#FFB000', '#648FFF', '#785EF0', '#DC267F', '#FE6100', '#3D1EB2', '#894D80', '#16AA02', '#B5592B', '#A3C1ED'] |
|
|
|
|
|
return custom_theme, colour_palette |
|
|
|
|
|
|
|
|
def sport_id_dict(self): |
|
|
|
|
|
dict = {1:'MLB', |
|
|
11:'AAA', |
|
|
12:'AA', |
|
|
13:'A+', |
|
|
14:'A', |
|
|
17:'AFL', |
|
|
22:'College', |
|
|
21:'Prospects', |
|
|
51:'International' } |
|
|
return dict |
|
|
|
|
|
|
|
|
def team_logos(self): |
|
|
|
|
|
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"} |
|
|
] |
|
|
|
|
|
|
|
|
df_image = pl.DataFrame(mlb_teams) |
|
|
|
|
|
image_dict = df_image.select(['team', 'logo_url']).to_dict(as_series=False)['logo_url'] |
|
|
|
|
|
|
|
|
image_dict = {row['team']: row['logo_url'] for row in df_image.select(['team', 'logo_url']).to_dicts()} |
|
|
|
|
|
return image_dict |
|
|
|
|
|
|
|
|
def player_headshot(self, pitcher_id: str, ax: plt.Axes, sport_id: int): |
|
|
""" |
|
|
Fetches and displays the player's headshot image on the given axis. |
|
|
|
|
|
Parameters: |
|
|
pitcher_id (str): The ID of the pitcher. |
|
|
ax (plt.Axes): The matplotlib axis to display the image on. |
|
|
sport_id (int): The sport ID to determine the URL format. |
|
|
""" |
|
|
|
|
|
try: |
|
|
if sport_id == 1: |
|
|
url = f'https://img.mlbstatic.com/mlb-photos/image/'\ |
|
|
f'upload/d_people:generic:headshot:67:current.png'\ |
|
|
f'/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.set_xlim(0, 1) |
|
|
ax.set_ylim(0, 1) |
|
|
if sport_id == 1: |
|
|
ax.imshow(img, extent=[0, 1, 0, 1], origin='upper') |
|
|
else: |
|
|
ax.imshow(img, extent=[1/6, 5/6, 0, 1], origin='upper') |
|
|
|
|
|
except PIL.UnidentifiedImageError: |
|
|
print('NA') |
|
|
ax.axis('off') |
|
|
|
|
|
|
|
|
def player_bio(self, pitcher_id: str, ax: plt.Axes, start_date: str, end_date: str, batter_hand: list,game_type: list = ['R']): |
|
|
""" |
|
|
Fetches and displays the player's bio information on the given axis. |
|
|
|
|
|
Parameters: |
|
|
pitcher_id (str): The ID of the pitcher. |
|
|
ax (plt.Axes): The matplotlib axis to display the bio information on. |
|
|
start_date (str): The start date for the bio information. |
|
|
end_date (str): The end date for the bio information. |
|
|
batter_hand (list): The list of batter hands (e.g., ['R'] or ['L']). |
|
|
""" |
|
|
type_dict = {'R':'Regular Season', |
|
|
'S':'Spring', |
|
|
'P':'Playoffs' } |
|
|
|
|
|
split_title = { |
|
|
'all':'', |
|
|
'right':' vs RHH', |
|
|
'left':' vs LHH' |
|
|
} |
|
|
|
|
|
|
|
|
url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam" |
|
|
|
|
|
data = requests.get(url).json() |
|
|
|
|
|
player_name = data['people'][0]['fullName'] |
|
|
pitcher_hand = data['people'][0]['pitchHand']['code'] |
|
|
age = data['people'][0]['currentAge'] |
|
|
height = data['people'][0]['height'] |
|
|
weight = data['people'][0]['weight'] |
|
|
|
|
|
ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=20) |
|
|
ax.text(0.5, 0.65, f'{pitcher_hand}HP, Age: {age}, {height}/{weight}', va='top', ha='center', fontsize=12) |
|
|
|
|
|
if batter_hand == ['R']: |
|
|
batter_hand_text = ', vs RHH' |
|
|
elif batter_hand == ['L']: |
|
|
batter_hand_text = ', vs LHH' |
|
|
else: |
|
|
batter_hand_text = '' |
|
|
|
|
|
|
|
|
if game_type[0] in ['S','P']: |
|
|
ax.text(0.5, 0.4, f'{start_date} to {end_date} ({type_dict[game_type[0]]}){batter_hand_text}',va='top', ha='center', |
|
|
fontsize=12, fontstyle='italic') |
|
|
|
|
|
else: |
|
|
ax.text(0.5, 0.4, f'{start_date} to {end_date}{batter_hand_text}',va='top', ha='center', |
|
|
fontsize=12, fontstyle='italic') |
|
|
|
|
|
|
|
|
ax.axis('off') |
|
|
|
|
|
|
|
|
def plot_logo(self, pitcher_id: str, ax: plt.Axes): |
|
|
""" |
|
|
Fetches and displays the team logo on the given axis. |
|
|
|
|
|
Parameters: |
|
|
pitcher_id (str): The ID of the pitcher. |
|
|
ax (plt.Axes): The matplotlib axis to display the logo on. |
|
|
""" |
|
|
|
|
|
url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam" |
|
|
|
|
|
data = requests.get(url).json() |
|
|
|
|
|
try: |
|
|
url_team = 'https://statsapi.mlb.com/' + data['people'][0]['currentTeam']['link'] |
|
|
|
|
|
data_team = requests.get(url_team).json() |
|
|
|
|
|
|
|
|
if data_team['teams'][0]['sport']['id'] == 1: |
|
|
team_abb = data_team['teams'][0]['abbreviation'] |
|
|
logo_url = self.team_logos()[team_abb] |
|
|
else: |
|
|
team_abb = data_team['teams'][0]['parentOrgId'] |
|
|
logo_url = self.team_logos()[dict(scraper.get_teams().select(['team_id', 'parent_org_abbreviation']).iter_rows())[team_abb]] |
|
|
except KeyError: |
|
|
logo_url = "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png?w=500&h=500&transparent=true" |
|
|
|
|
|
response = requests.get(logo_url) |
|
|
|
|
|
img = Image.open(BytesIO(response.content)) |
|
|
|
|
|
ax.set_xlim(0, 1) |
|
|
ax.set_ylim(0, 1) |
|
|
ax.imshow(img, extent=[0, 1, 0, 1], origin='upper') |
|
|
|
|
|
ax.axis('off') |
|
|
|
|
|
|
|
|
def confidence_ellipse( self, |
|
|
x:np.array, |
|
|
y:np.array, |
|
|
ax:plt.Axes, |
|
|
n_std:float=3.0, |
|
|
facecolor:str='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.shape != y.shape: |
|
|
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]) |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
scale_x = np.sqrt(cov[0, 0]) * n_std |
|
|
mean_x = x.mean() |
|
|
|
|
|
|
|
|
|
|
|
scale_y = np.sqrt(cov[1, 1]) * n_std |
|
|
mean_y = y.mean() |
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
def break_plot_big(self, df: pl.DataFrame, ax: plt.Axes, sport_id: int): |
|
|
""" |
|
|
Plots a big break plot for the given DataFrame on the provided axis. |
|
|
|
|
|
Parameters: |
|
|
df (pl.DataFrame): The DataFrame containing pitch data. |
|
|
ax (plt.Axes): The matplotlib axis to plot on. |
|
|
sport_id (int): The sport ID to determine the plot title. |
|
|
""" |
|
|
|
|
|
font_properties = {'size': 10} |
|
|
font_properties_titles = {'size': 16} |
|
|
font_properties_axes = {'size': 14} |
|
|
|
|
|
|
|
|
label_labels = df.sort(by=['prop', 'pitch_type'], descending=[False, True])['pitch_type'].unique() |
|
|
j = 0 |
|
|
dict_colour, dict_pitch = self.pitch_colours() |
|
|
custom_theme, colour_palette = self.sns_custom_theme() |
|
|
|
|
|
|
|
|
for label in label_labels: |
|
|
subset = df.filter(pl.col('pitch_type') == label) |
|
|
if len(subset) > 4: |
|
|
try: |
|
|
if df['pitcher_hand'][0] == 'R': |
|
|
self.confidence_ellipse(subset['hb']* 1, subset['ivb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2) |
|
|
if df['pitcher_hand'][0] == 'L': |
|
|
self.confidence_ellipse(subset['hb'] * 1, subset['ivb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2) |
|
|
except ValueError: |
|
|
return |
|
|
j += 1 |
|
|
else: |
|
|
j += 1 |
|
|
|
|
|
|
|
|
if df['pitcher_hand'][0] == 'R': |
|
|
sns.scatterplot(ax=ax, x=df['hb'] * 1, y=df['ivb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=35) |
|
|
if df['pitcher_hand'][0] == 'L': |
|
|
sns.scatterplot(ax=ax, x=df['hb'] * 1, y=df['ivb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=35) |
|
|
|
|
|
|
|
|
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(f"{self.sport_id_dict()[sport_id]} - Short Form Pitch Movement Plot", fontdict=font_properties_titles) |
|
|
|
|
|
|
|
|
ax.get_legend().remove() |
|
|
ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties) |
|
|
ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties) |
|
|
|
|
|
|
|
|
if df['pitcher_hand'][0] == 'R': |
|
|
ax.text(-24.5, -24.5, s='← Glove Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=13, zorder=3) |
|
|
ax.text(24.5, -24.5, s='Arm Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=13, zorder=3) |
|
|
if df['pitcher_hand'][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=13, zorder=3) |
|
|
ax.text(-24.5, -24.5, s='Glove Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=13, zorder=3) |
|
|
|
|
|
|
|
|
ax.set_aspect('equal', adjustable='box') |
|
|
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x))) |
|
|
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x))) |
|
|
|
|
|
|
|
|
def break_plot_big_long(self, df: pl.DataFrame, ax: plt.Axes, sport_id: int): |
|
|
""" |
|
|
Plots a long break plot for the given DataFrame on the provided axis. |
|
|
|
|
|
Parameters: |
|
|
df (pl.DataFrame): The DataFrame containing pitch data. |
|
|
ax (plt.Axes): The matplotlib axis to plot on. |
|
|
sport_id (int): The sport ID to determine the plot title. |
|
|
""" |
|
|
|
|
|
font_properties = {'size': 20} |
|
|
font_properties_titles = {'size': 32} |
|
|
font_properties_axes = {'size': 24} |
|
|
|
|
|
|
|
|
label_labels = df.sort(by=['prop', 'pitch_type'], descending=[False, True])['pitch_type'].unique() |
|
|
dict_colour, dict_pitch = self.pitch_colours() |
|
|
custom_theme, colour_palette = self.sns_custom_theme() |
|
|
j = 0 |
|
|
|
|
|
|
|
|
for label in label_labels: |
|
|
subset = df.filter(pl.col('pitch_type') == label) |
|
|
print(label) |
|
|
if len(subset) > 4: |
|
|
try: |
|
|
if df['pitcher_hand'][0] == 'R': |
|
|
self.confidence_ellipse(subset['hb'], subset['vb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2) |
|
|
if df['pitcher_hand'][0] == 'L': |
|
|
self.confidence_ellipse(subset['hb'] * -1, subset['vb'], ax=ax, edgecolor=dict_colour[label], n_std=2, facecolor=dict_colour[label], alpha=0.2) |
|
|
except ValueError: |
|
|
return |
|
|
j += 1 |
|
|
else: |
|
|
j += 1 |
|
|
|
|
|
|
|
|
if df['pitcher_hand'][0] == 'R': |
|
|
sns.scatterplot(ax=ax, x=df['hb'] * 1, y=df['vb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50) |
|
|
if df['pitcher_hand'][0] == 'L': |
|
|
sns.scatterplot(ax=ax, x=df['hb'] * -1, y=df['vb'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50) |
|
|
|
|
|
|
|
|
ax.set_xlim((-40, 40)) |
|
|
ax.set_ylim((-80, 0)) |
|
|
ax.axhline(y=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1) |
|
|
ax.axvline(x=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1) |
|
|
ax.set_xlabel('Horizontal Break (in)', fontdict=font_properties_axes) |
|
|
ax.set_ylabel('Vertical Break (in)', fontdict=font_properties_axes) |
|
|
ax.set_title(f"{self.sport_id_dict()[sport_id]} - Long Form Pitch Movement Plot", fontdict=font_properties_titles) |
|
|
|
|
|
|
|
|
ax.get_legend().remove() |
|
|
ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties) |
|
|
ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties) |
|
|
|
|
|
|
|
|
if df['pitcher_hand'][0] == 'R': |
|
|
ax.text(-39.5, -79.5, s='← Glove Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3) |
|
|
ax.text(39.5, -79.5, s='Arm Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3) |
|
|
if df['pitcher_hand'][0] == 'L': |
|
|
ax.invert_xaxis() |
|
|
ax.text(39.5, -79.5, s='← Arm Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3) |
|
|
ax.text(-39.5, -79.5, s='Glove Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3) |
|
|
|
|
|
|
|
|
ax.set_aspect('equal', adjustable='box') |
|
|
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x))) |
|
|
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x))) |
|
|
|
|
|
|
|
|
def release_point_plot(self, df: pl.DataFrame, ax: plt.Axes, sport_id: int): |
|
|
""" |
|
|
Plots the release points for the given DataFrame on the provided axis. |
|
|
|
|
|
Parameters: |
|
|
df (pl.DataFrame): The DataFrame containing pitch data. |
|
|
ax (plt.Axes): The matplotlib axis to plot on. |
|
|
sport_id (int): The sport ID to determine the plot title. |
|
|
""" |
|
|
|
|
|
font_properties = {'size': 20} |
|
|
font_properties_titles = {'size': 32} |
|
|
font_properties_axes = {'size': 24} |
|
|
dict_colour, dict_pitch = self.pitch_colours() |
|
|
custom_theme, colour_palette = self.sns_custom_theme() |
|
|
|
|
|
|
|
|
if df['pitcher_hand'][0] == 'R': |
|
|
sns.scatterplot(ax=ax, x=df['x0'] * -1, y=df['z0'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50) |
|
|
if df['pitcher_hand'][0] == 'L': |
|
|
sns.scatterplot(ax=ax, x=df['x0'] * 1, y=df['z0'] * 1, hue=df['pitch_type'], palette=dict_colour, ec='black', alpha=1, zorder=2, s=50) |
|
|
|
|
|
|
|
|
ax.add_patch(plt.Circle((0, 10 / 12 - 18), radius=18, edgecolor='black', facecolor='#a63b17')) |
|
|
ax.add_patch(plt.Rectangle((-0.5, 9 / 12), 1, 1 / 6, edgecolor='black', facecolor='white')) |
|
|
|
|
|
|
|
|
ax.set_xlim((-4, 4)) |
|
|
ax.set_ylim((0, 8)) |
|
|
ax.axhline(y=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1) |
|
|
ax.axvline(x=0, color=colour_palette[8], alpha=0.5, linestyle='--', zorder=1) |
|
|
ax.set_ylabel('Vertical Release (ft)', fontdict=font_properties_axes) |
|
|
ax.set_xlabel('Horizontal Release (ft)', fontdict=font_properties_axes) |
|
|
ax.set_title(f"{self.sport_id_dict()[sport_id]} - Release Points Catcher Perspective", fontdict=font_properties_titles) |
|
|
|
|
|
|
|
|
ax.get_legend().remove() |
|
|
ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties) |
|
|
ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties) |
|
|
|
|
|
|
|
|
if df['pitcher_hand'][0] == 'L': |
|
|
ax.text(-3.95, 0.05, s='← Glove Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3) |
|
|
ax.text(3.95, 0.05, s='Arm Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3) |
|
|
if df['pitcher_hand'][0] == 'R': |
|
|
ax.invert_xaxis() |
|
|
ax.text(3.95, 0.05, s='← Arm Side', fontstyle='italic', ha='left', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3) |
|
|
ax.text(-3.95, 0.05, s='Glove Side →', fontstyle='italic', ha='right', va='bottom', bbox=dict(facecolor='white', edgecolor='black'), fontsize=16, zorder=3) |
|
|
|
|
|
|
|
|
ax.set_aspect('equal', adjustable='box') |
|
|
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x))) |
|
|
ax.yaxis.set_major_formatter(FuncFormatter(lambda x, _: int(x))) |
|
|
|
|
|
def df_to_polars(self, df_original: pl.DataFrame, pitcher_id: str, start_date: str, end_date: str, batter_hand: list): |
|
|
""" |
|
|
Filters and processes the original DataFrame to a Polars DataFrame. |
|
|
|
|
|
Parameters: |
|
|
df_original (pl.DataFrame): The original DataFrame containing pitch data. |
|
|
pitcher_id (str): The ID of the pitcher. |
|
|
start_date (str): The start date for filtering the data. |
|
|
end_date (str): The end date for filtering the data. |
|
|
batter_hand (list): The list of batter hands (e.g., ['R'] or ['L']). |
|
|
|
|
|
Returns: |
|
|
pl.DataFrame: The filtered and processed Polars DataFrame. |
|
|
""" |
|
|
df = df_original.clone() |
|
|
df = df.filter((pl.col('pitcher_id') == pitcher_id) & |
|
|
(pl.col('is_pitch')) & (pl.col('pitch_type').is_not_null()) & |
|
|
(pl.col('pitch_type') != 'NaN') & |
|
|
(pl.col('game_date') >= start_date) & |
|
|
(pl.col('game_date') <= end_date) & |
|
|
(pl.col('batter_hand').is_in(batter_hand))) |
|
|
df = df.with_columns( |
|
|
prop_percent=(pl.col('is_pitch') / pl.col('is_pitch').sum()).over("pitch_type"), |
|
|
prop=pl.col('is_pitch').sum().over("pitch_type") |
|
|
) |
|
|
return df |
|
|
|
|
|
def final_plot(self, df: pl.DataFrame, pitcher_id: str, plot_picker: str, sport_id: int,game_type: list = ['R']): |
|
|
""" |
|
|
Creates a final plot with player headshot, bio, logo, and pitch movement plots. |
|
|
|
|
|
Parameters: |
|
|
df (pl.DataFrame): The DataFrame containing pitch data. |
|
|
pitcher_id (str): The ID of the pitcher. |
|
|
plot_picker (str): The type of plot to create ('short_form_movement', 'long_form_movement', 'release_point'). |
|
|
sport_id (int): The sport ID to determine the plot title. |
|
|
""" |
|
|
|
|
|
sns.set_theme(style="whitegrid", rc=self.sns_custom_theme()[0]) |
|
|
|
|
|
|
|
|
fig = plt.figure(figsize=(9, 9)) |
|
|
fig.set_facecolor('#ffffff') |
|
|
gs = gridspec.GridSpec(6, 5, figure=fig, height_ratios=[0.001, 5, 30, 7, 2, 0.001], width_ratios=[8.501, 10, 10, 10, 8.501]) |
|
|
gs.update(hspace=0.1, wspace=0.1) |
|
|
|
|
|
ax_headshot = fig.add_subplot(gs[1, 0]) |
|
|
ax_bio = fig.add_subplot(gs[1, 1:4]) |
|
|
ax_logo = fig.add_subplot(gs[1, 4]) |
|
|
|
|
|
|
|
|
start_date = df['game_date'].min() |
|
|
end_date = df['game_date'].max() |
|
|
batter_hand = list(df['batter_hand'].unique()) |
|
|
|
|
|
|
|
|
self.player_headshot(pitcher_id=pitcher_id, ax=ax_headshot, sport_id=sport_id) |
|
|
self.player_bio(pitcher_id=pitcher_id, ax=ax_bio, start_date=start_date, end_date=end_date, batter_hand=batter_hand,game_type=game_type) |
|
|
self.plot_logo(pitcher_id=pitcher_id, ax=ax_logo) |
|
|
|
|
|
|
|
|
ax_main_plot = fig.add_subplot(gs[2, 1:-1]) |
|
|
|
|
|
|
|
|
ax_legend = fig.add_subplot(gs[3, :]) |
|
|
|
|
|
|
|
|
|
|
|
ax_footer = fig.add_subplot(gs[-2, :]) |
|
|
|
|
|
|
|
|
if plot_picker == 'short_form_movement': |
|
|
self.break_plot_big(df, ax_main_plot, sport_id=sport_id) |
|
|
elif plot_picker == 'long_form_movement': |
|
|
self.break_plot_big_long(df, ax_main_plot, sport_id=sport_id) |
|
|
elif plot_picker == 'release_point': |
|
|
self.release_point_plot(df, ax_main_plot, sport_id=sport_id) |
|
|
|
|
|
|
|
|
items_in_order = list(df.sort(by=['prop', 'pitch_type'], descending=[True, True])['pitch_type'].unique(maintain_order=True)) |
|
|
|
|
|
|
|
|
dict_colour, dict_pitch = self.pitch_colours() |
|
|
ordered_colors = [dict_colour[x] for x in items_in_order] |
|
|
items_in_order = [dict_pitch[x] for x in items_in_order] |
|
|
|
|
|
|
|
|
legend_handles = [mlines.Line2D([], [], color=color, marker='o', linestyle='None', markersize=5, label=label) for color, label in zip(ordered_colors, items_in_order)] |
|
|
|
|
|
|
|
|
if len(items_in_order) <= 5: |
|
|
ax_legend.legend(handles=legend_handles, bbox_to_anchor=(0.1, 0, 0.8, 0.7), ncol=5, fancybox=True, loc='center', fontsize=10, framealpha=1.0, markerscale=2, prop={'size': 10}) |
|
|
else: |
|
|
ax_legend.legend(handles=legend_handles, bbox_to_anchor=(0.1, 0, 0.8, 0.7), ncol=5, fancybox=True, loc='center', fontsize=10, framealpha=1.0, markerscale=2, prop={'size': 10}) |
|
|
|
|
|
|
|
|
ax_footer.text(x=0.075, y=0, s='By: Thomas Nestico\n @TJStats', fontname='Calibri', ha='left', fontsize=12, va='bottom') |
|
|
ax_footer.text(x=1-0.075, y=0, s='Data: MLB', ha='right', fontname='Calibri', fontsize=12, va='bottom') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ax_top_border = fig.add_subplot(gs[0, :]) |
|
|
ax_left_border = fig.add_subplot(gs[:, 0]) |
|
|
ax_right_border = fig.add_subplot(gs[:, -1]) |
|
|
ax_bottom_border = fig.add_subplot(gs[-1, :]) |
|
|
|
|
|
|
|
|
ax_top_border.axis('off') |
|
|
ax_left_border.axis('off') |
|
|
ax_right_border.axis('off') |
|
|
ax_bottom_border.axis('off') |
|
|
ax_footer.axis('off') |
|
|
ax_legend.axis('off') |
|
|
|
|
|
|
|
|
|
|
|
fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|