File size: 4,814 Bytes
d1858cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1ada03e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d1858cc
 
 
 
 
 
 
 
d290f9e
 
 
d1858cc
d290f9e
 
d1858cc
 
 
 
1ada03e
 
d1858cc
 
 
 
1ada03e
d1858cc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
"""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()