"""Dibujo de la predicción de cruce sobre canchas: - Ataque por pasillo: media cancha con 3 pasillos (Izq/Centro/Der), % y xT. - Bloque defensivo: el 75% defensivo dividido en bajo (verde, cerca del arco propio), medio (naranja) y alto (rojo), sombreado según el % previsto. Devuelve figuras de matplotlib (una por cancha).""" from __future__ import annotations import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt from mplsoccer import VerticalPitch PITCH = dict(pitch_type="opta", line_color="#cdd6e6", linewidth=1.2) GREEN, ORANGE, RED = "#2bb673", "#f5912b", "#e0392b" LANE_C = "#2E74D6" TEXT = "#10243f" def _pitch(ax, half=False): p = VerticalPitch(half=half, pad_top=2, pitch_color="none", **PITCH) p.draw(ax=ax) return p def attack_fig(side: dict, team: str) -> plt.Figure: """Media cancha de ataque con los 3 pasillos sombreados por % y rotulados con % y xT.""" fig, ax = plt.subplots(figsize=(4.2, 5.2)) fig.patch.set_facecolor("white") p = _pitch(ax, half=True) pas, xt = side["pasillo"], side["xt"] mx = max(pas.values()) or 1.0 # x = ancho (0-100). Izquierda a la izquierda del espectador. bounds = [(0, 33.33, "Der"), (33.33, 66.67, "Centro"), (66.67, 100, "Izq")] for x0, x1, lane in bounds: frac = pas[lane] ax.fill_betweenx([50, 100], x0, x1, color=LANE_C, alpha=0.12 + 0.55 * frac / mx, zorder=0.5) ax.plot([x0, x0], [50, 100], color="#9fb0c9", lw=1, ls="--", zorder=1) ax.text((x0 + x1) / 2, 88, f"{frac*100:.0f}%", ha="center", va="center", fontsize=17, fontweight="bold", color=TEXT, zorder=3) ax.text((x0 + x1) / 2, 79, lane, ha="center", va="center", fontsize=10.5, color="#42546e", zorder=3) ax.text((x0 + x1) / 2, 73, f"xT {xt[lane]:.2f}", ha="center", va="center", fontsize=9, color="#5a6b85", zorder=3) ax.set_title(f"{team}\nPor dónde ataca", fontsize=12.5, fontweight="bold", color=TEXT, pad=6) fig.tight_layout() return fig def defense_fig(side: dict, team: str) -> plt.Figure: """Cancha con el 75% defensivo en 3 bandas: bajo (verde) cerca del arco propio, medio (naranja) y alto (rojo); sombreado por % previsto.""" fig, ax = plt.subplots(figsize=(4.2, 5.2)) fig.patch.set_facecolor("white") _pitch(ax, half=False) blo = side["bloque"] mx = max(blo.values()) or 1.0 # arco propio abajo (y=0). Bandas a lo largo: bajo [0-25], medio [25-50], alto [50-75]. bands = [(0, 25, "bajo", GREEN), (25, 50, "medio", ORANGE), (50, 75, "alto", RED)] for y0, y1, name, col in bands: frac = blo[name] ax.fill_between([0, 100], y0, y1, color=col, alpha=0.18 + 0.6 * frac / mx, zorder=0.5) ax.plot([0, 100], [y1, y1], color="#9fb0c9", lw=0.8, ls="--", zorder=1) ax.text(50, (y0 + y1) / 2 + 3.5, f"{frac*100:.0f}%", ha="center", va="center", fontsize=16, fontweight="bold", color=TEXT, zorder=3) ax.text(50, (y0 + y1) / 2 - 4, f"Bloque {name}", ha="center", va="center", fontsize=10.5, color="#33455f", zorder=3) ax.annotate("arco propio", (50, 1.5), ha="center", va="bottom", fontsize=8.5, color="#7a889e", zorder=3) ax.set_title(f"{team}\nCómo defiende", fontsize=12.5, fontweight="bold", color=TEXT, pad=6) fig.tight_layout() return fig def section_html(pred: dict, png_paths: dict) -> str: """Bloque HTML autocontenido (imágenes base64 + insight + tabla) para inyectar en el HTML completo del reporte. ``png_paths`` = {'ah','dh','aa','da': Path}.""" import base64 from pathlib import Path def img(key): p = Path(png_paths[key]) b = base64.b64encode(p.read_bytes()).decode() return f'' ta, tb = pred["team_a"], pred["team_b"] rows = "" for side, team in ((pred["a"], ta), (pred["b"], tb)): b, pa, xt = side["bloque"], side["pasillo"], side["xt"] rows += ("" + team + "" + f"{b['alto']*100:.0f}%{b['medio']*100:.0f}%{b['bajo']*100:.0f}%" + f"{pa['Izq']*100:.0f}%{pa['Centro']*100:.0f}%{pa['Der']*100:.0f}%" + f"{xt['Izq']:.2f}{xt['Centro']:.2f}{xt['Der']:.2f}") th = ("EquipoBloq. altoBloq. medio" "Bloq. bajoAtq. IzqAtq. CentroAtq. Der" "xT IzqxT CentroxT Der") return f"""

