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("
Carica il file Excel con i dati dei giocatori. La prima riga deve contenere le intestazioni: 'Player', 'Role 1', 'Role 2', 'Role 3', 'xB90'.
") 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)