Add backfill scripts for MetricasDiarias gaps caused by the month-change bug
Some checks failed
Weekly Strategic Report / run (push) Has been cancelled

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.
This commit is contained in:
Jose Manuel 2026-07-03 00:43:41 +02:00
parent 31193c57ee
commit a7f8ddad08
3 changed files with 418 additions and 0 deletions

View File

@ -0,0 +1,127 @@
"""
Reconstruye el día 30/06/2026 en MetricasDiarias para las GACampaignMes de
junio a las que les falta esa clave.
Motivo: en el cambio de mes (1 julio), run.py leía MetricasDiarias del
registro de julio (recién creado, casi vacío) en vez del de junio, así que
al redirigir la escritura hacia el registro de junio correcto se
sobrescribía con un dict casi vacío (perdiendo el día 30 real, que en
algunos casos quedó mal grabado en el registro de julio). Aquí se recalcula
el día 30 desde Google Ads (coste, conversiones) y Leads Lake, y se añade
SOLO a los registros de junio que no tienen ya esa clave no se toca nada
más de su histórico.
Uso:
python backfill_metricas_30junio.py # dry run — solo muestra
python backfill_metricas_30junio.py --apply # escribe en Airtable
"""
import json
import re
import sys
from airtable_client import AirtableClient
from google_ads_client import GoogleAdsClient
DATE_STR = "2026-06-30"
DAY_KEY = "30"
MES, ANIO = 6, 2026
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)", [])
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),
"metricas_diarias": f.get("MetricasDiarias") or "{}",
})
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"]
# Detectar registros a los que YA les falta el día 30
targets = []
for c in campaigns:
try:
md = json.loads(c["metricas_diarias"])
except (json.JSONDecodeError, TypeError):
md = {}
if DAY_KEY not in md:
c["md"] = md
targets.append(c)
print(f"{len(targets)} campañas sin el día {DAY_KEY} en su MetricasDiarias\n")
if not targets:
print("Nada que hacer.")
return
ads_metrics = gads.get_metrics_for_date(DATE_STR)
leads_lake_counts = at.get_leads_by_campaign_on_date(DATE_STR, cursoid_to_campaign)
metricas_updates = []
print(f"{'Campaña':45} {'Coste':>10} {'Conv':>6} {'Ingreso':>10} {'Margen':>10} {'LeadsLake':>10}")
for c in targets:
cid = c["google_campaign_id"]
today_m = ads_metrics.get(cid, {})
coste_hoy = round(today_m.get("cost", 0), 2)
conv_hoy = today_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_counts.get(cid, 0)
print(f"{c['curso'][:45]:45} {coste_hoy:>9.2f}{conv_hoy:>6.0f} "
f"{ingreso_hoy:>9.2f}{margen_hoy:>9.2f}{leads_lake_hoy:>10}")
md = dict(c["md"])
md[DAY_KEY] = {
"coste": coste_hoy,
"ingreso": ingreso_hoy,
"margen": margen_hoy,
"leads": int(conv_hoy),
"leads_lake": leads_lake_hoy,
}
metricas_updates.append({
"airtable_id": c["airtable_id"],
"metricas_json": json.dumps(md, ensure_ascii=False),
})
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)

View File

@ -0,0 +1,142 @@
"""
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)

149
backfill_metricas_mes.py Normal file
View File

@ -0,0 +1,149 @@
"""
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)