""" 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 ===")