# app.py # Gradio Space: formulario numérico + GIF embebido (base64) + créditos import json import base64 import joblib import pandas as pd import gradio as gr MODEL_PKL = "rf_win_model.pkl" SCHEMA_JSON = "input_schema.json" META_JSON = "model_metadata.json" GIF_PATH = "faker.gif" CLIP_TO_OBSERVED = True # ------------------------- # Helpers # ------------------------- def gif_data_uri(path: str) -> str: """ Devuelve un data URI base64 para un GIF local. Si el archivo no existe, devuelve string vacío. """ try: with open(path, "rb") as f: b64 = base64.b64encode(f.read()).decode("utf-8") return f"data:image/gif;base64,{b64}" except FileNotFoundError: return "" def fmt_int(x: float) -> str: return f"{int(round(x))}" # ------------------------- # Load artifacts # ------------------------- clf = joblib.load(MODEL_PKL) with open(SCHEMA_JSON, "r", encoding="utf-8") as f: schema = json.load(f) with open(META_JSON, "r", encoding="utf-8") as f: meta = json.load(f) FEATURES = meta["features_order"] feat_map = {f["name"]: f for f in schema["features"]} UI_LABELS = { "TeamTotalGold": "Oro total del equipo", "TeamXp": "Experiencia total del equipo", "TeamTotalKills": "Kills del equipo", "TeamDragonKills": "Dragones conseguidos", "TeamHeraldKills": "Heraldos conseguidos", "TeamTurretPlatesDestroyed": "Placas de torre destruidas", "TeamWardsPlaced": "Wards colocados", "TeamControlWardsPlaced": "Wards de control colocados", } GROUPS = { "Economía y pelea": ["TeamTotalGold", "TeamXp", "TeamTotalKills"], "Objetivos": ["TeamDragonKills", "TeamHeraldKills", "TeamTurretPlatesDestroyed"], "Visión": ["TeamWardsPlaced", "TeamControlWardsPlaced"], } def reset_values(): return [float(feat_map[c]["ui_default"]) for c in FEATURES] def predict(*vals): user_inputs = dict(zip(FEATURES, vals)) warnings = [] cleaned = {} for k, v in user_inputs.items(): info = feat_map[k] if v is None: v = float(info["ui_default"]) warnings.append(f"- {UI_LABELS.get(k,k)} estaba vacío; se usó un valor típico.") try: v = float(v) except Exception: v = float(info["ui_default"]) warnings.append(f"- {UI_LABELS.get(k,k)} no era numérico; se usó un valor típico.") rec_min, rec_max = float(info["p05"]), float(info["p95"]) obs_min, obs_max = float(info["min"]), float(info["max"]) if v < rec_min or v > rec_max: warnings.append( f"- {UI_LABELS.get(k,k)} fuera de rango típico (típico {fmt_int(rec_min)}–{fmt_int(rec_max)})." ) if v < obs_min or v > obs_max: warnings.append( f"- {UI_LABELS.get(k,k)} fuera del rango observado (obs {fmt_int(obs_min)}–{fmt_int(obs_max)})." ) if CLIP_TO_OBSERVED: v_clipped = min(max(v, obs_min), obs_max) if v_clipped != v: warnings.append( f"- {UI_LABELS.get(k,k)} se ajustó a {fmt_int(v_clipped)} para mantenerlo dentro del rango observado." ) v = v_clipped cleaned[k] = v X_in = pd.DataFrame([cleaned], columns=FEATURES) proba = float(clf.predict_proba(X_in)[:, 1][0]) if proba >= 0.70: verdict = "Alta probabilidad de victoria" elif proba >= 0.55: verdict = "Probabilidad moderada de victoria" else: verdict = "Baja probabilidad de victoria" warn_text = "Sin advertencias." if not warnings else "\n".join(warnings) return proba, verdict, warn_text # ------------------------- # Build inputs (Number) con rangos en label (sin min/max) # ------------------------- input_components_by_col = {} for col in FEATURES: info = feat_map[col] rec_min = float(info["p05"]) rec_max = float(info["p95"]) obs_min = float(info["min"]) obs_max = float(info["max"]) default = float(info["ui_default"]) step = float(info.get("ui_step", 1.0)) if col in ("TeamTotalGold", "TeamXp"): step = max(step, 50.0) label = ( f"{UI_LABELS.get(col, col)}\n" f"(típico {fmt_int(rec_min)}–{fmt_int(rec_max)}; observado {fmt_int(obs_min)}–{fmt_int(obs_max)})" ) input_components_by_col[col] = gr.Number( label=label, value=default, step=step, precision=0, ) inputs_in_order = [input_components_by_col[c] for c in FEATURES] # ------------------------- # UI # ------------------------- CSS = """ #wrap { max-width: 1040px; margin: 0 auto; } .card { border: 1px solid rgba(255,255,255,0.12); border-radius: 16px; background: rgba(255,255,255,0.06); padding: 16px; } .small { opacity: 0.85; font-size: 0.95rem; line-height: 1.3rem; } .muted { opacity: 0.80; font-size: 0.9rem; } .gifbox { border: 1px solid rgba(255,255,255,0.12); border-radius: 16px; background: rgba(255,255,255,0.06); padding: 12px; } .gifbox img { width: 100%; border-radius: 12px; display: block; } """ theme = gr.themes.Soft() gif_uri = gif_data_uri(GIF_PATH) gif_html = f"""
""".strip() with gr.Blocks(title="Predicción de victoria (demo)") as demo: with gr.Column(elem_id="wrap"): with gr.Row(): with gr.Column(scale=3): gr.Markdown( """ # Predicción de victoria (demo) Formulario para estimar probabilidad de ganar a partir de métricas del equipo en early game (primeros 15 minutos). """.strip() ) gr.Markdown( "