import polars as pl import numpy as np import matplotlib.pyplot as plt import seaborn as sns 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 from stuff_model import calculate_arm_angles as caa # Initialize the scraper scraper = api_scraper.MLB_Scrape() class PitchPlotFunctions: # Define the pitch_colours method def pitch_colours(self): # Dictionary of pitch types and their corresponding colors and names 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'}, } # Create dictionaries mapping pitch types to their colors and names 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 # Define the sns_custom_theme method def sns_custom_theme(self): # Custom theme for seaborn plots 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 } # Color palette for the plots colour_palette = ['#FFB000', '#648FFF', '#785EF0', '#DC267F', '#FE6100', '#3D1EB2', '#894D80', '#16AA02', '#B5592B', '#A3C1ED'] return custom_theme, colour_palette # Define the sport_id_dict method def sport_id_dict(self): # Dictionary mapping sport IDs to their names dict = {1:'MLB', 11:'AAA', 12:'AA', 13:'A+', 14:'A', 17:'AFL', 22:'College', 21:'Prospects', 51:'International' } return dict # Define the team_logos method def team_logos(self): # List of MLB teams and their corresponding ESPN logo URLs 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"} ] # Create a DataFrame from the list of dictionaries df_image = pl.DataFrame(mlb_teams) # Set the index to 'team' and convert 'logo_url' to a dictionary image_dict = df_image.select(['team', 'logo_url']).to_dict(as_series=False)['logo_url'] # Convert to the desired dictionary format image_dict = {row['team']: row['logo_url'] for row in df_image.select(['team', 'logo_url']).to_dicts()} return image_dict # Function to get an image from a URL and display it on the given axis 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. """ # Construct the URL for the player's headshot image 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' # Send a GET request to the URL response = requests.get(url) # Open the image from the response content img = Image.open(BytesIO(response.content)) # Display the image on the axis 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') # Turn off the axis ax.axis('off') # Function to display player bio information on the given axis 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' } # Construct the URL to fetch player data url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam" # Send a GET request to the URL and parse the JSON response data = requests.get(url).json() # Extract player information from the JSON data 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'] # Display the player's name, handedness, age, height, and weight on the axis 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) # Determine the batter hand text if batter_hand == ['R']: batter_hand_text = ', vs RHH' elif batter_hand == ['L']: batter_hand_text = ', vs LHH' else: batter_hand_text = '' # Set header 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.text(0.5, 0.40, f'{start_date} to {end_date}{batter_hand_text}', va='top', ha='center', fontsize=12, fontstyle='italic') # Turn off the axis ax.axis('off') # Function to display the team logo on the given axis 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. """ # Construct the URL to fetch player data url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam" # Send a GET request to the URL and parse the JSON response data = requests.get(url).json() # Construct the URL to fetch team data try: url_team = 'https://statsapi.mlb.com/' + data['people'][0]['currentTeam']['link'] # Send a GET request to the team URL and parse the JSON response data_team = requests.get(url_team).json() # Get the logo URL from the image dictionary using the team abbreviation 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" # Send a GET request to the logo URL response = requests.get(logo_url) # Open the image from the response content img = Image.open(BytesIO(response.content)) # Display the image on the axis ax.set_xlim(0, 1) ax.set_ylim(0, 1) ax.imshow(img, extent=[0, 1, 0, 1], origin='upper') # Turn off the axis ax.axis('off') ### PITCH ELLIPSE ### 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]) # 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 = x.mean() # calculating the standard deviation of y ... 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. """ # Set font properties for different elements of the plot font_properties = {'size': 10} font_properties_titles = {'size': 16} font_properties_axes = {'size': 14} # Get unique pitch types sorted by 'prop' and 'pitch_type' 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() # Loop through each pitch type and plot confidence ellipses 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 # Plot scatter plot of pitch data 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) # Set plot limits and labels ax.set_xlim((-25, 25)) ax.set_ylim((-25, 25)) df_aa = caa.calculate_arm_angles(df,df['pitcher_id'][0])['arm_angle'] # Plot average arm angle mean_arm_angle = df_aa.mean() x_end = 30 y_end = x_end * np.tan(np.radians(mean_arm_angle)) ax.plot([0, x_end], [0, y_end], color='grey', linestyle='--', linewidth=2,zorder=0,alpha=0.7) 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(F'Horizontal Break (in)\nArm Angle: {mean_arm_angle:.0f}°', 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) # Remove legend and set tick labels ax.get_legend().remove() ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties) ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties) # Add text annotations based on pitcher hand 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) # Set aspect ratio and format tick labels 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))) ### BREAK PLOT ### 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. """ # Set font properties for different elements of the plot font_properties = {'size': 20} font_properties_titles = {'size': 32} font_properties_axes = {'size': 24} # Get unique pitch types sorted by 'prop' and 'pitch_type' 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 # Loop through each pitch type and plot confidence ellipses 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 # Plot scatter plot of pitch data 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) # Set plot limits and labels 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) # Remove legend and set tick labels ax.get_legend().remove() ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties) ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties) # Add text annotations based on pitcher hand 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) # Set aspect ratio and format tick labels 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))) ### BREAK PLOT ### 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. """ # Set font properties for different elements of the plot 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() # Plot scatter plot of release points based on pitcher hand 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) # Add patches to the plot 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')) # Set plot limits and labels 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) # Remove legend and set tick labels ax.get_legend().remove() ax.set_xticklabels(ax.get_xticks(), fontdict=font_properties) ax.set_yticklabels(ax.get_yticks(), fontdict=font_properties) # Add text annotations based on pitcher hand 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) # Set aspect ratio and format tick labels 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. """ # Set the theme for seaborn plots sns.set_theme(style="whitegrid", rc=self.sns_custom_theme()[0]) # Create a figure and a gridspec with 6 rows and 5 columns 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) # Create subplots for player headshot, bio, and logo 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]) # Get the start and end dates and unique batter hands from the DataFrame start_date = df['game_date'].min() end_date = df['game_date'].max() batter_hand = list(df['batter_hand'].unique()) # Plot player headshot, bio, and logo 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) # Create subplot for the main plot ax_main_plot = fig.add_subplot(gs[2, 1:-1]) # Create subplot for the legend ax_legend = fig.add_subplot(gs[3, :]) # Create subplot for the footer ax_footer = fig.add_subplot(gs[-2, :]) # Plot the selected pitch movement plot 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) # Sort the DataFrame and get unique pitch types items_in_order = list(df.sort(by=['prop', 'pitch_type'], descending=[True, True])['pitch_type'].unique(maintain_order=True)) # Get pitch colors and names 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] # Create custom legend handles with circles legend_handles = [mlines.Line2D([], [], color=color, marker='o', linestyle='None', markersize=5, label=label) for color, label in zip(ordered_colors, items_in_order)] # Add legend to ax_legend 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}) # Add footer text 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') # Create subplots for the borders 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, :]) # Turn off the axes for the border subplots 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') # Adjust layout and show the figure # fig.tight_layout() fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01) # st.pyplot(fig)