spjasper commited on
Commit
1357529
Β·
verified Β·
1 Parent(s): a4623b1

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +234 -38
src/streamlit_app.py CHANGED
@@ -1,40 +1,236 @@
1
- import altair as alt
2
- import numpy as np
3
- import pandas as pd
4
  import streamlit as st
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- """
7
- # Welcome to Streamlit!
8
-
9
- Edit `/streamlit_app.py` to customize this app to your heart's desire :heart:.
10
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
11
- forums](https://discuss.streamlit.io).
12
-
13
- In the meantime, below is an example of what you can do with just a few lines of code:
14
- """
15
-
16
- num_points = st.slider("Number of points in spiral", 1, 10000, 1100)
17
- num_turns = st.slider("Number of turns in spiral", 1, 300, 31)
18
-
19
- indices = np.linspace(0, 1, num_points)
20
- theta = 2 * np.pi * num_turns * indices
21
- radius = indices
22
-
23
- x = radius * np.cos(theta)
24
- y = radius * np.sin(theta)
25
-
26
- df = pd.DataFrame({
27
- "x": x,
28
- "y": y,
29
- "idx": indices,
30
- "rand": np.random.randn(num_points),
31
- })
32
-
33
- st.altair_chart(alt.Chart(df, height=700, width=700)
34
- .mark_point(filled=True)
35
- .encode(
36
- x=alt.X("x", axis=None),
37
- y=alt.Y("y", axis=None),
38
- color=alt.Color("idx", legend=None, scale=alt.Scale()),
39
- size=alt.Size("rand", legend=None, scale=alt.Scale(range=[1, 150])),
40
- ))
 
 
 
 
1
  import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import requests
