Spaces:
Running
Running
File size: 6,677 Bytes
46dd349 b3df273 46dd349 b3df273 c72c8e6 d6106d6 b3df273 46dd349 281bc09 b3df273 a259cb8 b3df273 46dd349 b3df273 46dd349 b3df273 46dd349 b3df273 bf4531b b3df273 bf4531b b3df273 46dd349 b3df273 46dd349 b3df273 46dd349 b3df273 46dd349 b3df273 46dd349 bf4531b 46dd349 bf4531b 46dd349 b3df273 46dd349 b3df273 bf4531b b3df273 bf4531b 46dd349 b3df273 46dd349 bf4531b b3df273 bf4531b 34a5455 b3df273 46dd349 bf4531b 34a5455 bf4531b b3df273 | 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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | """Datos de la sección "Recursos" (scatter + tablas por equipo).
Lee el artefacto bundleado ``vendor/data/lowblock/team_resources.parquet``: una fila por
(liga, temporada, equipo, lado of/def, recurso, bloque bajo/medio/alto) con **sumas crudas**.
El bloque se elige en el front (uno o varios); acá se **suman** las sumas de los bloques
elegidos y se recalculan las tasas, para que cualquier combinación de bloques sea consistente.
``n_matches`` (partidos jugados por equipo-temporada) da "intentos por 90'".
"""
from __future__ import annotations
from functools import lru_cache
import pandas as pd
from racing_reports import vendor_env
PATH = vendor_env.DATA_DIR / "lowblock" / "team_resources.parquet"
RECURSO_LABELS = {
"todos": "Todos los recursos (construcción)",
"pase_entre_lineas": "Pase al espacio (throughball)",
"cutback": "Cutback",
"centro_colgado": "Centro colgado",
"centro_inswinger": "Centro inswinger",
"centro_outswinger": "Centro outswinger",
"centro_raso": "Centro raso",
"rompe_lineas_ok": "Rompe-líneas (completado)",
"gambeta": "1 vs 1",
"tiro_lejano": "Tiro desde fuera del área",
}
METRICA_LABELS = {
"pv_jugada": "Peligro por jugada (pv ‰)",
"pv_jugada_exitosa": "Peligro por jugada exitosa (pv ‰)",
"evento_clave": "Veces evento clave del ataque",
"tiros_jugada": "Tiros por jugada",
"xg_acum": "xG acumulado",
"xg_jugada": "xG por jugada",
"xg_jugada_exitosa": "xG por jugada exitosa",
"goles": "Goles",
"pct_goles": "% de goles",
"pct_exito": "% de éxito",
"n_intentos": "Cantidad de intentos",
"intentos_90": "Intentos por 90'",
}
BLOQUE_LABELS = {"bajo": "Bloque bajo", "medio": "Bloque medio", "alto": "Bloque alto"}
BLOQUES = ("bajo", "medio", "alto")
# sumas crudas del artefacto que se agregan al combinar bloques
RAW = ["n_intentos", "n_exitos", "pv_sum", "pv_n", "pv_ok_sum", "pv_ok_n", "evento_clave",
"n_seqs", "tiros", "goles", "xg_acum", "n_ok_seqs", "xg_ok"]
KEYS = ["liga", "temporada", "equipo", "lado", "recurso"]
@lru_cache(maxsize=1)
def _df() -> pd.DataFrame:
return pd.read_parquet(PATH)
def _parse_bloques(bloques: str | None) -> list[str]:
if not bloques:
return list(BLOQUES)
sel = [b.strip() for b in bloques.split(",") if b.strip() in BLOQUES]
return sel or list(BLOQUES)
def _combine(df: pd.DataFrame) -> pd.DataFrame:
"""Suma las sumas crudas de los bloques ya filtrados y recalcula tasas por fila."""
g = df.groupby(KEYS, as_index=False)
out = g[RAW].sum()
out = out.merge(g["n_matches"].max(), on=KEYS)
def rate(num, den, factor=1.0, nd=2):
return (out[num] / out[den].where(out[den] > 0) * factor).round(nd)
out["pct_exito"] = rate("n_exitos", "n_intentos", 100, 1)
out["pv_jugada"] = rate("pv_sum", "pv_n", 1000, 2)
out["pv_jugada_exitosa"] = rate("pv_ok_sum", "pv_ok_n", 1000, 2)
out["tiros_jugada"] = rate("tiros", "n_seqs", 1, 3)
out["xg_jugada"] = rate("xg_acum", "n_seqs", 1, 4)
out["xg_jugada_exitosa"] = rate("xg_ok", "n_ok_seqs", 1, 4)
out["pct_goles"] = rate("goles", "n_seqs", 100, 2)
out["intentos_90"] = rate("n_intentos", "n_matches", 1, 1)
out["xg_acum"] = out["xg_acum"].round(2)
# recursos donde algunas métricas no aplican (ver build): tiro_lejano REALIZA el peligro,
# rompe-líneas solo existe completado.
tiro = out["recurso"] == "tiro_lejano"
out.loc[tiro, ["pv_jugada", "pv_jugada_exitosa", "evento_clave"]] = pd.NA
out.loc[out["recurso"] == "rompe_lineas_ok", "pct_exito"] = pd.NA
return out
DISPLAY = ["liga", "temporada", "equipo", "lado", "recurso", "n_intentos", "intentos_90",
"n_matches", "pct_exito", "pv_jugada", "pv_jugada_exitosa", "evento_clave",
"tiros_jugada", "xg_acum", "xg_jugada", "xg_jugada_exitosa", "goles", "pct_goles"]
def options() -> dict:
df = _df()
seasons = {lg: sorted(g["temporada"].astype(str).unique(), reverse=True)
for lg, g in df.groupby("liga")}
return {
"leagues": sorted(df["liga"].unique().tolist()),
"seasons": seasons,
"recursos": RECURSO_LABELS,
"metricas": METRICA_LABELS,
"lados": {"of": "Ofensivo", "def": "Defensivo"},
"bloques": BLOQUE_LABELS,
}
def scatter_rows(league: str, bloques: str | None = None) -> list[dict]:
df = _df()
df = df[df["bloque"].isin(_parse_bloques(bloques))]
if league and league != "__all__":
df = df[df["liga"] == league]
out = _combine(df)[DISPLAY]
return out.where(pd.notna(out), None).to_dict("records")
# métricas de tasa (media de liga ponderada por intentos) vs de conteo (media simple)
DIF_METRICS = {"pct_exito": 1, "pv_jugada": 2, "pv_jugada_exitosa": 2,
"tiros_jugada": 3, "xg_jugada": 4, "pct_goles": 2}
COUNT_METRICS = {"n_intentos": 1, "intentos_90": 1, "evento_clave": 1, "goles": 1}
def team_tables(league: str, equipo: str, season: str | None = None,
bloques: str | None = None) -> dict:
df = _df()
df = df[df["bloque"].isin(_parse_bloques(bloques))]
liga_df = df[df["liga"] == league]
if season:
liga_df = liga_df[liga_df["temporada"].astype(str) == str(season)]
if liga_df.empty:
raise ValueError(f"Liga/temporada sin datos: {league} {season or ''}")
comb = _combine(liga_df)
out = {}
for lado in ("of", "def"):
sub = comb[comb["lado"] == lado]
# media de la liga ponderada por intentos, por recurso y métrica
medias: dict[str, dict] = {}
for rec, g in sub.groupby("recurso"):
w = g["n_intentos"].clip(lower=1)
medias[rec] = {}
for m, nd in DIF_METRICS.items():
v = (g[m] * w).sum() / w.sum()
medias[rec][m] = round(float(v), nd) if pd.notna(v) else None
for m, nd in COUNT_METRICS.items():
v = g[m].mean()
medias[rec][m] = round(float(v), nd) if pd.notna(v) else None
rows = sub[sub["equipo"] == equipo].sort_values("pv_jugada", ascending=False)
recs = rows[DISPLAY].where(pd.notna(rows[DISPLAY]), None).to_dict("records")
for r in recs:
mu = medias.get(r["recurso"], {})
r["dif"] = {}
for m, nd in {**DIF_METRICS, **COUNT_METRICS}.items():
if r.get(m) is not None and mu.get(m) is not None:
r["dif"][m] = round(float(r[m]) - mu[m], nd)
out[lado] = {"rows": recs, "liga_media": medias}
return {"league": league, "season": season, "equipo": equipo,
"recursos": RECURSO_LABELS, **out}
|