"""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 += ("
Bloque defensivo, pasillo de ataque y xT esperados por equipo (forma a la fecha).
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"| Métrica | Predicho | Real | Δ |
|---|
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.)