Compare commits
No commits in common. "94745cfda3a001b9d39041a7a312fcfcdbef4717" and "2ffc28cfbc58cdc026204ada00c0d26414363456" have entirely different histories.
94745cfda3
...
2ffc28cfbc
90
dashboard.py
90
dashboard.py
@ -1,7 +1,6 @@
|
|||||||
"""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
|
||||||
@ -21,19 +20,6 @@ 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 = {
|
||||||
@ -197,48 +183,6 @@ 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")
|
||||||
@ -333,35 +277,6 @@ 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:
|
||||||
@ -539,9 +454,6 @@ 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"]]
|
||||||
@ -628,7 +540,7 @@ with hist_camp_tab:
|
|||||||
else:
|
else:
|
||||||
st.info("Sin métricas diarias para esta campaña.")
|
st.info("Sin métricas diarias para esta campaña.")
|
||||||
|
|
||||||
with hist_portfolio_tab:
|
st.divider()
|
||||||
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...")
|
||||||
|
|||||||
9
run.py
9
run.py
@ -400,8 +400,6 @@ def run():
|
|||||||
if cambio_mes:
|
if cambio_mes:
|
||||||
# Ayer pertenece al mes anterior: redirigir escritura al GACampaignMes correcto
|
# Ayer pertenece al mes anterior: redirigir escritura al GACampaignMes correcto
|
||||||
prev_map = at.get_gcm_id_map_for_month(ayer.month, ayer.year)
|
prev_map = at.get_gcm_id_map_for_month(ayer.month, ayer.year)
|
||||||
redirected = []
|
|
||||||
omitidos = []
|
|
||||||
for u in metricas_updates:
|
for u in metricas_updates:
|
||||||
gid = next(
|
gid = next(
|
||||||
(item["campaign"]["google_campaign_id"]
|
(item["campaign"]["google_campaign_id"]
|
||||||
@ -410,13 +408,6 @@ def run():
|
|||||||
)
|
)
|
||||||
if gid and gid in prev_map:
|
if gid and gid in prev_map:
|
||||||
u["airtable_id"] = prev_map[gid]
|
u["airtable_id"] = prev_map[gid]
|
||||||
redirected.append(u)
|
|
||||||
else:
|
|
||||||
omitidos.append(gid)
|
|
||||||
metricas_updates = redirected
|
|
||||||
if omitidos:
|
|
||||||
print(f" ⚠️ {len(omitidos)} campañas sin GACampaignMes en {ayer.month}/{ayer.year}, "
|
|
||||||
f"MetricasDiarias del día anterior omitido para no contaminar el mes nuevo: {omitidos}")
|
|
||||||
at.batch_update_metricas_diarias(metricas_updates)
|
at.batch_update_metricas_diarias(metricas_updates)
|
||||||
print(" ✓ MetricasDiarias actualizado.")
|
print(" ✓ MetricasDiarias actualizado.")
|
||||||
|
|
||||||
|
|||||||
@ -73,9 +73,8 @@ def _trunc(text: str, limit: int = 2950) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _fmt_eur(v: float) -> str:
|
def _fmt_eur(v: float) -> str:
|
||||||
v_int = round(v)
|
sign = "+" if v > 0 else ""
|
||||||
sign = "+" if v_int > 0 else ""
|
return f"{sign}{v:,.0f}€".replace(",", ".")
|
||||||
return f"{sign}{v_int:,.0f}€".replace(",", ".")
|
|
||||||
|
|
||||||
|
|
||||||
def _curso(name: str, max_len: int = 40) -> str:
|
def _curso(name: str, max_len: int = 40) -> str:
|
||||||
@ -197,8 +196,7 @@ def build_and_send(collected: list, dry_run: bool, prev_month_metricas: dict = N
|
|||||||
return f"{v:,.0f}€".replace(",", ".")
|
return f"{v:,.0f}€".replace(",", ".")
|
||||||
|
|
||||||
def _marg(v: float) -> str:
|
def _marg(v: float) -> str:
|
||||||
v_int = round(v)
|
return ("+" if v >= 0 else "") + f"{v:,.0f}€".replace(",", ".")
|
||||||
return ("+" if v_int >= 0 else "") + f"{v_int:,.0f}€".replace(",", ".")
|
|
||||||
|
|
||||||
def _pct(v: float) -> str:
|
def _pct(v: float) -> str:
|
||||||
return ("+" if v >= 0 else "") + f"{v:.1f}%"
|
return ("+" if v >= 0 else "") + f"{v:.1f}%"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user