Update src/streamlit_app.py
Browse files- 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 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|