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:
parent
a94c18c13c
commit
b30a075c59
@ -133,6 +133,66 @@ class GoogleAdsClient:
|
||||
print(f" ❌ Error obteniendo métricas mensuales: {e}")
|
||||
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:
|
||||
"""Métricas del mes en curso para una campaña concreta (acumulado mensual)."""
|
||||
ga_service = self.client.get_service("GoogleAdsService")
|
||||
|
||||
36
run.py
36
run.py
@ -156,6 +156,15 @@ def run():
|
||||
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 ===
|
||||
collected = []
|
||||
skipped = []
|
||||
@ -167,7 +176,7 @@ def run():
|
||||
leads, lead_ids = at.get_leads_this_month_gads(cid, campaign["curso"])
|
||||
at.update_gacampaignmes_leads_lake(campaign["airtable_id"], lead_ids)
|
||||
|
||||
metrics = gads.get_campaign_metrics(cid)
|
||||
metrics = monthly_metrics.get(cid)
|
||||
if not metrics:
|
||||
skipped.append(f"[{cid}] {campaign['curso']}")
|
||||
continue
|
||||
@ -181,13 +190,20 @@ def run():
|
||||
google_status,
|
||||
))
|
||||
|
||||
# Para PMX con companion Search: usar conversiones Google como leads de análisis
|
||||
course_num = _course_num(campaign["curso"])
|
||||
is_pmx_with_companion = (
|
||||
"pmx" in campaign["curso"].lower()
|
||||
and course_num in courses_with_both
|
||||
)
|
||||
leads_grupo = int(metrics.get("conversions", 0)) if is_pmx_with_companion else leads
|
||||
is_pmx = "pmx" in campaign["curso"].lower() and "_leadform" not in campaign["curso"].lower()
|
||||
|
||||
# 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
|
||||
# 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)
|
||||
decision = decide(analysis)
|
||||
@ -200,6 +216,7 @@ def run():
|
||||
"analysis": analysis,
|
||||
"decision": decision,
|
||||
"today_metrics": today_metrics.get(cid, {}),
|
||||
"is_pmx_with_leadform": is_pmx_with_leadform,
|
||||
})
|
||||
|
||||
# Actualizar status en GACampaignMes y Google Ads Campaigns
|
||||
@ -293,8 +310,9 @@ def run():
|
||||
is_leadform = "_leadform" in campaign["curso"].lower()
|
||||
if is_leadform:
|
||||
log_text += " | ℹ️ LEADFORM: leads capturados directamente en Google (sin visitar la web) — no llegan a Airtable"
|
||||
elif course_num in courses_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"
|
||||
elif item.get("is_pmx_with_leadform"):
|
||||
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
|
||||
coste_hoy = round(today_m.get("cost", 0), 2)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user