RRC / scripts /gen_referencia.py
pablogrois's picture
Claude Opus 5
Integrar el diccionario de variables del club
31157f4
Raw History Blame Contribute Delete
13.5 kB
#!/usr/bin/env python3
"""Arma docs/diccionarios/referencia.html: navegador de la documentación, organizado
como las apps (elegís app → sección → categoría → variable).
NO tiene contenido propio: todo sale de las mismas fuentes que los .md/.csv
(por eso no se puede desincronizar, que fue lo que le pasó al referencia.html viejo).
USO: python scripts/gen_referencia.py
"""
from __future__ import annotations
import csv, json, re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
RACING = ROOT.parent
DIC = ROOT / "docs" / "diccionarios"
var_dict = json.loads((ROOT / "vendor/data/var_dict.json").read_text(encoding="utf-8"))
groups = json.loads((ROOT / "scripts/feature_groups.json").read_text(encoding="utf-8"))
cats_rep = json.loads((DIC / "_reportes_categorias.json").read_text(encoding="utf-8"))
ejes_def = json.loads((RACING / "data/analysis/next_goal/ejes_definition.json").read_text(encoding="utf-8"))
lb = json.loads((ROOT / "webapp-static/site/lowblock.json").read_text(encoding="utf-8"))["options"]
# ── variables (de variables.csv, la salida del otro generador) ────────────────
VARS: dict[str, dict] = {}
for r in csv.DictReader((DIC / "variables.csv").open(encoding="utf-8")):
key = r["variable"] if r["variable"] not in VARS else f'{r["variable"]}@{r["dataset"]}'
VARS[key] = {"n": r["variable"], "l": r["nombre_de_uso"], "ds": r["dataset"],
"o": r["origen"], "g": r["generado_por"], "e": r["entradas"],
"c": r["como_se_calcula"], "t": r["tipo"], "sc": r.get("subcategoria", ""),
"gr": r["grupos"], "nt": r["nota"]}
# ── resumen "De qué se trata" de cada .md ─────────────────────────────────────
def resumen(rel: str) -> str:
p = DIC / f"{rel}.md"
if not p.exists():
return ""
m = re.search(r"## De qué se trata\s*\n(.+?)(?=\n#{2,3} |\n\| )", p.read_text(encoding="utf-8"), re.S)
if not m:
return ""
txt = m.group(1).strip()
# re.S: en los .md las negritas/código suelen cruzar el salto de línea
txt = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", txt, flags=re.S)
txt = re.sub(r"`(.+?)`", r"<code>\1</code>", txt, flags=re.S)
txt = re.sub(r"\[(.+?)\]\([^)]+\)", r"\1", txt, flags=re.S)
txt = re.sub(r"(?<![*\w])\*([^*\n]+?)\*(?![*\w])", r"<em>\1</em>", txt) # itálicas
out = []
for b in txt.split("\n\n"):
b = b.strip()
if not b:
continue
lines = [l.strip() for l in b.split("\n")]
if all(re.match(r"^(\d+\.|[-•])\s+", l) for l in lines): # lista
tag = "ol" if re.match(r"^\d+\.", lines[0]) else "ul"
items = "".join(f"<li>{re.sub(r'^(\d+\.|[-•])\s+', '', l)}</li>" for l in lines)
out.append(f"<{tag}>{items}</{tag}>")
else:
out.append(f"<p>{' '.join(lines)}</p>")
return "".join(out)
# ── markdown → HTML (lo justo para nuestras fichas) ──────────────────────────
def _inline(t: str) -> str:
t = (t.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"))
t = re.sub(r"`(.+?)`", r"<code>\1</code>", t)
t = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", t)
t = re.sub(r"(?<![*\w])\*([^*\n]+?)\*(?![*\w])", r"<em>\1</em>", t)
t = re.sub(r"\[(.+?)\]\([^)]+\)", r"\1", t) # links: se deja el texto
return t
def md_to_html(rel: str) -> str:
"""Convierte la ficha .md entera a HTML (headings, tablas, listas, citas)."""
path = DIC / f"{rel}.md"
if not path.exists():
return ""
txt = path.read_text(encoding="utf-8")
txt = re.sub(r"\n---\n\n\*\*Versión CSV\*\*.*\Z", "", txt, flags=re.S) # el pie del CSV sobra acá
txt = re.sub(r"^# .+?\n", "", txt, count=1).lstrip("\n") # el h1 ya es el título
# el "De qué se trata" ya se muestra arriba como resumen: no repetirlo en la ficha
txt = re.sub(r"^## De qué se trata\s*\n.*?(?=\n## )", "", txt, count=1, flags=re.S).lstrip("\n")
out, i, lines = [], 0, txt.split("\n")
while i < len(lines):
ln = lines[i]
if not ln.strip() or ln.strip() == "---":
i += 1; continue
if ln.startswith("### "):
out.append(f"<h4>{_inline(ln[4:].strip())}</h4>"); i += 1; continue
if ln.startswith("## "):
out.append(f"<h3>{_inline(ln[3:].strip())}</h3>"); i += 1; continue
if ln.lstrip().startswith("|"): # tabla
rows = []
while i < len(lines) and lines[i].lstrip().startswith("|"):
rows.append([c.strip() for c in lines[i].strip().strip("|").split("|")]); i += 1
if len(rows) >= 2 and set("".join(rows[1]).replace(" ", "")) <= set("-:"):
head, body = rows[0], rows[2:]
else:
head, body = None, rows
t = ['<div class="tw"><table class="doc">']
if head and any(h for h in head):
t.append("<thead><tr>" + "".join(f"<th>{_inline(c)}</th>" for c in head) + "</tr></thead>")
t.append("<tbody>" + "".join("<tr>" + "".join(f"<td>{_inline(c)}</td>" for c in r) + "</tr>"
for r in body) + "</tbody></table></div>")
out.append("".join(t)); continue
if ln.startswith("> "): # cita
buf = []
while i < len(lines) and lines[i].startswith(">"):
buf.append(lines[i].lstrip("> ").rstrip()); i += 1
out.append(f'<blockquote>{_inline(" ".join(buf))}</blockquote>'); continue
m = re.match(r"^(\d+\.|[-*])\s+", ln) # lista
if m:
tag = "ol" if ln[0].isdigit() else "ul"
items = []
while i < len(lines) and re.match(r"^(\d+\.|[-*])\s+", lines[i]):
cur = re.sub(r"^(\d+\.|[-*])\s+", "", lines[i]); i += 1
while i < len(lines) and lines[i].startswith(" ") and lines[i].strip() \
and not re.match(r"^\s*(\d+\.|[-*])\s+", lines[i]):
cur += " " + lines[i].strip(); i += 1
items.append(f"<li>{_inline(cur)}</li>")
out.append(f"<{tag}>{''.join(items)}</{tag}>"); continue
buf = [] # párrafo
while i < len(lines) and lines[i].strip() and not re.match(
r"^(#{2,3} |\||> |\d+\.\s|[-*]\s|---)", lines[i]):
buf.append(lines[i].strip()); i += 1
if buf:
out.append(f"<p>{_inline(' '.join(buf))}</p>")
return "".join(out)
def cat_grupos() -> list:
porg: dict[str, list[str]] = {}
for col, gs in groups.items():
for g in gs:
porg.setdefault(g, []).append(col)
return [{"n": g, "v": sorted(porg[g], key=lambda c: (var_dict.get(c, {}).get("label", c) or c).lower())}
for g in sorted(porg)]
def cat_tipos() -> list:
port: dict[str, list[str]] = {}
for c, i in var_dict.items():
port.setdefault(i.get("tipo", "Otras"), []).append(c)
return [{"n": t, "v": sorted(port[t], key=lambda c: (var_dict.get(c, {}).get("label", c) or c).lower())}
for t in sorted(port)]
EJE_LBL = {"verticalidad": "Verticalidad", "elaboracion": "Elaboración", "individual": "Juego individual",
"rotura_profundidad": "Rotura de líneas / profundidad", "amplitud_centro": "Amplitud / centros",
"robo_alto": "Robo alto", "aereo_segunda": "Aéreo / 2ª jugada"}
cat_ejes = [{"n": EJE_LBL.get(e, e), "v": list(cs)} for e, cs in ejes_def.items()]
cat_recursos = [
{"n": "Lado", "v": [], "txt": ["Ofensivo — el equipo usa el recurso", "Defensivo — se lo hacen al equipo"]},
{"n": "Bloque", "v": [], "txt": list(lb.get("bloques", {}).values())},
{"n": "Recursos", "v": [], "txt": list(lb.get("recursos", {}).values())},
{"n": "Métricas", "v": ["pv_jugada", "pv_jugada_exitosa", "evento_clave", "tiros_jugada", "xg_acum",
"xg_jugada", "xg_jugada_exitosa", "goles", "pct_goles", "pct_exito",
"n_intentos", "intentos_90"]},
]
def cats_de(rel: str) -> list:
"""Categorías de un reporte/modelo: si el ítem es una variable conocida, se enlaza."""
out = []
for cat, items in cats_rep.get(rel, {}).items():
v = [i for i in items if i in VARS]
txt = [i for i in items if i not in VARS]
out.append({"n": cat, "v": v, "txt": txt})
return out
EV = [
("top-equipos", "Top equipos", "equipos-y-variables/top-equipos", cat_grupos(), "Los grupos del botón «Agregar grupo»"),
("perfil-y-similares", "Perfil y similares", "equipos-y-variables/perfil-y-similares", cat_grupos(), "Los grupos del botón «Agregar grupo»"),
("ejes", "Ejes ataque/defensa", "equipos-y-variables/ejes-ataque-defensa", cat_ejes, "Los 7 ejes y las métricas que promedian"),
("recursos", "Recursos", "equipos-y-variables/recursos", cat_recursos, "Las opciones de cada desplegable"),
("scatter-libre", "Scatter libre", "equipos-y-variables/scatter-libre", cat_tipos(), "Las 262 variables, por tipo"),
]
REPORTES = [("pre_match", "Pre-partido (head-to-head)"), ("block_zscore", "Bloques (z-score)"),
("triple_multitag", "Multi-Tag"), ("triple_centros", "Centros"),
("recuperaciones", "Recuperaciones"), ("post_match", "Post-partido"),
("ejes_pre", "Ejes — previo"), ("ejes_post", "Ejes — post")]
MODELOS = [("attack_matchup_gnn", "Zonas de ataque (GNN)"), ("pv_distribution_gnn", "Peligro por zona (GNN)"),
("cruce", "Cruce: bloque y pasillos"), ("ptiro", "P(tiro) por jugada")]
def sec_ev():
return [{"id": i, "n": n, "res": resumen(rel), "doc": md_to_html(rel), "cats": c, "hint": h}
for i, n, rel, c, h in EV]
APPS = [
{"id": "vercel", "n": "Vercel", "sub": "Gráficos de equipos",
"url": "graficos-equipos-racing.vercel.app",
"d": "La app estática. Solo consulta de equipos y variables: no genera reportes de partido.",
"grupos": [{"n": "Equipos y variables", "secs": sec_ev()}]},
{"id": "hf", "n": "Hugging Face", "sub": "Space RRC",
"url": "pablogrois/RRC",
"d": "La app completa. Tiene lo mismo que Vercel más los reportes por partido y los modelos.",
"grupos": [
{"n": "Equipos y variables", "secs": sec_ev()},
{"n": "Reportes de partido", "secs": [
{"id": f"r_{i}", "n": n, "res": resumen(f"reportes-de-partido/{i}"),
"doc": md_to_html(f"reportes-de-partido/{i}"),
"cats": cats_de(f"reportes-de-partido/{i}"), "hint": "Qué usa cada parte del reporte"}
for i, n in REPORTES]},
{"n": "Modelos", "secs": [
{"id": f"m_{i}", "n": n, "res": resumen(f"modelos/{i}"),
"doc": md_to_html(f"modelos/{i}"),
"cats": cats_de(f"modelos/{i}"), "hint": "Datos, target y features"}
for i, n in MODELOS]},
]},
]
# ── índice inverso: variable → dónde aparece (app · sección · categoría) ─────
USOS: dict[str, list] = {}
for a in APPS:
for g in a["grupos"]:
for s in g["secs"]:
for ci, c in enumerate(s["cats"]):
for n in c.get("v", []):
USOS.setdefault(n, [])
ref = [a["id"], s["id"], ci, f'{s["n"]} · {c["n"]}', a["n"]]
if ref not in USOS[n]:
USOS[n].append(ref)
# nº de variables distintas por sección (para las tarjetas)
for a in APPS:
for g in a["grupos"]:
for s in g["secs"]:
s["nv"] = len({n for c in s["cats"] for n in c.get("v", [])})
from datetime import date
DB = {"apps": APPS, "vars": VARS, "usos": USOS,
"meta": {"n": len(VARS), "dir": sum(1 for v in VARS.values() if v["o"] == "directa"),
"fecha": date.today().isoformat()}}
(DIC / "_referencia_db.json").write_text(json.dumps(DB, ensure_ascii=False), encoding="utf-8")
# ── ensamblar el HTML (plantilla + datos embebidos) ──────────────────────────
HERE = Path(__file__).resolve().parent
head = (HERE / "_referencia_head.html").read_text(encoding="utf-8")
body = (HERE / "_referencia_body.html").read_text(encoding="utf-8")
n, d = len(VARS), DB["meta"]["dir"]
body = (body.replace("__DB__", json.dumps(DB, ensure_ascii=False))
.replace("__N__", str(n)).replace("__D__", str(d)).replace("__C__", str(n - d))
.replace("__F__", DB["meta"]["fecha"]))
html = head + "\n" + body
out = DIC / "referencia.html"
out.write_text(html, encoding="utf-8")
# copias servidas por las dos apps (mismo candado en cada una, sin proyectos nuevos):
# · Vercel: el sitio estático la publica tal cual
# · Space HF: va dentro de src/ porque es lo único que el Dockerfile copia
copias = [ROOT / "webapp-static" / "site" / "referencia.html",
ROOT / "src" / "racing_reports" / "web" / "templates" / "referencia.html"]
for c in copias:
c.write_text(html, encoding="utf-8")
print(f"OK referencia.html — {len(html)/1024:.0f} KB · {len(VARS)} variables · {len(APPS)} apps")
for c in copias:
print(f" copiada a {c.relative_to(ROOT)}")