Download app.py from cpflecht/oldham: direct link, hf CLI and curl.
- Browser
- Download file 27.9 kB
-
https://huggingface.co/spaces/cpflecht/oldham/resolve/main/app.py
- Command line
-
hf download hf://spaces/cpflecht/oldham/app.py
-
curl -L -o app.py https://huggingface.co/spaces/cpflecht/oldham/resolve/main/app.py
27.9 kB
| import gradio as gr | |
| import pandas as pd | |
| import numpy as np | |
| import plotly.graph_objects as go | |
| import plotly.express as px | |
| # ============================================================ | |
| # LOAD DATA | |
| # ============================================================ | |
| 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("/", "_") | |
| ) | |
| # ============================================================ | |
| # COLUMN SETUP | |
| # ============================================================ | |
| 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" | |
| # GK-specific scoring columns | |
| ARCHETYPE_COL = "best_gk_archetype" | |
| ARCHETYPE_SCORE_COL = "best_gk_archetype_score" | |
| GK_SCORE_COL = "gk_score" | |
| # GK category scores (gk_cat_*) | |
| 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", | |
| ] | |
| # GK archetype scores | |
| SCORING_METRICS = [ | |
| "shot_stopper_score", | |
| "sweeper_keeper_score", | |
| "ball_playing_gk_score", | |
| "organiser_score", | |
| "best_gk_archetype", | |
| "best_gk_archetype_score", | |
| "gk_score", | |
| ] | |
| # Key per-90 / ratio metrics shown in tables and profile | |
| 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 uses the GK category scores | |
| 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 bar chart metrics | |
| 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 calculator — weights map to these columns | |
| 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", | |
| ] | |
| # ============================================================ | |
| # CLEANING HELPERS | |
| # ============================================================ | |
| 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) | |
| # Coerce numeric columns | |
| _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") | |
| # ============================================================ | |
| # DROPDOWN OPTIONS | |
| # ============================================================ | |
| 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 = [] | |
| # ============================================================ | |
| # PLAYER SEARCH | |
| # ============================================================ | |
| 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) | |
| # ============================================================ | |
| # PLAYER PROFILE | |
| # ============================================================ | |
| 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) | |
| # ============================================================ | |
| # RADAR CHART | |
| # ============================================================ | |
| 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 | |
| # ============================================================ | |
| # PERCENTILE BARS | |
| # ============================================================ | |
| 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 | |
| # ============================================================ | |
| # PERFORMANCE OVER TIME | |
| # ============================================================ | |
| 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 | |
| # ============================================================ | |
| # PLAYER COMPARISON | |
| # ============================================================ | |
| 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 | |
| # ============================================================ | |
| # FIT SCORE CALCULATOR | |
| # ============================================================ | |
| 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) | |
| # ============================================================ | |
| # SIMILAR PLAYER FINDER | |
| # ============================================================ | |
| 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) | |
| # ============================================================ | |
| # SHORTLIST | |
| # ============================================================ | |
| 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 | |
| # ============================================================ | |
| # SCOUTING REPORT EXPORT | |
| # ============================================================ | |
| 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 | |
| # ============================================================ | |
| # APP LAYOUT | |
| # ============================================================ | |
| 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() |