Fix leadform attribution: sum PMX + leadform conversions for margin analysis

- Add get_monthly_metrics_all() bulk method to GoogleAdsClient
- Use bulk fetch in first pass instead of N individual API calls
- Pre-compute leadform conversions per course number
- For PMX campaigns with leadform companion, leads_grupo = PMX conv + leadform conv
- fco_pmx_60 now correctly shows 334 leads at 61% margin (was negative)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jose Manuel 2026-05-21 21:53:45 +02:00
parent a94c18c13c
commit b30a075c59
2 changed files with 87 additions and 9 deletions

View File

@ -133,6 +133,66 @@ class GoogleAdsClient:
print(f" ❌ Error obteniendo métricas mensuales: {e}") print(f" ❌ Error obteniendo métricas mensuales: {e}")
return result return result
def get_monthly_metrics_all(self) -> dict:
"""
Métricas del mes en curso para TODAS las campañas en una sola query.
Retorna dict {campaign_id: {cost, conversions, clicks, impressions, ctr,
status, budget_daily, budget_resource_name, name}}.
"""
ga_service = self.client.get_service("GoogleAdsService")
query = """
SELECT
campaign.id,
campaign.name,
campaign.status,
campaign_budget.amount_micros,
campaign_budget.resource_name,
metrics.cost_micros,
metrics.conversions,
metrics.clicks,
metrics.impressions
FROM campaign
WHERE campaign.status != 'REMOVED'
AND segments.date DURING THIS_MONTH
"""
raw: dict = {}
try:
response = ga_service.search(customer_id=self.customer_id, query=query)
for row in response:
cid = str(row.campaign.id)
if cid not in raw:
raw[cid] = {
"name": row.campaign.name,
"status": row.campaign.status.name,
"budget_daily": row.campaign_budget.amount_micros / 1_000_000,
"budget_resource_name": row.campaign_budget.resource_name,
"cost": 0.0, "conversions": 0.0, "clicks": 0, "impressions": 0,
}
m = row.metrics
raw[cid]["cost"] += m.cost_micros / 1_000_000
raw[cid]["conversions"] += m.conversions
raw[cid]["clicks"] += m.clicks
raw[cid]["impressions"] += m.impressions
except GoogleAdsException as e:
print(f" ❌ Error obteniendo métricas mensuales bulk: {e}")
result = {}
for cid, d in raw.items():
imp = d["impressions"]
result[cid] = {
"campaign_id": cid,
"name": d["name"],
"status": d["status"],
"budget_daily": round(d["budget_daily"], 2),
"budget_resource_name": d["budget_resource_name"],
"cost": round(d["cost"], 2),
"conversions": d["conversions"],
"clicks": d["clicks"],
"impressions": imp,
"ctr": round(d["clicks"] / imp * 100, 2) if imp > 0 else 0.0,
}
return result
def get_campaign_metrics(self, campaign_id: str) -> dict: def get_campaign_metrics(self, campaign_id: str) -> dict:
"""Métricas del mes en curso para una campaña concreta (acumulado mensual).""" """Métricas del mes en curso para una campaña concreta (acumulado mensual)."""
ga_service = self.client.get_service("GoogleAdsService") ga_service = self.client.get_service("GoogleAdsService")

36
run.py
View File

@ -156,6 +156,15 @@ def run():
if "_leadform" in c["curso"].lower() and _course_num(c["curso"]) if "_leadform" in c["curso"].lower() and _course_num(c["curso"])
} }
# Precomputar conversiones Google de cada campaña leadform por número de curso
leadform_conv_by_course: dict[str, int] = {}
for c in campaigns:
if "_leadform" in c["curso"].lower():
num = _course_num(c["curso"])
if num:
lf_conv = int(monthly_metrics.get(c["google_campaign_id"], {}).get("conversions", 0))
leadform_conv_by_course[num] = leadform_conv_by_course.get(num, 0) + lf_conv
# === PRIMERA PASADA: recopilar datos de todas las campañas === # === PRIMERA PASADA: recopilar datos de todas las campañas ===
collected = [] collected = []
skipped = [] skipped = []
@ -167,7 +176,7 @@ def run():
leads, lead_ids = at.get_leads_this_month_gads(cid, campaign["curso"]) leads, lead_ids = at.get_leads_this_month_gads(cid, campaign["curso"])
at.update_gacampaignmes_leads_lake(campaign["airtable_id"], lead_ids) at.update_gacampaignmes_leads_lake(campaign["airtable_id"], lead_ids)
metrics = gads.get_campaign_metrics(cid) metrics = monthly_metrics.get(cid)
if not metrics: if not metrics:
skipped.append(f"[{cid}] {campaign['curso']}") skipped.append(f"[{cid}] {campaign['curso']}")
continue continue
@ -181,13 +190,20 @@ def run():
google_status, google_status,
)) ))
# Para PMX con companion Search: usar conversiones Google como leads de análisis
course_num = _course_num(campaign["curso"]) course_num = _course_num(campaign["curso"])
is_pmx_with_companion = ( is_pmx = "pmx" in campaign["curso"].lower() and "_leadform" not in campaign["curso"].lower()
"pmx" in campaign["curso"].lower()
and course_num in courses_with_both # PMX con companion Search: conversiones Google del PMX (search fluye hacia PMX)
) is_pmx_with_companion = is_pmx and course_num in courses_with_both
leads_grupo = int(metrics.get("conversions", 0)) if is_pmx_with_companion else leads # PMX con companion Leadform: conv PMX + conv leadform (leads no llegan a Airtable)
is_pmx_with_leadform = is_pmx and course_num in courses_with_leadform
if is_pmx_with_leadform:
leads_grupo = int(metrics.get("conversions", 0)) + leadform_conv_by_course.get(course_num, 0)
elif is_pmx_with_companion:
leads_grupo = int(metrics.get("conversions", 0))
else:
leads_grupo = leads
analysis = analyze(campaign, leads_grupo, metrics) analysis = analyze(campaign, leads_grupo, metrics)
decision = decide(analysis) decision = decide(analysis)
@ -200,6 +216,7 @@ def run():
"analysis": analysis, "analysis": analysis,
"decision": decision, "decision": decision,
"today_metrics": today_metrics.get(cid, {}), "today_metrics": today_metrics.get(cid, {}),
"is_pmx_with_leadform": is_pmx_with_leadform,
}) })
# Actualizar status en GACampaignMes y Google Ads Campaigns # Actualizar status en GACampaignMes y Google Ads Campaigns
@ -293,8 +310,9 @@ def run():
is_leadform = "_leadform" in campaign["curso"].lower() is_leadform = "_leadform" in campaign["curso"].lower()
if is_leadform: if is_leadform:
log_text += " | LEADFORM: leads capturados directamente en Google (sin visitar la web) — no llegan a Airtable" log_text += " | LEADFORM: leads capturados directamente en Google (sin visitar la web) — no llegan a Airtable"
elif course_num in courses_with_leadform: elif item.get("is_pmx_with_leadform"):
log_text += " | ⚠️ LEADFORM COMPANION: existe una campaña _leadform activa para este curso — parte de las conversiones de Google pueden provenir de leads capturados directamente en Google" lf_conv = leadform_conv_by_course.get(course_num, 0)
log_text += f" | LEADFORM COMPANION: {lf_conv} conv leadform sumadas a leads_grupo para este curso"
# Métricas diarias: coste hoy, ingreso (conversiones × PPL) y margen # Métricas diarias: coste hoy, ingreso (conversiones × PPL) y margen
coste_hoy = round(today_m.get("cost", 0), 2) coste_hoy = round(today_m.get("cost", 0), 2)