Modelo de propensión a conversión
Un primer modelo con ROC-AUC 0.988 que usaba información de la propia sesión, y la versión corregida que solo usa lo disponible antes de decidir.
ROC-AUC en test
0.878
IC 95 %: 0.869 – 0.887 (junio 2017)
Lift del top 5 %
×7.8
El 5 % de sesiones con mayor score concentra el 38.8 % de las compras
Sesiones analizadas
393 mil
308 mil usuarios, 6 meses de Google Analytics
Tasa de conversión
1.3 %
Clase muy desbalanceada
El problema
Con seis meses de datos de la tienda de merchandising de Google en Google Analytics (enero–junio de 2017), el objetivo era estimar, para cada sesión, la probabilidad de que termine en al menos una transacción, de modo que marketing pudiera priorizar audiencias y campañas.
Solo alrededor del 1.3 % de las sesiones convierte, así que el desbalance define casi todas las decisiones: cómo dividir los datos, qué métrica reportar y cómo interpretar el score.
Conversión por canal
Antes de modelar hice un análisis exploratorio orientado a decisiones. El canal Referral convierte 4.7 % frente a apenas 0.8 % de la búsqueda orgánica, mostrando siempre el n de cada canal para no confiar en tasas ruidosas con poca muestra.
Pasá el mouse por cada barra para ver la tasa de conversión y el tamaño de muestra (n).
El desbalance de clases
Solo 1.34 % de las 392,892 sesiones termina en una compra.
Conversión por dispositivo
Desktop concentra el tráfico y convierte notablemente mejor que mobile o tablet.
Actividad en el sitio: convirtió vs. no convirtió
Hits, páginas vistas y tiempo en sitio, en escala log por su fuerte asimetría. Elegí una métrica para comparar los dos grupos.
Evolución semanal
La conversión sube 43 % entre el primer y el último mes aunque el volumen semanal de sesiones casi no cambia (+6 %).
Metodología
- 1
Split temporal
Entrenamiento con todo lo anterior a junio de 2017 y test en junio (63,578 sesiones, 946 compras). Un split aleatorio mezclaría el futuro con el pasado.
- 2
Baseline y modelo
LightGBM con class_weight='balanced', num_leaves=20 y learning_rate=0.1, comparado contra un baseline simple. El tuning apenas movió el PR-AUC (+0.45 %), y así lo dejé documentado.
- 3
Umbral de decisión
Umbral elegido para maximizar F1 sobre el test temporal. Como el balanceo de clases corre las probabilidades, el score se usa para rankear, no como probabilidad literal.
- 4
Explicabilidad
SHAP global y por sesión para mostrar por qué el modelo asigna cada puntaje, no solo qué promedio tiene cada variable.
Auditoría: qué información está disponible al decidir
El modelo original usaba totales de la propia sesión (hits, páginas vistas, tiempo en sitio, bounces y variables derivadas), algo razonable para describir sesiones ya terminadas. Pero el caso planteado era decidir a quién incluir o excluir de campañas, es decir antes de la compra, y esos valores solo se conocen cuando la sesión termina; la compra misma los infla, porque comprar implica recorrer el checkout y generar más páginas y hits. La prueba más simple: usar solo las páginas vistas, sin ningún modelo, ya da un AUC de 0.978.
Reentrené con el mismo split temporal, los mismos hiperparámetros y el mismo pipeline en tres variantes: la original, una con solo lo conocido al inicio de la sesión (A) y otra que suma el historial del usuario en sesiones anteriores (B).
ROC-AUC en test según la información disponible
Misma partición temporal, mismos hiperparámetros. La barra roja usa datos que no existen al momento de decidir.
Cada variable por sí sola (AUC sin modelo)
Las variables de fin de sesión separan casi solas; las de historial aportan una señal moderada y honesta.
Cómo construí el historial sin mirar el futuro
g = df.groupby("fullVisitorID")
df["prev_sessions"] = g.cumcount()
for col in ["pageviews", "hits", "timeOnSite", "y"]:
filled = df[col].fillna(0)
# acumulado hasta la sesión actual, menos la sesión actual
df[f"prev_{col}_sum"] = filled.groupby(df["fullVisitorID"]).cumsum() - filled
df["days_since_prev"] = g["date"].diff().dt.days.fillna(-1)
df["prev_converted"] = (df["prev_y_sum"] > 0).astype(int)Acumulados por usuario restando la fila actual: cada sesión solo ve lo ocurrido antes.
Resultado final (variante B)
- ROC-AUC 0.878 (IC 95 % bootstrap: 0.869–0.887) y PR-AUC 0.109 sobre una tasa base de 1.5 % en junio.
- El 5 % de sesiones con mayor score concentra el 38.8 % de las compras reales: lift ×7.8 frente a elegir al azar.
- Sin el historial (variante A) el lift baja a ×6.1, es decir que saber quién es el usuario aporta señal real.
- Las variables más importantes son el día de la semana, el historial de hits y tiempo previo, el número de visita y los días desde la sesión anterior.
Despliegue
Además del notebook armé una implementación mínima para mostrar cómo llegaría a producción.
- 1
API con FastAPI
Endpoints /health, /model-info y /predict, más scoring masivo desde CSV y un dashboard web mínimo servido por la misma API.
- 2
Contenedor y Cloud Run
Dockerfile listo para desplegar en Cloud Run.
- 3
Features desde BigQuery
Un SQL de ejemplo replica las mismas variables a partir del export nativo de GA4 a BigQuery.
- 4
Uso por marketing
El score se usa para rankear sesiones y priorizar el top N % de las campañas; los segmentos alto/medio/bajo derivan del mismo umbral.
Qué aprendí
- Definir primero el momento de la predicción (inicio de sesión vs. fin) y recién después elegir variables.
- Reportar una métrica que responda a la decisión (lift del top-k) y no solo AUC, con intervalos de confianza para dimensionar la incertidumbre.
- Contrastar el resultado con baselines de una sola variable: las páginas vistas solas ya daban un AUC de 0.978.
- Siguiente paso: anticipar la compra antes de que la persona entre al sitio, usando solo su historial de visitas anteriores.