RRC / scripts /build_match_team_stats.py
pablogrois's picture
Claude Opus 5
Top equipos: variables por lado, y "Defensa" se fusiona en "Defensiva"
307ee85
Raw History Blame Contribute Delete
3.51 kB
"""team_stats a nivel PARTIDO (una fila por equipo-partido), sin agregar.
`build_team_stats_agg.py` promedia por (competencia, temporada, equipo) y con eso alcanza
para la app de Equipos y variables. El modelo ofensivo/defensivo de la app de Estilos, en
cambio, necesita el partido: los ejes defensivos se miden sobre **el rival de ese partido**
(self-join por `matchid`), y la vista de partido a partido grafica cada encuentro.
Baja el mismo CSV de Azure, se queda con las filas "90 mins", deduplica a la versión
coherente (misma lógica que el agregado) y guarda todo sin promediar.
Salida: vendor/data/match_team_stats.parquet
Uso: python scripts/build_match_team_stats.py [--csv /ruta/local.csv]
"""
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
sys.path.insert(0, str(Path(__file__).resolve().parent))
from build_team_stats_agg import CHUNK, META, REMOTE, _download, _pick_consistent_version # noqa: E402
OUT = Path(__file__).resolve().parents[1] / "vendor" / "data" / "match_team_stats.parquet"
KEEP_META = ["competencia", "temporada", "equipo", "rival", "fecha", "matchid",
"formacion", "formacion_rival"]
def extract(csv_path: Path) -> pd.DataFrame:
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()]
have_meta = [c for c in KEEP_META if c in head.columns]
parts = []
for i, chunk in enumerate(pd.read_csv(
csv_path, usecols=have_meta + ["estado"] + metric_cols, low_memory=False,
chunksize=CHUNK, dtype={"matchid": str, "temporada": str}), 1):
parts.append(chunk[chunk["estado"] == "90 mins"])
if i % 20 == 0:
print(f" chunk {i}…", flush=True)
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)
# El parquet queda FIEL al proveedor, igual que team_stats_agg.parquet. El saneo del
# 0 de tracking (= dato faltante) se aplica aguas abajo, en el build de cada app, para
# que las tres compartan exactamente la misma limpieza.
# Misma derivada que el agregado, pero por partido.
if {"cross_completados", "cross_total"} <= set(full.columns):
tot = full["cross_total"].where(full["cross_total"] > 0)
full["pct_exito_centros"] = (full["cross_completados"] / tot).round(4)
print(f" filas equipo-partido: {len(full):,} · partidos: {full['matchid'].nunique():,} · "
f"ligas: {full['competencia'].nunique()} · métricas: {len(metric_cols)}", flush=True)
return full
def main() -> None:
local = None
for i, a in enumerate(sys.argv):
if a == "--csv" and i + 1 < len(sys.argv):
local = Path(sys.argv[i + 1])
OUT.parent.mkdir(parents=True, exist_ok=True)
if local and local.exists():
print(f"[match_stats] usando CSV local {local}", flush=True)
df = extract(local)
else:
with tempfile.TemporaryDirectory() as td:
tmp = Path(td) / "team_stats.csv"
_download(tmp)
df = extract(tmp)
df.to_parquet(OUT, index=False)
print(f"[match_stats] OK → {OUT} ({OUT.stat().st_size/1e6:.1f} MB)", flush=True)
if __name__ == "__main__":
main()