from __future__ import annotations
import base64
from pathlib import Path
import numpy as np
import pandas as pd
from racing_reports.utils import slugify
DARK_BG = "#0b1721"
CARD_BG = "#111821"
TEXT = "#f5f5f5"
MUTED = "#9ca3af"
GREEN = "#006b3f"
LIGHT_GREEN = "#00a86b"
AMBER = "#d69a2d"
RED = "#d94b4b"
BLUE = "#5B8DB8"
SHOT_EVENTS = {"Goal", "MissedShots", "SavedShot", "ShotOnPost", "ChanceMissed"}
RECOVERY_EVENTS = {"BallRecovery", "Interception", "Tackle", "BlockedPass", "Aerial"}
ZONE_RECTS = {
"ext izq 3/4": (68, 83, 79, 100),
"int izq 3/4": (68, 83, 63, 79),
"centro 3/4": (68, 83, 37, 63),
"int der 3/4": (68, 83, 21, 37),
"ext der 3/4": (68, 83, 0, 21),
"ext izq area": (83, 100, 79, 100),
"int izq area": (83, 100, 63, 79),
"centro area": (83, 100, 37, 63),
"int der area": (83, 100, 21, 37),
"ext der area": (83, 100, 0, 21),
}
ZONE_ORDER = list(ZONE_RECTS)
def output_match_dir(output_root: Path, league: str, season: str, home: str, away: str, match_id: str) -> Path:
return output_root / slugify(league) / str(season) / f"{slugify(home)}_vs_{slugify(away)}_{match_id}"
def read_preprocessed(path: Path) -> pd.DataFrame:
return pd.read_csv(path, low_memory=False, dtype={"matchId": str, "teamId": str})
def match_events(df: pd.DataFrame, match_id: str) -> pd.DataFrame:
out = df[df["matchId"].astype(str) == str(match_id)].copy()
if out.empty:
raise ValueError(f"No hay eventos para matchId={match_id}")
return out
def html_page(title: str, subtitle: str, body: str) -> str:
return f"""
{title}
Racing Reports
{title}
{subtitle}
{body}
"""
def add_attack_zone(df: pd.DataFrame) -> pd.DataFrame:
out = df.copy()
out["attack_zone"] = pd.Series(index=out.index, dtype="object")
x = pd.to_numeric(out.get("x"), errors="coerce")
y = pd.to_numeric(out.get("y"), errors="coerce")
for zone, (x0, x1, y0, y1) in ZONE_RECTS.items():
mask = x.ge(x0) & x.lt(x1) & y.ge(y0) & y.lt(y1)
out.loc[mask, "attack_zone"] = zone
return out
def team_kpis(events: pd.DataFrame, team: str) -> dict[str, float]:
t = events[events["TeamName"].astype(str) == team].copy()
if t.empty:
return {"events": 0, "shots": 0, "xg": 0.0, "pv": 0.0, "goals": 0.0}
xg_source = t["xG"] if "xG" in t.columns else pd.Series(0, index=t.index)
pv_source = t["pvAdded"] if "pvAdded" in t.columns else pd.Series(0, index=t.index)
goal_source = t["goal_int"] if "goal_int" in t.columns else pd.Series(0, index=t.index)
return {
"events": float(len(t)),
"shots": float(t["event_name"].isin(SHOT_EVENTS).sum()) if "event_name" in t.columns else 0.0,
"xg": float(pd.to_numeric(xg_source, errors="coerce").fillna(0).sum()),
"pv": float(pd.to_numeric(pv_source, errors="coerce").fillna(0).sum()),
"goals": float(pd.to_numeric(goal_source, errors="coerce").fillna(0).sum()),
}
def season_team_events(df: pd.DataFrame, team: str, before_date: str | None = None) -> pd.DataFrame:
out = df[df["TeamName"].astype(str) == team].copy()
if before_date and "fecha" in out.columns:
dates = parse_event_dates(out["fecha"])
out = out[dates < pd.to_datetime(before_date, errors="coerce", utc=True)]
return out
def parse_event_dates(series: pd.Series) -> pd.Series:
cleaned = series.astype(str).str.replace("Z", "", regex=False).str[:10]
return pd.to_datetime(cleaned, errors="coerce", utc=True, format="%Y-%m-%d")
def save_figure(fig, out_dir: Path, name: str, *, svg: bool = True, dpi: int = 160) -> tuple[Path, Path | None]:
"""Persiste una figura matplotlib como PNG (y opcionalmente SVG).
Devuelve los paths absolutos. NO cierra la figura: el caller decide
(algunos flujos también la serializan a base64 después).
"""
figs_dir = out_dir / "figures"
figs_dir.mkdir(parents=True, exist_ok=True)
png_path = figs_dir / f"{name}.png"
fig.savefig(png_path, dpi=dpi, bbox_inches="tight", facecolor=fig.get_facecolor())
svg_path: Path | None = None
if svg:
svg_path = figs_dir / f"{name}.svg"
fig.savefig(svg_path, format="svg", bbox_inches="tight", facecolor=fig.get_facecolor())
return png_path, svg_path
def save_table(df: pd.DataFrame, out_dir: Path, name: str) -> Path:
tables_dir = out_dir / "tables"
tables_dir.mkdir(parents=True, exist_ok=True)
csv_path = tables_dir / f"{name}.csv"
df.to_csv(csv_path, index=False)
return csv_path
def png_to_data_uri(path: Path) -> str:
return "data:image/png;base64," + base64.b64encode(path.read_bytes()).decode("ascii")