Spaces:
Sleeping
Sleeping
import gradio as gr | |
import pandas as pd | |
import itertools | |
import logging | |
# Set up logging for debugging. | |
logging.basicConfig(level=logging.DEBUG, format="%(asctime)s [%(levelname)s] %(message)s") | |
# Global DataFrame variable. | |
df = None | |
def load_excel(file): | |
""" | |
Reads the Excel file and normalizes header names. | |
Expected headers (row 1): "Player", "Role 1", "Role 2", "Role 3", "xB90". | |
Returns six Dropdown components: three for selected roles and three for preferred proposal roles, | |
populated with the union of roles from all three columns (with a blank option prepended). | |
""" | |
global df | |
logging.debug("load_excel: Received file %s", file.name) | |
try: | |
df = pd.read_excel(file.name, header=0) | |
logging.debug("Excel file read successfully") | |
except Exception as e: | |
logging.error("Error reading Excel file: %s", e) | |
empty = gr.Dropdown(choices=[], interactive=True) | |
return empty, empty, empty, empty, empty, empty | |
# Normalize headers. | |
df.columns = [col.strip().lower().replace(" ", "") for col in df.columns] | |
required_cols = ["player", "role1", "role2", "role3", "xb90"] | |
for col in required_cols: | |
if col not in df.columns: | |
logging.error("Missing required column: %s", col) | |
empty = gr.Dropdown(choices=[], interactive=True) | |
return empty, empty, empty, empty, empty, empty | |
# Clean up data. | |
df["player"] = df["player"].astype(str).str.strip() | |
df["role1"] = df["role1"].astype(str).str.strip() | |
df["role2"] = df["role2"].astype(str).str.strip() | |
df["role3"] = df["role3"].astype(str).str.strip() | |
df["xb90"] = pd.to_numeric(df["xb90"], errors="coerce") | |
# Get union of roles from all three columns. | |
roles = pd.concat([df["role1"], df["role2"], df["role3"]]).dropna().unique().tolist() | |
roles = sorted(roles) | |
roles = [""] + roles # Prepend blank. | |
logging.debug("Roles found: %s", roles) | |
return ( | |
gr.Dropdown(choices=roles, interactive=True), | |
gr.Dropdown(choices=roles, interactive=True), | |
gr.Dropdown(choices=roles, interactive=True), | |
gr.Dropdown(choices=roles, interactive=True), | |
gr.Dropdown(choices=roles, interactive=True), | |
gr.Dropdown(choices=roles, interactive=True) | |
) | |
def update_players(selected_role): | |
""" | |
Given a selected role, returns a new Dropdown with players whose name appears | |
in any of the role columns. | |
A blank option is added. | |
""" | |
global df | |
logging.debug("update_players: Selected role: %s", selected_role) | |
if df is None or not selected_role: | |
return gr.Dropdown(choices=[], interactive=True) | |
players = df[ | |
(df["role1"] == selected_role) | | |
(df["role2"] == selected_role) | | |
(df["role3"] == selected_role) | |
]["player"].dropna().unique().tolist() | |
players = sorted(players) | |
players = [""] + players | |
logging.debug("Players for role %s: %s", selected_role, players) | |
return gr.Dropdown(choices=players, interactive=True) | |
def sync_pref(selected_role): | |
""" | |
If the selected role is "Por", returns "Por" for the corresponding preferred proposal role. | |
Otherwise, returns an empty string. | |
""" | |
if selected_role == "Por": | |
return "Por" | |
return "" | |
def get_xb90_value(role, player): | |
""" | |
Returns the xB90 value (as a string) for the given player where the selected role appears in any role column. | |
""" | |
global df | |
logging.debug("get_xb90_value: role=%s, player=%s", role, player) | |
if df is None or not role or not player: | |
return "" | |
row = df[(df["player"] == player) & (((df["role1"] == role) | (df["role2"] == role) | (df["role3"] == role)))] | |
if not row.empty: | |
value = str(row.iloc[0]["xb90"]) | |
logging.debug("xB90 for %s: %s", player, value) | |
return value | |
return "" | |
def get_candidate_roles(candidate_name): | |
""" | |
Returns a comma-separated string of roles for the candidate. | |
Omits any role that is 'nan' (case-insensitive). | |
""" | |
global df | |
if df is None or not candidate_name: | |
return "" | |
row = df[df["player"] == candidate_name] | |
if not row.empty: | |
roles = set() | |
for col in ["role1", "role2", "role3"]: | |
val = row.iloc[0][col] | |
val_str = str(val).strip() | |
if pd.notna(val) and val_str and val_str.lower() != "nan": | |
roles.add(val_str) | |
return ", ".join(sorted(roles)) | |
return "" | |
def compute_exchange(role1, player1, pref1, role2, player2, pref2, role3, player3, pref3, multiplier): | |
""" | |
Computes exchange proposals: | |
- Actual value: sum of selected players’ xB90. | |
- Target: actual * multiplier. | |
- For each selected player, gathers candidate replacements from rows where: | |
(a) the candidate has the selected role (in any column), and | |
(b) if a preferred proposal role is provided, candidate must have it. | |
If no candidate meets the preferred criterion, the preference constraint is relaxed. | |
- Excludes any candidate that is among the selected players. | |
- Generates candidate combinations and selects up to three proposals ensuring no candidate | |
is repeated across proposals. | |
- Enforces that if a player's role is "Por", then the preferred must be "Por"; if not, returns an error. | |
""" | |
global df | |
logging.debug("compute_exchange: Starting computation") | |
selected = [] | |
for i, (r, p, pref) in enumerate([(role1, player1, pref1), (role2, player2, pref2), (role3, player3, pref3)], start=1): | |
if r and p: | |
if r == "Por": | |
if pref != "Por": | |
msg = f"Puoi scambiare un portiere solo per un altro portiere. (Giocatore {i})" | |
logging.error(msg) | |
return msg | |
pref = "Por" | |
xb90_val = get_xb90_value(r, p) | |
if xb90_val == "": | |
msg = f"Error: Could not find xB90 for Player {i}." | |
logging.error(msg) | |
return msg | |
try: | |
xb90_val = float(xb90_val) | |
except: | |
msg = f"Error: Invalid xB90 value for Player {i}." | |
logging.error(msg) | |
return msg | |
selected.append({"role": r, "player": p, "xb90": xb90_val, "pref": pref}) | |
logging.debug("Selected Player %d: %s, Role: %s, Preferred: %s, xB90: %s", i, p, r, pref, xb90_val) | |
if len(selected) == 0: | |
return "Please select at least one player." | |
actual_value = sum(item["xb90"] for item in selected) | |
target = actual_value * multiplier | |
logging.debug("Actual value: %s, Target: %s (multiplier=%s)", actual_value, target, multiplier) | |
selected_names = {sel["player"] for sel in selected} | |
candidate_lists = [] | |
for sel in selected: | |
r = sel["role"] | |
pref = sel["pref"] | |
logging.debug("Gathering candidates for role %s with preference %s", r, pref) | |
if pref: | |
candidates_df = df[ | |
(((df["role1"] == r) | (df["role2"] == r) | (df["role3"] == r))) & | |
(((df["role1"] == pref) | (df["role2"] == pref) | (df["role3"] == pref))) & | |
(~df["player"].isin(selected_names)) | |
] | |
candidates = candidates_df[["player", "xb90"]].dropna().to_dict(orient="records") | |
if not candidates: | |
logging.debug("No candidates found matching preference %s; relaxing preference", pref) | |
candidates_df = df[ | |
(((df["role1"] == r) | (df["role2"] == r) | (df["role3"] == r))) & | |
(~df["player"].isin(selected_names)) | |
] | |
candidates = candidates_df[["player", "xb90"]].dropna().to_dict(orient="records") | |
else: | |
candidates_df = df[ | |
(((df["role1"] == r) | (df["role2"] == r) | (df["role3"] == r))) & | |
(~df["player"].isin(selected_names)) | |
] | |
candidates = candidates_df[["player", "xb90"]].dropna().to_dict(orient="records") | |
logging.debug("Candidates for role %s: %s", r, candidates) | |
if not candidates: | |
return f"No alternative candidates found for role '{r}' (excluding selected players) matching preference '{pref}'." | |
candidate_lists.append(candidates) | |
all_combos = list(itertools.product(*candidate_lists)) | |
valid_combos = [] | |
for combo in all_combos: | |
candidate_names = [candidate["player"] for candidate in combo] | |
if len(set(candidate_names)) == len(candidate_names): | |
valid_combos.append(combo) | |
logging.debug("Found %d valid candidate combinations", len(valid_combos)) | |
if not valid_combos: | |
return "No valid candidate combinations found." | |
combo_diffs = [] | |
for combo in valid_combos: | |
combo_value = sum(candidate["xb90"] for candidate in combo) | |
diff = abs(combo_value - target) | |
combo_diffs.append((combo, combo_value, diff)) | |
combo_diffs.sort(key=lambda x: x[2]) | |
# Choose up to three proposals ensuring no candidate is repeated across proposals. | |
selected_options = [] | |
used_candidates = set() | |
for combo, combo_value, diff in combo_diffs: | |
candidate_set = set(candidate["player"] for candidate in combo) | |
if candidate_set.isdisjoint(used_candidates): | |
selected_options.append((combo, combo_value, diff, candidate_set)) | |
used_candidates.update(candidate_set) | |
if len(selected_options) == 3: | |
break | |
logging.debug("Selected %d proposals", len(selected_options)) | |
if not selected_options: | |
return "No valid, disjoint candidate combinations found." | |
output = ( | |
f"Actual value: {actual_value:.2f}\n" | |
f"Value for equal exchange (target): {target:.2f}\n\n" | |
"Top alternative exchange options:\n" | |
) | |
for idx, (combo, combo_value, diff, _) in enumerate(selected_options, start=1): | |
option_str = f"Option {idx} (Total xB90: {combo_value:.2f}, Diff: {diff:.2f}):\n" | |
for i, candidate in enumerate(combo): | |
cand_roles = get_candidate_roles(candidate["player"]) | |
option_str += ( | |
f" - Replacement for '{selected[i]['player']}' (Role: {selected[i]['role']}): " | |
f"{candidate['player']} (Role: {cand_roles}, xB90: {candidate['xb90']})\n" | |
) | |
output += option_str + "\n" | |
logging.debug("Exchange proposals computed successfully.") | |
return output | |
# Build the Gradio Blocks interface with advanced custom CSS. | |
with gr.Blocks(theme=gr.themes.Soft(), css=""" | |
@import url('https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap'); | |
body, .gradio-container { | |
font-family: 'Montserrat', sans-serif; | |
} | |
.gradio-container { | |
background: linear-gradient(rgba(0,0,0,0.6), rgba(0,0,0,0.6)), url('https://source.unsplash.com/1600x900/?football,stadium') no-repeat center center fixed; | |
background-size: cover; | |
} | |
.gradio-interface { | |
background-color: rgba(255, 255, 255, 0.95) !important; | |
border-radius: 10px; | |
padding: 20px; | |
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); | |
} | |
h2, h3, h4, p { | |
color: #FFD700; | |
text-shadow: 1px 1px 4px rgba(0,0,0,0.8); | |
} | |
.gr-button { | |
background-color: #006400 !important; | |
color: #FFF !important; | |
border: none !important; | |
border-radius: 5px !important; | |
padding: 10px 20px !important; | |
font-size: 1.1em !important; | |
cursor: pointer !important; | |
transition: background-color 0.3s ease !important; | |
} | |
.gr-button:hover { | |
background-color: #228B22 !important; | |
} | |
@media (max-width: 600px) { | |
.gradio-container { padding: 10px; } | |
.gradio-interface { padding: 10px; } | |
} | |
""") as demo: | |
gr.Markdown("<h2 style='text-align:center;'>Fantastico Calcio Andria - il foglietto dell'uomo algoritmo</h2>") | |
gr.Markdown("<p style='text-align:center;'>Carica il file Excel con i dati dei giocatori. La prima riga deve contenere le intestazioni: 'Player', 'Role 1', 'Role 2', 'Role 3', 'xB90'.</p>") | |
file_input = gr.File(label="Carica file Excel (.xlsx)", file_types=[".xlsx"]) | |
with gr.Row(): | |
with gr.Column(): | |
role1 = gr.Dropdown(label="Ruolo per Giocatore 1", choices=[], interactive=True) | |
player1 = gr.Dropdown(label="Giocatore 1", choices=[], interactive=True) | |
xb90_1 = gr.Textbox(label="xB90 per Giocatore 1", interactive=False) | |
pref1 = gr.Dropdown(label="Ruolo Preferito per Proposta (1)", choices=[], interactive=True) | |
with gr.Column(): | |
role2 = gr.Dropdown(label="Ruolo per Giocatore 2 (Opzionale)", choices=[], interactive=True) | |
player2 = gr.Dropdown(label="Giocatore 2 (Opzionale)", choices=[], interactive=True) | |
xb90_2 = gr.Textbox(label="xB90 per Giocatore 2", interactive=False) | |
pref2 = gr.Dropdown(label="Ruolo Preferito per Proposta (2)", choices=[], interactive=True) | |
with gr.Column(): | |
role3 = gr.Dropdown(label="Ruolo per Giocatore 3 (Opzionale)", choices=[], interactive=True) | |
player3 = gr.Dropdown(label="Giocatore 3 (Opzionale)", choices=[], interactive=True) | |
xb90_3 = gr.Textbox(label="xB90 per Giocatore 3", interactive=False) | |
pref3 = gr.Dropdown(label="Ruolo Preferito per Proposta (3)", choices=[], interactive=True) | |
multiplier_slider = gr.Slider(minimum=0.5, maximum=1.5, value=1.05, step=0.01, label="Moltiplicatore di Scambio", interactive=True) | |
file_input.change(fn=load_excel, inputs=file_input, outputs=[role1, role2, role3, pref1, pref2, pref3]) | |
role1.change(fn=update_players, inputs=role1, outputs=player1) | |
role2.change(fn=update_players, inputs=role2, outputs=player2) | |
role3.change(fn=update_players, inputs=role3, outputs=player3) | |
# Sync preferred role if "Por" is selected. | |
role1.change(fn=sync_pref, inputs=role1, outputs=pref1) | |
role2.change(fn=sync_pref, inputs=role2, outputs=pref2) | |
role3.change(fn=sync_pref, inputs=role3, outputs=pref3) | |
player1.change(fn=get_xb90_value, inputs=[role1, player1], outputs=xb90_1) | |
player2.change(fn=get_xb90_value, inputs=[role2, player2], outputs=xb90_2) | |
player3.change(fn=get_xb90_value, inputs=[role3, player3], outputs=xb90_3) | |
exchange_button = gr.Button("Calcola Proposte di Scambio") | |
output_box = gr.Textbox(label="Proposte di Scambio", lines=14) | |
exchange_button.click( | |
fn=compute_exchange, | |
inputs=[role1, player1, pref1, role2, player2, pref2, role3, player3, pref3, multiplier_slider], | |
outputs=output_box, | |
) | |
# Launch the app with debug enabled. | |
demo.launch(debug=True) | |