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 exchange roles and three for preferred proposal roles) populated with the union of roles from the three role columns (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 # Blank option. logging.debug("Roles found: %s", roles) # Return six dropdown components. return ( gr.Dropdown(choices=roles, interactive=True), # role1 for exchange gr.Dropdown(choices=roles, interactive=True), # role2 for exchange gr.Dropdown(choices=roles, interactive=True), # role3 for exchange gr.Dropdown(choices=roles, interactive=True), # preferred role for player1 gr.Dropdown(choices=roles, interactive=True), # preferred role for player2 gr.Dropdown(choices=roles, interactive=True) # preferred role for player3 ) 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 get_xb90_value(role, player): """ Returns the xB90 value for the given player and selected role. Searches rows where the player's name matches and the 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 compute_exchange(role1, player1, pref1, role2, player2, pref2, role3, player3, pref3): """ Computes exchange proposals: - Actual value: sum of selected players’ xB90. - Target: actual * 1.05. - For each selected player, gather candidate replacements from rows where: (a) the candidate has the selected role (in any role column), and (b) if a preferred role is provided for that player, then candidate must have it in some role column. If no candidate meets the preferred criterion, the preference is relaxed. - Excludes any candidate that is among the selected players. - Generates candidate combinations (Cartesian product) and selects up to three proposals ensuring that no candidate appears in more than one proposal. """ global df logging.debug("compute_exchange: Starting computation") selected = [] # Collect selected players. for i, (r, p, pref) in enumerate([(role1, player1, pref1), (role2, player2, pref2), (role3, player3, pref3)], start=1): if r and p: 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 * 1.05 logging.debug("Actual value: %s, Target: %s", actual_value, target) 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 selected role %s with preference %s", r, pref) # First, try filtering by both role and preferred role if provided. 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) # Relax the preference constraint. 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 %s: %s", r, candidates) if not candidates: return f"No alternative candidates found for role '{r}' (excluding selected players)." 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): option_str += ( f" - Replacement for '{selected[i]['player']}' (Role: {selected[i]['role']}): " f"{candidate['player']} (xB90: {candidate['xb90']})\n" ) output += option_str + "\n" logging.debug("Exchange proposals computed successfully.") return output # Build the Gradio Blocks interface with Soft theme and custom CSS for mobile. with gr.Blocks(theme=gr.themes.Soft(), css=""" .gradio-container { max-width: 800px; margin: auto; } .gradio-interface { padding: 20px; } @media (max-width: 600px) { .gradio-container { padding: 10px; } .gradio-interface { padding: 10px; } } """) as demo: gr.Markdown("
Upload an Excel file with player data. Row 1 must have headers: 'Player', 'Role 1', 'Role 2', 'Role 3', 'xB90'.
") file_input = gr.File(label="Upload Excel File (.xlsx)", file_types=[".xlsx"]) with gr.Row(): with gr.Column(): role1 = gr.Dropdown(label="Role for Player 1", choices=[], interactive=True) player1 = gr.Dropdown(label="Player 1", choices=[], interactive=True) xb90_1 = gr.Textbox(label="xB90 for Player 1", interactive=False) pref1 = gr.Dropdown(label="Preferred Proposal Role for Player 1", choices=[], interactive=True) with gr.Column(): role2 = gr.Dropdown(label="Role for Player 2 (Optional)", choices=[], interactive=True) player2 = gr.Dropdown(label="Player 2 (Optional)", choices=[], interactive=True) xb90_2 = gr.Textbox(label="xB90 for Player 2", interactive=False) pref2 = gr.Dropdown(label="Preferred Proposal Role for Player 2", choices=[], interactive=True) with gr.Column(): role3 = gr.Dropdown(label="Role for Player 3 (Optional)", choices=[], interactive=True) player3 = gr.Dropdown(label="Player 3 (Optional)", choices=[], interactive=True) xb90_3 = gr.Textbox(label="xB90 for Player 3", interactive=False) pref3 = gr.Dropdown(label="Preferred Proposal Role for Player 3", choices=[], interactive=True) # When file is uploaded, update all six dropdowns with the union of roles. file_input.change(fn=load_excel, inputs=file_input, outputs=[role1, role2, role3, pref1, pref2, pref3]) # When a role is selected, update the corresponding player dropdown. 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) # When a player is selected, display the xB90 value. 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("Compute Exchange Proposals") output_box = gr.Textbox(label="Exchange Proposals", lines=12) exchange_button.click( fn=compute_exchange, inputs=[role1, player1, pref1, role2, player2, pref2, role3, player3, pref3], outputs=output_box, ) # Launch the app with debug=True. demo.launch(debug=True)