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:
parent
2ffc28cfbc
commit
bfd130578a
90
dashboard.py
90
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(
|
||||
"""
|
||||
<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 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
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...")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user