"""Construye el agregado por equipo de team_stats para la sección "Equipos y variables". Baja ``raw/eventing/team_stats/team_stats_all_leagues.csv`` (stats POR PARTIDO y por equipo, ~870 MB) de Azure y lo agrega a un **perfil por (competencia, temporada, equipo)**: promedio por partido de cada métrica + nº de partidos. Sale a ``vendor/data/team_stats_agg.parquet`` (chico), que la app carga para el scoring z-score — así no hay que bajar el CSV gigante en el Space. Uso: python scripts/build_team_stats_agg.py """ from __future__ import annotations import sys import tempfile from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) import pandas as pd from racing_reports.datastore import DataStore REMOTE = "raw/eventing/team_stats/team_stats_all_leagues.csv" OUT = Path(__file__).resolve().parents[1] / "vendor" / "data" / "team_stats_agg.parquet" # Columnas de identificación / no-métricas (no entran como variables). META = ["equipo", "rival", "competencia", "fecha", "temporada", "matchid", "estado", "formacion", "formacion_rival"] KEYS = ["competencia", "temporada", "equipo"] CHUNK = 200_000 def _download(local: Path) -> None: fs = DataStore()._filesystem_client() fc = fs.get_file_client(REMOTE) print(f"Descargando {REMOTE} → {local} …", flush=True) with open(local, "wb") as fh: fc.download_file().readinto(fh) print(f" {local.stat().st_size / 1e6:.0f} MB", flush=True) def _pick_consistent_version(full: pd.DataFrame) -> pd.DataFrame: """Cada equipo-partido aparece con 2 versiones (conteos idénticos pero posesión/ derivadas distintas; una está mal). Por (equipo, partido) nos quedamos con la versión cuya posesión es COHERENTE con el dominio de pases: el equipo que metió más pases que el rival debe tener >50% de posesión. Así descartamos la versión espuria (p.ej. 73% con menos pases que el rival).""" mid = KEYS + ["matchid"] match_keys = ["competencia", "temporada", "matchid"] # pases del equipo por partido (idénticos entre versiones) y dominio vs rival: # el total del partido suma los DOS equipos (agrupar por partido, sin 'equipo'). tp = full.groupby(mid, dropna=False)["pases_totales"].first().reset_index() tot = tp.groupby(match_keys)["pases_totales"].transform("sum") tp["dominant"] = tp["pases_totales"] > (tot - tp["pases_totales"]) full = full.merge(tp[mid + ["dominant"]], on=mid, how="left") # rank: dominante → mayor posesión; no dominante → menor posesión. pos = pd.to_numeric(full["pct_posesion"], errors="coerce") full["__poskey"] = pos.where(full["dominant"], -pos).fillna(-1e9) keep = full.groupby(mid, dropna=False)["__poskey"].idxmax() return full.loc[keep].drop(columns=["dominant", "__poskey"]).reset_index(drop=True) def _aggregate(csv_path: Path) -> pd.DataFrame: # Detectar columnas métricas (numéricas) desde el header. head = pd.read_csv(csv_path, nrows=2000) metric_cols = [c for c in head.columns if c not in META and pd.to_numeric(head[c], errors="coerce").notna().any()] # Quedarnos solo con las filas "90 mins" (total del partido) — son pocas (~200k), # así que las juntamos para deduplicar por matchid de forma global y agregar. parts = [] # matchid y temporada SIEMPRE como str: hay matchids numéricos y alfanuméricos, # y leer por chunks infiere el tipo por chunk → el mismo matchid numérico queda # int en un chunk y str en otro y NO agrupa → versiones duplicadas sin colapsar. for chunk in pd.read_csv(csv_path, usecols=KEYS + ["estado", "matchid"] + metric_cols, low_memory=False, chunksize=CHUNK, dtype={"matchid": str, "temporada": str}): parts.append(chunk[chunk["estado"] == "90 mins"]) full = pd.concat(parts, ignore_index=True).drop(columns=["estado"]) for c in metric_cols: full[c] = pd.to_numeric(full[c], errors="coerce") full = _pick_consistent_version(full) full = full.drop(columns=["matchid"]) g = full.groupby(KEYS, dropna=False) mean = g[metric_cols].mean() out = mean.reset_index() out["n_partidos"] = g.size().values print(f"Partidos (versión consistente): {len(full):,} | equipos-temporada: {len(out):,} | " f"métricas: {len(metric_cols)}", flush=True) return out def main() -> None: OUT.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory() as td: csv_path = Path(td) / "team_stats_all_leagues.csv" _download(csv_path) agg = _aggregate(csv_path) agg.to_parquet(OUT, index=False) print(f"Guardado: {OUT} ({OUT.stat().st_size / 1e6:.1f} MB)") if __name__ == "__main__": main()