Remove sidebar: move date/vertical filters into each tab independently

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Manuel 2026-06-26 10:14:39 +02:00
parent 2c12a48407
commit c782db5a75

View File

@ -20,10 +20,11 @@ def _extract_vertical(name: str) -> str:
start = 1 if parts and parts[0].isdigit() else 0 start = 1 if parts and parts[0].isdigit() else 0
return parts[start].lower() if start < len(parts) else "otros" return parts[start].lower() if start < len(parts) else "otros"
st.set_page_config( st.set_page_config(
page_title=f"Meta Optimizer — {config.META_CAMPAIGN_PREFIX}", page_title=f"Meta Optimizer — {config.META_CAMPAIGN_PREFIX}",
layout="wide", layout="wide",
initial_sidebar_state="expanded", initial_sidebar_state="collapsed",
) )
import streamlit.components.v1 as components import streamlit.components.v1 as components
@ -43,6 +44,18 @@ _STRATEGY_LABELS = {
"MINIMUM_ROAS": "ROAS mín.", "MINIMUM_ROAS": "ROAS mín.",
} }
_ACTION_COLORS = {
"INCREASE_BUDGET": "🟢",
"REDUCE_BUDGET": "🟠",
"PAUSE": "🔴",
"REVIEW_CREATIVES": "🟣",
"MAINTAIN": "",
}
_today = date.today()
_yesterday = _today - timedelta(days=1)
_default_from = _yesterday - timedelta(days=6)
def _eur(val: float) -> str: def _eur(val: float) -> str:
return f"{val:.2f}" return f"{val:.2f}"
@ -60,6 +73,20 @@ def _status(leads: int, spend: float) -> str:
return "" return ""
def _date_row(key: str, n_extra_cols: int = 0) -> tuple:
"""Renders [Desde | Hasta | ...extra... | 🔄] columns. Returns (date_from, date_to, *extra_cols)."""
cols = st.columns([2, 2] + [2] * n_extra_cols + [1])
d_from = cols[0].date_input("Desde", value=_default_from, max_value=_yesterday, key=f"{key}_from")
d_to = cols[1].date_input("Hasta", value=_yesterday, min_value=d_from, max_value=_yesterday, key=f"{key}_to")
if cols[-1].button("🔄", key=f"{key}_ref", use_container_width=True, help="Limpiar caché"):
st.cache_data.clear()
st.rerun()
extra = tuple(cols[2:-1])
return (d_from, d_to) + extra
# ── Cached data loaders ───────────────────────────────────────────────────────
@st.cache_data(ttl=300, show_spinner="Cargando datos de Meta API...") @st.cache_data(ttl=300, show_spinner="Cargando datos de Meta API...")
def _load_data(date_from: str, date_to: str): def _load_data(date_from: str, date_to: str):
meta = MetaAdsClient() meta = MetaAdsClient()
@ -92,48 +119,98 @@ def _load_detail(campaign_id: str, date_from: str, date_to: str):
return adsets, ads, bid return adsets, ads, bid
# ── Sidebar ─────────────────────────────────────────────────────────────────── @st.cache_data(ttl=3600, show_spinner=False)
def _load_campaign_names() -> dict:
"""Returns {campaign_id: campaign_name} for the last 30 days. Cached 1h."""
meta = MetaAdsClient()
end = _yesterday.strftime("%Y-%m-%d")
start = (_yesterday - timedelta(days=29)).strftime("%Y-%m-%d")
try:
metrics = meta.get_campaign_metrics(start, end)
return {cid: m["name"] for cid, m in metrics.items()}
except Exception:
return {}
st.sidebar.title("Filtros")
today = date.today()
yesterday = today - timedelta(days=1)
default_from = yesterday - timedelta(days=6) # últimos 7 días por defecto
first_of_month = today.replace(day=1)
c1, c2 = st.sidebar.columns(2) @st.cache_data(ttl=120, show_spinner="Cargando fechas disponibles...")
date_from = c1.date_input("Desde", value=default_from, max_value=yesterday) def _load_snapshot_dates():
date_to = c2.date_input("Hasta", value=yesterday, min_value=date_from, max_value=yesterday) return BaserowClient().get_snapshot_dates()
if st.sidebar.button("🔄 Actualizar", use_container_width=True):
st.cache_data.clear()
date_from_str = date_from.strftime("%Y-%m-%d") @st.cache_data(ttl=120, show_spinner="Cargando análisis del día...")
date_to_str = date_to.strftime("%Y-%m-%d") def _load_snapshots(run_date: str):
import json
rows = BaserowClient().get_snapshots_for_date(run_date)
result = []
for r in rows:
try:
adsets = json.loads(r.get("adsets_json") or "[]")
except Exception:
adsets = []
try:
ads = json.loads(r.get("ads_json") or "[]")
except Exception:
ads = []
result.append({
"campaign_name": r.get("campaign_name", ""),
"vertical": r.get("vertical", ""),
"spend": float(r.get("spend") or 0),
"leads": int(r.get("leads") or 0),
"cpl": float(r.get("cpl") or 0),
"margin": float(r.get("margin") or 0),
"action_type": r.get("action_type", "MAINTAIN"),
"justification": r.get("justification", ""),
"adsets": adsets,
"ads": ads,
})
return sorted(result, key=lambda x: -x["spend"])
if date_from > date_to:
@st.cache_data(ttl=300, show_spinner="Cargando análisis de creatividades...")
def _load_creatives():
return BaserowClient().get_all_creative_analyses()
# ── Header ────────────────────────────────────────────────────────────────────
st.title(f"Meta Optimizer — {config.META_CAMPAIGN_PREFIX}")
# ── Tabs ──────────────────────────────────────────────────────────────────────
tab1, tab2, tab3, tab4, tab5 = st.tabs(
["📅 Por día", "📊 Campañas", "🏷️ Verticales", "🗂️ Histórico", "🎨 Creatividades"]
)
# ── Tab 1: Por día ────────────────────────────────────────────────────────────
with tab1:
d_from_1, d_to_1 = _date_row("t1")
if d_from_1 > d_to_1:
st.error("La fecha inicio debe ser anterior a la fecha fin.") st.error("La fecha inicio debe ser anterior a la fecha fin.")
st.stop() else:
try:
daily_rows, _cm1, vertical_cpls_1 = _load_data(
d_from_1.strftime("%Y-%m-%d"), d_to_1.strftime("%Y-%m-%d")
)
except Exception as e:
st.error(f"Error cargando datos de Meta API: {e}")
daily_rows, vertical_cpls_1 = [], {}
# ── Data ────────────────────────────────────────────────────────────────────── _daily: dict = {}
for row in daily_rows:
try:
daily_rows, campaign_metrics, vertical_cpls = _load_data(date_from_str, date_to_str)
except Exception as _e:
st.error(f"Error cargando datos de Meta API: {_e}")
st.stop()
# Aggregate daily totals with per-vertical margins
_daily: dict = {}
for row in daily_rows:
v = _extract_vertical(row["campaign_name"]) v = _extract_vertical(row["campaign_name"])
target = vertical_cpls.get(v, config.META_TARGET_CPL) target = vertical_cpls_1.get(v, config.META_TARGET_CPL)
margin = round((target - row["spend"] / row["leads"]) * row["leads"], 2) if row["leads"] > 0 else round(-row["spend"], 2) margin = (
round((target - row["spend"] / row["leads"]) * row["leads"], 2)
if row["leads"] > 0 else round(-row["spend"], 2)
)
d = _daily.setdefault(row["date"], {"spend": 0.0, "leads": 0, "margin": 0.0}) d = _daily.setdefault(row["date"], {"spend": 0.0, "leads": 0, "margin": 0.0})
d["spend"] += row["spend"] d["spend"] += row["spend"]
d["leads"] += row["leads"] d["leads"] += row["leads"]
d["margin"] += margin d["margin"] += margin
daily_totals = [ daily_totals = [
{ {
"date": dt, "date": dt,
"spend": round(d["spend"], 2), "spend": round(d["spend"], 2),
@ -142,54 +219,20 @@ daily_totals = [
"margin": round(d["margin"], 2), "margin": round(d["margin"], 2),
} }
for dt, d in sorted(_daily.items()) for dt, d in sorted(_daily.items())
] ]
# Aggregate verticals total_spend = sum(d["spend"] for d in daily_totals)
verticals: dict = {} total_leads = sum(d["leads"] for d in daily_totals)
for cid, m in campaign_metrics.items(): total_cpl = round(total_spend / total_leads, 2) if total_leads > 0 else 0.0
v = _extract_vertical(m["name"]) total_margin = sum(d["margin"] for d in daily_totals)
target = vertical_cpls.get(v, config.META_TARGET_CPL)
margin = round((target - m["cpl"]) * m["leads"], 2) if m["leads"] > 0 else round(-m["spend"], 2)
if v not in verticals:
verticals[v] = {"spend": 0.0, "leads": 0, "margin": 0.0, "target_cpl": target}
verticals[v]["spend"] += m["spend"]
verticals[v]["leads"] += m["leads"]
verticals[v]["margin"] += margin
# Vertical filter (populated after load) k1, k2, k3, k4 = st.columns(4)
v_options = ["Todos"] + sorted(verticals.keys()) k1.metric("Gasto total", _eur(total_spend))
selected_vertical = st.sidebar.selectbox("Vertical", v_options) k2.metric("Leads totales", f"{total_leads:,}")
if selected_vertical != "Todos": k3.metric("CPL medio", _eur(total_cpl))
campaign_metrics = { k4.metric("Margen total", _margin(total_margin))
cid: m for cid, m in campaign_metrics.items() st.divider()
if _extract_vertical(m["name"]) == selected_vertical
}
# ── Header ────────────────────────────────────────────────────────────────────
st.title(f"Meta Optimizer — {config.META_CAMPAIGN_PREFIX}")
st.caption(f"Período: **{date_from.strftime('%d/%m/%Y')}** → **{date_to.strftime('%d/%m/%Y')}**")
total_spend = sum(d["spend"] for d in daily_totals)
total_leads = sum(d["leads"] for d in daily_totals)
total_cpl = round(total_spend / total_leads, 2) if total_leads > 0 else 0.0
total_margin = sum(d["margin"] for d in daily_totals)
k1, k2, k3, k4 = st.columns(4)
k1.metric("Gasto total", _eur(total_spend))
k2.metric("Leads totales", f"{total_leads:,}")
k3.metric("CPL medio", _eur(total_cpl))
k4.metric("Margen total", _margin(total_margin))
st.divider()
# ── Tabs ──────────────────────────────────────────────────────────────────────
tab1, tab2, tab3, tab4, tab5 = st.tabs(["📅 Por día", "📊 Campañas", "🏷️ Verticales", "🗂️ Histórico", "🎨 Creatividades"])
# ── Tab 1: Por día ────────────────────────────────────────────────────────────
with tab1:
if not daily_totals: if not daily_totals:
st.info("Sin datos para el período seleccionado.") st.info("Sin datos para el período seleccionado.")
else: else:
@ -212,28 +255,29 @@ with tab1:
"Selecciona un día", "Selecciona un día",
day_opts, day_opts,
format_func=lambda s: s[8:10] + "/" + s[5:7] + "/" + s[:4], format_func=lambda s: s[8:10] + "/" + s[5:7] + "/" + s[:4],
key="t1_day",
) )
if selected_day: if selected_day:
day_camp: dict = {} day_camp: dict = {}
for row in daily_rows: for row in daily_rows:
if row["date"] != selected_day: if row["date"] != selected_day:
continue continue
key = row["campaign_name"] k = row["campaign_name"]
if key not in day_camp: if k not in day_camp:
v = _extract_vertical(key) v = _extract_vertical(k)
target = vertical_cpls.get(v, config.META_TARGET_CPL) target = vertical_cpls_1.get(v, config.META_TARGET_CPL)
day_camp[key] = { day_camp[k] = {"name": k, "vertical": v, "spend": 0.0, "leads": 0, "target_cpl": target}
"name": key, "vertical": v, day_camp[k]["spend"] += row["spend"]
"spend": 0.0, "leads": 0, "target_cpl": target, day_camp[k]["leads"] += row["leads"]
}
day_camp[key]["spend"] += row["spend"]
day_camp[key]["leads"] += row["leads"]
rows = [] camp_rows = []
for c in sorted(day_camp.values(), key=lambda x: -x["spend"]): for c in sorted(day_camp.values(), key=lambda x: -x["spend"]):
cpl = round(c["spend"] / c["leads"], 2) if c["leads"] > 0 else 0.0 cpl = round(c["spend"] / c["leads"], 2) if c["leads"] > 0 else 0.0
margin = round((c["target_cpl"] - cpl) * c["leads"], 2) if c["leads"] > 0 else round(-c["spend"], 2) margin = (
rows.append({ round((c["target_cpl"] - cpl) * c["leads"], 2)
if c["leads"] > 0 else round(-c["spend"], 2)
)
camp_rows.append({
"Campaña": c["name"], "Campaña": c["name"],
"Vertical": c["vertical"], "Vertical": c["vertical"],
"Gasto": _eur(c["spend"]), "Gasto": _eur(c["spend"]),
@ -242,21 +286,43 @@ with tab1:
"Obj": _eur(c["target_cpl"]), "Obj": _eur(c["target_cpl"]),
"Margen": _margin(margin), "Margen": _margin(margin),
}) })
if rows: if camp_rows:
st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True) st.dataframe(pd.DataFrame(camp_rows), use_container_width=True, hide_index=True)
else: else:
st.info("Sin campañas activas ese día.") st.info("Sin campañas activas ese día.")
# ── Tab 2: Campañas ─────────────────────────────────────────────────────────── # ── Tab 2: Campañas ───────────────────────────────────────────────────────────
with tab2: with tab2:
if not campaign_metrics: d_from_2, d_to_2, col_vert_2 = _date_row("t2", n_extra_cols=1)
if d_from_2 > d_to_2:
st.error("La fecha inicio debe ser anterior a la fecha fin.")
else:
try:
_dr2, campaign_metrics_2, vertical_cpls_2 = _load_data(
d_from_2.strftime("%Y-%m-%d"), d_to_2.strftime("%Y-%m-%d")
)
except Exception as e:
st.error(f"Error cargando datos de Meta API: {e}")
campaign_metrics_2, vertical_cpls_2 = {}, {}
v_opts_2 = ["Todos"] + sorted({_extract_vertical(m["name"]) for m in campaign_metrics_2.values()})
sel_vert_2 = col_vert_2.selectbox("Vertical", v_opts_2, key="t2_vert")
if sel_vert_2 != "Todos":
campaign_metrics_2 = {
cid: m for cid, m in campaign_metrics_2.items()
if _extract_vertical(m["name"]) == sel_vert_2
}
if not campaign_metrics_2:
st.info("Sin campañas para el período seleccionado.") st.info("Sin campañas para el período seleccionado.")
else: else:
camp_rows = [] camp_rows = []
for cid, m in sorted(campaign_metrics.items(), key=lambda x: -x[1]["spend"]): for cid, m in sorted(campaign_metrics_2.items(), key=lambda x: -x[1]["spend"]):
v = _extract_vertical(m["name"]) v = _extract_vertical(m["name"])
target = vertical_cpls.get(v, config.META_TARGET_CPL) target = vertical_cpls_2.get(v, config.META_TARGET_CPL)
margin = round((target - m["cpl"]) * m["leads"], 2) if m["leads"] > 0 else round(-m["spend"], 2) margin = round((target - m["cpl"]) * m["leads"], 2) if m["leads"] > 0 else round(-m["spend"], 2)
camp_rows.append({ camp_rows.append({
"Campaña": m["name"], "Campaña": m["name"],
@ -275,11 +341,15 @@ with tab2:
st.subheader("Detalle de campaña") st.subheader("Detalle de campaña")
camp_id_map = {r["Campaña"]: r["_cid"] for r in camp_rows} camp_id_map = {r["Campaña"]: r["_cid"] for r in camp_rows}
selected_camp = st.selectbox("Selecciona una campaña", list(camp_id_map.keys())) selected_camp = st.selectbox("Selecciona una campaña", list(camp_id_map.keys()), key="t2_camp")
if selected_camp: if selected_camp:
selected_cid = camp_id_map[selected_camp] selected_cid = camp_id_map[selected_camp]
adsets, ads, bid_cfg = _load_detail(selected_cid, date_from_str, date_to_str) adsets, ads, bid_cfg = _load_detail(
selected_cid,
d_from_2.strftime("%Y-%m-%d"),
d_to_2.strftime("%Y-%m-%d"),
)
strategy = bid_cfg.get("bid_strategy", "") strategy = bid_cfg.get("bid_strategy", "")
strat_label = _STRATEGY_LABELS.get(strategy, strategy or "") strat_label = _STRATEGY_LABELS.get(strategy, strategy or "")
@ -324,11 +394,35 @@ with tab2:
# ── Tab 3: Verticales ───────────────────────────────────────────────────────── # ── Tab 3: Verticales ─────────────────────────────────────────────────────────
with tab3: with tab3:
if not verticals: d_from_3, d_to_3 = _date_row("t3")
if d_from_3 > d_to_3:
st.error("La fecha inicio debe ser anterior a la fecha fin.")
else:
try:
_dr3, campaign_metrics_3, vertical_cpls_3 = _load_data(
d_from_3.strftime("%Y-%m-%d"), d_to_3.strftime("%Y-%m-%d")
)
except Exception as e:
st.error(f"Error cargando datos de Meta API: {e}")
campaign_metrics_3, vertical_cpls_3 = {}, {}
verticals_3: dict = {}
for cid, m in campaign_metrics_3.items():
v = _extract_vertical(m["name"])
target = vertical_cpls_3.get(v, config.META_TARGET_CPL)
margin = round((target - m["cpl"]) * m["leads"], 2) if m["leads"] > 0 else round(-m["spend"], 2)
if v not in verticals_3:
verticals_3[v] = {"spend": 0.0, "leads": 0, "margin": 0.0, "target_cpl": target}
verticals_3[v]["spend"] += m["spend"]
verticals_3[v]["leads"] += m["leads"]
verticals_3[v]["margin"] += margin
if not verticals_3:
st.info("Sin datos de verticales.") st.info("Sin datos de verticales.")
else: else:
vert_rows = [] vert_rows = []
for v, data in sorted(verticals.items(), key=lambda x: -x[1]["margin"]): for v, data in sorted(verticals_3.items(), key=lambda x: -x[1]["margin"]):
v_leads = data["leads"] v_leads = data["leads"]
v_spend = data["spend"] v_spend = data["spend"]
v_cpl = round(v_spend / v_leads, 2) if v_leads > 0 else 0.0 v_cpl = round(v_spend / v_leads, 2) if v_leads > 0 else 0.0
@ -344,70 +438,23 @@ with tab3:
# ── Tab 4: Histórico ────────────────────────────────────────────────────────── # ── Tab 4: Histórico ──────────────────────────────────────────────────────────
_ACTION_COLORS = {
"INCREASE_BUDGET": "🟢",
"REDUCE_BUDGET": "🟠",
"PAUSE": "🔴",
"REVIEW_CREATIVES": "🟣",
"MAINTAIN": "",
}
@st.cache_data(ttl=120, show_spinner="Cargando fechas disponibles...")
def _load_snapshot_dates():
return BaserowClient().get_snapshot_dates()
@st.cache_data(ttl=120, show_spinner="Cargando análisis del día...")
def _load_snapshots(run_date: str):
import json
rows = BaserowClient().get_snapshots_for_date(run_date)
result = []
for r in rows:
try:
adsets = json.loads(r.get("adsets_json") or "[]")
except Exception:
adsets = []
try:
ads = json.loads(r.get("ads_json") or "[]")
except Exception:
ads = []
result.append({
"campaign_name": r.get("campaign_name", ""),
"vertical": r.get("vertical", ""),
"spend": float(r.get("spend") or 0),
"leads": int(r.get("leads") or 0),
"cpl": float(r.get("cpl") or 0),
"margin": float(r.get("margin") or 0),
"action_type": r.get("action_type", "MAINTAIN"),
"justification": r.get("justification", ""),
"adsets": adsets,
"ads": ads,
})
return sorted(result, key=lambda x: -x["spend"])
with tab4: with tab4:
dates = _load_snapshot_dates() dates = _load_snapshot_dates()
if not dates: if not dates:
st.info("Sin análisis guardados aún. Los snapshots se generan al ejecutar run.py.") st.info("Sin análisis guardados aún. Los snapshots se generan al ejecutar run.py.")
else: else:
c1, c2 = st.columns([3, 1])
fmt_date = lambda s: s[8:10] + "/" + s[5:7] + "/" + s[:4] fmt_date = lambda s: s[8:10] + "/" + s[5:7] + "/" + s[:4]
selected_date = st.selectbox( selected_date = c1.selectbox("Fecha del análisis", dates, format_func=fmt_date, key="t4_date")
"Fecha del análisis", if c2.button("🔄 Recargar", key="t4_ref", use_container_width=True):
dates,
format_func=fmt_date,
)
if st.button("🔄 Recargar", key="reload_hist"):
st.cache_data.clear() st.cache_data.clear()
st.rerun()
snapshots = _load_snapshots(selected_date) snapshots = _load_snapshots(selected_date)
if not snapshots: if not snapshots:
st.info("Sin datos para esa fecha.") st.info("Sin datos para esa fecha.")
else: else:
# ── Resumen del día ───────────────────────────────────────────────
d_spend = sum(s["spend"] for s in snapshots) d_spend = sum(s["spend"] for s in snapshots)
d_leads = sum(s["leads"] for s in snapshots) d_leads = sum(s["leads"] for s in snapshots)
d_cpl = round(d_spend / d_leads, 2) if d_leads > 0 else 0.0 d_cpl = round(d_spend / d_leads, 2) if d_leads > 0 else 0.0
@ -419,7 +466,6 @@ with tab4:
h4.metric("Margen", _margin(d_margin)) h4.metric("Margen", _margin(d_margin))
st.divider() st.divider()
# ── Tabla de campañas clicable ────────────────────────────────────
df_snap = pd.DataFrame([ df_snap = pd.DataFrame([
{ {
"Acción": _ACTION_COLORS.get(s["action_type"], "") + " " + s["action_type"], "Acción": _ACTION_COLORS.get(s["action_type"], "") + " " + s["action_type"],
@ -452,10 +498,9 @@ with tab4:
if snap["justification"]: if snap["justification"]:
st.info(snap["justification"]) st.info(snap["justification"])
# ── Adsets — expanders con evaluación visible ─────────────────
adsets = snap["adsets"] adsets = snap["adsets"]
if adsets: if adsets:
st.markdown("**Conjuntos de anuncios**") st.markdown("**Conjuntos de anuncios** _(últimos 3 días)_")
for a in adsets: for a in adsets:
label = ( label = (
f"{a['name']}" f"{a['name']}"
@ -471,10 +516,9 @@ with tab4:
if a.get("recomendacion"): if a.get("recomendacion"):
st.write(f"{a['recomendacion']}") st.write(f"{a['recomendacion']}")
# ── Anuncios — expanders con evaluación visible ───────────────
ads = snap["ads"] ads = snap["ads"]
if ads: if ads:
st.markdown("**Anuncios**") st.markdown("**Anuncios** _(últimos 7 días)_")
for a in ads: for a in ads:
label = ( label = (
f"{a['name']}" f"{a['name']}"
@ -492,33 +536,27 @@ with tab4:
# ── Tab 5: Creatividades ────────────────────────────────────────────────────── # ── Tab 5: Creatividades ──────────────────────────────────────────────────────
with tab5: with tab5:
@st.cache_data(ttl=300, show_spinner="Cargando análisis de creatividades...")
def _load_creatives():
return BaserowClient().get_all_creative_analyses()
creatives_raw = _load_creatives() creatives_raw = _load_creatives()
if not creatives_raw: if not creatives_raw:
st.info("No hay análisis de creatividades. Ejecuta `python analyze_creatives.py` para generar datos.") st.info("No hay análisis de creatividades. Ejecuta `python analyze_creatives.py` para generar datos.")
st.stop() else:
# Build dataframe
df_all = pd.DataFrame(creatives_raw) df_all = pd.DataFrame(creatives_raw)
df_all["score"] = pd.to_numeric(df_all.get("score", 0), errors="coerce").fillna(0) df_all["score"] = pd.to_numeric(df_all.get("score", 0), errors="coerce").fillna(0)
df_all["created_at"] = df_all.get("created_at", pd.Series(dtype=str)) df_all["created_at"] = df_all.get("created_at", pd.Series(dtype=str))
# Map campaign_id → campaign_name using already-loaded Meta data # Map campaign_id → name using a dedicated cached call (last 30d)
camp_id_to_name = {cid: m["name"] for cid, m in campaign_metrics.items()} camp_id_to_name = _load_campaign_names()
df_all["campaign_name"] = df_all["campaign_id"].map( df_all["campaign_name"] = df_all["campaign_id"].map(
lambda cid: camp_id_to_name.get(str(cid), str(cid)) lambda cid: camp_id_to_name.get(str(cid), str(cid))
) )
df_all["vertical"] = df_all["campaign_name"].map(_extract_vertical) df_all["vertical"] = df_all["campaign_name"].map(_extract_vertical)
# ── Filters ─────────────────────────────────────────────────────────────── # ── Filters ───────────────────────────────────────────────────────────
f1, f2, f3, f4 = st.columns([2, 2, 2, 2]) f1, f2, f3, f4 = st.columns([2, 2, 2, 2])
dates_available = sorted(df_all["created_at"].dropna().unique(), reverse=True) dates_available = sorted(df_all["created_at"].dropna().unique(), reverse=True)
sel_date = f1.selectbox("Fecha análisis", dates_available) sel_date = f1.selectbox("Fecha análisis", dates_available, key="cr_date")
verts_available = sorted(df_all["vertical"].dropna().unique().tolist()) verts_available = sorted(df_all["vertical"].dropna().unique().tolist())
sel_vert_cr = f2.selectbox("Vertical", ["Todas"] + verts_available, key="cr_vert") sel_vert_cr = f2.selectbox("Vertical", ["Todas"] + verts_available, key="cr_vert")
@ -526,7 +564,7 @@ with tab5:
camp_names = sorted(df_all["campaign_name"].dropna().unique().tolist()) camp_names = sorted(df_all["campaign_name"].dropna().unique().tolist())
sel_camp = f3.selectbox("Campaña", ["Todas"] + camp_names, key="cr_camp") sel_camp = f3.selectbox("Campaña", ["Todas"] + camp_names, key="cr_camp")
score_min = f4.slider("Score mínimo", 0.0, 10.0, 0.0, step=0.5) score_min = f4.slider("Score mínimo", 0.0, 10.0, 0.0, step=0.5, key="cr_score")
# Apply filters # Apply filters
df = df_all.copy() df = df_all.copy()
@ -539,7 +577,7 @@ with tab5:
if score_min > 0: if score_min > 0:
df = df[df["score"] >= score_min] df = df[df["score"] >= score_min]
# ── KPIs ────────────────────────────────────────────────────────────────── # ── KPIs ──────────────────────────────────────────────────────────────
scored_df = df[df["score"] > 0] scored_df = df[df["score"] > 0]
avg_sc = round(scored_df["score"].mean(), 1) if not scored_df.empty else 0.0 avg_sc = round(scored_df["score"].mean(), 1) if not scored_df.empty else 0.0
fatigue_n = int(df["analysis"].str.contains("FATIGA", na=False).sum()) if "analysis" in df.columns else 0 fatigue_n = int(df["analysis"].str.contains("FATIGA", na=False).sum()) if "analysis" in df.columns else 0
@ -551,7 +589,6 @@ with tab5:
k3.metric("Con fatiga", fatigue_n) k3.metric("Con fatiga", fatigue_n)
k4.metric("Última ejecución", last_run) k4.metric("Última ejecución", last_run)
# ── Fatigue alerts ────────────────────────────────────────────────────────
if fatigue_n: if fatigue_n:
fatigued = df[df["analysis"].str.contains("FATIGA", na=False)] fatigued = df[df["analysis"].str.contains("FATIGA", na=False)]
with st.expander(f"⚠️ {fatigue_n} anuncios con fatiga creativa", expanded=True): with st.expander(f"⚠️ {fatigue_n} anuncios con fatiga creativa", expanded=True):
@ -560,7 +597,7 @@ with tab5:
st.divider() st.divider()
# ── Table + Detail panel ────────────────────────────────────────────────── # ── Table + Detail panel ──────────────────────────────────────────────
rename_map = { rename_map = {
"campaign_name": "Campaña", "campaign_name": "Campaña",
"vertical": "Vertical", "vertical": "Vertical",
@ -615,7 +652,6 @@ with tab5:
st.markdown("**Recomendaciones**") st.markdown("**Recomendaciones**")
st.info(rec) st.info(rec)
# Score evolution across runs
ad_id = str(row.get("ad_id", "")) ad_id = str(row.get("ad_id", ""))
if ad_id: if ad_id:
history = BaserowClient().get_creative_history_by_ad(ad_id) history = BaserowClient().get_creative_history_by_ad(ad_id)