Spaces:
Running
Running
| """ | |
| 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 ===") | |