6
+ from io import StringIO
7
+ from scipy import stats
8
+ from scipy.stats import poisson, mode
9
+
10
+ # ────────────────────────────────────────────────
11
+ # CONFIGURACIΓ“N DE PÁGINA
12
+ # ────────────────────────────────────────────────
13
+ st.set_page_config(
14
+ page_title="AnΓ‘lisis Equipo vs Rival + Momios",
15
+ layout="wide",
16
+ initial_sidebar_state="collapsed"
17
+ )
18
+
19
+ st.markdown("""
20
+ <style>
21
+ .main > div { padding: 0.8rem 0.8rem; }
22
+ .stApp { font-size: 12px; }
23
+ h1 { font-size: 1.3rem !important; margin: 0.3rem 0 0.5rem; }
24
+ h2, h3 { font-size: 1.05rem !important; margin: 0.5rem 0 0.3rem; }
25
+ .stButton > button { padding: 0.4rem 0.8rem; font-size: 13px; }
26
+ .stDataFrame { font-size: 11.5px; }
27
+ header { visibility: hidden; }
28
+ .element-container { margin: 0.3rem 0; }
29
+ hr { margin: 0.5rem 0; }
30
+ .form-win { color: #28a745; font-weight: bold; }
31
+ .form-draw { color: #ffc107; font-weight: bold; }
32
+ .form-loss { color: #dc3545; font-weight: bold; }
33
+ </style>
34
+ """, unsafe_allow_html=True)
35
+
36
+ plt.rcParams['figure.dpi'] = 90
37
+ plt.rcParams['figure.figsize'] = (6.5, 3.2)
38
+ plt.rcParams['font.size'] = 9
39
+
40
+ DEFAULT_URL = "https://www.football-data.co.uk/mmz4281/2526/E0.csv"
41
+
42
+ # ────────────────────────────────────────────────
43
+ # MÉTRICAS Y CARGA
44
+ # ────────────────────────────────────────────────
45
+ METRICS = {
46
+ "Goles anotados (FT)": ("FTHG", "FTAG"),
47
+ "Goles recibidos (FT)": ("FTAG", "FTHG"),
48
+ "Goles anotados (1T)": ("HTHG", "HTAG"),
49
+ "Goles recibidos (1T)": ("HTAG", "HTHG"),
50
+ "Corners a favor": ("HC", "AC"),
51
+ "Corners en contra": ("AC", "HC"),
52
+ "Tiros a favor": ("HS", "AS"),
53
+ "Tiros en contra": ("AS", "HS"),
54
+ "Tiros a puerta a favor": ("HST", "AST"),
55
+ "Tiros a puerta en contra": ("AST", "HST"),
56
+ "Faltas cometidas": ("HF", "AF"),
57
+ "Faltas recibidas": ("AF", "HF"),
58
+ "Tarjetas amarillas": ("HY", "AY"),
59
+ "Tarjetas rojas": ("HR", "AR")
60
+ }
61
+
62
+ @st.cache_data(show_spinner=False)
63
+ def load_data(url):
64
+ try:
65
+ r = requests.get(url, timeout=12)
66
+ r.raise_for_status()
67
+ df = pd.read_csv(StringIO(r.text))
68
+ num_cols = ['FTHG','FTAG','HTHG','HTAG','HC','AC','HS','AS','HST','AST','HF','AF','HY','AY','HR','AR']
69
+ for col in num_cols:
70
+ if col in df.columns:
71
+ df[col] = pd.to_numeric(df[col], errors='coerce')
72
+ return df
73
+ except Exception as e:
74
+ st.error(f"Error al cargar datos: {str(e)}")
75
+ return None
76
+
77
+ # FunciΓ³n modificada con INDICADORES DE COLOR (Emoji)
78
+ def get_team_form(df, team, n=5):
79
+ team_matches = df[(df['HomeTeam'] == team) | (df['AwayTeam'] == team)].tail(n)
80
+ results = []
81
+ points = 0
82
+ for _, row in team_matches.iterrows():
83
+ is_home = row['HomeTeam'] == team
84
+ goals_for = row['FTHG'] if is_home else row['FTAG']
85
+ goals_ag = row['FTAG'] if is_home else row['FTHG']
86
+ res = row['FTR']
87
+ if (is_home and res == 'H') or (not is_home and res == 'A'):
88
+ results.append(f"🟒 G({goals_for}-{goals_ag})")
89
+ points += 3
90
+ elif res == 'D':
91
+ results.append(f"🟑 E({goals_for}-{goals_ag})")
92
+ points += 1
93
+ else:
94
+ results.append(f"πŸ”΄ P({goals_for}-{goals_ag})")
95
+ return results[::-1], points # Mostrar del mΓ‘s reciente al mΓ‘s antiguo
96
+
97
+ def prob_to_decimal(p):
98
+ return round(1 / p, 2) if 0 < p < 1 else "β€”"
99
+
100
+ # ────────────────────────────────────────────────
101
+ # LΓ“GICA DE INTERFAZ
102
+ # ────────────────────────────────────────────────
103
+ st.title("⚽ AnÑlisis Equipo vs Rival + Momios Poisson")
104
+ data_url = st.text_input("URL CSV", value=DEFAULT_URL, label_visibility="collapsed")
105
+
106
+ if "df" not in st.session_state or st.session_state.get("last_url") != data_url:
107
+ df = load_data(data_url)
108
+ if df is not None:
109
+ st.session_state.df = df
110
+ st.session_state.last_url = data_url
111
+ else: st.stop()
112
+
113
+ df = st.session_state.df
114
+ teams = sorted(set(df["HomeTeam"]).union(set(df["AwayTeam"])))
115
+
116
+ col1, col2, col3, col4 = st.columns([2, 2, 2.8, 1.8])
117
+ with col1:
118
+ equipo = st.selectbox("Equipo principal", teams, index=teams.index("Arsenal") if "Arsenal" in teams else 0)
119
+ with col2:
120
+ rival = st.selectbox("Equipo rival", [t for t in teams if t != equipo])
121
+ with col3:
122
+ analysis_mode = st.radio("Modo de anΓ‘lisis", ["Contexto Local vs Visitante", "Historial general"], horizontal=True)
123
+ with col4:
124
+ period = st.radio("Período", ["Temporada completa", "Últimos 5 partidos"], horizontal=True)
125
+
126
+ if st.button("Analizar β†’", type="primary", use_container_width=True):
127
+ is_context_mode = analysis_mode.startswith("Contexto")
128
+ series_team, series_rival = {}, {}
129
+ for name in METRICS:
130
+ if is_context_mode:
131
+ s_team = df[df["HomeTeam"] == equipo][METRICS[name][0]].dropna()
132
+ s_rival = df[df["AwayTeam"] == rival][METRICS[name][1]].dropna()
133
+ else:
134
+ s_team = pd.concat([df[df["HomeTeam"]==equipo][METRICS[name][0]], df[df["AwayTeam"]==equipo][METRICS[name][1]]]).dropna()
135
+ s_rival = pd.concat([df[df["HomeTeam"]==rival][METRICS[name][0]], df[df["AwayTeam"]==rival][METRICS[name][1]]]).dropna()
136
+ if period == "Últimos 5 partidos":
137
+ s_team, s_rival = s_team.tail(5), s_rival.tail(5)
138
+ series_team[name], series_rival[name] = s_team, s_rival
139
+
140
+ st.session_state.update({"series_team": series_team, "series_rival": series_rival, "equipo": equipo, "rival": rival, "mode": analysis_mode, "period": period})
141
+
142
+ # ────────────────────────────────────────────────
143
+ # RESULTADOS
144
+ # ────────────────────────────────────────────────
145
+ if "series_team" in st.session_state:
146
+ eq, rv = st.session_state.equipo, st.session_state.rival
147
+
148
+ st.markdown(f"### **{eq}** vs **{rv}**")
149
+ st.caption(f"_{st.session_state.mode} β€’ {st.session_state.period}_")
150
+
151
+ # 1. Historial Reciente con Colores Visuales
152
+ st.subheader("πŸ•’ Historial Reciente (Últimos 5 partidos)")
153
+ form_eq, pts_eq = get_team_form(df, eq)
154
+ form_rv, pts_rv = get_team_form(df, rv)
155
+
156
+ cf1, cf2 = st.columns(2)
157
+ with cf1:
158
+ st.markdown(f"**{eq}**")
159
+ st.write(" | ".join(form_eq))
160
+ st.caption(f"Puntos obtenidos: {pts_eq}/15")
161
+ with cf2:
162
+ st.markdown(f"**{rv}**")
163
+ st.write(" | ".join(form_rv))
164
+ st.caption(f"Puntos obtenidos: {pts_rv}/15")
165
+ st.divider()
166
+
167
+ # 2. EstadΓ­sticas Detalladas
168
+ st.subheader("πŸ“Š EstadΓ­sticas detalladas")
169
+ stats_rows = []
170
+ for metric in METRICS:
171
+ t, r = st.session_state.series_team[metric], st.session_state.series_rival[metric]
172
+ if len(t) > 0 and len(r) > 0:
173
+ stats_rows.append({
174
+ "MΓ©trica": metric, f"{eq} ΞΌ": round(t.mean(),2), f"{eq} Med": round(t.median(),2),
175
+ f"{eq} Mo": round(float(mode(t, keepdims=True).mode[0]), 1),
176
+ f"{rv} ΞΌ": round(r.mean(),2), f"{rv} Med": round(r.median(),2),
177
+ f"{rv} Mo": round(float(mode(r, keepdims=True).mode[0]), 1),
178
+ "Ξ”": round(t.mean() - r.mean(), 2)
179
+ })
180
+ st.dataframe(pd.DataFrame(stats_rows).set_index("MΓ©trica"), use_container_width=True, height=350)
181
+
182
+ # 3. Ataque vs Defensa
183
+ st.subheader("βš”οΈ Ataque vs Defensa")
184
+ cross_rows = []
185
+ pairs = [("Goles anotados (FT)", "Goles recibidos (FT)", "⚽ Goles"), ("Goles anotados (1T)", "Goles recibidos (1T)", "⏰ Goles 1T"), ("Tiros a puerta a favor", "Tiros a puerta en contra", "πŸ₯… Tiros"), ("Corners a favor", "Corners en contra", "🚩 Corners")]
186
+ for atk, dfn, lbl in pairs:
187
+ t_a, r_d = st.session_state.series_team[atk].mean(), st.session_state.series_rival[dfn].mean()
188
+ r_a, t_d = st.session_state.series_rival[atk].mean(), st.session_state.series_team[dfn].mean()
189
+ cross_rows.append({"": lbl, "Eq": eq, "Atq": round(t_a,2), "vs": "β†’", "Riv": rv, "Def": round(r_d,2), "Ξ”": round(t_a - r_d,2)})
190
+ cross_rows.append({"": "", "Eq": rv, "Atq": round(r_a,2), "vs": "β†’", "Riv": eq, "Def": round(t_d,2), "Ξ”": round(r_a - t_d,2)})
191
+ st.dataframe(pd.DataFrame(cross_rows), use_container_width=True, height=250)
192
+
193
+ # 4. Momios Poisson Ampliados + Ajuste
194
+ st.subheader("πŸ’° Momios estimados (Poisson)")
195
+ l_eq, l_rv = st.session_state.series_team["Goles anotados (FT)"].mean(), st.session_state.series_rival["Goles anotados (FT)"].mean()
196
+ l_eq_ht, l_rv_ht = st.session_state.series_team["Goles anotados (1T)"].mean(), st.session_state.series_rival["Goles anotados (1T)"].mean()
197
+ lam_yellow = (st.session_state.series_team["Tarjetas amarillas"].mean() + st.session_state.series_rival["Tarjetas amarillas"].mean()) / 2
198
+ lam_red = (st.session_state.series_team["Tarjetas rojas"].mean() + st.session_state.series_rival["Tarjetas rojas"].mean()) / 2
199
+
200
+ p_h, p_d, p_a = 0, 0, 0
201
+ for h in range(12):
202
+ for a in range(12):
203
+ prob = poisson.pmf(h, l_eq) * poisson.pmf(a, l_rv)
204
+ if h > a: p_h += prob
205
+ elif h == a: p_d += prob
206
+ else: p_a += prob
207
+
208
+ def get_adj(pts):
209
+ if pts >= 10: return "πŸ”₯ Valor (Racha)"
210
+ if pts <= 4: return "⚠️ Riesgo (Baja)"
211
+ return "Normal"
212
+
213
+ momios = [
214
+ {"Mercado": f"1 ({eq})", "Prob%": f"{p_h*100:.1f}", "Momio": prob_to_decimal(p_h), "Ajuste": get_adj(pts_eq)},
215
+ {"Mercado": "X (Empate)", "Prob%": f"{p_d*100:.1f}", "Momio": prob_to_decimal(p_d), "Ajuste": "Estable"},
216
+ {"Mercado": f"2 ({rv})", "Prob%": f"{p_a*100:.1f}", "Momio": prob_to_decimal(p_a), "Ajuste": get_adj(pts_rv)},
217
+ {"Mercado": "Ambos anotan", "Prob%": f"{(1 - poisson.pmf(0,l_eq)*poisson.pmf(0,l_rv))*100:.1f}", "Momio": prob_to_decimal(1 - poisson.pmf(0,l_eq)*poisson.pmf(0,l_rv)), "Ajuste": "-"},
218
+ {"Mercado": "+2.5 goles", "Prob%": f"{(1 - poisson.cdf(2, l_eq + l_rv))*100:.1f}", "Momio": prob_to_decimal(1 - poisson.cdf(2, l_eq + l_rv)), "Ajuste": "-"},
219
+ {"Mercado": "-2.5 goles", "Prob%": f"{poisson.cdf(2, l_eq + l_rv)*100:.1f}", "Momio": prob_to_decimal(poisson.cdf(2, l_eq + l_rv)), "Ajuste": "-"},
220
+ {"Mercado": "+1.5 goles", "Prob%": f"{(1 - poisson.cdf(1, l_eq + l_rv))*100:.1f}", "Momio": prob_to_decimal(1 - poisson.cdf(1, l_eq + l_rv)), "Ajuste": "-"},
221
+ {"Mercado": "+3.5 goles", "Prob%": f"{(1 - poisson.cdf(3, l_eq + l_rv))*100:.1f}", "Momio": prob_to_decimal(1 - poisson.cdf(3, l_eq + l_rv)), "Ajuste": "-"},
222
+ {"Mercado": "+1.5 goles 1T", "Prob%": f"{(1 - poisson.cdf(1, l_eq_ht + l_rv_ht))*100:.1f}", "Momio": prob_to_decimal(1 - poisson.cdf(1, l_eq_ht + l_rv_ht)), "Ajuste": "-"},
223
+ {"Mercado": "+4.5 amarillas", "Prob%": f"{(1 - poisson.cdf(4, lam_yellow))*100:.1f}", "Momio": prob_to_decimal(1 - poisson.cdf(4, lam_yellow)), "Ajuste": "-"},
224
+ {"Mercado": "+0.5 rojas", "Prob%": f"{(1 - poisson.pmf(0, lam_red))*100:.1f}", "Momio": prob_to_decimal(1 - poisson.pmf(0, lam_red)), "Ajuste": "-"},
225
+ ]
226
+ st.dataframe(pd.DataFrame(momios), use_container_width=True, height=400)
227
 
228
+ # 5. GrΓ‘fico evolutivo
229
+ st.subheader("πŸ“ˆ EvoluciΓ³n por partido")
230
+ metric_viz = st.selectbox("MΓ©trica", options=list(METRICS.keys()), label_visibility="collapsed")
231
+ fig, ax = plt.subplots()
232
+ t, r = st.session_state.series_team[metric_viz], st.session_state.series_rival[metric_viz]
233
+ if len(t) > 0: ax.plot(t.values, "o-", label=eq, ms=5); ax.axhline(t.mean(), color="blue", ls="--", alpha=0.3)
234
+ if len(r) > 0: ax.plot(r.values, "s-", label=rv, ms=5); ax.axhline(r.mean(), color="orange", ls="--", alpha=0.3)
235
+ ax.legend(); ax.grid(alpha=0.2); ax.set_title(metric_viz)
236
+ st.pyplot(fig)