leads-optimizer/backfill_metricas_junio_completo.py
José Manuel Gómez a7f8ddad08
Some checks failed
Weekly Strategic Report / run (push) Has been cancelled
Add backfill scripts for MetricasDiarias gaps caused by the month-change bug
One-off scripts to reconstruct missing daily coste/ingreso/margen/leads
from Google Ads and Leads Lake for days lost to the overwrite bug fixed
in run.py. backfill_metricas_mes.py generalizes the approach (mes/año
args) and was used to repair both mayo and junio 2026; the two junio-
specific scripts document the narrower fixes applied first.
2026-07-03 00:43:41 +02:00

143 lines
5.4 KiB
Python

"""
Reconstruye los huecos en MetricasDiarias de las GACampaignMes de junio 2026:
- 35 campañas perdieron los días 01-29 (el bug de cambio de mes sobrescribió
su histórico completo dejando solo el día 30).
- 32 campañas no tienen el día 30 (nunca se llegó a escribir — coste y
conversiones fueron 0 ese día, confirmado contra Google Ads).
Recalcula cada día que falte a partir de Google Ads (coste, conversiones) y
Leads Lake (leads_lake), exactamente igual que lo habría hecho run.py cada
día, y hace merge con lo que ya existe en cada registro — no toca los días
que ya están bien.
Uso:
python backfill_metricas_junio_completo.py # dry run
python backfill_metricas_junio_completo.py --apply # escribe en Airtable
"""
import json
import re
import sys
from airtable_client import AirtableClient
from google_ads_client import GoogleAdsClient
MES, ANIO = 6, 2026
FULL_MONTH_DAYS = [str(d).zfill(2) for d in range(1, 31)]
def _course_num(name: str) -> str | None:
m = re.search(r'fco_(?:search|pmx)_(\d+)', name, re.IGNORECASE)
return m.group(1) if m else None
def run(apply: bool):
at = AirtableClient()
gads = GoogleAdsClient()
formula = f"AND({{Mes}}='{MES}',{{Año}}='{ANIO}')"
records = at.gacampaignmes.all(
formula=formula,
fields=["CampaignID", "PPL", "MetricasDiarias", "Campaign Name (from CampaignID)"],
)
campaigns_records = at.campaigns.all(fields=["CampaignID"])
at_id_to_gid = {r["id"]: str(r["fields"].get("CampaignID", "")).strip() for r in campaigns_records}
campaigns = []
for r in records:
f = r["fields"]
at_cids = f.get("CampaignID", [])
gid = at_id_to_gid.get(at_cids[0], "") if at_cids else ""
if not gid:
continue
name_list = f.get("Campaign Name (from CampaignID)", [])
try:
md = json.loads(f.get("MetricasDiarias") or "{}")
except (json.JSONDecodeError, TypeError):
md = {}
campaigns.append({
"airtable_id": r["id"],
"google_campaign_id": gid,
"curso": name_list[0] if name_list else "Sin nombre",
"ppl": float(f.get("PPL") or 0),
"md": md,
})
print(f"{len(campaigns)} campañas de {MES}/{ANIO} en GACampaignMes")
# Mapping cursoid → PMX campaign_id, igual que run.py, para atribuir leadforms
cursoid_to_campaign: dict[str, str] = {}
for c in campaigns:
num = _course_num(c["curso"])
if num and "pmx" in c["curso"].lower() and "_leadform" not in c["curso"].lower():
cursoid_to_campaign[num] = c["google_campaign_id"]
# Días que le faltan a cada campaña
for c in campaigns:
c["missing"] = [d for d in FULL_MONTH_DAYS if d not in c["md"]]
targets = [c for c in campaigns if c["missing"]]
all_missing_dates = sorted({f"{ANIO}-{MES:02d}-{d}" for c in targets for d in c["missing"]})
print(f"{len(targets)} campañas con días faltantes ({len(all_missing_dates)} fechas distintas a consultar)\n")
if not targets:
print("Nada que hacer.")
return
print("→ Consultando Google Ads (coste + conversiones diarias de junio)...")
daily_metrics = gads.get_daily_metrics_for_month(ANIO, MES)
print("→ Consultando Leads Lake por fecha...")
leads_lake_by_date = {}
for date_str in all_missing_dates:
leads_lake_by_date[date_str] = at.get_leads_by_campaign_on_date(date_str, cursoid_to_campaign)
print()
metricas_updates = []
total_coste = total_ingreso = 0.0
print(f"{'Campaña':45} {'Días':>5} {'Coste rec.':>11} {'Ingreso rec.':>13} {'Margen rec.':>12}")
for c in targets:
cid = c["google_campaign_id"]
md = dict(c["md"])
camp_coste = camp_ingreso = 0.0
for d in c["missing"]:
date_str = f"{ANIO}-{MES:02d}-{d}"
m = daily_metrics.get(date_str, {}).get(cid, {})
coste_hoy = round(m.get("cost", 0), 2)
conv_hoy = m.get("conversions", 0)
ingreso_hoy = round(conv_hoy * c["ppl"], 2)
margen_hoy = round(ingreso_hoy - coste_hoy, 2)
leads_lake_hoy = leads_lake_by_date.get(date_str, {}).get(cid, 0)
md[d] = {
"coste": coste_hoy,
"ingreso": ingreso_hoy,
"margen": margen_hoy,
"leads": int(conv_hoy),
"leads_lake": leads_lake_hoy,
}
camp_coste += coste_hoy
camp_ingreso += ingreso_hoy
total_coste += camp_coste
total_ingreso += camp_ingreso
print(f"{c['curso'][:45]:45} {len(c['missing']):>5} {camp_coste:>10.2f}"
f"{camp_ingreso:>12.2f}{camp_ingreso - camp_coste:>11.2f}")
metricas_updates.append({
"airtable_id": c["airtable_id"],
"metricas_json": json.dumps(md, ensure_ascii=False, sort_keys=True),
})
print(f"\n{'TOTAL recuperado':45} {'':>5} {total_coste:>10.2f}{total_ingreso:>12.2f}"
f"{total_ingreso - total_coste:>11.2f}")
print()
if apply:
at.batch_update_metricas_diarias(metricas_updates)
print(f"{len(metricas_updates)} registros actualizados en Airtable.")
else:
print(f"DRY RUN — {len(metricas_updates)} registros se actualizarían. "
f"Ejecuta con --apply para escribir en Airtable.")
if __name__ == "__main__":
run(apply="--apply" in sys.argv)