Skip to content

Commit 8f0abf7

Browse files
style: pragmatic refactor
1 parent 4e6e624 commit 8f0abf7

11 files changed

Lines changed: 239 additions & 398 deletions

File tree

README.md

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,29 @@
11
[![CI](https://github.com/alvarosalinaso/united-passing-efficiency-24-25/actions/workflows/ci.yml/badge.svg)](https://github.com/alvarosalinaso/united-passing-efficiency-24-25/actions/workflows/ci.yml)
22

3-
Proyecto: Análisis de pases — Manchester United 2024-2025
3+
# Manchester United 24/25: Inteligencia de Pases
44

5-
Descripción:
6-
Repositorio con datos y scripts para analizar la eficiencia de pases del Manchester United (temporada 2024-2025). Contiene un CSV con estadísticas por jugador, un reporte filtrado para mediocampistas y scripts para limpieza, análisis y visualización.
5+
Métricas crudas del mediocampo de Old Trafford para esta temporada. Aquí medimos quién rompe líneas de verdad (xT, pases progresivos) y quién juega a la segura inflando su precisión.
76

8-
Contenido principal:
9-
- `passing.csv`: datos de pases y estadísticas por jugador (limpio).
10-
- `reporte_mediocampo.csv`: reporte filtrado con métricas por jugador del mediocampo.
11-
- `data_load.py`, `clean_data.py`, `analyze_mediocampo.py`, `plot_pases.py`: scripts de ayuda y análisis.
12-
- `LICENSE`: licencia MIT.
7+
## Archivos clave
8+
- `passing.csv`: Stats puras de FBRef / StatsBomb para toda la plantilla.
9+
- `reporte_mediocampo.csv`: Foco quirúrgico en jugadores de la medular.
1310

14-
Requisitos:
15-
- Python 3.8+
16-
- Instalar dependencias listadas en `requirements.txt`.
11+
## Setup
1712

18-
Instalación rápida:
1913
```powershell
2014
python -m venv .venv
2115
.\.venv\Scripts\activate
2216
pip install -r requirements.txt
2317
```
2418

25-
Uso básico:
19+
Para usar el core analítico rápido:
2620
```python
27-
import pandas as pd
2821
from data_load import load_data
29-
df, report = load_data('passing.csv', 'reporte_mediocampo.csv')
22+
df, df_m = load_data('passing.csv', 'reporte_mediocampo.csv')
3023
```
3124

32-
Notas:
33-
- He unificado nombres de archivos y eliminado duplicados redundantes para facilitar el uso.
34-
- CI está configurado para ejecutar pruebas si están presentes en el repositorio.
25+
## Disclaimer
26+
Scripts purgados y unificados. La pipeline limpia tira directamente los datasets para que el dash en Streamlit los escupa.
3527

36-
Licencia:
37-
Este proyecto se publica bajo la licencia MIT. Consulta `LICENSE`.
38-
39-
📞 Contacto
40-
- **Autor**: Álvaro Salinas Ortiz
41-
- **LinkedIn**: https://www.linkedin.com/in/alvaro-salinas-ortiz/
42-
- **Email**: alvarosalinasortiz@gmail.com
28+
> Bajo licencia MIT.
29+
> Álvaro Salinas Ortiz | alvarosalinasortiz@gmail.com

app.py

Lines changed: 64 additions & 149 deletions
Original file line numberDiff line numberDiff line change
@@ -1,168 +1,83 @@
11
import streamlit as st
2-
3-
# Modulos internos (Clean Architecture)
42
from src.streamlit.dashboard_data import load_streamlit_data
5-
from src.streamlit.dashboard_plots import (
6-
graficar_ranking,
7-
graficar_radar,
8-
graficar_heatmap_zonas,
9-
graficar_heatmap_xt,
10-
graficar_red_pases,
11-
graficar_evolucion
12-
)
13-
14-
# ─────────────────────────────── CONFIG ───────────────────────────────
15-
st.set_page_config(
16-
page_title="United Passing Intelligence | alvarosalinaso",
17-
page_icon="🏟️",
18-
layout="wide",
19-
initial_sidebar_state="expanded",
20-
)
3+
from src.streamlit.dashboard_plots import graficar_ranking, graficar_radar, graficar_heatmap_zonas, graficar_heatmap_xt, graficar_red_pases, graficar_evolucion
4+
5+
st.set_page_config(page_title="United Passing Data", page_icon="🏟️", layout="wide", initial_sidebar_state="expanded")
216

227
st.markdown("""
238
<style>
24-
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap');
9+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap');
2510
html, body, [class*="css"] { font-family: 'Inter', sans-serif; }
26-
.block-container { padding: 1.5rem 2rem; }
2711
.stApp { background-color: #0d1117; color: #e6edf3; }
28-
29-
/* SIDEBAR & METRICS */
30-
[data-testid="stSidebar"] { background: #161b22 !important; border-right: 1px solid #30363d; }
31-
[data-testid="metric-container"] { background: #161b22 !important; border: 1px solid #30363d; border-radius: 12px; padding: 1rem; }
12+
[data-testid="metric-container"] { background: #161b22 !important; border: 1px solid #30363d; border-radius: 8px; padding: 1rem; }
3213
[data-testid="stMetricValue"] { color: #58a6ff !important; font-weight: 800 !important; }
33-
[data-testid="stMetricDelta"] { font-weight: 600 !important; }
34-
35-
/* Text & Sections */
36-
h1 { color: #DA291C !important; font-weight: 800 !important; letter-spacing: -1px; }
37-
.section-header {
38-
font-weight: 800;
39-
font-size: 1.8rem;
40-
color: #DA291C;
41-
margin-top: 3rem;
42-
margin-bottom: 0.5rem;
43-
border-bottom: 1px solid #30363d;
44-
padding-bottom: 0.5rem;
45-
}
46-
.section-text { font-size: 1.05rem; color: #8b949e; margin-bottom: 1.5rem; line-height: 1.6; }
14+
.section-header { font-weight: 800; font-size: 1.8rem; color: #DA291C; margin-top: 3rem; border-bottom: 1px solid #30363d; padding-bottom: 0.5rem; }
15+
.section-text { font-size: 1.05rem; color: #8b949e; margin-bottom: 1.5rem; }
4716
</style>
4817
""", unsafe_allow_html=True)
4918

50-
# ─────────────────────────────── DATA LAYER ───────────────────────────
51-
df, df_time, kpis = load_streamlit_data()
19+
df_all, df_t, k_dict = load_streamlit_data()
5220

53-
# ─────────────────────────────── SIDEBAR ──────────────────────────────
5421
with st.sidebar:
55-
st.markdown("""
56-
<div style='text-align:center;margin-bottom:1.2rem;'>
57-
<div style='font-size:3rem;'>🏟️</div>
58-
<div style='font-weight:800;font-size:1.2rem;color:#DA291C;'>United Passing Intelligence</div>
59-
<div style='font-size:.8rem;color:#8b949e;margin-top:.2rem;'>Temporada 2024-25 · Premier League</div>
60-
</div>
61-
<hr>
62-
""", unsafe_allow_html=True)
63-
64-
st.markdown("**🎛️ Filtros Globales**")
65-
sel_pos = st.selectbox("Posición Táctica", ["Todos"] + sorted(df["pos"].unique().tolist()))
66-
min_apps = st.slider("Partidos Mínimos Jugados", 0, 32, 15)
67-
focus_player = st.selectbox("Jugador en Foco (Radares)", ["Todos"] + df["player"].unique().tolist())
22+
st.markdown("<h2 style='color:#DA291C;text-align:center;'>United Passing Stats</h2><hr>", unsafe_allow_html=True)
6823

24+
pos_filter = st.selectbox("Posición", ["Todos"] + sorted(df_all["pos"].unique().tolist()))
25+
min_games = st.slider("Min Partidos", 0, 32, 15)
26+
f_player = st.selectbox("Foco (Radar)", ["Todos"] + df_all["player"].unique().tolist())
27+
6928
st.markdown("---")
70-
st.markdown("**⚔️ Contexto Opositor (Filtro Senior)**")
71-
sel_context = st.radio("Nivel de Presión Rival", ["Resto PL", "Top 6 (Alta Presión)"], help="Audita qué jugador 'infla' sus estadísticas frente a equipos de baja tabal y quién sobrevive a la presión asfixiante de la élite de Inglaterra.")
72-
29+
opp_tier = st.radio("Dificultad de Partido", ["Resto PL", "Top 6"])
30+
7331
st.markdown("---")
74-
st.markdown("**📌 Métrica de Enfoque Principal**")
75-
metric_map = {
76-
"xT Generado (Ameaza Esperada)": "xT_gen", "Pases Progresivos / 90": "prog_passes",
77-
"Precisión de pases (%)": "pass_acc", "Índice de Verticalidad": "vert_idx",
78-
"Pérdidas / 90": "losses"
79-
}
80-
sel_metric_label = st.selectbox("Seleccionar", list(metric_map.keys()))
81-
sel_metric = metric_map[sel_metric_label]
82-
83-
# ─────────────────────────────── FILTER ───────────────────────────────
84-
df_f = df.copy()
85-
86-
# 1. Filtro Contextual (Crucial para el análisis Senior)
87-
target_tier = sel_context.replace(" (Alta Presión)", "")
88-
df_f = df_f[df_f["opponent_tier"] == target_tier]
89-
90-
if sel_pos != "Todos":
91-
df_f = df_f[df_f["pos"] == sel_pos]
92-
df_f = df_f[df_f["apps"] >= min_apps]
93-
94-
# ─────────────────────────────── HERO SECTION ───────────────────────────────
95-
st.markdown("<h1 style='margin-bottom:0;'>⚽ Manchester United: Midfield Intelligence</h1>", unsafe_allow_html=True)
96-
st.markdown("<p style='font-size:1.1rem; color:#8b949e; margin-top:.2rem;'>Un análisis interactivo del comportamiento en la construcción de juego durante la temporada 2024-25. Evalúa qué mediocampistas rompen líneas, quiénes retienen la posesión, y cómo fluye el balón en Old Trafford.</p><hr>", unsafe_allow_html=True)
97-
98-
# KPI ROW
99-
k1, k2, k3, k4, k5 = st.columns(5)
100-
k1.metric("Precisión Promedio", f"{df_f['pass_acc'].mean():.1f}%", delta=f"{df_f['pass_acc'].mean() - 87.0:+.1f}% vs liga")
101-
k2.metric("Prog. Passes / 90", f"{df_f['prog_passes'].mean():.1f}", delta=f"{df_f['prog_passes'].mean() - 3.8:+.1f} vs avg")
102-
k3.metric("xT Promedio / 90", f"{df_f['xT_gen'].mean():.3f}", delta="+2.1% vs prev")
103-
avg_vert = df_f['vert_idx'].mean()
104-
k4.metric("Índice Vertical", f"{avg_vert:.2f}", delta=f"{'↑ alto' if avg_vert > 0.65 else '↓ bajo'}")
105-
total_losses = df_f["losses"].mean()
106-
k5.metric("Pérdidas / 90", f"{total_losses:.1f}", delta=f"{-(total_losses - 2.1):+.1f} vs prev", delta_color="inverse")
107-
108-
st.info("💡 **Metodología Avanzada:** El **xT (Expected Threat)** cuantifica cuánto aumenta la probabilidad de que el United anote un gol producto directo de la zona a la que el jugador envía el pase. El **Índice de Verticalidad** castiga los pases horizontales de seguridad, premiando a los jugadores que asumen riesgos hacia el área rival.")
109-
110-
# ─────────────────────────────── SECCIÓN 1 ───────────────────────────────
111-
st.markdown("<div class='section-header'>1. Rankings y Perfiles Multidimensionales 📊</div>", unsafe_allow_html=True)
112-
st.markdown("<div class='section-text'>Todo análisis comienza aislando a los líderes estadísticos. Compara lado a lado cómo la plantilla rinde en la métrica seleccionada en el menú lateral, y evalúa el polígono de rendimiento completo del jugador en foco frente al resto de sus compañeros.</div>", unsafe_allow_html=True)
113-
114-
col_l, col_r = st.columns([3, 2])
115-
with col_l:
116-
df_rank = df_f.sort_values(sel_metric, ascending=(sel_metric == "losses"))
117-
st.plotly_chart(graficar_ranking(df_rank, sel_metric, sel_metric_label), use_container_width=True)
118-
119-
with col_r:
120-
st.plotly_chart(graficar_radar(df_f, focus_player), use_container_width=True)
121-
122-
with st.expander("Ver tabla completa de datos consolidados"):
123-
display_cols = {"player": "Jugador", "pos": "Pos.", "apps": "PJ", "pass_acc": "Precisión %", "prog_passes": "Prog/90", "key_passes": "Claves/90", "vert_idx": "Verticalidad", "xT_gen": "xT generado", "xA": "xA", "losses": "Pérdidas/90"}
124-
df_disp = df_f[list(display_cols.keys())].rename(columns=display_cols).round(2)
125-
if not df_disp.empty:
126-
cols_numericas = df_disp.select_dtypes(include=['number']).columns.tolist()
127-
try:
128-
styled_df = df_disp.style.background_gradient(cmap='viridis', subset=cols_numericas if cols_numericas else None).format(precision=2)
129-
st.dataframe(styled_df, use_container_width=True, hide_index=True)
130-
except ValueError:
131-
st.dataframe(df_disp, use_container_width=True, hide_index=True)
132-
133-
# ─────────────────────────────── SECCIÓN 2 ───────────────────────────────
134-
st.markdown("<div class='section-header'>2. Radiografía Táctica Funcional 🔥</div>", unsafe_allow_html=True)
135-
st.markdown("<div class='section-text'>El fútbol no se juega en una hoja de Excel, sino en el césped. Aisla a un jugador para observar empíricamente en qué zonas específicas del campo ejecuta sus entregas de balón (izquierda) y en qué cuadrantes genera el mayor grado de amenaza xT contra el arco rival (derecha).</div>", unsafe_allow_html=True)
136-
137-
col_sel, _ = st.columns([2, 5])
138-
hm_player = col_sel.selectbox("Filtrar jugador para los mapas de calor tácticos", df_f["player"].unique().tolist(), key="hm_player")
139-
player_row = df_f[df_f["player"] == hm_player].iloc[0]
32+
m_map = {"xT Generado": "xT_gen", "Pases Prog": "prog_passes", "Precisión": "pass_acc", "Verticalidad": "vert_idx", "Pérdidas": "losses"}
33+
target_stat = st.selectbox("Métrica Foco", list(m_map.keys()))
34+
sel_var = m_map[target_stat]
35+
36+
# filtro core
37+
cur_val = opp_tier.replace("Top 6", "Top 6")
38+
df_work = df_all[df_all["opponent_tier"] == cur_val].copy()
39+
40+
if pos_filter != "Todos": df_work = df_work[df_work["pos"] == pos_filter]
41+
df_work = df_work[df_work["apps"] >= min_games]
42+
43+
st.title("⚽ United: Foco en la Medular")
44+
st.markdown("Métricas transaccionales del mediocampo actual. Análisis duro sin fanatismos.")
45+
46+
c1, c2, c3, c4, c5 = st.columns(5)
47+
c1.metric("Acc%", f"{df_work['pass_acc'].mean():.1f}%")
48+
c2.metric("ProgP/90", f"{df_work['prog_passes'].mean():.1f}")
49+
c3.metric("xT/90", f"{df_work['xT_gen'].mean():.3f}")
50+
c4.metric("Índice Vert", f"{df_work['vert_idx'].mean():.2f}")
51+
c5.metric("Pérdidas/90", f"{df_work['losses'].mean():.1f}", delta_color="inverse")
52+
53+
st.markdown("<div class='section-header'>1. Leaderboards</div>", unsafe_allow_html=True)
54+
ca, cb = st.columns([3, 2])
55+
with ca:
56+
r_df = df_work.sort_values(sel_var, ascending=(sel_var == "losses"))
57+
st.plotly_chart(graficar_ranking(r_df, sel_var, target_stat), use_container_width=True)
58+
with cb: st.plotly_chart(graficar_radar(df_work, f_player), use_container_width=True)
59+
60+
with st.expander("Tabla Raw"):
61+
cols = {"player": "Jugador", "pos": "Pos", "apps": "PJ", "pass_acc": "Acc%", "prog_passes": "Prog/90", "vert_idx": "Vert", "xT_gen": "xT", "losses": "Pérdidas"}
62+
disp = df_work[list(cols.keys())].rename(columns=cols).round(2)
63+
st.dataframe(disp.style.background_gradient(cmap='viridis', subset=disp.select_dtypes('number').columns).format(precision=2), use_container_width=True, hide_index=True)
64+
65+
st.markdown("<div class='section-header'>2. Zonas de Amenaza (xT)</div>", unsafe_allow_html=True)
66+
p_sel = st.selectbox("Jugador para heatmap táctico", df_work["player"].unique().tolist())
67+
p_row = df_work[df_work["player"] == p_sel].iloc[0]
14068

14169
h1, h2 = st.columns(2)
142-
with h1:
143-
st.plotly_chart(graficar_heatmap_zonas(player_row), use_container_width=True)
144-
with h2:
145-
st.plotly_chart(graficar_heatmap_xt(player_row), use_container_width=True)
146-
147-
# ─────────────────────────────── SECCIÓN 3 ───────────────────────────────
148-
st.markdown("<div class='section-header'>3. Red de Asociaciones en el Campo 🕸️</div>", unsafe_allow_html=True)
149-
st.markdown("<div class='section-text'>La fortaleza de un mediocampo radica en sus triángulos de asociación. Observa el flujo microscópico de la pelota entre nuestras figuras. <b>El grosor del cable</b> señala el colosal volumen de pases exitosos completados entre ambos jugadores. <b>El tamaño del nodo</b> de cada jugador simboliza su contribución directa de xT en cada toque.</div>", unsafe_allow_html=True)
150-
151-
st.plotly_chart(graficar_red_pases(df), use_container_width=True)
152-
153-
# ─────────────────────────────── SECCIÓN 4 ───────────────────────────────
154-
st.markdown("<div class='section-header'>4. Consistencia y Evolución a lo largo de la Premier League 📈</div>", unsafe_allow_html=True)
155-
st.markdown("<div class='section-text'>Incluso las mayores estrellas sufren caídas de rendimiento. Compara a tus jugadores favoritos a lo largo de todos los partidos de la temporada usando una <b>Media Móvil de 5 Jornadas</b> que purga el ruido y clarifica la genuina curva de forma de los jugadores. Selecciona libremente qué métrica temporal contrastar.</div>", unsafe_allow_html=True)
156-
157-
ec1, ec2, _ = st.columns([2, 2, 6])
158-
with ec1:
159-
players_evo = st.multiselect("Vincular jugadores a comparar", df_f["player"].tolist(), default=df_f["player"].tolist()[:2], key="evo_players")
160-
with ec2:
161-
mevo_label = st.selectbox("Seleccionar Métrica a medir", ["Precisión %", "Pases prog/90", "xT generado"], key="metric_evo")
162-
163-
mevo_col = {"Precisión %": "pass_acc", "Pases prog/90": "prog_passes", "xT generado": "xT_gen"}[mevo_label]
70+
with h1: st.plotly_chart(graficar_heatmap_zonas(p_row), use_container_width=True)
71+
with h2: st.plotly_chart(graficar_heatmap_xt(p_row), use_container_width=True)
72+
73+
st.markdown("<div class='section-header'>3. Red de Flujo</div>", unsafe_allow_html=True)
74+
st.plotly_chart(graficar_red_pases(df_all), use_container_width=True)
75+
76+
st.markdown("<div class='section-header'>4. Tracking Estacional</div>", unsafe_allow_html=True)
77+
tr_c1, tr_c2 = st.columns([1,1])
78+
with tr_c1: p_track = st.multiselect("Benchmark jugadores", df_work["player"].tolist(), default=df_work["player"].tolist()[:2])
79+
with tr_c2: m_track_lb = st.selectbox("Benchmark Métrica", ["Precisión", "Pases Prog", "xT generado"])
16480

165-
if players_evo:
166-
st.plotly_chart(graficar_evolucion(df_time, df_f, players_evo, mevo_col, mevo_label), use_container_width=True)
167-
else:
168-
st.warning("Selecciona al menos un jugador para graficar su curva de evolución.")
81+
m_track_col = {"Precisión": "pass_acc", "Pases Prog": "prog_passes", "xT generado": "xT_gen"}[m_track_lb]
82+
if p_track: st.plotly_chart(graficar_evolucion(df_t, df_work, p_track, m_track_col, m_track_lb), use_container_width=True)
83+
else: st.warning("Mete a alguien para trazar la curva.")

0 commit comments

Comments
 (0)