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}")
|
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
36
run.py
@ -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)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user