CoolFace
Apppublic

TJStatsApps/mlb_spring_statcast_cards

sourceHugging Facemitupdated 2y agoView on Hugging Face
0likes
app.py863 linesDownload Raw Back to root
1import polars as pl2import api_scraper3import pandas as pd4scrape = api_scraper.MLB_Scrape()5 6# import df_update7# update = df_update.df_update()8from matplotlib.colors import LinearSegmentedColormap, Normalize9import numpy as np10import requests11from io import BytesIO12from PIL import Image13from matplotlib.gridspec import GridSpec14 15import matplotlib.pyplot as plt16import matplotlib.patches as patches17import PIL18 19level_dict =  {'1':'MLB',20               '11':'AAA',21               '14':'A',}22 23 24 25def player_bio(pitcher_id: str, ax: plt.Axes, sport_id: int, year_input: int):26    """27    Display the player's bio information on the given axis.28    Parameters29    ----------30    pitcher_id : str31        The player's ID.32    ax : plt.Axes33        The axis to display the bio information on.34    sport_id : int35        The sport ID (1 for MLB, other for minor leagues).36    year_input : int37        The season year.38    """39    # Construct the URL to fetch player data40    url = f"https://statsapi.mlb.com/api/v1/people?personIds={pitcher_id}&hydrate=currentTeam"41 42    # Send a GET request to the URL and parse the JSON response43    data = requests.get(url).json()44 45    # Extract player information from the JSON data46    player_name = data['people'][0]['fullName']47    position = data['people'][0]['primaryPosition']['abbreviation']48    bat_side = data['people'][0]['batSide']['code']49    pitcher_hand = data['people'][0]['pitchHand']['code']50    age = data['people'][0]['currentAge']51    height = data['people'][0]['height']52    weight = data['people'][0]['weight']53 54    # Display the player's name, handedness, age, height, and weight on the axis55    ax.text(0.5, 1, f'{player_name}', va='top', ha='center', fontsize=30)56    ax.text(0.5, 0.65, f'{position}, B/T: {bat_side}/{pitcher_hand}, Age: {age}, {height}/{weight}', va='top', ha='center', fontsize=20)57    if position == 'P':58        ax.text(0.5, 0.38, f'Season Pitching Percentiles', va='top', ha='center', fontsize=16)59    else:60        ax.text(0.5, 0.41, f'Season Batting Percentiles', va='top', ha='center', fontsize=16)61 62    # Make API call to retrieve sports information63    response = requests.get(url='https://statsapi.mlb.com/api/v1/sports').json()64    65    # Convert the JSON response into a Polars DataFrame66    df_sport_id = pl.DataFrame(response['sports'])    67    abb = df_sport_id.filter(pl.col('id') == sport_id)['abbreviation'][0]68 69    # Display the season and sport abbreviation70    ax.text(0.5, 0.20, f'{year_input} {abb} Spring Training', va='top', ha='center', fontsize=14, fontstyle='italic')71 72    # Turn off the axis73    ax.axis('off')74 75 76df_teams = scrape.get_teams()77team_dict = dict(zip(df_teams['team_id'],df_teams['parent_org_abbreviation']))78 79 80# List of MLB teams and their corresponding ESPN logo URLs81mlb_teams = [82    {"team": "AZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/ari.png&h=500&w=500"},83    {"team": "ATH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/oak.png&h=500&w=500"},84    {"team": "ATL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/atl.png&h=500&w=500"},85    {"team": "BAL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bal.png&h=500&w=500"},86    {"team": "BOS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/bos.png&h=500&w=500"},87    {"team": "CHC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chc.png&h=500&w=500"},88    {"team": "CWS", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/chw.png&h=500&w=500"},89    {"team": "CIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cin.png&h=500&w=500"},90    {"team": "CLE", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/cle.png&h=500&w=500"},91    {"team": "COL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/col.png&h=500&w=500"},92    {"team": "DET", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/det.png&h=500&w=500"},93    {"team": "HOU", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/hou.png&h=500&w=500"},94    {"team": "KC", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/kc.png&h=500&w=500"},95    {"team": "LAA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/laa.png&h=500&w=500"},96    {"team": "LAD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/lad.png&h=500&w=500"},97    {"team": "MIA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mia.png&h=500&w=500"},98    {"team": "MIL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/mil.png&h=500&w=500"},99    {"team": "MIN", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/min.png&h=500&w=500"},100    {"team": "NYM", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nym.png&h=500&w=500"},101    {"team": "NYY", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/nyy.png&h=500&w=500"},102    {"team": "PHI", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/phi.png&h=500&w=500"},103    {"team": "PIT", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/pit.png&h=500&w=500"},104    {"team": "SD", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sd.png&h=500&w=500"},105    {"team": "SF", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sf.png&h=500&w=500"},106    {"team": "SEA", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/sea.png&h=500&w=500"},107    {"team": "STL", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/stl.png&h=500&w=500"},108    {"team": "TB", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tb.png&h=500&w=500"},109    {"team": "TEX", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tex.png&h=500&w=500"},110    {"team": "TOR", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/tor.png&h=500&w=500"},111    {"team": "WSH", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/mlb/500/scoreboard/wsh.png&h=500&w=500"},112    {"team": "ZZZ", "logo_url": "https://a.espncdn.com/combiner/i?img=/i/teamlogos/leagues/500/mlb.png&w=500&h=500"}   113]114 115df_image = pd.DataFrame(mlb_teams)116image_dict = df_image.set_index('team')['logo_url'].to_dict()117image_dict_flip = df_image.set_index('logo_url')['team'].to_dict()118 119# level_dict =  {'1':'MLB',120#                '11':'AAA'}121 122level_dict =  {'1':'MLB',123               # '11':'AAA',124               # '14':'A (FSL)'125              }126 127 128level_dict_file =  {'1':'mlb',129               # '11':'aaa',130               # '14':'a'131                   }132 133 134 135year_list = [2025]136 137 138from shiny import App, reactive, ui, render139from shiny.ui import h2, tags140 141# Define the UI layout for the app142app_ui = ui.page_fluid(143 144 145    ui.tags.div(146         {"style": "width:90%;margin: 0 auto;max-width: 1600px;"},147        ui.tags.style(148            """149            h4 {150                margin-top: 1em;font-size:35px;151            }152            h2{153                font-size:25px;154            }155            """156         ),157 158    ui.tags.h4("TJStats"),159    ui.tags.i("Baseball Analytics and Visualizations"),160    ui.markdown("""<a href='https://x.com/TJStats'>Follow me on Twitter</a><sup>1</sup>"""),161    ui.markdown("""<a href='https://www.patreon.com/tj_stats'>Support me on Patreon for Access to 2024 Apps</a><sup>1</sup>"""),162 163    ui.markdown("### MiLB Statcast Batting Summaries"),164    ui.markdown("""This Shiny App allows you to generate Baseball Savant-style percentile bars for MiLB players in the 2024 Season. 165                Currently, MiLB Statcast is only available for AAA and A (Florida State League) levels."""),166 167    ui.layout_sidebar(168        ui.panel_sidebar(169            # Row for selecting season and level170            ui.row(171                ui.column(6, ui.input_select('year_input', 'Select Season', year_list, selected=2024)),172                ui.column(6, ui.input_select('level_input', 'Select Level', level_dict)),173            ),174            # Row for the action button to get player list175            ui.row(ui.input_action_button("player_button", "Get Player List", class_="btn-primary")),176            # Row for selecting the player177            ui.row(ui.column(12, ui.output_ui('player_select_ui', 'Select Player'))),178 179            ui.row(180                ui.column(6, ui.input_switch("switch", "Custom Team?", False)),181                ui.column(6, ui.input_select('logo_select', 'Select Custom Logo', image_dict_flip, multiple=False))182            ),183            184            # Row for the action button to generate plot185            ui.row(ui.input_action_button("generate_plot", "Generate Plot", class_="btn-primary")),186            width=3,187        ),188                189        ui.panel_main(190            ui.navset_tab(191                # Tab for game summary plot192                ui.nav("Batter Summary",193                       ui.output_text("status_batter"),194                       ui.output_plot('batter_plot', width='1200px', height='1200px')195                ),196                ui.nav("Pitcher Summary",197                       ui.output_text("status_pitcher"),198                       ui.output_plot('pitcher_plot', width='1200px', height='1200px')199                )200                ,id="tabset"201            )202        )203    )204)205)206 207def server(input, output, session):208    @render.ui209    @reactive.event(input.player_button,input.tabset, ignore_none=False)210    def player_select_ui():211        if input.tabset() == "Batter Summary":212            #Get the list of pitchers for the selected level and season213            # df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input())).filter(214            #     ~pl.col("position").is_in(['P'])).sort("name")215            216            217 218            # Create a dictionary of pitcher IDs and names219            # batter_dict_pos = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['position']))220 221            year = int(input.year_input())222            sport_id = int(input.level_input())223            batter_summary = pl.read_parquet(f"hf://datasets/TJStatsApps/mlb_data/summary/batter_summary_{level_dict_file[str(sport_id)]}_{year}_spring.parquet").sort('batter_name',descending=False)224            batter_summary = batter_summary.filter(pl.col('pa')>0)225            # Map elements in Polars DataFrame from a dictionary226            # batter_summary = batter_summary.with_columns(227                # pl.col("batter_id").map_elements(lambda x: batter_dict_pos.get(x, x)).alias("position")228            # )229 230 231            # batter_dict_pos = dict(zip(batter_summary['batter_id'], batter_summary['batter_name']))232            # Create a dictionary of pitcher IDs and names233            batter_dict = dict(zip(batter_summary['batter_id'], batter_summary['batter_name'] + ' - ' + batter_summary['batter_id']))234            235            # Return a select input for choosing a pitcher236            return ui.input_select("batter_id", "Select Batter", batter_dict, selectize=True)237 238        if input.tabset() == "Pitcher Summary":239            #Get the list of pitchers for the selected level and season240            df_pitcher_info = scrape.get_players(sport_id=int(input.level_input()), season=int(input.year_input())).filter(241                pl.col("position").is_in(['P','TWP'])).sort("name")242            243            244 245            # Create a dictionary of pitcher IDs and names246            batter_dict_pos = dict(zip(df_pitcher_info['player_id'], df_pitcher_info['position']))247 248            year = int(input.year_input())249            sport_id = int(input.level_input())250            batter_summary = pl.read_parquet(f"hf://datasets/TJStatsApps/mlb_data/summary/pitcher_summary_{level_dict_file[str(sport_id)]}_{year}_spring.parquet").sort('pitcher_name',descending=False)251            # Map elements in Polars DataFrame from a dictionary252            batter_summary = batter_summary.with_columns(253                pl.col("pitcher_id").map_elements(lambda x: batter_dict_pos.get(x, x)).alias("position")254            )255 256 257            batter_dict_pos = dict(zip(batter_summary['pitcher_id'], batter_summary['pitcher_name']))258            # Create a dictionary of pitcher IDs and names259            batter_dict = dict(zip(batter_summary['pitcher_id'], batter_summary['pitcher_name'] + ' - ' + batter_summary['position']))260            261            # Return a select input for choosing a pitcher262            return ui.input_select("pitcher_id", "Select Batter", batter_dict, selectize=True)        263 264 265    266    @output267    @render.plot268    @reactive.event(input.generate_plot, ignore_none=False)   269    def batter_plot():     270 271 272        merged_dict = {273            "woba_percent": { "format": '.3f', "percentile_flip": False, "stat_title": "wOBA" },274            "xwoba_percent": { "format": '.3f', "percentile_flip": False, "stat_title": "xwOBA" },275            "launch_speed": { "format": '.1f', "percentile_flip": False, "stat_title": "Average EV"},276            "launch_speed_90": { "format": '.1f', "percentile_flip": False, "stat_title": "90th% EV"},277            "max_launch_speed": { "format": '.1f', "percentile_flip": False, "stat_title": "Max EV"},278            "barrel_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Barrel%" },279            "hard_hit_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Hard-Hit%" },280            "sweet_spot_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "LA Sweet-Spot%" },281            "zone_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Zone%" }, 282            "zone_swing_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Z-Swing%" },283            "chase_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "O-Swing%" },284            "whiff_rate": { "format": '.1%', "percentile_flip": True, "stat_title": "Whiff%" },285            "k_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "K%" },286            "bb_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "BB%" },287            "pull_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Pull%" },288            "pulled_fly_ball_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Pull FB%" },289        }290        # Show progress/loading notification291        with ui.Progress(min=0, max=1) as p:292 293            def draw_baseball_savant_percentiles(new_player_metrics, new_player_percentiles, colors=None,294                                                                                sport_id=None,295                                            year_input=None):296                """297                Draw Baseball Savant-style percentile bars with proper alignment and scaling.298 299                :param new_player_metrics: DataFrame containing new player metrics.300                :param new_player_percentiles: DataFrame containing new player percentiles.301                :param colors: List of colors for bars (optional, red/blue default).302                """303                # Extract player information304                batter_id = new_player_metrics['batter_id'][0]305                player_name = batter_name_id[batter_id]306                stats = [merged_dict[x]['stat_title'] for x in merged_dict.keys()]307 308                # Calculate percentiles and values309                percentiles = [int((1 - x) * 100) if merged_dict[stat]["percentile_flip"] else int(x * 100) for x, stat in zip(new_player_percentiles.select(merged_dict.keys()).to_numpy()[0], merged_dict.keys())]310                percentiles = np.clip(percentiles, 1, 100)311                values = [str(f'{x:{merged_dict[stat]["format"]}}').strip('%') for x, stat in zip(new_player_metrics.select(merged_dict.keys()).to_numpy()[0], merged_dict.keys())]312 313 314 315                # Create a custom colormap316                color_list = ['#3661AD', '#B4CFD1', '#D82129']317                cmap = LinearSegmentedColormap.from_list("custom_cmap", color_list)318                norm = Normalize(vmin=0.1, vmax=0.9)319                norm_percentiles = norm(percentiles / 100)320                colors = [cmap(p) for p in norm_percentiles]321 322                # Figure setup323                num_stats = len(stats)324                bar_height = 4.5325                spacing = 1326                fig_height = (bar_height + spacing) * num_stats327                fig = plt.figure(figsize=(12, 12))328                gs = GridSpec(6, 5, height_ratios=[0.1, 1.5, 0.9, 0.9, 7.6, 0.1], width_ratios=[0.2, 1.5, 7, 1.5, 0.2])329 330                # Define subplots331                ax_title = fig.add_subplot(gs[1, 2])332                ax_table = fig.add_subplot(gs[2, :])333                ax_fv_table = fig.add_subplot(gs[3, :])334                ax_fv_table.axis('off')335                ax = fig.add_subplot(gs[4, :])336                ax_logo = fig.add_subplot(gs[1, 3])337 338                ax.set_xlim(-1, 99)339                ax.set_ylim(-1, 99)340                ax.set_aspect("equal")341                ax.axis("off")342 343                # Draw each bar344                for i, (stat, percentile, value, color) in enumerate(zip(stats, percentiles, values, colors)):345                    y = fig_height - (i + 1) * (bar_height + spacing)346                    ax.add_patch(patches.Rectangle((0, y + bar_height / 4), 100, bar_height / 2, color="#C7DCDC", lw=0))347                    ax.add_patch(patches.Rectangle((0, y), percentile, bar_height, color=color, lw=0))348                    circle_y = y + bar_height - bar_height / 2349                    circle = plt.Circle((percentile, circle_y), bar_height / 2, color=color, ec='white', lw=1.5, zorder=10)350                    ax.add_patch(circle)351                    fs = 14352                    ax.text(percentile, circle_y, f"{percentile}", ha="center", va="center", fontsize=10, color='white', zorder=10, fontweight='bold')353                    ax.text(-5, y + bar_height / 2, stat, ha="right", va="center", fontsize=fs)354                    ax.text(115, y + bar_height / 2, str(value), ha="right", va="center", fontsize=fs, zorder=5)355                    if i < len(stats) and i > 0:356                        ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=-33, xmax=0)357                        ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=100, xmax=115)358 359                # Draw vertical lines for 10%, 50%, and 90% with labels360                for x, label, align, color in zip([10, 50, 90], ["Poor", "Average", "Great"], ['center', 'center', 'center'], color_list):361                    ax.axvline(x=x, ymin=0, ymax=1, color='#FFF', linestyle='-', lw=1, zorder=1, alpha=0.5)362                    ax.text(x, fig_height + 4, label, ha=align, va='center', fontsize=12, fontweight='bold', color=color)363                    triangle = patches.RegularPolygon((x, fig_height + 1), 3, radius=1, orientation=0, color=color, zorder=2)364                    ax.add_patch(triangle)365 366                # # Title367                # ax_title.set_ylim(0, 1)368                # ax_title.text(0.5, 0.5, f"{player_name} - {player_position_dict[batter_id]}\nPercentile Rankings - 2024 AAA", ha="center", va="center", fontsize=24)369                # ax_title.axis("off")370                player_bio(batter_id, ax=ax_title, sport_id=sport_id, year_input=year_input)371 372 373                                # Get team logo URL374                375                # Add team logo376                #response = requests.get(logo_url)377                if input.switch():378                    response = requests.get(input.logo_select())379                else:380                    logo_url = image_dict[team_dict[player_team_dict[batter_id]]]381                    response = requests.get(logo_url)382   383                img = Image.open(BytesIO(response.content))384 385                ax_logo.set_xlim(0, 1.3)386                ax_logo.set_ylim(0, 1)387                ax_logo.imshow(img, extent=[0, 1, 0, 1], origin='upper')                388                ax_logo.axis("off")389                ax.axis('equal')390 391                # Metrics data table392                metrics_data = {393                    "Pitches": new_player_metrics['pitches'][0],394                    "PA": new_player_metrics['pa'][0],395                    "BIP": new_player_metrics['bip'][0],396                    "HR": f"{new_player_metrics['home_run'][0]:.0f}",397                    "AVG": f"{new_player_metrics['avg'][0]:.3f}",398                    "OBP": f"{new_player_metrics['obp'][0]:.3f}",399                    "SLG": f"{new_player_metrics['slg'][0]:.3f}",400                    "OPS": f"{new_player_metrics['obp'][0] + new_player_metrics['slg'][0]:.3f}",401                }402                df_table = pd.DataFrame(metrics_data, index=[0])403                ax_table.axis('off')404                table = ax_table.table(cellText=df_table.values, colLabels=df_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])405                for key, cell in table.get_celld().items():406                    if key[0] == 0:407                        cell.set_text_props(fontweight='bold')408                table.auto_set_font_size(False)409                table.set_fontsize(12)410                table.scale(1, 1.5)411 412                # Additional subplots for spacing413                ax_top = fig.add_subplot(gs[0, :])414                ax_bot = fig.add_subplot(gs[-1, :])415                ax_top.axis('off')416                ax_bot.axis('off')417                ax_bot.text(0.05, 2, "By: Thomas Nestico (@TJStats)", ha="left", va="center", fontsize=14)418                ax_bot.text(0.95, 2, "Data: MLB, Fangraphs", ha="right", va="center", fontsize=14)419                fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)420 421                # Player headshot422                ax_headshot = fig.add_subplot(gs[1, 1])423                try:424                    url = f'https://img.mlbstatic.com/mlb-photos/image/upload/w_640,d_people:generic:headshot:silo:current.png,q_auto:best,f_auto/v1/people/{batter_id}/headshot/silo/current'425                    response = requests.get(url)426                    img = Image.open(BytesIO(response.content))427                    ax_headshot.set_xlim(0, 1.3)428                    ax_headshot.set_ylim(0, 1)429                    ax_headshot.imshow(img, extent=[0.3, 1.3, 0, 1], origin='upper')430                except PIL.UnidentifiedImageError:431                    ax_headshot.axis('off')432                    #return433                ax_headshot.axis('off')434                ax_table.set_title('Season Summary', style='italic')435 436                # Fangraphs scouting grades table437                print(batter_id)438                439                if batter_id not in dict_mlb_fg.keys():440                    ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))441                    return442                df_fv_table = df_prospects[(df_prospects['minorMasterId'] == dict_mlb_fg[batter_id])][['cFV', 'Hit', 'Game', 'Raw', 'Spd', 'Fld']].reset_index(drop=True)443                ax_fv_table.axis('off')444                if df_fv_table.empty:445                    ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))446                    return447                df_fv_table.columns = ['FV', 'Hit', 'Game', 'Raw', 'Spd', 'Fld']448                table_fv = ax_fv_table.table(cellText=df_fv_table.values, colLabels=df_fv_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])449                for key, cell in table_fv.get_celld().items():450                    if key[0] == 0:451                        cell.set_text_props(fontweight='bold')452                table_fv.auto_set_font_size(False)453                table_fv.set_fontsize(12)454                table_fv.scale(1, 1.5)455                ax_fv_table.set_title('Fangraphs Scouting Grades', style='italic')456 457 458 459                #plt.show()460 461 462            def calculate_new_player_percentiles(player_id, new_player_metrics, player_summary_filtered):463                """464                Calculate percentiles for a new player's metrics.465 466                :param player_id: ID of the player.467                :param new_player_metrics: DataFrame containing new player metrics.468                :param player_summary_filtered: Filtered player summary DataFrame.469                :return: DataFrame containing new player percentiles.470                """471                filtered_summary_clone = player_summary_filtered[['batter_id'] + stat_list].filter(pl.col('batter_id') != player_id).clone()472                combined_data = pl.concat([filtered_summary_clone, new_player_metrics], how="vertical").to_pandas()473                combined_percentiles = pl.DataFrame(pd.concat([combined_data['batter_id'], combined_data[stat_list].rank(pct=True)], axis=1))474                new_player_percentiles = combined_percentiles.filter(pl.col('batter_id') == player_id)475                return new_player_percentiles476 477 478 479            p.set(message="Generating plot", detail="This may take a while...")480 481            482            p.set(0.3, "Gathering data...")483 484            # Example: New player's metrics485            year = int(input.year_input())486            sport_id = int(input.level_input())487            batter_id = int(input.batter_id())488 489 490            df_player = scrape.get_players(sport_id=sport_id,season=year,game_type=['S'])491 492 493 494 495 496            497            # batter_name_id = dict(zip(df_player['player_id'],df_player['name']))498            player_team_dict = dict(zip(df_player['player_id'],df_player['team']))499            # player_position_dict = dict(zip(df_player['player_id'],df_player['position']))500 501 502            batter_summary = pl.read_parquet(f"hf://datasets/TJStatsApps/mlb_data/summary/batter_summary_{level_dict_file[str(sport_id)]}_{year}_spring.parquet").sort('batter_name',descending=False)503 504            batter_name_id = dict(zip(batter_summary['batter_id'],batter_summary['batter_name']))505            # player_team_dict = dict(zip(batter_summary['batter_id'],batter_summary['batter_team']))            506            507            df_prospects = pd.read_csv(f'data/prospects/prospects_{year}.csv')508            df_rosters = pd.read_csv(f'data/rosters/fangraphs_rosters_{year}.csv')509            df_small = df_rosters[['minorbamid','minormasterid']].dropna()510            dict_mlb_fg=dict(zip(df_small['minorbamid'].astype(int),df_small['minormasterid']))511 512 513 514 515            batter_summary_filter = batter_summary.filter((pl.col('pa') >= 20) & (pl.col('launch_speed') >= 0))516            stat_list = batter_summary.columns[2:]517            batter_summary_filter_pd = batter_summary_filter.to_pandas()518            new_player_metrics = batter_summary.filter(pl.col('batter_id') == batter_id)[['batter_id'] + stat_list]519 520            # Get percentiles for the new player521            new_player_percentiles = calculate_new_player_percentiles(batter_id, new_player_metrics, batter_summary_filter)522 523            p.set(0.6, "Creating plot...")524            # Draw Baseball Savant-style percentile bars525            draw_baseball_savant_percentiles(new_player_metrics=new_player_metrics, 526                                            new_player_percentiles=new_player_percentiles,527                                            sport_id=sport_id,528                                            year_input=year)529 530    @output531    @render.plot532    @reactive.event(input.generate_plot, ignore_none=False)  533    def pitcher_plot():    534        merged_dict = {535                "avg_start_speed_ff": { "format": '.1f', "percentile_flip": False, "stat_title": "Fastball Velocity" },536                "extension": { "format": '.1f', "percentile_flip": False, "stat_title": "Extension" },537                "woba_percent": { "format": '.3f', "percentile_flip": True, "stat_title": "wOBA" },538                "xwoba_percent": { "format": '.3f', "percentile_flip": True, "stat_title": "xwOBA" },539                "launch_speed": { "format": '.1f', "percentile_flip": True, "stat_title": "Average EV"},540                "barrel_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "Barrel%" },541                "hard_hit_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "Hard-Hit%" },542                "whiff_rate": { "format": '.1%', "percentile_flip": False, "stat_title": "Whiff%" },543                "zone_contact_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "Z-Contact%" }, 544                "zone_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "Zone%" }, 545                "chase_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "O-Swing%" },546                "csw_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "CSW%" },547                "k_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "K%" },548                "bb_percent": { "format": '.1%', "percentile_flip": True, "stat_title": "BB%" },549                "k_minus_bb_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "K - BB%" },550                "ground_ball_percent": { "format": '.1%', "percentile_flip": False, "stat_title": "GB%" },551            }552        553        with ui.Progress(min=0, max=1) as p: 554 555            def draw_baseball_savant_percentiles(new_player_metrics, new_player_percentiles, colors=None,556                                                                                sport_id=None,557                                            year_input=None):558                """559                Draw Baseball Savant-style percentile bars with proper alignment and scaling.560 561                :param new_player_metrics: DataFrame containing new player metrics.562                :param new_player_percentiles: DataFrame containing new player percentiles.563                :param colors: List of colors for bars (optional, red/blue default).564                """565                # Extract player information566                pitcher_id = new_player_metrics['pitcher_id'][0]567                player_name = pitcher_name_id[pitcher_id]568                stats = [merged_dict[x]['stat_title'] for x in merged_dict.keys()]569 570                # Calculate percentiles and values571                percentiles = [int((1 - x) * 100) if merged_dict[stat]["percentile_flip"] else int(x * 100) for x, stat in zip(new_player_percentiles.select(merged_dict.keys()).to_numpy()[0], merged_dict.keys())]572                percentiles = np.clip(percentiles, 1, 100)573                values = [str(f'{x:{merged_dict[stat]["format"]}}').strip('%') for x, stat in zip(new_player_metrics.select(merged_dict.keys()).to_numpy()[0], merged_dict.keys())]574 575                # Get team logo URL576                logo_url = image_dict[team_dict[player_team_dict[pitcher_id]]]577 578                # Create a custom colormap579                color_list = ['#3661AD', '#B4CFD1', '#D82129']580                cmap = LinearSegmentedColormap.from_list("custom_cmap", color_list)581                norm = Normalize(vmin=0.1, vmax=0.9)582                norm_percentiles = norm(percentiles / 100)583                colors = [cmap(p) for p in norm_percentiles]584 585                # Figure setup586                num_stats = len(stats)587                bar_height = 4.4588                spacing = 0.7589                fig_height = (bar_height + spacing) * num_stats590                fig = plt.figure(figsize=(12, 12))591                gs = GridSpec(7, 5, height_ratios=[0.05, 1.5, 0.75, 0.75,0.75, 7.7, 0.1], width_ratios=[0.2, 1.5, 7, 1.5, 0.2])592 593                # Define subplots594                ax_title = fig.add_subplot(gs[1, 2])595                ax_table = fig.add_subplot(gs[2, :])596                ax_fv_table = fig.add_subplot(gs[3, :])597                ax_fv_table.axis('off')598                ax_stuff = fig.add_subplot(gs[4, :])599                ax = fig.add_subplot(gs[5, :])600                ax_logo = fig.add_subplot(gs[1, 3])601 602                ax.set_xlim(-1, 99)603                ax.set_ylim(-1, 99)604                ax.set_aspect("equal")605                ax.axis("off")606 607                # Draw each bar608                for i, (stat, percentile, value, color) in enumerate(zip(stats, percentiles, values, colors)):609                    y = fig_height - (i + 1) * (bar_height + spacing)610                    ax.add_patch(patches.Rectangle((0, y + bar_height / 4), 100, bar_height / 2, color="#C7DCDC", lw=0))611                    ax.add_patch(patches.Rectangle((0, y), percentile, bar_height, color=color, lw=0))612                    circle_y = y + bar_height - bar_height / 2613                    circle = plt.Circle((percentile, circle_y), bar_height / 2, color=color, ec='white', lw=1.5, zorder=10)614                    ax.add_patch(circle)615                    fs = 14616                    ax.text(percentile, circle_y, f"{percentile}", ha="center", va="center", fontsize=10, color='white', zorder=10, fontweight='bold')617                    ax.text(-5, y + bar_height / 2, stat, ha="right", va="center", fontsize=fs)618                    ax.text(115, y + bar_height / 2, str(value), ha="right", va="center", fontsize=fs, zorder=5)619                    if i < len(stats) and i > 0:620                        ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=-33, xmax=0)621                        ax.hlines(y=y + bar_height + spacing / 2, color='#399098', linestyle=(0, (5, 5)), linewidth=1, xmin=100, xmax=115)622 623                # Draw vertical lines for 10%, 50%, and 90% with labels624                for x, label, align, color in zip([10, 50, 90], ["Poor", "Average", "Great"], ['center', 'center', 'center'], color_list):625                    ax.axvline(x=x, ymin=0, ymax=1, color='#FFF', linestyle='-', lw=1, zorder=1, alpha=0.5)626                    ax.text(x, fig_height + 4, label, ha=align, va='center', fontsize=12, fontweight='bold', color=color)627                    triangle = patches.RegularPolygon((x, fig_height + 1), 3, radius=1, orientation=0, color=color, zorder=2)628                    ax.add_patch(triangle)629 630                # # Title631                # ax_title.set_ylim(0, 1)632                # ax_title.text(0.5, 0.5, f"{player_name} - {player_position_dict[pitcher_id]}\nPercentile Rankings - 2024 AAA", ha="center", va="center", fontsize=24)633                # ax_title.axis("off")634                player_bio(pitcher_id, ax=ax_title, sport_id=sport_id, year_input=year_input)635 636                # Add team logo637                #response = requests.get(logo_url)638                #######if input.switch():639                ########    response = requests.get(input.logo_select())640                ######else:641                response = requests.get(logo_url)642                img = Image.open(BytesIO(response.content))643                ax_logo.imshow(img)644                ax_logo.axis("off")645                ax.axis('equal')646                lg_dict = {647                    11:'all',648                    14:10649                    }650                levelt = {651                    11:1,652                    14:4653                    }654 655 656                fg_api = f'https://www.fangraphs.com/api/leaders/minor-league/data?pos=all&level={levelt[sport_id]}&lg={lg_dict[sport_id]}&stats=pit&qual=0&type=2&team=&season=2024&seasonEnd=2024&org=&ind=0&splitTeam=false'657                response = requests.get(fg_api)658                data = response.json()659                df_fg = pl.DataFrame(data)660                if pitcher_id not in dict_mlb_fg.keys():661                    #ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))662                    metrics_data = {663                        "Pitches": new_player_metrics['pitches'][0],664                        "PA": new_player_metrics['pa'][0],665                        "BIP": new_player_metrics['bip'][0],666                        "HR": f"{new_player_metrics['home_run'][0]:.0f}",667                        "K": f"{new_player_metrics['k'][0]:.0f}",668                        "BB": f"{new_player_metrics['bb'][0]:.0f}",669                    }670                else:671                    df_fg_filter = df_fg.filter(pl.col('minormasterid') == dict_mlb_fg[pitcher_id])672                    # Metrics data table673                    metrics_data = {674                        "G": f"{df_fg_filter['G'][0]:.0f}",675                        "IP": f"{df_fg_filter['IP'][0]:.1f}",676                        "Pitches": f"{new_player_metrics['pitches'][0]:.0f}",677                        "PA": f"{df_fg_filter['TBF'][0]:.0f}",678                        "BIP": new_player_metrics['bip'][0],679                        "ERA": f"{df_fg_filter['ERA'][0]:.2f}",680                        "FIP": f"{df_fg_filter['FIP'][0]:.2f}",681                        "WHIP": f"{df_fg_filter['WHIP'][0]:.2f}",682                    }683                df_table = pd.DataFrame(metrics_data, index=[0])684                ax_table.axis('off')685                table = ax_table.table(cellText=df_table.values, colLabels=df_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])686                for key, cell in table.get_celld().items():687                    if key[0] == 0:688                        cell.set_text_props(fontweight='bold')689                table.auto_set_font_size(False)690                table.set_fontsize(12)691                table.scale(1, 1.5)692 693                # Additional subplots for spacing694                ax_top = fig.add_subplot(gs[0, :])695                ax_bot = fig.add_subplot(gs[-1, :])696                ax_top.axis('off')697                ax_bot.axis('off')698                ax_bot.text(0.05, 2, "By: Thomas Nestico (@TJStats)", ha="left", va="center", fontsize=14)699                ax_bot.text(0.95, 2, "Data: MLB, Fangraphs", ha="right", va="center", fontsize=14)700                701 702                # Player headshot703                ax_headshot = fig.add_subplot(gs[1, 1])704                try:705                    url = f'https://img.mlbstatic.com/mlb-photos/image/upload/c_fill,g_auto/w_640/v1/people/{pitcher_id}/headshot/milb/current.png'706                    response = requests.get(url)707                    img = Image.open(BytesIO(response.content))708                    ax_headshot.set_xlim(0, 1)709                    ax_headshot.set_ylim(0, 1)710                    ax_headshot.imshow(img, extent=[0, 1, 0, 1], origin='upper')711                except PIL.UnidentifiedImageError:712                    ax_headshot.axis('off')713                    #return714                ax_headshot.axis('off')715                ax_table.set_title('Season Summary', style='italic')716 717                # Fangraphs scouting grades table718                print(pitcher_id)719                720                if pitcher_id not in dict_mlb_fg.keys():721                    ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))722                    #return723                df_fv_table = df_prospects[(df_prospects['minorMasterId'] == dict_mlb_fg[pitcher_id])][['cFV','FB', 'SL', 'CB', 'CH', 'SPL', 'CT','CMD']].dropna(axis=1).reset_index(drop=True)724                ax_fv_table.axis('off')725                if df_fv_table.empty:726                    ax_fv_table.text(x=0.5, y=0.5, s='No Scouting Data', style='italic', ha='center', va='center', fontsize=20, bbox=dict(facecolor='white', alpha=1, pad=10))727                    #return728                else:729                    df_fv_table.columns = ['FV']+[x.upper() for x in df_fv_table.columns[1:]]730                    table_fv = ax_fv_table.table(cellText=df_fv_table.values, colLabels=df_fv_table.columns, cellLoc='center', loc='bottom', bbox=[0.07, 0, 0.86, 1])731                    for key, cell in table_fv.get_celld().items():732                        if key[0] == 0:733                            cell.set_text_props(fontweight='bold')734                    table_fv.auto_set_font_size(False)735                    table_fv.set_fontsize(12)736                    table_fv.scale(1, 1.5)737                    ax_fv_table.set_title('Fangraphs Scouting Grades', style='italic')738 739 740                # df_stuff_filter = df_stuff.filter(pl.col('pitcher_id')==pitcher_id)  741 742                stuff_table = ax_stuff.table(cellText=[df_stuff_filter['tj_stuff_plus']], 743                                    colLabels=df_stuff_filter['pitch_type'], 744                                    cellLoc='center', 745                                    loc='center', bbox=[0.07, 0, 0.86, 1])746                stuff_table.auto_set_font_size(False)747                stuff_table.set_fontsize(12)748                stuff_table.scale(1, 1.5)749                ax_stuff.axis('off')750                ax_stuff.set_title('tjStuff+', style='italic')751                for key, cell in stuff_table.get_celld().items():752                    if key[0] == 0:753                        cell.set_text_props(fontweight='bold')754 755                # Color the stuff_table values based on the cmap defined756                for (i, j), cell in stuff_table.get_celld().items():757                    if i == 0:758                        cell.set_text_props(fontweight='bold')759                    else:760                        norm = Normalize(vmin=90, vmax=110)761                        value = float(cell.get_text().get_text())762                        color = cmap(norm(value))763                        cell.set_facecolor(color)764                        #cell.set_text_props(color='white' if value < 100 else 'black')765 766 767 768 769 770                fig.subplots_adjust(left=0.01, right=0.99, top=0.99, bottom=0.01)771 772 773 774 775 776            def calculate_new_player_percentiles(player_id, new_player_metrics, player_summary_filtered):777                """778                Calculate percentiles for a new player's metrics.779 780                :param player_id: ID of the player.781                :param new_player_metrics: DataFrame containing new player metrics.782                :param player_summary_filtered: Filtered player summary DataFrame.783                :return: DataFrame containing new player percentiles.784                """785                filtered_summary_clone = player_summary_filtered[['pitcher_id'] + stat_list].filter(pl.col('pitcher_id') != player_id).clone()786                combined_data = pl.concat([filtered_summary_clone, new_player_metrics], how="vertical").to_pandas()787                combined_percentiles = pl.DataFrame(pd.concat([combined_data['pitcher_id'], combined_data[stat_list].rank(pct=True)], axis=1))788                new_player_percentiles = combined_percentiles.filter(pl.col('pitcher_id') == player_id)789                return new_player_percentiles790 791            p.set(message="Generating plot", detail="This may take a while...")792 793            794            p.set(0.3, "Gathering data...")795 796 797            df_teams = scrape.get_teams()798            team_dict = dict(zip(df_teams['team_id'],df_teams['parent_org_abbreviation']))799 800            # Example: New player's metrics801            # Example: New player's metrics802            year = int(input.year_input())803            sport_id = int(input.level_input())804            pitcher_id = int(input.pitcher_id())805 806            df_player = scrape.get_players(sport_id=sport_id,season=2024)807            pitcher_name_id = dict(zip(df_player['player_id'],df_player['name']))808            player_team_dict = dict(zip(df_player['player_id'],df_player['team']))809            player_position_dict = dict(zip(df_player['player_id'],df_player['position']))810            player_position_dict = dict(zip(df_player['player_id'],df_player['position']))811 812 813 814 815            pitcher_summary = pl.read_parquet(f"hf://datasets/TJStatsApps/mlb_data/summary/pitcher_summary_{level_dict_file[str(sport_id)]}_{year}_spring.parquet").sort('batter_name',descending=False)816            df_prospects = pd.read_csv(f'data/prospects/prospects_{year}.csv')817            df_rosters = pd.read_csv(f'data/rosters/fangraphs_rosters_{year}.csv')818            df_small = df_rosters[['minorbamid','minormasterid']].dropna()819            dict_mlb_fg=dict(zip(df_small['minorbamid'].astype(int),df_small['minormasterid']))820 821            df_stuff = pl.read_csv(f'data/stuff/stuff_{level_dict_file[str(sport_id)]}_{year}.csv')822            # Filter out the "All" row823            filtered_df = df_stuff.filter(pl.col("pitch_type") != "All")824 825            filtered_all_df = df_stuff.filter(pl.col("pitch_type") == "All")826            # Calculate total pitches for each pitcher and proportion of each pitch type827            result_df = (828                filtered_df829                .with_columns([830                    # Total pitches for each pitcher831                    pl.col("pitches").sum().over("pitcher_id").alias("total_pitches"),832                    # Proportion of pitches833                    (pl.col("pitches") / pl.col("pitches").sum().over("pitcher_id")).alias("pitch_proportion"),834                ])835            ).filter(pl.col("pitch_proportion") > 0.05)836 837            df_stuff = pl.concat([filtered_all_df.with_columns(838                [pl.col("pitches").sum().over("pitcher_id").alias("total_pitches"),839                (pl.col("pitches") / pl.col("pitches").sum().over("pitcher_id")).alias("pitch_proportion")]840            ), result_df])841 842 843 844 845            df_stuff_filter = df_stuff.filter(pl.col('pitcher_id')==pitcher_id)  846 847            pitcher_summary_filter = pitcher_summary.filter((pl.col('pa') >= 300) & (pl.col('launch_speed') >= 0))848            stat_list = pitcher_summary.columns[2:]849            pitcher_summary_filter_pd = pitcher_summary_filter.to_pandas()850            new_player_metrics = pitcher_summary.filter(pl.col('pitcher_id') == pitcher_id)[['pitcher_id'] + stat_list]851 852            # Get percentiles for the new player853            new_player_percentiles = calculate_new_player_percentiles(pitcher_id, new_player_metrics, pitcher_summary_filter)854 855            p.set(0.6, "Creating plot...")856            # Draw Baseball Savant-style percentile bars857            draw_baseball_savant_percentiles(new_player_metrics=new_player_metrics, 858                                            new_player_percentiles=new_player_percentiles,859                                            sport_id=sport_id,860                                            year_input=year)861 862 863app = App(app_ui, server)