Eliecerrr's picture
Upload 2 files
3f8ae48 verified
Raw
History Blame Contribute Delete
8.48 kB
# 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"""
<div class="gifbox">
{"<img src='" + gif_uri + "' alt='Faker GIF'>" if gif_uri else "<div class='muted'>No se encontró faker.gif en el repo.</div>"}
<div class="muted" style="margin-top:8px;">
Fuente del GIF:
<a href="https://thegamehaus.com/league-of-legends/league-of-legends-faker-history-of-success/2019/04/14/"
target="_blank" rel="noopener noreferrer">TheGameHaus (2019)</a>
</div>
</div>
""".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(
"<div class='small'>Ingresa valores y presiona Predecir. Si te sales de rangos típicos, la app te avisa (y puede ajustar al rango observado).</div>"
)
with gr.Column(scale=2):
gr.HTML(gif_html)
with gr.Row():
with gr.Column(scale=3):
with gr.Column(elem_classes="card"):
gr.Markdown("## Ingresa los valores de cada variable")
for group_name, cols in GROUPS.items():
with gr.Accordion(label=group_name, open=True):
for c in cols:
input_components_by_col[c].render()
with gr.Row():
btn_predict = gr.Button("Predecir", variant="primary")
btn_reset = gr.Button("Restaurar valores típicos", variant="secondary")
with gr.Column(scale=2):
with gr.Column(elem_classes="card"):
gr.Markdown("## Resultado")
out_proba = gr.Number(label="Probabilidad de victoria", precision=4)
out_verdict = gr.Textbox(label="Interpretación", lines=1, interactive=False)
out_warn = gr.Textbox(label="Advertencias", lines=10, interactive=False)
with gr.Column(elem_classes="card"):
gr.Markdown("## Datos y créditos")
gr.Markdown(
"""
<div class="small">
El modelo fue entrenado con el dataset de Kaggle:
<a href="https://www.kaggle.com/datasets/karlorusovan/league-of-legends-soloq-matches-at-10-minutes-2024/data"
target="_blank" rel="noopener noreferrer">League of Legends SoloQ Matches at 10 Minutes (2024)</a>.
</div>
""".strip()
)
btn_predict.click(
fn=predict,
inputs=inputs_in_order,
outputs=[out_proba, out_verdict, out_warn],
)
btn_reset.click(
fn=reset_values,
inputs=[],
outputs=inputs_in_order,
)
demo.launch(css=CSS, theme=theme, ssr_mode=False)