Add leadform campaign detection and log notes for attribution discrepancies
- Detect courses with _leadform companion campaigns before first pass - Add ℹ️ LEADFORM note in Log for campaigns that use Google's native lead form (leads bypass the website and don't reach Airtable) - Add ⚠️ LEADFORM COMPANION note for sibling campaigns of the same course (explains conversion count discrepancy) - Also moves _course_num and courses_with_both detection to pre-pass (before first loop) so PMX analysis uses correct leads_grupo Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
9edd74b9ad
commit
369bca604e
86
run.py
86
run.py
@ -125,6 +125,32 @@ def run():
|
|||||||
print(f"→ {len(campaigns)} campañas con actividad este mes")
|
print(f"→ {len(campaigns)} campañas con actividad este mes")
|
||||||
print("→ Analizando...\n")
|
print("→ Analizando...\n")
|
||||||
|
|
||||||
|
# Detección anticipada de cursos con Search Y PMX simultáneos
|
||||||
|
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
|
||||||
|
|
||||||
|
course_types_pre: dict[str, set] = {}
|
||||||
|
for c in campaigns:
|
||||||
|
num = _course_num(c["curso"])
|
||||||
|
if num:
|
||||||
|
course_types_pre.setdefault(num, set())
|
||||||
|
if "pmx" in c["curso"].lower():
|
||||||
|
course_types_pre[num].add("pmx")
|
||||||
|
elif "search" in c["curso"].lower():
|
||||||
|
course_types_pre[num].add("search")
|
||||||
|
courses_with_both = {
|
||||||
|
num for num, types in course_types_pre.items()
|
||||||
|
if "pmx" in types and "search" in types
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detección anticipada de cursos con campaña _leadform activa
|
||||||
|
courses_with_leadform = {
|
||||||
|
_course_num(c["curso"])
|
||||||
|
for c in campaigns
|
||||||
|
if "_leadform" in c["curso"].lower() and _course_num(c["curso"])
|
||||||
|
}
|
||||||
|
|
||||||
# === PRIMERA PASADA: recopilar datos de todas las campañas ===
|
# === PRIMERA PASADA: recopilar datos de todas las campañas ===
|
||||||
collected = []
|
collected = []
|
||||||
skipped = []
|
skipped = []
|
||||||
@ -150,12 +176,21 @@ def run():
|
|||||||
google_status,
|
google_status,
|
||||||
))
|
))
|
||||||
|
|
||||||
analysis = analyze(campaign, leads, metrics)
|
# 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
|
||||||
|
|
||||||
|
analysis = analyze(campaign, leads_grupo, metrics)
|
||||||
decision = decide(analysis)
|
decision = decide(analysis)
|
||||||
|
|
||||||
collected.append({
|
collected.append({
|
||||||
"campaign": campaign,
|
"campaign": campaign,
|
||||||
"leads": leads,
|
"leads": leads,
|
||||||
|
"leads_grupo": leads_grupo,
|
||||||
"metrics": metrics,
|
"metrics": metrics,
|
||||||
"analysis": analysis,
|
"analysis": analysis,
|
||||||
"decision": decision,
|
"decision": decision,
|
||||||
@ -174,23 +209,6 @@ def run():
|
|||||||
# Ordenar: 0=críticas → 1=no críticas → 2=mantener
|
# Ordenar: 0=críticas → 1=no críticas → 2=mantener
|
||||||
collected.sort(key=_priority)
|
collected.sort(key=_priority)
|
||||||
|
|
||||||
# Detectar cursos con campaña Search Y PMX simultáneas (posible reatribución PMX)
|
|
||||||
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
|
|
||||||
|
|
||||||
course_types: dict[str, set] = {}
|
|
||||||
for item in collected:
|
|
||||||
name = item["campaign"]["curso"]
|
|
||||||
num = _course_num(name)
|
|
||||||
if num:
|
|
||||||
course_types.setdefault(num, set())
|
|
||||||
if "pmx" in name.lower():
|
|
||||||
course_types[num].add("pmx")
|
|
||||||
elif "search" in name.lower():
|
|
||||||
course_types[num].add("search")
|
|
||||||
courses_with_both = {num for num, types in course_types.items() if "pmx" in types and "search" in types}
|
|
||||||
|
|
||||||
# === SEGUNDA PASADA: imprimir en orden + aplicar decisiones ===
|
# === SEGUNDA PASADA: imprimir en orden + aplicar decisiones ===
|
||||||
resumen = []
|
resumen = []
|
||||||
advice_updates = [] # (gcm_record_id, consejo, criticidad) para batch update final
|
advice_updates = [] # (gcm_record_id, consejo, criticidad) para batch update final
|
||||||
@ -258,6 +276,13 @@ def run():
|
|||||||
if "search" in campaign["curso"].lower() and course_num in courses_with_both:
|
if "search" in campaign["curso"].lower() and course_num in courses_with_both:
|
||||||
log_text += " | ⚠️ PMX ATTRIBUTION: campaña Search con companion PMX activo — parte de las conversiones de Google pueden estar reatribuidas a la campaña PMX"
|
log_text += " | ⚠️ PMX ATTRIBUTION: campaña Search con companion PMX activo — parte de las conversiones de Google pueden estar reatribuidas a la campaña PMX"
|
||||||
|
|
||||||
|
# Nota leadform: los leads se capturan en Google sin pasar por la web
|
||||||
|
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"
|
||||||
|
|
||||||
advice_updates.append((
|
advice_updates.append((
|
||||||
campaign["airtable_id"],
|
campaign["airtable_id"],
|
||||||
decision.get("consejo", ""),
|
decision.get("consejo", ""),
|
||||||
@ -286,26 +311,15 @@ def run():
|
|||||||
print(" ✓ Consejos y criticidad guardados.")
|
print(" ✓ Consejos y criticidad guardados.")
|
||||||
|
|
||||||
# Snapshot diario: ConvLeadsLakeMesFinal + ConvLeadsLakeMesGrupo
|
# Snapshot diario: ConvLeadsLakeMesFinal + ConvLeadsLakeMesGrupo
|
||||||
# Para PMX con Search companion del mismo curso, Grupo = leads del Search
|
# PMX con companion Search → Grupo = conversiones Google (ya calculado en leads_grupo)
|
||||||
course_search_leads = {
|
final_leads_data = [
|
||||||
_course_num(item["campaign"]["curso"]): item["leads"]
|
{
|
||||||
for item in collected
|
|
||||||
if "search" in item["campaign"]["curso"].lower()
|
|
||||||
and _course_num(item["campaign"]["curso"])
|
|
||||||
}
|
|
||||||
final_leads_data = []
|
|
||||||
for item in collected:
|
|
||||||
name = item["campaign"]["curso"]
|
|
||||||
num = _course_num(name)
|
|
||||||
if num and "pmx" in name.lower() and num in courses_with_both:
|
|
||||||
grupo = course_search_leads.get(num, item["leads"])
|
|
||||||
else:
|
|
||||||
grupo = item["leads"]
|
|
||||||
final_leads_data.append({
|
|
||||||
"airtable_id": item["campaign"]["airtable_id"],
|
"airtable_id": item["campaign"]["airtable_id"],
|
||||||
"conv_leads_lake_mes": item["leads"],
|
"conv_leads_lake_mes": item["leads"],
|
||||||
"conv_leads_lake_mes_grupo": grupo,
|
"conv_leads_lake_mes_grupo": item["leads_grupo"],
|
||||||
})
|
}
|
||||||
|
for item in collected
|
||||||
|
]
|
||||||
if final_leads_data:
|
if final_leads_data:
|
||||||
print(f"→ Actualizando ConvLeadsLakeMesFinal ({len(final_leads_data)} registros)...")
|
print(f"→ Actualizando ConvLeadsLakeMesFinal ({len(final_leads_data)} registros)...")
|
||||||
at.batch_update_gacampaignmes_final_leads(final_leads_data)
|
at.batch_update_gacampaignmes_final_leads(final_leads_data)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user