RRC / scripts /build_team_stats_agg.py
pablogrois's picture
team_stats agg: matchid como str (fix de duplicados no colapsados)
d290f9e
Raw
History Blame Contribute Delete
4.81 kB
"""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()