CoolFace
Apppublic

TerryYou/Usage_Location

sourceHugging Faceupdated 27d agoView on Hugging Face
0likes
app.py364 linesDownload Raw Back to root
1import streamlit as st2import pandas as pd3import matplotlib.pyplot as plt4import seaborn as sns5import numpy as np6from matplotlib import patches7from sklearn.covariance import MinCovDet8from scipy.stats import chi29from numpy.linalg import eig10import io11from PIL import Image12 13# Set the page configuration for a wider layout14st.set_page_config(layout="wide")15 16# --- User Login and Authentication ---17def check_password():18    """Returns `True` if the user's password is correct."""19    def password_entered():20        """Checks whether a password entered by the user is correct."""21        if st.session_state["password"] == st.secrets["password"]:22            st.session_state["password_correct"] = True23            st.session_state["page"] = "app"24        else:25            st.session_state["password_correct"] = False26 27    if "password_correct" not in st.session_state:28        st.text_input(29            "Password", type="password", on_change=password_entered, key="password"30        )31        return False32    elif not st.session_state["password_correct"]:33        st.text_input(34            "Password", type="password", on_change=password_entered, key="password"35        )36        st.error("๐Ÿ˜• Password incorrect")37        return False38    else:39        return True40 41@st.cache_data42def load_data():43    """44    Loads the baseball data from a parquet file.45    Caches the data to avoid reloading on every interaction.46    """47    file_path = "mlb_statcast_data_2025-03-18_to_2025-09-28_regular_season.parquet"48    try:49        df = pd.read_parquet(file_path)50    except FileNotFoundError:51        st.error(f"Error: The file '{file_path}' was not found.")52        st.stop()53    return df54 55def categorize_count(df):56    """57    Categorizes the pitch counts into 'Ahead', 'Even', 'Behind', and '0-0'.58    Also adds a boolean column for 2-strike counts.59    """60    ahead_counts = [(0, 1), (0, 2), (1, 2)]61    even_counts = [(1, 1), (2, 2)]62    behind_counts = [(1, 0), (2, 0), (2, 1), (3, 0), (3, 1)]63    two_strike_counts = [(0, 2), (1, 2), (2, 2), (3, 2)]64 65    df["count_category"] = "Unknown"66    df.loc[df[["balls", "strikes"]].apply(tuple, axis=1).isin(even_counts), "count_category"] = "Even"67    df.loc[df[["balls", "strikes"]].apply(tuple, axis=1).isin(ahead_counts), "count_category"] = "Ahead"68    df.loc[df[["balls", "strikes"]].apply(tuple, axis=1).isin(behind_counts), "count_category"] = "Behind"69    df.loc[df[["balls", "strikes"]].apply(tuple, axis=1) == (0, 0), "count_category"] = "0-0"70    df.loc[df["count_category"] == "Unknown", "count_category"] = "0-0"71 72    df["is_2_strikes"] = df[["balls", "strikes"]].apply(tuple, axis=1).isin(two_strike_counts)73    return df74 75def map_pitch_types(df):76    """77    Maps pitch type abbreviations to full names.78    """79    pitch_type_map = {80        "FF": "4-Seam Fastball", "SI": "Sinker", "FC": "Cutter", "CH": "Changeup",81        "FS": "Split-finger", "FO": "Forkball", "SC": "Screwball", "CU": "Curveball",82        "KC": "Knuckle Curve", "CS": "Slow Curve", "SL": "Slider", "ST": "Sweeper",83        "SV": "Slurve", "KN": "Knuckleball", "EP": "Eephus", "PO": "Pitchout", "FA": "Other"84    }85    df["pitch_type"] = df["pitch_type"].map(pitch_type_map).fillna(df["pitch_type"])86    return df87 88def plot_pitch_usage(df, player_name, date_range):89    """90    Plots a grid of pie charts showing pitch usage by count category and batter hand.91    Returns the generated figure.92    """93    fig, axes = plt.subplots(2, 5, figsize=(15, 8))94    pitch_colors = {95        "4-Seam Fastball": "#FF007D", "Sinker": "#98165D", "Cutter": "#BE5FA0",96        "Changeup": "#F79E70", "Split-finger": "#FE6100", "Forkball": "#F08223",97        "Screwball": "#FFB000", "Curveball": "#67E18D", "Knuckle Curve": "#1BB999",98        "Slow Curve": "#376748", "Slider": "#311DB8", "Sweeper": "#59c9eb",99        "Slurve": "#274BFC", "Knuckleball": "#648FFF", "Eephus": "#867A08", "Pitchout": "#472C30", "Other": "#C0C0C0"100    }101    categories = ["0-0", "Ahead", "Behind", "Even", "2 Strikes"]102    custom_order = [103        "4-Seam Fastball", "Sinker", "Cutter", "Changeup", "Split-finger", "Forkball", "Screwball",104        "Curveball", "Knuckle Curve", "Slow Curve", "Slider", "Sweeper", "Slurve", "Knuckleball", "Eephus", "Pitchout", "Other"105    ]106 107    available_pitch_types = df["pitch_type"].unique()108    ordered_pitch_types = [pitch for pitch in custom_order if pitch in available_pitch_types]109 110    for i, batter_hand in enumerate(["L", "R"]):111        hand_df = df[df["stand"] == batter_hand]112        for j, category in enumerate(categories):113            ax = axes[i, j]114 115            if category == "2 Strikes":116                subset = hand_df[hand_df["is_2_strikes"] == True]117            else:118                subset = hand_df[hand_df["count_category"] == category]119 120            if not subset.empty:121                pitch_counts = subset["pitch_type"].value_counts()122                sorted_pitches = pitch_counts.index123                colors = [pitch_colors[p] for p in sorted_pitches if p in pitch_colors]124 125                pitch_counts.plot.pie(126                    autopct='%1.1f%%',127                    colors=colors,128                    ax=ax, labels=None129                )130            else:131                ax.pie([1], colors=['white'], labels=[''])132 133            ax.set_ylabel("")134            ax.set_title(f"{category} vs. {batter_hand}HB")135 136    plt.suptitle(f"{date_range}\n\nPitch Usage by Count for {player_name}", fontsize=16, fontweight="bold", y=1.02)137    fig.text(1.02, 1.02, "Made by Terry",138             fontsize=12, color="gray", weight="bold",139             verticalalignment="top", horizontalalignment="left")140    plt.tight_layout()141 142    handles = [plt.Line2D([0], [0], marker='o', color='w', markerfacecolor=pitch_colors[p], markersize=10)143               for p in ordered_pitch_types]144    plt.legend(handles=handles, labels=ordered_pitch_types, loc='center left', bbox_to_anchor=(1.05, 1.05))145    return fig146 147def plot_robust_mahalanobis_ellipse(ax, data, label, pitch_colors):148    """149    Plots a robust Mahalanobis ellipse for a given pitch type.150    """151    if data.empty or len(data) < 3:152        return153 154    try:155        mcd = MinCovDet().fit(data[["px", "pz"]])156        robust_mean = mcd.location_157        robust_cov = mcd.covariance_158    except ValueError:159        return160 161    eigenvalues, eigenvectors = eig(robust_cov)162    if min(eigenvalues) < 1e-3:163        return164 165    sorted_indices = np.argsort(eigenvalues)[::-1]166    eigenvalues = eigenvalues[sorted_indices]167    eigenvectors = eigenvectors[:, sorted_indices]168 169    chi_square_val = np.sqrt(chi2.ppf(0.68, df=2))170    width, height = 2 * chi_square_val * np.sqrt(eigenvalues)171    angle = np.degrees(np.arctan2(eigenvectors[1, 0], eigenvectors[0, 0]))172 173    mean_x, mean_y = robust_mean174 175    ellipse = patches.Ellipse(176        (mean_x, mean_y), width, height, angle=angle,177        edgecolor=pitch_colors[label],178        facecolor=pitch_colors[label],179        alpha=0.3180    )181    ax.add_patch(ellipse)182 183    mean_point_size = 10 + len(data) * 2184    ax.scatter(mean_x, mean_y, color=pitch_colors[label], s=mean_point_size,185               edgecolor="black", linewidth=1.2)186 187def plot_pitch_locations(df, player_name):188    """189    Plots a grid of scatter plots showing pitch locations by count category and batter hand,190    with robust Mahalanobis ellipses.191    Returns the generated figure.192    """193    df["px"] = df["plate_x"] * -1194    df["pz"] = df["plate_z"]195 196    fig, axes = plt.subplots(2, 5, figsize=(15, 8))197    pitch_colors = {198        "4-Seam Fastball": "#FF007D", "Sinker": "#98165D", "Cutter": "#BE5FA0",199        "Changeup": "#F79E70", "Split-finger": "#FE6100", "Forkball": "#F08223",200        "Screwball": "#FFB000", "Curveball": "#67E18D", "Knuckle Curve": "#1BB999",201        "Slow Curve": "#376748", "Slider": "#311DB8", "Sweeper": "#59c9eb",202        "Slurve": "#274BFC", "Knuckleball": "#648FFF", "Eephus": "#867A08",203        "Pitchout": "#472C30", "Other": "#C0C0C0"204    }205    categories = ["0-0", "Ahead", "Behind", "Even", "2 Strikes"]206 207    used_pitch_types = set()208 209    for i, batter_hand in enumerate(["L", "R"]):210        hand_df = df[df["stand"] == batter_hand]211 212        for j, category in enumerate(categories):213            ax = axes[i, j]214 215            if category == "2 Strikes":216                subset = hand_df[hand_df["is_2_strikes"] == True]217            else:218                subset = hand_df[hand_df["count_category"] == category]219 220            if not subset.empty:221                used_pitch_types.update(subset["pitch_type"].unique())222                for pitch_type, pitch_data in subset.groupby("pitch_type"):223                  plot_robust_mahalanobis_ellipse(ax, pitch_data, pitch_type, pitch_colors)224 225            strike_zone = plt.Rectangle((-0.83, 1.5), 1.66, 2, linewidth=2, edgecolor='black', facecolor='none')226            ax.add_patch(strike_zone)227            ax.set_xlim(-2, 2)228            ax.set_ylim(0, 5)229            ax.set_xticks([])230            ax.set_yticks([])231            ax.set_title(f"{category} vs. {batter_hand}HB")232            ax.set_xlabel("")233            ax.set_ylabel("")234 235    plt.suptitle(f"Pitch Locations by Count for {player_name}", fontsize=16, fontweight="bold", y=1)236    fig.text(0.5, 0, "Pitcher's POV", ha='center', fontsize=14)237 238    used_pitch_types = [p for p in used_pitch_types if p == p]239    custom_order = [240        "4-Seam Fastball", "Sinker", "Cutter", "Changeup", "Split-finger", "Forkball", "Screwball",241        "Curveball", "Knuckle Curve", "Slow Curve", "Slider", "Sweeper", "Slurve", "Knuckleball", "Eephus", "Pitchout", "Other"242    ]243    used_pitch_types = [p for p in custom_order if p in used_pitch_types]244 245    handles = [plt.Line2D([0], [0], marker='o', color='w', markerfacecolor=pitch_colors[p], markersize=10)246               for p in used_pitch_types]247 248    fig.subplots_adjust(right=0.85)249    fig.legend(handles=handles, labels=used_pitch_types, loc='center left', bbox_to_anchor=(1, 0.46))250    plt.tight_layout()251    return fig252 253def main():254    """255    Main function to run the Streamlit application.256    """257    if check_password():258        st.title("2025 MLB Pitch Usage and Locations")259 260        df = load_data()261 262        if "game_date" in df.columns:263            df["game_date"] = pd.to_datetime(df["game_date"])264            start_date = df["game_date"].min()265            end_date = df["game_date"].max()266        else:267            st.error("The 'game_date' column is missing from the dataset.")268            st.stop()269 270        # Get a list of unique player names271        player_names_raw = sorted(df["player_name"].unique())272        # Format "Last, First" to "First Last"273        player_names = [f"{n.split(', ')[1]} {n.split(', ')[0]}" if ", " in n else n for n in player_names_raw]274        player_name_map = {f"{n.split(', ')[1]} {n.split(', ')[0]}" if ", " in n else n: n for n in player_names_raw}275 276        # Add interactive filters277        st.sidebar.header("Filters")278        selected_player_formatted = st.sidebar.selectbox("Select Player:", player_names)279        selected_player_raw = player_name_map[selected_player_formatted]280 281        date_range = st.sidebar.date_input(282            "Select Date Range:",283            value=(start_date, end_date),284            min_value=start_date,285            max_value=end_date286        )287 288        if len(date_range) == 2:289            start_date_filter, end_date_filter = date_range290            df_filtered = df[291                (df["game_date"] >= pd.Timestamp(start_date_filter)) &292                (df["game_date"] <= pd.Timestamp(end_date_filter))293            ]294            date_range_str = f"{start_date_filter.strftime('%Y-%m-%d')} to {end_date_filter.strftime('%Y-%m-%d')}"295        else:296            st.warning("Please select a valid date range.")297            df_filtered = pd.DataFrame() # Create an empty DataFrame298            date_range_str = "Unknown Date Range"299 300        # Filter dataset for the selected player301        df_filtered_player = df_filtered[df_filtered["player_name"] == selected_player_raw]302 303        if df_filtered_player.empty:304            st.warning("No data available for the selected player and date range.")305        else:306            df_processed = categorize_count(df_filtered_player.copy())307            df_processed = map_pitch_types(df_processed)308 309            # --- Displaying original plots ---310            st.subheader("Pitch Usage Analysis")311            usage_fig = plot_pitch_usage(df_processed, selected_player_formatted, date_range_str)312            st.pyplot(usage_fig)313 314            st.subheader("Pitch Location Analysis")315            location_fig = plot_pitch_locations(df_processed, selected_player_formatted)316            st.pyplot(location_fig)317            318            # --- Combine plots and create download button ---319            st.subheader("Combined Plot")320            321            # Create in-memory buffers for each figure322            usage_buf = io.BytesIO()323            location_buf = io.BytesIO()324            325            # Save figures to buffers326            usage_fig.savefig(usage_buf, format='png', bbox_inches='tight')327            location_fig.savefig(location_buf, format='png', bbox_inches='tight')328            329            # Reset buffer pointers330            usage_buf.seek(0)331            location_buf.seek(0)332            333            # Open images from buffers334            usage_img = Image.open(usage_buf)335            location_img = Image.open(location_buf)336            337            # Get dimensions and create a new combined image338            width1, height1 = usage_img.size339            width2, height2 = location_img.size340            341            combined_width = max(width1, width2)342            combined_height = height1 + height2343            344            combined_img = Image.new('RGB', (combined_width, combined_height), 'white')345            346            # Paste images onto the new canvas347            combined_img.paste(usage_img, (0, 0))348            combined_img.paste(location_img, (0, height1))349            350            # Save the combined image to a final buffer351            final_buf = io.BytesIO()352            combined_img.save(final_buf, format='png')353            final_buf.seek(0)354            355            # Create a download button for the final image356            st.download_button(357                label="Save Combined Plots as PNG",358                data=final_buf,359                file_name=f"{selected_player_formatted}_pitch_analysis_combined.png",360                mime="image/png"361            )362 363if __name__ == "__main__":364    main()