oldham / app.py
cpflecht's picture
Update app.py
a8d496a verified
Raw
History Blame Contribute Delete
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()