| import gradio as gr |
| import pandas as pd |
| import numpy as np |
| import plotly.graph_objects as go |
| import plotly.express as px |
|
|
| |
| |
| |
|
|
| DATA_FILE = "gk_scores_final.csv" |
|
|
| df = pd.read_csv(DATA_FILE) |
|
|
| df.columns = ( |
| df.columns |
| .str.strip() |
| .str.lower() |
| .str.replace(" ", "_") |
| .str.replace("-", "_") |
| .str.replace("/", "_") |
| ) |
|
|
| |
| |
| |
|
|
| PLAYER_COL = "player_name" |
| TEAM_COL = "team_name" |
| COMP_COL = "competition_name" |
| POSITION_COL = "primary_position" |
| AGE_COL = "age" |
| SEASON_COL = "season_name" |
|
|
| HEIGHT_COL = "player_height" |
| MINUTES_COL = "player_season_minutes" |
|
|
| |
| ARCHETYPE_COL = "best_gk_archetype" |
| ARCHETYPE_SCORE_COL = "best_gk_archetype_score" |
| GK_SCORE_COL = "gk_score" |
|
|
| |
| CATEGORY_METRICS = [ |
| "gk_cat_shot_stopping", |
| "gk_cat_sweeping", |
| "gk_cat_short_passing", |
| "gk_cat_long_passing", |
| "gk_cat_ball_claiming", |
| "gk_cat_overall_value", |
| ] |
|
|
| |
| SCORING_METRICS = [ |
| "shot_stopper_score", |
| "sweeper_keeper_score", |
| "ball_playing_gk_score", |
| "organiser_score", |
| "best_gk_archetype", |
| "best_gk_archetype_score", |
| "gk_score", |
| ] |
|
|
| |
| KEY_METRICS = [ |
| "player_season_save_ratio", |
| "player_season_gsaa_90", |
| "player_season_gsaa", |
| "player_season_shots_faced_90", |
| "player_season_goals_faced_90", |
| "player_season_errors_90", |
| "player_season_clcaa", |
| "player_season_da_aggressive_distance", |
| "player_season_passing_ratio", |
| "player_season_long_ball_ratio", |
| "player_season_aerial_ratio", |
| "player_season_obv_gk_90", |
| "player_season_obv_90", |
| "player_season_pressures_90", |
| ] |
|
|
| |
| RADAR_METRICS = [ |
| "gk_cat_shot_stopping", |
| "gk_cat_sweeping", |
| "gk_cat_short_passing", |
| "gk_cat_long_passing", |
| "gk_cat_ball_claiming", |
| "gk_cat_overall_value", |
| ] |
|
|
| |
| PERCENTILE_METRICS = [ |
| "player_season_save_ratio", |
| "player_season_gsaa_90", |
| "player_season_shots_faced_90", |
| "player_season_goals_faced_90", |
| "player_season_errors_90", |
| "player_season_clcaa", |
| "player_season_da_aggressive_distance", |
| "player_season_passing_ratio", |
| "player_season_long_ball_ratio", |
| "player_season_aerial_ratio", |
| "player_season_obv_gk_90", |
| "gk_cat_shot_stopping", |
| "gk_cat_sweeping", |
| "gk_cat_overall_value", |
| ] |
|
|
| |
| FIT_SCORE_METRICS = [ |
| "gk_cat_shot_stopping", |
| "gk_cat_sweeping", |
| "gk_cat_short_passing", |
| "gk_cat_long_passing", |
| "gk_cat_ball_claiming", |
| "gk_cat_overall_value", |
| ] |
|
|
| |
| |
| |
|
|
| def available_cols(cols): |
| return [c for c in cols if c in df.columns] |
|
|
| def label_col(col): |
| return ( |
| col.replace("player_season_", "") |
| .replace("gk_cat_", "") |
| .replace("_90", " per 90") |
| .replace("_", " ") |
| .title() |
| ) |
|
|
| def safe_numeric(data, col): |
| if col in data.columns: |
| return pd.to_numeric(data[col], errors="coerce") |
| return pd.Series(dtype=float) |
|
|
| |
| _numeric_cols = available_cols( |
| KEY_METRICS + CATEGORY_METRICS + SCORING_METRICS + PERCENTILE_METRICS + FIT_SCORE_METRICS |
| ) |
| for col in _numeric_cols: |
| if col != ARCHETYPE_COL: |
| df[col] = pd.to_numeric(df[col], errors="coerce") |
|
|
| if AGE_COL in df.columns: |
| df[AGE_COL] = pd.to_numeric(df[AGE_COL], errors="coerce") |
|
|
| |
| |
| |
|
|
| player_options = sorted(df[PLAYER_COL].dropna().astype(str).unique().tolist()) |
| competition_options = sorted(df[COMP_COL].dropna().astype(str).unique().tolist()) |
| team_options = sorted(df[TEAM_COL].dropna().astype(str).unique().tolist()) |
| position_options = sorted(df[POSITION_COL].dropna().astype(str).unique().tolist()) |
|
|
| age_min = int(np.floor(df[AGE_COL].min())) if AGE_COL in df.columns else 15 |
| age_max = int(np.ceil(df[AGE_COL].max())) if AGE_COL in df.columns else 45 |
|
|
| metric_options = available_cols(KEY_METRICS + CATEGORY_METRICS + SCORING_METRICS) |
|
|
| shortlist = [] |
|
|
| |
| |
| |
|
|
| def search_players(search, competitions, teams, positions, min_age, max_age, min_minutes): |
| data = df.copy() |
|
|
| if search and PLAYER_COL in data.columns: |
| data = data[data[PLAYER_COL].astype(str).str.contains(str(search), case=False, na=False)] |
|
|
| if competitions and COMP_COL in data.columns: |
| data = data[data[COMP_COL].astype(str).isin(competitions)] |
|
|
| if teams and TEAM_COL in data.columns: |
| data = data[data[TEAM_COL].astype(str).isin(teams)] |
|
|
| if positions and POSITION_COL in data.columns: |
| data = data[data[POSITION_COL].astype(str).isin(positions)] |
|
|
| if AGE_COL in data.columns: |
| data[AGE_COL] = pd.to_numeric(data[AGE_COL], errors="coerce") |
| data = data[(data[AGE_COL] >= min_age) & (data[AGE_COL] <= max_age)] |
|
|
| if MINUTES_COL in data.columns: |
| data[MINUTES_COL] = pd.to_numeric(data[MINUTES_COL], errors="coerce") |
| data = data[data[MINUTES_COL].fillna(0) >= min_minutes] |
|
|
| table_cols = available_cols([ |
| PLAYER_COL, TEAM_COL, COMP_COL, POSITION_COL, |
| AGE_COL, MINUTES_COL, |
| ARCHETYPE_COL, ARCHETYPE_SCORE_COL, GK_SCORE_COL, |
| ] + KEY_METRICS) |
|
|
| out = data[table_cols].copy() |
|
|
| if out.empty: |
| return pd.DataFrame({"Message": ["No players found. Try clearing some filters."]}) |
|
|
| if AGE_COL in out.columns: |
| out[AGE_COL] = pd.to_numeric(out[AGE_COL], errors="coerce").round(1) |
|
|
| numeric_cols = out.select_dtypes(include=np.number).columns |
| out[numeric_cols] = out[numeric_cols].round(2) |
|
|
| if GK_SCORE_COL in out.columns: |
| out = out.sort_values(by=GK_SCORE_COL, ascending=False) |
|
|
| return out.reset_index(drop=True) |
|
|
| |
| |
| |
|
|
| def get_player_row(player): |
| rows = df[df[PLAYER_COL].astype(str) == str(player)] |
| if rows.empty: |
| return None |
| return rows.iloc[0] |
|
|
| def player_profile(player): |
| row = get_player_row(player) |
| if row is None: |
| return "Select a player to view their profile." |
|
|
| lines = [] |
| lines.append(f"# {row[PLAYER_COL]}") |
| lines.append(f"### {row.get(TEAM_COL, 'N/A')} | {row.get(COMP_COL, 'N/A')}") |
| lines.append("") |
| lines.append("## Player Details") |
| lines.append(f"- **Position:** {row.get(POSITION_COL, 'N/A')}") |
| lines.append(f"- **Age:** {round(row.get(AGE_COL, np.nan), 1) if pd.notna(row.get(AGE_COL, np.nan)) else 'N/A'}") |
| lines.append(f"- **Height:** {row.get(HEIGHT_COL, 'N/A')} cm") |
| lines.append(f"- **Minutes:** {round(row.get(MINUTES_COL, 0), 0)}") |
| lines.append(f"- **Appearances:** {row.get('player_season_appearances', 'N/A')}") |
| lines.append("") |
| lines.append("## GK Scoring") |
| lines.append(f"- **Best Archetype:** {row.get(ARCHETYPE_COL, 'N/A')}") |
| lines.append(f"- **Best Archetype Score:** {round(row.get(ARCHETYPE_SCORE_COL, np.nan), 2) if pd.notna(row.get(ARCHETYPE_SCORE_COL, np.nan)) else 'N/A'}") |
| lines.append(f"- **Overall GK Score:** {round(row.get(GK_SCORE_COL, np.nan), 2) if pd.notna(row.get(GK_SCORE_COL, np.nan)) else 'N/A'}") |
| lines.append("") |
| lines.append("## Archetype Scores") |
| for col in ["shot_stopper_score", "sweeper_keeper_score", "ball_playing_gk_score", "organiser_score"]: |
| val = row.get(col, np.nan) |
| if col in df.columns and pd.notna(val): |
| lines.append(f"- **{label_col(col.replace('_score', ''))}:** {round(val, 2)}") |
| lines.append("") |
| lines.append("## Key Season Stats") |
| for col in available_cols(KEY_METRICS): |
| value = row.get(col, np.nan) |
| if pd.notna(value): |
| lines.append(f"- **{label_col(col)}:** {round(value, 2)}") |
|
|
| return "\n".join(lines) |
|
|
| def category_table(player): |
| row = get_player_row(player) |
| if row is None: |
| return pd.DataFrame({"Message": ["Select a player."]}) |
|
|
| rows = [] |
| for col in available_cols(CATEGORY_METRICS): |
| value = row.get(col, np.nan) |
| if pd.notna(value): |
| rows.append({ |
| "Category": label_col(col), |
| "Score": round(value, 2) |
| }) |
|
|
| if not rows: |
| return pd.DataFrame({"Message": ["No category data available."]}) |
|
|
| return pd.DataFrame(rows).sort_values("Score", ascending=False) |
|
|
| |
| |
| |
|
|
| def radar_chart(player): |
| row = get_player_row(player) |
| if row is None: |
| return go.Figure() |
|
|
| metrics = available_cols(RADAR_METRICS) |
| if len(metrics) < 3: |
| fig = go.Figure() |
| fig.update_layout(title="Need at least 3 radar metrics.") |
| return fig |
|
|
| comp = row[COMP_COL] |
| group = df[df[COMP_COL] == comp].copy() |
|
|
| labels = [label_col(m) for m in metrics] |
| player_values = [row[m] if pd.notna(row[m]) else 0 for m in metrics] |
| avg_values = [group[m].mean() for m in metrics] |
|
|
| fig = go.Figure() |
| fig.add_trace(go.Scatterpolar(r=player_values, theta=labels, fill="toself", name=str(player))) |
| fig.add_trace(go.Scatterpolar(r=avg_values, theta=labels, fill="toself", name=f"GK Avg in {comp}")) |
|
|
| fig.update_layout( |
| title=f"{player} vs Competition Average", |
| polar=dict(radialaxis=dict(visible=True, range=[0, 100])), |
| showlegend=True |
| ) |
| return fig |
|
|
| |
| |
| |
|
|
| def percentile_chart(player): |
| row = get_player_row(player) |
| if row is None: |
| return go.Figure() |
|
|
| comp = row[COMP_COL] |
| group = df[df[COMP_COL] == comp].copy() |
|
|
| rows = [] |
| for metric in available_cols(PERCENTILE_METRICS): |
| value = row.get(metric, np.nan) |
| values = pd.to_numeric(group[metric], errors="coerce").dropna() |
| if pd.notna(value) and len(values) > 1: |
| pct = (values < value).mean() * 100 |
| rows.append({"Metric": label_col(metric), "Percentile": round(pct, 1), "Value": round(value, 2)}) |
|
|
| if not rows: |
| fig = go.Figure() |
| fig.update_layout(title="No percentile data available.") |
| return fig |
|
|
| plot_df = pd.DataFrame(rows) |
| fig = px.bar( |
| plot_df.sort_values("Percentile"), |
| x="Percentile", y="Metric", orientation="h", |
| hover_data=["Value"], |
| title=f"{player} Percentiles vs Same Competition", |
| range_x=[0, 100] |
| ) |
| fig.update_layout(yaxis_title="", xaxis_title="Percentile") |
| return fig |
|
|
| |
| |
| |
|
|
| def performance_chart(player, metric): |
| row_data = df[df[PLAYER_COL].astype(str) == str(player)].copy() |
|
|
| if row_data.empty or metric not in df.columns: |
| return go.Figure() |
|
|
| if SEASON_COL not in df.columns or row_data[SEASON_COL].nunique() <= 1: |
| fig = go.Figure() |
| fig.add_trace(go.Bar(x=[label_col(metric)], y=[row_data.iloc[0][metric]])) |
| fig.update_layout( |
| title="Only one season in this file — showing single-season value.", |
| yaxis_title=label_col(metric) |
| ) |
| return fig |
|
|
| row_data[metric] = pd.to_numeric(row_data[metric], errors="coerce") |
| fig = px.line(row_data, x=SEASON_COL, y=metric, markers=True, |
| title=f"{player}: {label_col(metric)} Over Time") |
| return fig |
|
|
| |
| |
| |
|
|
| def compare_players(player_1, player_2, player_3): |
| players = [p for p in [player_1, player_2, player_3] if p] |
| if not players: |
| return pd.DataFrame({"Message": ["Select at least one player."]}) |
|
|
| data = df[df[PLAYER_COL].astype(str).isin(players)].copy() |
|
|
| cols = available_cols([ |
| PLAYER_COL, TEAM_COL, COMP_COL, POSITION_COL, |
| AGE_COL, MINUTES_COL, HEIGHT_COL, |
| ARCHETYPE_COL, ARCHETYPE_SCORE_COL, GK_SCORE_COL, |
| "shot_stopper_score", "sweeper_keeper_score", |
| "ball_playing_gk_score", "organiser_score", |
| ] + KEY_METRICS + CATEGORY_METRICS) |
|
|
| out = data[cols].copy() |
| numeric_cols = out.select_dtypes(include=np.number).columns |
| out[numeric_cols] = out[numeric_cols].round(2) |
| return out.reset_index(drop=True) |
|
|
| def comparison_radar(player_1, player_2, player_3): |
| players = [p for p in [player_1, player_2, player_3] if p] |
| metrics = available_cols(RADAR_METRICS) |
| fig = go.Figure() |
|
|
| if not players or len(metrics) < 3: |
| fig.update_layout(title="Select players to compare.") |
| return fig |
|
|
| labels = [label_col(m) for m in metrics] |
| for player in players: |
| row = get_player_row(player) |
| if row is not None: |
| fig.add_trace(go.Scatterpolar( |
| r=[row[m] if pd.notna(row[m]) else 0 for m in metrics], |
| theta=labels, fill="toself", name=str(player) |
| )) |
|
|
| fig.update_layout( |
| title="Side-by-Side Radar Comparison", |
| polar=dict(radialaxis=dict(visible=True, range=[0, 100])), |
| showlegend=True |
| ) |
| return fig |
|
|
| |
| |
| |
|
|
| def fit_score(shot_stopping_w, sweeping_w, short_passing_w, long_passing_w, ball_claiming_w, overall_value_w): |
| weights = { |
| "gk_cat_shot_stopping": shot_stopping_w, |
| "gk_cat_sweeping": sweeping_w, |
| "gk_cat_short_passing": short_passing_w, |
| "gk_cat_long_passing": long_passing_w, |
| "gk_cat_ball_claiming": ball_claiming_w, |
| "gk_cat_overall_value": overall_value_w, |
| } |
|
|
| data = df.copy() |
| total_weight = sum(weights.values()) |
|
|
| if total_weight == 0: |
| return pd.DataFrame({"Message": ["At least one weight must be above 0."]}) |
|
|
| score = 0 |
| for col, weight in weights.items(): |
| if col in data.columns: |
| values = pd.to_numeric(data[col], errors="coerce") |
| min_v, max_v = values.min(), values.max() |
| if pd.notna(min_v) and pd.notna(max_v) and max_v != min_v: |
| normalized = ((values - min_v) / (max_v - min_v)) * 100 |
| else: |
| normalized = values |
| score += normalized.fillna(0) * weight |
|
|
| data["custom_fit_score"] = score / total_weight |
|
|
| cols = available_cols([ |
| PLAYER_COL, TEAM_COL, COMP_COL, AGE_COL, MINUTES_COL, |
| ARCHETYPE_COL, ARCHETYPE_SCORE_COL, GK_SCORE_COL, |
| ]) + ["custom_fit_score"] |
|
|
| out = data[cols].sort_values("custom_fit_score", ascending=False).head(25).copy() |
| numeric_cols = out.select_dtypes(include=np.number).columns |
| out[numeric_cols] = out[numeric_cols].round(2) |
| return out.reset_index(drop=True) |
|
|
| |
| |
| |
|
|
| def similar_players(player): |
| row = get_player_row(player) |
| if row is None: |
| return pd.DataFrame({"Message": ["Select a player."]}) |
|
|
| metrics = available_cols(CATEGORY_METRICS + KEY_METRICS) |
| candidates = df[df[PLAYER_COL].astype(str) != str(player)].copy() |
|
|
| for metric in metrics: |
| candidates[metric] = pd.to_numeric(candidates[metric], errors="coerce") |
| sd = pd.to_numeric(df[metric], errors="coerce").std() |
| player_val = row[metric] if pd.notna(row[metric]) else 0 |
| if pd.isna(sd) or sd == 0: |
| candidates[f"dist_{metric}"] = 0 |
| else: |
| candidates[f"dist_{metric}"] = ((candidates[metric] - player_val) / sd) ** 2 |
|
|
| dist_cols = [f"dist_{m}" for m in metrics] |
| candidates["similarity_distance"] = candidates[dist_cols].sum(axis=1) |
| candidates["similarity_score"] = 100 / (1 + candidates["similarity_distance"]) |
|
|
| cols = available_cols([ |
| PLAYER_COL, TEAM_COL, COMP_COL, AGE_COL, MINUTES_COL, |
| ARCHETYPE_COL, ARCHETYPE_SCORE_COL, GK_SCORE_COL, |
| ]) + ["similarity_score"] |
|
|
| out = candidates[cols].sort_values("similarity_score", ascending=False).head(5).copy() |
| numeric_cols = out.select_dtypes(include=np.number).columns |
| out[numeric_cols] = out[numeric_cols].round(2) |
| return out.reset_index(drop=True) |
|
|
| |
| |
| |
|
|
| def add_to_shortlist(player): |
| global shortlist |
| if player and player not in shortlist: |
| shortlist.append(player) |
| return view_shortlist() |
|
|
| def clear_shortlist(): |
| global shortlist |
| shortlist = [] |
| return view_shortlist() |
|
|
| def view_shortlist(): |
| if not shortlist: |
| return pd.DataFrame({"Message": ["No players added to shortlist yet."]}) |
|
|
| data = df[df[PLAYER_COL].astype(str).isin(shortlist)].copy() |
| cols = available_cols([ |
| PLAYER_COL, TEAM_COL, COMP_COL, POSITION_COL, |
| AGE_COL, MINUTES_COL, |
| ARCHETYPE_COL, ARCHETYPE_SCORE_COL, GK_SCORE_COL, |
| ] + KEY_METRICS) |
| out = data[cols].copy() |
| numeric_cols = out.select_dtypes(include=np.number).columns |
| out[numeric_cols] = out[numeric_cols].round(2) |
| return out.reset_index(drop=True) |
|
|
| def export_shortlist_csv(): |
| if not shortlist: |
| return None |
| data = df[df[PLAYER_COL].astype(str).isin(shortlist)].copy() |
| out_file = "shortlist_export.csv" |
| data.to_csv(out_file, index=False) |
| return out_file |
|
|
| |
| |
| |
|
|
| def export_player_report(player, notes): |
| row = get_player_row(player) |
| if row is None: |
| return None |
|
|
| report = [] |
| report.append(f"GK Scouting Report: {row[PLAYER_COL]}") |
| report.append("=" * 60) |
| report.append("") |
| report.append(f"Club: {row.get(TEAM_COL, 'N/A')}") |
| report.append(f"Competition: {row.get(COMP_COL, 'N/A')}") |
| report.append(f"Position: {row.get(POSITION_COL, 'N/A')}") |
| report.append(f"Age: {round(row.get(AGE_COL, np.nan), 1) if pd.notna(row.get(AGE_COL, np.nan)) else 'N/A'}") |
| report.append(f"Height: {row.get(HEIGHT_COL, 'N/A')} cm") |
| report.append(f"Minutes: {round(row.get(MINUTES_COL, 0), 0)}") |
| report.append("") |
| report.append("GK Scoring") |
| report.append("-" * 60) |
| report.append(f"Best Archetype: {row.get(ARCHETYPE_COL, 'N/A')}") |
| report.append(f"Best Archetype Score: {round(row.get(ARCHETYPE_SCORE_COL, np.nan), 2) if pd.notna(row.get(ARCHETYPE_SCORE_COL, np.nan)) else 'N/A'}") |
| report.append(f"Overall GK Score: {round(row.get(GK_SCORE_COL, np.nan), 2) if pd.notna(row.get(GK_SCORE_COL, np.nan)) else 'N/A'}") |
| report.append("") |
| report.append("Archetype Scores") |
| report.append("-" * 60) |
| for col in ["shot_stopper_score", "sweeper_keeper_score", "ball_playing_gk_score", "organiser_score"]: |
| if col in df.columns: |
| val = row.get(col, np.nan) |
| if pd.notna(val): |
| report.append(f"{label_col(col.replace('_score', ''))}: {round(val, 2)}") |
| report.append("") |
| report.append("Key Season Metrics") |
| report.append("-" * 60) |
| for col in available_cols(KEY_METRICS): |
| value = row.get(col, np.nan) |
| if pd.notna(value): |
| report.append(f"{label_col(col)}: {round(value, 2)}") |
| report.append("") |
| report.append("Category Scores") |
| report.append("-" * 60) |
| for col in available_cols(CATEGORY_METRICS): |
| value = row.get(col, np.nan) |
| if pd.notna(value): |
| report.append(f"{label_col(col)}: {round(value, 2)}") |
| report.append("") |
| report.append("Scout Notes") |
| report.append("-" * 60) |
| report.append(notes if notes else "No notes entered.") |
|
|
| safe_name = str(row[PLAYER_COL]).replace(" ", "_").replace("/", "_") |
| out_file = f"{safe_name}_gk_scouting_report.txt" |
| with open(out_file, "w", encoding="utf-8") as f: |
| f.write("\n".join(report)) |
| return out_file |
|
|
| |
| |
| |
|
|
| with gr.Blocks(title="Oldham Athletic GK Scouting") as app: |
|
|
| gr.Markdown( |
| """ |
| # Oldham Athletic GK Scouting |
| Interactive goalkeeper scouting dashboard powered by StatsBomb data. |
| """ |
| ) |
|
|
| with gr.Tab("Player Search"): |
| gr.Markdown("## Search and Filter Goalkeepers") |
|
|
| with gr.Row(): |
| search_box = gr.Textbox(label="Search Player Name") |
| competition_filter = gr.Dropdown( |
| choices=competition_options, value=[], label="Competition", multiselect=True |
| ) |
| team_filter = gr.Dropdown( |
| choices=team_options, value=[], label="Team", multiselect=True |
| ) |
|
|
| with gr.Row(): |
| position_filter = gr.Dropdown( |
| choices=position_options, value=[], label="Position", multiselect=True |
| ) |
| min_age_filter = gr.Slider(minimum=age_min, maximum=age_max, value=age_min, step=1, label="Minimum Age") |
| max_age_filter = gr.Slider(minimum=age_min, maximum=age_max, value=age_max, step=1, label="Maximum Age") |
| minutes_filter = gr.Slider( |
| minimum=0, |
| maximum=int(df[MINUTES_COL].max()) if MINUTES_COL in df.columns else 3000, |
| value=0, step=100, label="Minimum Minutes" |
| ) |
|
|
| search_button = gr.Button("Search Players") |
| search_results = gr.Dataframe(label="Goalkeeper Results (sorted by GK Score)", interactive=False) |
|
|
| search_button.click( |
| fn=search_players, |
| inputs=[search_box, competition_filter, team_filter, position_filter, |
| min_age_filter, max_age_filter, minutes_filter], |
| outputs=search_results |
| ) |
|
|
| with gr.Tab("Player Profile"): |
| gr.Markdown("## Full GK Profile") |
|
|
| selected_player = gr.Dropdown(player_options, label="Select Player") |
|
|
| with gr.Row(): |
| profile_output = gr.Markdown() |
| category_output = gr.Dataframe(label="GK Category Scores", interactive=False) |
|
|
| with gr.Row(): |
| radar_output = gr.Plot(label="Radar Chart") |
| percentile_output = gr.Plot(label="Percentile Bars") |
|
|
| with gr.Row(): |
| profile_metric = gr.Dropdown( |
| metric_options, |
| value=metric_options[0] if metric_options else None, |
| label="Performance Metric" |
| ) |
| trend_button = gr.Button("Show Performance Chart") |
|
|
| trend_output = gr.Plot(label="Performance Over Time") |
|
|
| scout_notes = gr.Textbox(label="Scout Notes", lines=5, placeholder="Enter notes to include in the scouting report.") |
| report_button = gr.Button("Generate Scouting Report") |
| report_file = gr.File(label="Download Scouting Report") |
|
|
| shortlist_button = gr.Button("Add Player to Shortlist") |
| shortlist_from_profile = gr.Dataframe(label="Current Shortlist", interactive=False) |
|
|
| selected_player.change(player_profile, selected_player, profile_output) |
| selected_player.change(category_table, selected_player, category_output) |
| selected_player.change(radar_chart, selected_player, radar_output) |
| selected_player.change(percentile_chart, selected_player, percentile_output) |
|
|
| trend_button.click(performance_chart, [selected_player, profile_metric], trend_output) |
| report_button.click(export_player_report,[selected_player, scout_notes], report_file) |
| shortlist_button.click(add_to_shortlist, selected_player, shortlist_from_profile) |
|
|
| with gr.Tab("Player Comparison Tool"): |
| gr.Markdown("## Compare Up To Three Goalkeepers") |
|
|
| with gr.Row(): |
| compare_1 = gr.Dropdown(player_options, label="Player 1") |
| compare_2 = gr.Dropdown(player_options, label="Player 2") |
| compare_3 = gr.Dropdown(player_options, label="Player 3") |
|
|
| compare_button = gr.Button("Compare Players") |
| comparison_table = gr.Dataframe(label="Stat Comparison Table", interactive=False) |
| comparison_radar_plot = gr.Plot(label="Side-by-Side Radar Chart") |
|
|
| compare_button.click(compare_players, [compare_1, compare_2, compare_3], comparison_table) |
| compare_button.click(comparison_radar, [compare_1, compare_2, compare_3], comparison_radar_plot) |
|
|
| with gr.Tab("Fit Score Calculator"): |
| gr.Markdown( |
| """ |
| ## Custom GK Fit Score Calculator |
| Weight the GK attributes that matter most to Oldham. |
| The app will rank all goalkeepers based on your custom profile. |
| """ |
| ) |
|
|
| with gr.Row(): |
| shot_stopping_w = gr.Slider(0, 10, value=5, step=1, label="Shot Stopping") |
| sweeping_w = gr.Slider(0, 10, value=5, step=1, label="Sweeping") |
| short_passing_w = gr.Slider(0, 10, value=5, step=1, label="Short Passing") |
|
|
| with gr.Row(): |
| long_passing_w = gr.Slider(0, 10, value=5, step=1, label="Long Passing") |
| ball_claiming_w = gr.Slider(0, 10, value=5, step=1, label="Ball Claiming") |
| overall_value_w = gr.Slider(0, 10, value=5, step=1, label="Overall Value") |
|
|
| fit_button = gr.Button("Generate Ranked Recommendations") |
| fit_table = gr.Dataframe(label="Ranked Recommendations", interactive=False) |
|
|
| fit_button.click( |
| fit_score, |
| [shot_stopping_w, sweeping_w, short_passing_w, long_passing_w, ball_claiming_w, overall_value_w], |
| fit_table |
| ) |
|
|
| with gr.Tab("Similar Player Finder"): |
| gr.Markdown("## Find Similar Goalkeepers") |
|
|
| similar_player_select = gr.Dropdown(player_options, label="Select Player") |
| similar_button = gr.Button("Find Similar Players") |
| similar_table = gr.Dataframe(label="Five Most Similar Goalkeepers", interactive=False) |
|
|
| similar_button.click(similar_players, similar_player_select, similar_table) |
|
|
| with gr.Tab("Shortlist Manager"): |
| gr.Markdown("## Shortlist Manager") |
|
|
| shortlist_player = gr.Dropdown(player_options, label="Add Player") |
| add_shortlist_button = gr.Button("Add to Shortlist") |
| clear_shortlist_button = gr.Button("Clear Shortlist") |
| export_shortlist_button = gr.Button("Export Shortlist CSV") |
|
|
| shortlist_table = gr.Dataframe(label="Saved Players", interactive=False) |
| shortlist_file = gr.File(label="Download Shortlist CSV") |
|
|
| add_shortlist_button.click(add_to_shortlist, shortlist_player, shortlist_table) |
| clear_shortlist_button.click(clear_shortlist, None, shortlist_table) |
| export_shortlist_button.click(export_shortlist_csv, None, shortlist_file) |
|
|
| app.launch() |