Improve SEM dashboard: split Histórico tabs, shrink KPI metrics, add daily P&L chart

- Separate campaign-level daily metrics from the global portfolio view in
  the Histórico tab using nested sub-tabs instead of a single stacked section.
- Shrink st.metric font sizes globally so large KPI values render correctly.
- Add a daily evolution chart in Resumen showing Inversión, Ingreso,
  Margen (sumatorio) and Margen (PPL) aggregated across filtered campaigns.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Jose Manuel 2026-07-02 17:43:55 +02:00
parent 2ffc28cfbc
commit bfd130578a

View File

@ -1,6 +1,7 @@
"""Dashboard interactivo para leads-optimizer — Streamlit.""" """Dashboard interactivo para leads-optimizer — Streamlit."""
import streamlit as st import streamlit as st
import pandas as pd import pandas as pd
import altair as alt
import json import json
import subprocess import subprocess
import sys import sys
@ -20,6 +21,19 @@ st.set_page_config(
initial_sidebar_state="expanded", initial_sidebar_state="expanded",
) )
# Los KPI (st.metric) por defecto son demasiado grandes y se cortan en pantallas
# más estrechas o cuando el valor es largo (p.ej. "1.234,56€"). Los reducimos.
st.markdown(
"""
<style>
[data-testid="stMetricValue"] { font-size: 1.5rem; }
[data-testid="stMetricLabel"] { font-size: 0.8rem; }
[data-testid="stMetricDelta"] { font-size: 0.8rem; }
</style>
""",
unsafe_allow_html=True,
)
# ── Helpers ─────────────────────────────────────────────────────────────────── # ── Helpers ───────────────────────────────────────────────────────────────────
URGENCIA_ICON = { URGENCIA_ICON = {
@ -183,6 +197,48 @@ def _compute_analysis(row: dict, dia_actual: int, dias_mes: int) -> dict:
} }
def _daily_summary(rows: list[dict]) -> pd.DataFrame:
"""Agrega Inversión/Ingreso/Margen día a día sumando las MetricasDiarias
de todas las campañas dadas, más un margen alternativo calculado con el
PPL fijo de cada campaña en lugar del ingreso reportado."""
daily = {}
for r in rows:
metricas = r.get("metricas")
if not metricas:
continue
items = metricas.items() if isinstance(metricas, dict) else [
(m.get("fecha"), m) for m in metricas
]
for fecha, m in items:
if not fecha or not isinstance(m, dict):
continue
gasto = revenue = leads = None
for k, v in m.items():
kl = k.lower()
if gasto is None and ("coste" in kl or "cost" in kl):
gasto = float(v or 0)
elif revenue is None and ("ingres" in kl or "revenue" in kl):
revenue = float(v or 0)
elif leads is None and "lead" in kl:
leads = float(v or 0)
entry = daily.setdefault(fecha, {"gasto": 0.0, "revenue": 0.0, "revenue_ppl": 0.0})
entry["gasto"] += gasto or 0.0
entry["revenue"] += revenue or 0.0
entry["revenue_ppl"] += (leads or 0.0) * r["ppl"]
if not daily:
return pd.DataFrame()
df = pd.DataFrame([{"Fecha": f, **v} for f, v in sorted(daily.items())])
return pd.DataFrame({
"Fecha": df["Fecha"],
"Inversión": df["gasto"],
"Ingreso": df["revenue"],
"Margen (sumatorio)": df["revenue"] - df["gasto"],
"Margen (PPL)": df["revenue_ppl"] - df["gasto"],
})
# ── Sidebar ─────────────────────────────────────────────────────────────────── # ── Sidebar ───────────────────────────────────────────────────────────────────
st.sidebar.title("Leads Optimizer") st.sidebar.title("Leads Optimizer")
@ -277,6 +333,35 @@ with tab_resumen:
st.divider() st.divider()
# ── Evolución diaria del mes (inversión / ingreso / margen) ──────────────────
st.subheader("Evolución diaria del mes")
df_summary = _daily_summary(filtered)
if df_summary.empty:
st.info("Sin métricas diarias disponibles para las campañas filtradas.")
else:
df_long = df_summary.melt("Fecha", var_name="Serie", value_name="Valor")
serie_order = ["Inversión", "Ingreso", "Margen (sumatorio)", "Margen (PPL)"]
color_scale = alt.Scale(
domain=serie_order,
range=["#2a78d6", "#1baf7a", "#4a3aa7", "#eb6834"],
)
chart = (
alt.Chart(df_long)
.mark_line(point=True, strokeWidth=2)
.encode(
x=alt.X("Fecha:O", title=None),
y=alt.Y("Valor:Q", title=""),
color=alt.Color("Serie:N", scale=color_scale, sort=serie_order, legend=alt.Legend(title=None)),
tooltip=["Fecha", "Serie", alt.Tooltip("Valor:Q", format=",.2f")],
)
.properties(height=320)
)
st.altair_chart(chart, use_container_width=True)
st.caption("Margen (sumatorio) = Ingreso reportado Gasto · Margen (PPL) = Leads del día × PPL de campaña Gasto")
st.divider()
# ── Alertas activas ─────────────────────────────────────────────────────── # ── Alertas activas ───────────────────────────────────────────────────────
alerts = [] alerts = []
for r in filtered: for r in filtered:
@ -454,6 +539,9 @@ with tab_campanas:
# ════════════════════════════════════════════════════════════════════════════ # # ════════════════════════════════════════════════════════════════════════════ #
with tab_historico: with tab_historico:
hist_camp_tab, hist_portfolio_tab = st.tabs(["📋 Campañas", "🌐 Portfolio global"])
with hist_camp_tab:
st.subheader("Métricas diarias por campaña") st.subheader("Métricas diarias por campaña")
camp_names = [r["nombre"] for r in filtered if r["metricas"]] camp_names = [r["nombre"] for r in filtered if r["metricas"]]
@ -540,7 +628,7 @@ with tab_historico:
else: else:
st.info("Sin métricas diarias para esta campaña.") st.info("Sin métricas diarias para esta campaña.")
st.divider() with hist_portfolio_tab:
st.subheader("Portfolio agregado (GAMes)") st.subheader("Portfolio agregado (GAMes)")
@st.cache_data(ttl=300, show_spinner="Cargando portfolio...") @st.cache_data(ttl=300, show_spinner="Cargando portfolio...")