CoolFace
Apppublic

lcavana2/NFL_Combine_Performance_Analytics

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
app.py771 linesDownload Raw Back to root
1import string2from pathlib import Path3 4import gradio as gr5import matplotlib.pyplot as plt6import numpy as np7import pandas as pd8import seaborn as sns9from scipy.spatial.distance import euclidean10from sklearn.linear_model import LinearRegression11from sklearn.model_selection import train_test_split12 13 14APP_DIR = Path(__file__).parent15DATA_DIRS = [APP_DIR, APP_DIR / "data"]16 17COMBINE_FILE = "NFL_Combine_Since_2000.csv"18WEEKLY_OFFENSE_FILE = "weekly_player_stats_offense.csv"19YEARLY_OFFENSE_FILE = "yearly_player_stats_offense.csv"20 21METRICS = ["40-yd Dash", "Vertical Jump", "Broad Jump", "Height", "Weight"]22CORE_EVENTS = ["40-yd Dash", "Vertical Jump", "Broad Jump"]23NFL_PERFORMANCE_METRICS = [24    "career_fantasy_points_standard",25    "career_fantasy_points_half_ppr",26    "career_fantasy_points_ppr",27    "best_single_season_ppr",28    "avg_ppr_per_season",29    "career_length_seasons",30]31METRIC_LABEL_MAP = {32    "career_fantasy_points_standard": "Career Fantasy Points (Std)",33    "career_fantasy_points_half_ppr": "Career Fantasy Points (Half PPR)",34    "career_fantasy_points_ppr": "Career Fantasy Points (PPR)",35    "best_single_season_ppr": "Best Single Season (PPR)",36    "avg_ppr_per_season": "Avg. PPR Per Season",37    "career_length_seasons": "Career Length (Seasons)",38}39 40 41def find_data_file(filename):42    for directory in DATA_DIRS:43        candidate = directory / filename44        if candidate.exists():45            return candidate46    return None47 48 49def clean_name(name):50    if isinstance(name, str):51        name = name.lower()52        name = name.translate(str.maketrans("", "", string.punctuation))53        return name.strip()54    return name55 56 57def load_and_prepare_data():58    required_files = {59        COMBINE_FILE: find_data_file(COMBINE_FILE),60        WEEKLY_OFFENSE_FILE: find_data_file(WEEKLY_OFFENSE_FILE),61        YEARLY_OFFENSE_FILE: find_data_file(YEARLY_OFFENSE_FILE),62    }63    missing_files = [filename for filename, path in required_files.items() if path is None]64    if missing_files:65        return None, missing_files66 67    df = pd.read_csv(required_files[COMBINE_FILE])68    weekly_offense = pd.read_csv(required_files[WEEKLY_OFFENSE_FILE])69    year_offense = pd.read_csv(required_files[YEARLY_OFFENSE_FILE])70 71    df["cleaned_player_name"] = df["Player"].apply(clean_name)72    year_offense["cleaned_player_name"] = year_offense["player_name"].apply(clean_name)73    weekly_offense["cleaned_player_name"] = weekly_offense["player_name"].apply(clean_name)74 75    df_percentiles = df.copy()76    for metric in METRICS:77        ascending_bool = metric != "40-yd Dash"78        df_percentiles[f"{metric}_percentile"] = (79            df_percentiles.groupby("Position")[metric].rank(pct=True, ascending=ascending_bool) * 10080        )81 82    percentile_cols = [f"{metric}_percentile" for metric in METRICS]83    df_percentiles["Overall_Combine_Score"] = df_percentiles[percentile_cols].mean(axis=1)84 85    df_rethought_score = df_percentiles.copy()86    core_percentile_cols = [f"{metric}_percentile" for metric in CORE_EVENTS]87    df_rethought_score["valid_core_events_count"] = df_rethought_score[core_percentile_cols].count(axis=1)88    df_rethought_score["Overall_Combine_Score_Rethought"] = np.nan89    eligible_players = df_rethought_score["valid_core_events_count"] > 090    df_rethought_score.loc[eligible_players, "Overall_Combine_Score_Rethought"] = df_rethought_score.loc[91        eligible_players, percentile_cols92    ].mean(axis=1)93    df_rethought_score = df_rethought_score.drop(columns=["valid_core_events_count"])94 95    first_nfl_season_df = year_offense.groupby("cleaned_player_name")["season"].min().reset_index()96    first_nfl_season_df = first_nfl_season_df.rename(columns={"season": "first_nfl_season"})97    df_merged_temp = pd.merge(df_rethought_score, first_nfl_season_df, on="cleaned_player_name", how="inner")98    df_filtered_by_season = df_merged_temp[df_merged_temp["Year"] == df_merged_temp["first_nfl_season"]]99    weekly_active_players = weekly_offense["cleaned_player_name"].unique()100    df_merged_data = df_filtered_by_season[101        df_filtered_by_season["cleaned_player_name"].isin(weekly_active_players)102    ].copy()103 104    career_stats_df = (105        year_offense.groupby("cleaned_player_name")106        .agg(107            career_fantasy_points_standard=("fantasy_points_standard", "sum"),108            career_fantasy_points_half_ppr=("fantasy_points_half_ppr", "sum"),109            career_fantasy_points_ppr=("fantasy_points_ppr", "sum"),110            best_single_season_ppr=("fantasy_points_ppr", "max"),111            avg_ppr_per_season=("fantasy_points_ppr", "mean"),112            career_length_seasons=("season", "nunique"),113        )114        .reset_index()115    )116 117    cols_to_merge = career_stats_df.columns.drop("cleaned_player_name")118    new_cols_to_add = [col for col in cols_to_merge if col not in df_merged_data.columns]119    if new_cols_to_add:120        career_stats_subset = career_stats_df[["cleaned_player_name"] + new_cols_to_add]121        df_merged_data = pd.merge(df_merged_data, career_stats_subset, on="cleaned_player_name", how="left")122 123    df_merged_data["best_single_season_ppr_percentile"] = (124        df_merged_data["best_single_season_ppr"].rank(pct=True, ascending=True) * 100125    )126 127    columns_for_search = [128        "Player",129        "cleaned_player_name",130        "Position",131        "Overall_Combine_Score_Rethought",132        "40-yd Dash_percentile",133        "Vertical Jump_percentile",134        "Broad Jump_percentile",135        "Height_percentile",136        "Weight_percentile",137        "best_single_season_ppr",138        "career_fantasy_points_ppr",139        "best_single_season_ppr_percentile",140    ]141    df_player_search = df_merged_data[columns_for_search].copy()142    df_player_search.drop_duplicates(subset=["cleaned_player_name"], keep="first", inplace=True)143 144    p_correlation_data = df_merged_data[["Overall_Combine_Score_Rethought"] + NFL_PERFORMANCE_METRICS].copy()145    overall_correlation_matrix = p_correlation_data.corr(method="pearson")146    combine_performance_correlations = overall_correlation_matrix.loc[147        ["Overall_Combine_Score_Rethought"], NFL_PERFORMANCE_METRICS148    ]149 150    position_correlations = {}151    for position, group_df in df_merged_data.groupby("Position"):152        if len(group_df) <= 1:153            continue154        group_correlation_data = group_df[["Overall_Combine_Score_Rethought"] + NFL_PERFORMANCE_METRICS].copy()155        group_correlation_data = group_correlation_data.dropna(156            subset=["Overall_Combine_Score_Rethought"] + NFL_PERFORMANCE_METRICS157        )158        if len(group_correlation_data) > 1:159            corr_matrix = group_correlation_data.corr(method="pearson")160            if "Overall_Combine_Score_Rethought" in corr_matrix.index:161                position_correlations[position] = corr_matrix.loc[162                    "Overall_Combine_Score_Rethought", NFL_PERFORMANCE_METRICS163                ]164 165    target_variable = "best_single_season_ppr"166    features = ["Overall_Combine_Score_Rethought"]167    model_data = df_merged_data[features + [target_variable]].copy()168    model_data.dropna(subset=features + [target_variable], inplace=True)169    model = LinearRegression()170    if len(model_data) >= 2:171        x = model_data[features]172        y = model_data[target_variable]173        x_train, _, y_train, _ = train_test_split(x, y, test_size=0.2, random_state=42)174        model.fit(x_train, y_train)175    else:176        model = None177 178    min_players_per_position = 5179    min_avg_metrics_per_player = 3180    percentile_cols_in_merged = [181        col for col in df_merged_data.columns if "_percentile" in col and col != "best_single_season_ppr_percentile"182    ]183    numeric_percentile_cols = [184        col for col in percentile_cols_in_merged if pd.api.types.is_numeric_dtype(df_merged_data[col])185    ]186 187    if not numeric_percentile_cols:188        sufficient_data_positions = df_merged_data["Position"].dropna().unique().tolist()189    else:190        temp_df_merged_data_for_metrics = df_merged_data.copy()191        temp_df_merged_data_for_metrics["temp_valid_metrics_count"] = temp_df_merged_data_for_metrics[192            numeric_percentile_cols193        ].notna().sum(axis=1)194        position_stats = temp_df_merged_data_for_metrics.groupby("Position").agg(195            num_players=("Player", "count"),196            avg_valid_metrics=("temp_valid_metrics_count", "mean"),197        )198        sufficient_data_positions = position_stats[199            (position_stats["num_players"] >= min_players_per_position)200            & (position_stats["avg_valid_metrics"] >= min_avg_metrics_per_player)201        ].index.tolist()202 203    prediction_tab_exclusions = ["C", "FB", "OT", "OL", "P"]204    projection_tool_additions = ["C", "OT", "OL"]205    filtered_prediction_positions = [206        position for position in sufficient_data_positions if position not in prediction_tab_exclusions207    ]208    projection_tool_positions = sorted(209        set(filtered_prediction_positions + [pos for pos in sufficient_data_positions if pos in projection_tool_additions])210    )211    filtered_prediction_positions.sort()212 213    return {214        "df_rethought_score": df_rethought_score,215        "df_merged_data": df_merged_data,216        "df_player_search": df_player_search,217        "combine_performance_correlations": combine_performance_correlations,218        "position_correlations": position_correlations,219        "model": model,220        "filtered_prediction_positions": filtered_prediction_positions,221        "projection_tool_positions": projection_tool_positions,222    }, []223 224 225DATA, MISSING_FILES = load_and_prepare_data()226 227 228def display_overall_correlations():229    correlations = DATA["combine_performance_correlations"].loc[230        "Overall_Combine_Score_Rethought", NFL_PERFORMANCE_METRICS231    ]232    plot_df = correlations.reset_index()233    plot_df.columns = ["Metric", "Correlation"]234    plot_df["Metric"] = plot_df["Metric"].map(METRIC_LABEL_MAP)235 236    fig = plt.figure(figsize=(10, 6))237    fig.patch.set_facecolor("#293c59")238    ax = fig.gca()239    ax.set_facecolor("#293c59")240 241    sns.barplot(x="Metric", y="Correlation", data=plot_df, palette="viridis", hue="Metric", legend=False, ax=ax)242 243    ax.tick_params(colors="white", labelcolor="white")244    ax.xaxis.label.set_color("white")245    ax.yaxis.label.set_color("white")246    ax.title.set_color("white")247    plt.title("Overall Correlation: Combine Score vs. NFL Metrics", color="white")248    plt.xticks(rotation=45, ha="right", color="white")249    plt.yticks(color="white")250    plt.xlabel("NFL Performance Metric", color="white")251    plt.ylabel("Pearson Correlation Coefficient", color="white")252    plt.ylim(-1, 1)253    plt.axhline(0, color="white", linewidth=0.8)254 255    for index, row in plot_df.iterrows():256        offset = 0.02 if row["Correlation"] >= 0 else -0.05257        plt.text(index, row["Correlation"] + offset, f"{row['Correlation']:.2f}", color="white", ha="center")258 259    plt.tight_layout()260    markdown_table = (261        "### Precise Correlation Values:<span style='color: #3385c3;'> "262        "(Overall Combine Score vs. NFL Metrics)</span>\n\n"263        + DATA["combine_performance_correlations"].rename(columns=METRIC_LABEL_MAP).to_markdown(index=True)264    )265    return fig, markdown_table266 267 268def display_position_correlations(position):269    fig = plt.figure(figsize=(10, 6))270    fig.patch.set_facecolor("#293c59")271    ax = fig.gca()272    ax.set_facecolor("#293c59")273 274    position_correlations = DATA["position_correlations"]275    if position in position_correlations and not position_correlations[position].empty:276        plot_df = position_correlations[position].reset_index()277        plot_df.columns = ["Metric", "Correlation"]278        plot_df["Metric"] = plot_df["Metric"].map(METRIC_LABEL_MAP)279 280        sns.lineplot(x="Metric", y="Correlation", data=plot_df, marker="o", color="#3385c3", ax=ax)281 282        ax.tick_params(colors="white", labelcolor="white")283        ax.xaxis.label.set_color("white")284        ax.yaxis.label.set_color("white")285        ax.title.set_color("white")286        plt.title(f"Correlation for {position}", color="white")287        plt.xlabel("NFL Performance Metric", color="white")288        plt.ylabel("Pearson Correlation Coefficient", color="white")289        plt.xticks(rotation=45, ha="right", color="white")290        plt.yticks(color="white")291        plt.ylim(-1, 1)292        plt.axhline(0, color="white", linewidth=0.8)293 294        for index, row in plot_df.iterrows():295            plt.text(index, row["Correlation"] + 0.05, f"{row['Correlation']:.2f}", color="white", ha="center")296 297        plt.tight_layout()298    else:299        ax.text(0.5, 0.5, "Insufficient data", ha="center", va="center", color="white", transform=ax.transAxes)300        ax.axis("off")301    return fig302 303 304def display_home_page_content():305    introduction = """306    <h1 style='color: #00336b;'>Welcome to the NFL Combine Analytics Dashboard!</h1>307    <p style='color: #0f2144;'>This interactive dashboard allows you to explore the relationship between NFL Combine performance and a player's professional career success. Leverage our tools to:</p>308    <ul style='color: #0f2144;'>309      <li><strong style='color: #00336b;'>Player Search:</strong> Look up specific players, view their combine percentile scores, and key NFL career statistics.</li>310      <li><strong style='color: #00336b;'>Predict Player Success:</strong> Input hypothetical combine metrics for a player and predict their best single-season PPR score.</li>311      <li><strong style='color: #00336b;'>Player Projection Tool:</strong> Find real-world players most similar to a hypothetical prospect based on combine percentiles and project their potential.</li>312      <li><strong style='color: #00336b;'>Correlation Analysis:</strong> Dive deep into the correlations between combine metrics and NFL success, both overall and position-specific.</li>313    </ul>314    <h3 style='color: #00336b;'>Understanding the Overall Relationship</h3>315    <p style='color: #0f2144;'>Let's start by examining the overall correlation between our calculated 'Overall Combine Score' and various NFL career performance metrics.</p>316    """317    fig, table_md = display_overall_correlations()318    interpretation = """319    <h3 style='color: #00336b;'>Interpreting Overall Correlations</h3>320    <p style='color: #0f2144;'>The bar chart above and the table below illustrate the Pearson correlation coefficients between a player's 'Overall Combine Score' and several key NFL career performance metrics across all positions. A positive correlation indicates that higher combine scores tend to be associated with higher values in that NFL metric, while a negative correlation suggests the opposite. A correlation close to zero implies a very weak linear relationship.</p>321    <p style='color: #0f2144;'><strong>Key Observations:</strong></p>322    <ul style='color: #0f2144;'>323      <li>We generally observe a <strong style='color: #00336b;'>weak positive correlation</strong> between the 'Overall Combine Score' and NFL career success metrics, such as fantasy points and career length. This suggests that while combine performance might have some bearing on a player's NFL career, it is far from being the sole determinant.</li>324      <li>Metrics like <code>avg_ppr_per_season</code> and <code>best_single_season_ppr</code> show slightly stronger, though still weak, positive correlations compared to total career fantasy points or career length. This may indicate that combine performance is a better indicator of peak individual season performance than overall career longevity or cumulative stats.</li>325      <li>The <strong style='color: #00336b;'>low absolute values</strong> of most correlations, many below 0.2, strongly suggest that many other factors beyond combine athleticism, such as skill development, coaching, scheme fit, injury luck, and college production, play a significant role in a player's NFL success.</li>326    </ul>327    <p style='color: #0f2144;'><strong>Next Steps:</strong></p>328    <p style='color: #0f2144;'>To gain deeper insights, we encourage you to:</p>329    <ul style='color: #0f2144;'>330      <li>Explore the <strong style='color: #00336b;'>'Correlation Analysis' tab</strong> to see how these correlations vary by individual player position.</li>331      <li>Use the <strong style='color: #00336b;'>'Predict Player Success' tab</strong> to see how the model estimates future performance based on combine percentiles.</li>332      <li>Utilize the <strong style='color: #00336b;'>'Player Projection Tool' tab</strong> to find players similar to a hypothetical prospect and learn from their career trajectories.</li>333    </ul>334    """335    return introduction, fig, table_md, interpretation336 337 338def get_raw_value_from_percentile(percentile, metric, position):339    filtered_data = DATA["df_rethought_score"][(DATA["df_rethought_score"]["Position"] == position)][metric].dropna()340    if len(filtered_data) < 2:341        return np.nan342    q = 100 - percentile if metric == "40-yd Dash" else percentile343    return np.percentile(filtered_data, q)344 345 346def update_raw_value_display(percentile, position, metric):347    if percentile is None or not position:348        return ""349    raw_val = get_raw_value_from_percentile(percentile, metric, position)350    if pd.isna(raw_val):351        return f"Insufficient data for {position} {metric}"352    unit = " seconds" if metric == "40-yd Dash" else " inches" if "Jump" in metric or "Height" in metric else " lbs"353    return f"Estimated raw value: {raw_val:.2f}{unit}"354 355 356def predict_player_success(position, *percentiles):357    del position358    valid_percentiles = [p for p in percentiles if p is not None]359    if not valid_percentiles:360        return "Provide at least one percentile."361    if DATA["model"] is None:362        return "Not enough data to train a prediction model."363    score = np.mean(valid_percentiles)364    pred = DATA["model"].predict(pd.DataFrame({"Overall_Combine_Score_Rethought": [score]}))[0]365    return (366        f"### <span style='color: #00336b;'>Predicted Best Season PPR: {pred:.2f}</span><br>"367        f"Based on Overall Combine Score: {score:.2f}"368    )369 370 371def find_similar_players(hypothetical_player_percentiles, position, top_n=5):372    if not hypothetical_player_percentiles:373        return pd.DataFrame()374 375    hypo_valid_percentile_cols = {col: val for col, val in hypothetical_player_percentiles.items() if pd.notna(val)}376    if not hypo_valid_percentile_cols:377        return pd.DataFrame()378 379    filtered_real_players = DATA["df_merged_data"][DATA["df_merged_data"]["Position"] == position].copy()380    real_player_percentile_cols = [col for col in hypo_valid_percentile_cols if col in filtered_real_players.columns]381    if not real_player_percentile_cols:382        return pd.DataFrame()383 384    distances = []385    comparable_players = []386    for _, row in filtered_real_players.iterrows():387        hypo_vector_subset = []388        real_vector_subset = []389        for metric_col in real_player_percentile_cols:390            if pd.notna(row[metric_col]) and pd.notna(hypo_valid_percentile_cols.get(metric_col)):391                hypo_vector_subset.append(hypo_valid_percentile_cols[metric_col])392                real_vector_subset.append(row[metric_col])393 394        if hypo_vector_subset:395            distance = euclidean(np.array(hypo_vector_subset), np.array(real_vector_subset))396            normalized_distance = distance / np.sqrt(len(hypo_vector_subset))397            distances.append(normalized_distance)398            comparable_players.append(row.copy())399 400    if not comparable_players:401        return pd.DataFrame()402 403    comparable_players_df = pd.DataFrame(comparable_players)404    comparable_players_df["similarity_distance"] = distances405    output_columns = (406        ["cleaned_player_name", "Player", "Position"]407        + [col for col in hypothetical_player_percentiles if col in comparable_players_df.columns]408        + ["best_single_season_ppr", "similarity_distance"]409    )410    similar_players = comparable_players_df.sort_values(by="similarity_distance", ascending=True).head(top_n)411    return similar_players[[col for col in output_columns if col in comparable_players_df.columns]]412 413 414def project_player_ppr(415    position,416    dash_40_yd_percentile,417    vertical_jump_percentile,418    broad_jump_percentile,419    height_percentile,420    weight_percentile,421):422    hypothetical_player_percentiles = {423        "40-yd Dash_percentile": dash_40_yd_percentile,424        "Vertical Jump_percentile": vertical_jump_percentile,425        "Broad Jump_percentile": broad_jump_percentile,426        "Height_percentile": height_percentile,427        "Weight_percentile": weight_percentile,428    }429    valid_hypo_percentiles = {k: v for k, v in hypothetical_player_percentiles.items() if v is not None}430    if not valid_hypo_percentiles:431        return "Please provide at least one combine percentile to find similar players."432 433    similar_players_df = find_similar_players(valid_hypo_percentiles, position, top_n=5)434    if similar_players_df.empty:435        return "Sorry, no comparable players found. Please try again."436 437    positions_without_ppr_display = ["C", "OT", "OL"]438    output_text = "#### <span style='color: #00336b;'>Top 5 Most Similar Players:</span>\n"439    for _, row in similar_players_df.iterrows():440        closeness_percentage = max(0, 100 - row["similarity_distance"])441        output_text += (442            f"- <strong style='color: #3385c3;'>{row['Player']}</strong> "443            f"(<span style='color: #0066b3;'>{row['Position']}</span>): "444            f"Closeness: {closeness_percentage:.1f}%"445        )446 447        if row["Position"] not in positions_without_ppr_display:448            output_text += f", Best Season PPR: <span style='color: #3385c3;'>{row.get('best_single_season_ppr', 'N/A')}</span>"449 450        percentile_details = []451        for metric_col in hypothetical_player_percentiles:452            if metric_col in row.index and pd.notna(row[metric_col]):453                metric_name = metric_col.replace("_percentile", "")454                percentile_details.append(455                    f"<span style='color: #788d9e;'>{metric_name}</span>: "456                    f"<span style='color: #3385c3;'>{row[metric_col]:.1f}%</span>"457                )458        if percentile_details:459            output_text += f" (Combine Percentiles: {', '.join(percentile_details)})\n"460        else:461            output_text += "\n"462 463    output_text += (464        "\n<em style='color: #43576f;'>*Note: This tool identifies similar players based on combine percentiles. "465        "The prediction tab offers a generalized estimate from a linear regression model.*</em>"466    )467    return output_text468 469 470def format_number(value):471    return "N/A" if pd.isna(value) else f"{value:.2f}"472 473 474def search_player_info(player_name_input):475    cleaned_input_name = clean_name(player_name_input)476    player_data = DATA["df_player_search"][DATA["df_player_search"]["cleaned_player_name"] == cleaned_input_name]477 478    if player_data.empty:479        return f"Player '{player_name_input}' not found or no combine data available."480 481    player = player_data.iloc[0]482    return f"""483    <div style='border: 2px solid #0066b3; padding: 15px; border-radius: 8px; background-color: #f0f2f5; font-family: sans-serif;'>484      <h3 style='color: #00336b; margin-top: 0;'>Player: {player['Player']}</h3>485      <p style='margin-bottom: 5px;'><strong>Position:</strong> {player['Position']}</p>486      <h4 style='color: #0066b3;'>Combine Performance:</h4>487      <ul style='list-style-type: none; padding: 0;'>488        <li><strong>Overall Combine Score:</strong> {format_number(player['Overall_Combine_Score_Rethought'])}</li>489        <li><strong>40-yd Dash Percentile:</strong> {format_number(player['40-yd Dash_percentile'])}%</li>490        <li><strong>Vertical Jump Percentile:</strong> {format_number(player['Vertical Jump_percentile'])}%</li>491        <li><strong>Broad Jump Percentile:</strong> {format_number(player['Broad Jump_percentile'])}%</li>492        <li><strong>Height Percentile:</strong> {format_number(player['Height_percentile'])}%</li>493        <li><strong>Weight Percentile:</strong> {format_number(player['Weight_percentile'])}%</li>494      </ul>495      <h4 style='color: #0066b3;'>NFL Career Success (PPR):</h4>496      <ul style='list-style-type: none; padding: 0;'>497        <li><strong>Best Single Season PPR:</strong> {format_number(player['best_single_season_ppr'])}</li>498        <li><strong>Best Single Season PPR Percentile:</strong> {format_number(player['best_single_season_ppr_percentile'])}%</li>499        <li><strong>Career Fantasy Points (PPR):</strong> {format_number(player['career_fantasy_points_ppr'])}</li>500      </ul>501    </div>502    """503 504 505custom_theme = gr.themes.Base(506    primary_hue=gr.themes.Color(507        name="primary_blue",508        c50="#e6f0f7",509        c100="#cce0ed",510        c200="#99c2df",511        c300="#66a3d1",512        c400="#3385c3",513        c500="#0066b3",514        c600="#0059a1",515        c700="#004d8f",516        c800="#00407d",517        c900="#00336b",518        c950="#002659",519    ),520    secondary_hue=gr.themes.Color(521        name="secondary_gray",522        c50="#f0f2f5",523        c100="#e1e5ea",524        c200="#c8d0da",525        c300="#aeb9c7",526        c400="#93a3b4",527        c500="#788d9e",528        c600="#5e7284",529        c700="#43576f",530        c800="#293c59",531        c900="#c8d0da",532        c950="#e1e5ea",533    ),534    neutral_hue=gr.themes.Color(535        name="neutral_light_gray",536        c50="#fcfcfc",537        c100="#f5f5f5",538        c200="#e5e5e5",539        c300="#d4d4d4",540        c400="#c4c4c4",541        c500="#b3b3b3",542        c600="#8c8c8c",543        c700="#666666",544        c800="#404040",545        c900="#1a1a1a",546        c950="#0a0a0a",547    ),548    font=gr.themes.GoogleFont("Inter"),549    font_mono=gr.themes.GoogleFont("IBM Plex Mono"),550).set(551    body_background_fill="#e1e5ea",552    background_fill_primary="#f0f2f5",553    background_fill_secondary="#c8d0da",554    border_color_primary="#0066b3",555    color_accent_soft="#3385c3",556    link_text_color="#004d8f",557    button_primary_background_fill="#0066b3",558    button_primary_text_color="#ffffff",559    button_secondary_background_fill="#aeb9c7",560    button_secondary_text_color="#ffffff",561)562 563 564def build_missing_data_app():565    missing_list = "".join(f"<li><code>{filename}</code></li>" for filename in MISSING_FILES)566    message = f"""567    <h1>NFL Combine Analytics</h1>568    <p>This Space is ready, but it needs the source CSV files before the dashboard can run.</p>569    <p>Add these files to the Space root folder or a <code>data/</code> folder:</p>570    <ul>{missing_list}</ul>571    """572    with gr.Blocks(title="NFL Combine Analytics", theme=custom_theme) as missing_data_demo:573        gr.HTML(message)574    return missing_data_demo575 576 577def build_app():578    player_names_for_dropdown = sorted(DATA["df_player_search"]["Player"].dropna().unique().tolist())579    filtered_prediction_positions = DATA["filtered_prediction_positions"]580    projection_tool_positions = DATA["projection_tool_positions"]581 582    with gr.Blocks(title="NFL Combine Analytics", theme=custom_theme) as demo:583        gr.Markdown("<h1 style='color: #00336b;'>NFL Combine Performance Analytics</h1>")584        gr.Markdown(585            "<p style='color: #0f2144;'>Explore NFL combine data, player career stats, correlations, and predicted player success.</p>"586        )587 588        with gr.Tab("Home"):589            intro_md, overall_corr_fig_obj, overall_corr_table_md, interpretation_md = display_home_page_content()590            gr.Markdown(intro_md)591            gr.Plot(overall_corr_fig_obj, label="Overall Correlation: Combine Score vs. NFL Metrics")592            gr.Markdown(overall_corr_table_md)593            gr.Markdown(interpretation_md)594 595        with gr.Tab("Player Search"):596            gr.Markdown("### <span style='color: #00336b;'>Search for a Player's Combine & NFL Stats</span>")597            gr.Markdown(598                "<p style='color: #0f2144;'>Enter a player's name to view combine percentile scores and key NFL career stats.</p>"599            )600            player_name_input = gr.Dropdown(601                choices=player_names_for_dropdown,602                label="Select or Type Player Name",603                allow_custom_value=True,604                filterable=True,605            )606            search_button = gr.Button("Search Player")607            player_search_output = gr.Markdown(label="Player Combine & NFL Stats")608            search_button.click(fn=search_player_info, inputs=player_name_input, outputs=player_search_output)609 610        with gr.Tab("Predict Player Success"):611            gr.Markdown("### <span style='color: #00336b;'>Predict Player Success</span>")612            gr.Markdown(613                "<p style='color: #0f2144;'>Enter a hypothetical player's combine metrics and position to predict best single-season PPR.</p>"614            )615 616            position_input = gr.Dropdown(617                choices=filtered_prediction_positions,618                label="Player Position",619                value=filtered_prediction_positions[0] if filtered_prediction_positions else None,620                interactive=True,621            )622            dash_40_yd_percentile_input = gr.Slider(0, 100, step=1, label="40-yd Dash Percentile (0-100)")623            dash_40_yd_raw_output = gr.Markdown()624            vertical_jump_percentile_input = gr.Slider(0, 100, step=1, label="Vertical Jump Percentile (0-100)")625            vertical_jump_raw_output = gr.Markdown()626            broad_jump_percentile_input = gr.Slider(0, 100, step=1, label="Broad Jump Percentile (0-100)")627            broad_jump_raw_output = gr.Markdown()628            height_percentile_input = gr.Slider(0, 100, step=1, label="Height Percentile (0-100)")629            height_raw_output = gr.Markdown()630            weight_percentile_input = gr.Slider(0, 100, step=1, label="Weight Percentile (0-100)")631            weight_raw_output = gr.Markdown()632 633            dash_40_yd_percentile_input.change(634                fn=lambda p, pos: update_raw_value_display(p, pos, "40-yd Dash"),635                inputs=[dash_40_yd_percentile_input, position_input],636                outputs=dash_40_yd_raw_output,637            )638            vertical_jump_percentile_input.change(639                fn=lambda p, pos: update_raw_value_display(p, pos, "Vertical Jump"),640                inputs=[vertical_jump_percentile_input, position_input],641                outputs=vertical_jump_raw_output,642            )643            broad_jump_percentile_input.change(644                fn=lambda p, pos: update_raw_value_display(p, pos, "Broad Jump"),645                inputs=[broad_jump_percentile_input, position_input],646                outputs=broad_jump_raw_output,647            )648            height_percentile_input.change(649                fn=lambda p, pos: update_raw_value_display(p, pos, "Height"),650                inputs=[height_percentile_input, position_input],651                outputs=height_raw_output,652            )653            weight_percentile_input.change(654                fn=lambda p, pos: update_raw_value_display(p, pos, "Weight"),655                inputs=[weight_percentile_input, position_input],656                outputs=weight_raw_output,657            )658 659            predict_btn = gr.Button("Predict Best Single Season PPR")660            prediction_output = gr.Markdown(label="Prediction Results")661            predict_btn.click(662                fn=predict_player_success,663                inputs=[664                    position_input,665                    dash_40_yd_percentile_input,666                    vertical_jump_percentile_input,667                    broad_jump_percentile_input,668                    height_percentile_input,669                    weight_percentile_input,670                ],671                outputs=prediction_output,672            )673 674        with gr.Tab("Player Projection Tool"):675            gr.Markdown("### <span style='color: #00336b;'>Player Projection Tool</span>")676            gr.Markdown(677                "<p style='color: #0f2144;'>Enter combine metric percentiles to find similar players and estimate comparable outcomes.</p>"678            )679 680            position_input_proj = gr.Dropdown(681                choices=projection_tool_positions,682                label="Player Position",683                value=projection_tool_positions[0] if projection_tool_positions else None,684                interactive=True,685            )686            dash_40_yd_percentile_input_proj = gr.Slider(0, 100, step=1, label="40-yd Dash Percentile (0-100)")687            dash_40_yd_raw_output_proj = gr.Markdown()688            vertical_jump_percentile_input_proj = gr.Slider(0, 100, step=1, label="Vertical Jump Percentile (0-100)")689            vertical_jump_raw_output_proj = gr.Markdown()690            broad_jump_percentile_input_proj = gr.Slider(0, 100, step=1, label="Broad Jump Percentile (0-100)")691            broad_jump_raw_output_proj = gr.Markdown()692            height_percentile_input_proj = gr.Slider(0, 100, step=1, label="Height Percentile (0-100)")693            height_raw_output_proj = gr.Markdown()694            weight_percentile_input_proj = gr.Slider(0, 100, step=1, label="Weight Percentile (0-100)")695            weight_raw_output_proj = gr.Markdown()696 697            dash_40_yd_percentile_input_proj.change(698                fn=lambda p, pos: update_raw_value_display(p, pos, "40-yd Dash"),699                inputs=[dash_40_yd_percentile_input_proj, position_input_proj],700                outputs=dash_40_yd_raw_output_proj,701            )702            vertical_jump_percentile_input_proj.change(703                fn=lambda p, pos: update_raw_value_display(p, pos, "Vertical Jump"),704                inputs=[vertical_jump_percentile_input_proj, position_input_proj],705                outputs=vertical_jump_raw_output_proj,706            )707            broad_jump_percentile_input_proj.change(708                fn=lambda p, pos: update_raw_value_display(p, pos, "Broad Jump"),709                inputs=[broad_jump_percentile_input_proj, position_input_proj],710                outputs=broad_jump_raw_output_proj,711            )712            height_percentile_input_proj.change(713                fn=lambda p, pos: update_raw_value_display(p, pos, "Height"),714                inputs=[height_percentile_input_proj, position_input_proj],715                outputs=height_raw_output_proj,716            )717            weight_percentile_input_proj.change(718                fn=lambda p, pos: update_raw_value_display(p, pos, "Weight"),719                inputs=[weight_percentile_input_proj, position_input_proj],720                outputs=weight_raw_output_proj,721            )722 723            project_btn = gr.Button("Project Similar Players")724            projection_output = gr.Markdown(label="Projection Results")725            project_btn.click(726                fn=project_player_ppr,727                inputs=[728                    position_input_proj,729                    dash_40_yd_percentile_input_proj,730                    vertical_jump_percentile_input_proj,731                    broad_jump_percentile_input_proj,732                    height_percentile_input_proj,733                    weight_percentile_input_proj,734                ],735                outputs=projection_output,736            )737 738        with gr.Tab("Correlation Analysis"):739            gr.Markdown(740                "### <span style='color: #00336b;'>Overall Correlation between Overall Combine Score and NFL Performance Metrics</span>"741            )742            overall_plot_output_fig, overall_table_output = display_overall_correlations()743            gr.Plot(overall_plot_output_fig)744            gr.Markdown(overall_table_output)745 746            gr.Markdown("### <span style='color: #00336b;'>Position-Specific Correlations</span>")747            position_dropdown = gr.Dropdown(748                choices=filtered_prediction_positions,749                label="Select Position",750                value=filtered_prediction_positions[0] if filtered_prediction_positions else None,751            )752            position_corr_plot_output = gr.Plot(753                label="Position Correlation",754                value=display_position_correlations(filtered_prediction_positions[0])755                if filtered_prediction_positions756                else None,757            )758            position_dropdown.change(759                fn=display_position_correlations,760                inputs=position_dropdown,761                outputs=position_corr_plot_output,762            )763 764    return demo765 766 767demo = build_missing_data_app() if DATA is None else build_app()768 769if __name__ == "__main__":770    demo.launch()771