Predicción del modelo — cruce {ta} vs {tb}

Bloque defensivo, pasillo de ataque y xT esperados por equipo (forma a la fecha).

{img('ah')}
{img('dh')}
{img('aa')}
{img('da')}
Lectura del cruce. {insight(pred)}
{th}{rows}
""" def comparison_html(pred: dict, actual: dict, home: str, away: str) -> str: """Sección HTML 'Predicción del modelo vs. lo que pasó' para el post-partido. ``pred`` = salida de predict_matchup; ``actual`` = salida de actual_from_match (keyed por TeamName).""" def team_block(side_pred, team): act = actual.get(team) if act is None: return f"

Sin datos reales para {team}.

" def rows(kind, keys, labels, pct=True, dec=0): r = "" for k, lab in zip(keys, labels): p = side_pred[kind][k]; a = act[kind][k] if pct: pv, av = f"{p*100:.0f}%", f"{a*100:.0f}%" d = (a - p) * 100; dv = f"{d:+.0f} pp" else: pv, av = f"{p:.2f}", f"{a:.2f}" d = a - p; dv = f"{d:+.2f}" col = "#2bb673" if abs(d) < (8 if pct else 0.08) else "#e0a83b" r += (f"{lab}{pv}{av}" f"{dv}") return r body = (rows("bloque", ["alto", "medio", "bajo"], ["Bloque alto", "Bloque medio", "Bloque bajo"]) + rows("pasillo", ["Izq", "Centro", "Der"], ["Ataque Izq", "Ataque Centro", "Ataque Der"]) + rows("xt", ["Izq", "Centro", "Der"], ["xT Izq", "xT Centro", "xT Der"], pct=False)) return f"""

{team}

{body}
Métrica PredichoReal Δ
""" a_html = team_block(pred["a"], home) b_html = team_block(pred["b"], away) return f"""

Modelo vs. realidad

Lo que el modelo esperaba del cruce contra lo que efectivamente pasó. Δ en verde = acierto cercano; en ámbar = diferencia mayor. (Predicción con la forma a la fecha.)

{a_html}{b_html}
""" def insight(pred: dict) -> str: """Lectura breve del cruce a partir de las predicciones de A y B.""" a, b = pred["a"], pred["b"] ta, tb = pred["team_a"], pred["team_b"] lane_es = {"Izq": "izquierda", "Centro": "el centro", "Der": "derecha"} blo_max = lambda s: max(s["bloque"], key=s["bloque"].get) xt_max = lambda s: max(s["xt"], key=s["xt"].get) out = [] # quién genera más peligro xa, xb = sum(a["xt"].values()), sum(b["xt"].values()) mas, men = (ta, tb) if xa >= xb else (tb, ta) out.append(f"El modelo espera más peligro total de {mas} ({max(xa,xb):.2f} vs {min(xa,xb):.2f} de xT).") # pasillo principal de cada uno out.append(f"{ta} apuntaría sobre todo por {lane_es[xt_max(a)]} " f"(xT {a['xt'][xt_max(a)]:.2f}); {tb}, por {lane_es[xt_max(b)]} " f"(xT {b['xt'][xt_max(b)]:.2f}).") # bloque defensivo de cada uno out.append(f"Defensivamente, {ta} se pararía mayormente en bloque {blo_max(a)} " f"y {tb} en bloque {blo_max(b)}.") # coincidencia de pasillo (ambos cargan el mismo lado) if xt_max(a) == xt_max(b) and xt_max(a) != "Centro": out.append(f"Ojo: ambos cargan el mismo sector ({lane_es[xt_max(a)]}), va a ser una zona disputada.") return " ".join(out)