From bfd130578a2896d74fb80e451f8c118fdcdc678a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Manuel=20G=C3=B3mez?= Date: Thu, 2 Jul 2026 17:43:55 +0200 Subject: [PATCH] =?UTF-8?q?Improve=20SEM=20dashboard:=20split=20Hist=C3=B3?= =?UTF-8?q?rico=20tabs,=20shrink=20KPI=20metrics,=20add=20daily=20P&L=20ch?= =?UTF-8?q?art?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- dashboard.py | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/dashboard.py b/dashboard.py index 835b665..7b0fc52 100644 --- a/dashboard.py +++ b/dashboard.py @@ -1,6 +1,7 @@ """Dashboard interactivo para leads-optimizer — Streamlit.""" import streamlit as st import pandas as pd +import altair as alt import json import subprocess import sys @@ -20,6 +21,19 @@ st.set_page_config( 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( + """ + + """, + unsafe_allow_html=True, +) + # ── Helpers ─────────────────────────────────────────────────────────────────── 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 ─────────────────────────────────────────────────────────────────── st.sidebar.title("Leads Optimizer") @@ -277,6 +333,35 @@ with tab_resumen: 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 ─────────────────────────────────────────────────────── alerts = [] for r in filtered: @@ -454,6 +539,9 @@ with tab_campanas: # ════════════════════════════════════════════════════════════════════════════ # 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") camp_names = [r["nombre"] for r in filtered if r["metricas"]] @@ -540,7 +628,7 @@ with tab_historico: else: st.info("Sin métricas diarias para esta campaña.") - st.divider() +with hist_portfolio_tab: st.subheader("Portfolio agregado (GAMes)") @st.cache_data(ttl=300, show_spinner="Cargando portfolio...")