Add backfill scripts for MetricasDiarias gaps caused by the month-change bug
Some checks failed
Weekly Strategic Report / run (push) Has been cancelled
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:
parent
31193c57ee
commit
a7f8ddad08
127
backfill_metricas_30junio.py
Normal file
127
backfill_metricas_30junio.py
Normal 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)
|
||||
142
backfill_metricas_junio_completo.py
Normal file
142
backfill_metricas_junio_completo.py
Normal 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
149
backfill_metricas_mes.py
Normal 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)
|
||||
Loading…
x
Reference in New Issue
Block a user