leads-optimizer/backfill_metricas_mes.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

150 lines
5.9 KiB
Python

"""
Reconstruye los huecos en MetricasDiarias de las GACampaignMes de un mes dado.
Motivo: el bug de cambio de mes en run.py (ver commit que añade merge con
get_metricas_diarias_prev_month) sobrescribía el histórico del mes saliente
con un dict casi vacío al redirigir la escritura del último día. Esto pasó en
todos los cambios de mes hasta que se arregló, dejando huecos — a veces todo
el mes salvo el último día, a veces solo el último día, a veces huecos
sueltos si Airtable falló algún día concreto.
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 ningún día
que ya esté guardado.
Uso:
python backfill_metricas_mes.py <mes> <año> # dry run
python backfill_metricas_mes.py <mes> <año> --apply # escribe en Airtable
"""
import calendar
import json
import re
import sys
from airtable_client import AirtableClient
from google_ads_client import GoogleAdsClient
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(mes: int, anio: int, apply: bool):
at = AirtableClient()
gads = GoogleAdsClient()
days_in_month = calendar.monthrange(anio, mes)[1]
full_month_days = [str(d).zfill(2) for d in range(1, days_in_month + 1)]
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(f"→ Consultando Google Ads (coste + conversiones diarias de {mes}/{anio})...")
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__":
args = [a for a in sys.argv[1:] if not a.startswith("--")]
if len(args) != 2:
print("Uso: python backfill_metricas_mes.py <mes> <año> [--apply]")
sys.exit(1)
run(mes=int(args[0]), anio=int(args[1]), apply="--apply" in sys.argv)