RRC / vendor /scripts /analyze_blocks_raw.py
pablogrois's picture
Deploy MVP: API JSON + SPA + bundle/cache de artifacts CORE
e58615a
Raw
History Blame Contribute Delete
9.99 kB
"""
analyze_blocks_raw.py
----------------------
Cruza el phaseLabel del preprocessed_SSD_25-26 con el eventing crudo (XLSX)
para analizar si se cumplen las condiciones de bloque alto/bajo.
Preguntas:
1. % posesiones con phaseLabel definido.
2. Bloque ALTO: % posesiones con ≥3 presiones en campo propio del equipo con
el balón. Comparar vs posesiones sin phaseLabel.
3. Bloque BAJO: % posesiones con ≥3 pases donde hay ≥7 opciones en campo rival
del equipo con el balón. Comparar vs sin phaseLabel.
Coords Opta normalizadas: x=0–100. El equipo con el balón ataca hacia x=100.
→ campo propio = x < 50
→ campo rival = x > 50
"""
import json
import sys
from pathlib import Path
import pandas as pd
PREPROCESSED = Path("/Users/pagrois/Documents/Racing/preprocessed_SSD_25-26.csv")
RAW_DIR = Path("/Users/pagrois/Documents/Racing/raw_events")
# ---------------------------------------------------------------------------
# Cargar preprocessed Racing — nivel posesión
# ---------------------------------------------------------------------------
print("Cargando preprocessed...")
df_pre = pd.read_csv(PREPROCESSED, low_memory=False)
mask = (
df_pre["TeamName"].str.contains("Racing de Santander", na=False)
| df_pre["TeamRival"].str.contains("Racing de Santander", na=False)
)
df_racing = df_pre[mask].copy()
df_racing["fecha_str"] = df_racing["fecha"].str[:10]
print(f" {len(df_racing):,} filas (posesiones) de Racing")
print(f" {df_racing['matchId'].nunique()} partidos")
# phaseLabel básico
total = len(df_racing)
with_phase = df_racing["phaseLabel"].notna().sum()
print(f"\n--- phaseLabel ---")
print(f" Con phaseLabel: {with_phase:,} / {total:,} ({100*with_phase/total:.1f}%)")
print()
print(df_racing["phaseLabel"].value_counts(dropna=False).to_string())
# Mapa matchId → fecha para buscar XLSX
match_info = (
df_racing[["matchId","fecha_str","TeamName","TeamRival"]]
.drop_duplicates("matchId")
.set_index("matchId")
)
# ---------------------------------------------------------------------------
# Cargar raw events de los 32 partidos
# ---------------------------------------------------------------------------
print("\nCargando raw events (XLSX)...")
# mapa fecha_str → archivo
xlsx_map = {f.name[:10]: f for f in RAW_DIR.glob("*.xlsx")}
all_events = []
for mid, row in match_info.iterrows():
fpath = xlsx_map.get(row["fecha_str"])
if fpath is None:
print(f" SIN XLSX: {row['fecha_str']} ({row['TeamName']} vs {row['TeamRival']})")
continue
df_ev = pd.read_excel(fpath, sheet_name="Eventos", engine="openpyxl")
# Asegurar matchId
if "matchId" not in df_ev.columns:
df_ev["matchId"] = mid
all_events.append(df_ev)
df_events = pd.concat(all_events, ignore_index=True)
print(f" {len(df_events):,} eventos totales de {len(all_events)} partidos")
# ---------------------------------------------------------------------------
# Helpers de parseo
# ---------------------------------------------------------------------------
def safe_parse(val):
if pd.isna(val):
return None
if isinstance(val, (dict, list)):
return val
try:
return json.loads(val)
except Exception:
return None
# ---------------------------------------------------------------------------
# Análisis de PRESIONES por posesión
# ---------------------------------------------------------------------------
# La columna `pressure` en el raw event es un dict {"pressureReceived": {...},
# "player": [{positionX, positionY, ...}, ...]}
# Queremos: ¿cuántas presiones (players) tiene esa posesión en x < 50?
# Un evento con pressure no-nulo = "este evento recibió presión".
# La posición relevante es la del evento mismo (x del receptor/portador), no
# la del presionador, para determinar en qué campo está la presión.
#
# Pero la pregunta original era "presiones en campo propio del equipo con el
# balón" → x < 50 del evento que tiene el balón (donde está el portador).
# El x del evento es la posición del jugador con el balón.
def count_pressures_own_half(ev_group: pd.DataFrame) -> int:
"""Cuenta eventos con presión en campo propio del equipo con el balón (x < 50)."""
pressed = ev_group[ev_group["pressure"].notna()]
in_own_half = pressed[pressed["x"] < 50]
return len(in_own_half)
def count_passes_7options_rival_half(ev_group: pd.DataFrame) -> int:
"""
Cuenta pases en campo rival (x > 50) con ≥7 opciones disponibles.
passOption = {"player": [...lista de opciones...]}
"""
pass_events = ev_group[ev_group["passOption"].notna()].copy()
if pass_events.empty:
return 0
count = 0
for _, ev in pass_events.iterrows():
# campo rival
if pd.isna(ev["x"]) or float(ev["x"]) <= 50:
continue
opts = safe_parse(ev["passOption"])
if opts is None:
continue
players = opts.get("player", []) if isinstance(opts, dict) else []
if len(players) >= 7:
count += 1
return count
# ---------------------------------------------------------------------------
# Agregar condiciones por posesión
# ---------------------------------------------------------------------------
print("\nAgreando condiciones por posesión...")
# Solo trabajamos con eventos que tienen possessionId
ev_with_poss = df_events[df_events["possessionId"].notna()].copy()
print(f" Eventos con possessionId: {len(ev_with_poss):,}")
# Agrupar
poss_stats = []
for (match_id, poss_id), grp in ev_with_poss.groupby(["matchId", "possessionId"]):
n_pressure = count_pressures_own_half(grp)
n_pass_7 = count_passes_7options_rival_half(grp)
poss_stats.append({
"matchId": match_id,
"possessionId": poss_id,
"n_pressure_own_half": n_pressure,
"n_pass_7opt_rival": n_pass_7,
"cond_alto": n_pressure >= 3,
"cond_bajo": n_pass_7 >= 3,
})
df_poss = pd.DataFrame(poss_stats)
print(f" {len(df_poss):,} posesiones únicas en raw events")
# ---------------------------------------------------------------------------
# Cruzar con phaseLabel del preprocessed
# ---------------------------------------------------------------------------
# El preprocessed tiene possessionId y matchId (aunque no todos tienen possessionId)
df_phase = df_racing[["matchId","possessionId","phaseLabel"]].dropna(subset=["possessionId"]).copy()
df_phase["possessionId"] = df_phase["possessionId"].astype(str)
df_poss["possessionId"] = df_poss["possessionId"].astype(str)
df_poss["matchId"] = df_poss["matchId"].astype(str)
df_merged = df_poss.merge(
df_phase[["matchId","possessionId","phaseLabel"]],
on=["matchId","possessionId"],
how="left"
)
has_phase = df_merged["phaseLabel"].notna().sum()
print(f"\n Posesiones en raw events con phaseLabel: {has_phase:,} / {len(df_merged):,}")
# ---------------------------------------------------------------------------
# Resultados BLOQUE ALTO
# ---------------------------------------------------------------------------
alto_mask = df_merged["phaseLabel"].str.contains("High Block", na=False)
no_ph_mask = df_merged["phaseLabel"].isna()
medio_mask = df_merged["phaseLabel"].str.contains("Medium Block", na=False)
df_alto = df_merged[alto_mask]
df_medio = df_merged[medio_mask]
df_noph = df_merged[no_ph_mask]
print("\n====== BLOQUE ALTO (≥3 presiones en campo propio) ======")
for label, subset in [
("High Block", df_alto),
("Medium Block", df_medio),
("Sin phaseLabel",df_noph),
]:
if subset.empty:
print(f" {label}: sin datos")
continue
n_true = subset["cond_alto"].sum()
n_tot = len(subset)
pct = 100 * n_true / n_tot
print(f" {label:20s}: {pct:5.1f}% ({n_true}/{n_tot})")
# Distribución de n_pressure
print("\n Distribución presiones campo propio (High Block):")
print(df_alto["n_pressure_own_half"].describe().to_string())
print("\n Distribución presiones campo propio (Sin phaseLabel, sample 5000):")
print(df_noph["n_pressure_own_half"].describe().to_string())
# ---------------------------------------------------------------------------
# Resultados BLOQUE BAJO
# ---------------------------------------------------------------------------
bajo_mask = df_merged["phaseLabel"].str.contains("Low Block", na=False)
df_bajo = df_merged[bajo_mask]
print("\n====== BLOQUE BAJO (≥3 pases con ≥7 opciones en campo rival) ======")
for label, subset in [
("Low Block", df_bajo),
("High Block", df_alto),
("Sin phaseLabel",df_noph),
]:
if subset.empty:
print(f" {label}: sin datos")
continue
n_true = subset["cond_bajo"].sum()
n_tot = len(subset)
pct = 100 * n_true / n_tot
print(f" {label:20s}: {pct:5.1f}% ({n_true}/{n_tot})")
print("\n Distribución pases ≥7 opciones campo rival (Low Block):")
print(df_bajo["n_pass_7opt_rival"].describe().to_string())
print("\n Distribución pases ≥7 opciones campo rival (Sin phaseLabel):")
print(df_noph["n_pass_7opt_rival"].describe().to_string())
# ---------------------------------------------------------------------------
# Cuántas opciones tienen en promedio los pases en campo rival
# ---------------------------------------------------------------------------
print("\n====== DEBUG: opciones promedio en pases ======")
# Tomar muestra de pases en campo rival
pass_ev = ev_with_poss[(ev_with_poss["passOption"].notna()) & (ev_with_poss["x"] > 50)].copy()
pass_ev["n_options"] = pass_ev["passOption"].apply(
lambda v: len(safe_parse(v).get("player", [])) if safe_parse(v) and isinstance(safe_parse(v), dict) else 0
)
print(f" Total pases en campo rival con passOption: {len(pass_ev):,}")
print(pass_ev["n_options"].describe().to_string())
print(f" Pases con ≥7 opciones: {(pass_ev['n_options'] >= 7).sum():,} ({100*(pass_ev['n_options'] >= 7).mean():.1f}%)")
print("\n=== Análisis completo ===")