Metodología
Cómo está calculado
Definiciones, la lógica de cada cálculo, la fuente de los datos y lo que este análisis no puede decir. Todo lo que aparece en los tableros sale de aquí.
Análisis de cohortes
Una cohorte agrupa a los clientes por el mes de su primera compra. Seguirlas por separado permite distinguir dos cosas que una métrica agregada mezcla: el efecto de la antigüedad del cliente y el efecto del calendario. Si la retención cae igual en todas las cohortes, el problema es estructural; si cae sólo en algunas, hay algo que cambió en ese momento.
| Término | Definición |
|---|---|
cohort_month | Mes de la primera compra del cliente (YYYY-MM) |
| Mes 0 | El mes de adquisición. 100% por definición |
| Mes N | N meses naturales después de la primera compra |
| Retención mes N | Clientes de la cohorte que compraron en el mes N, sobre el tamaño de la cohorte |
# Cohorte = mes de la primera compra
customers["cohort_month"] = (
orders.groupby("customer_unique_id")["order_purchase_timestamp"]
.min().dt.to_period("M").astype(str)
)
# Matriz de retención: cohorte x meses transcurridos
cohort_data = (
orders.groupby(["cohort_month", "months_since_cohort"])["customer_unique_id"]
.nunique()
)
retention_matrix = cohort_data.unstack(fill_value=0)
retention_pct = retention_matrix.div(retention_matrix[0], axis=0) * 100Celdas vacías, no ceros. Una cohorte de 2018-08 no puede tener un mes 12 en un dataset que termina en 2018-08. Aquí esas celdas se dejan en blanco y quedan fuera de los promedios. Es la única diferencia deliberada frente al tablero de Streamlit que este reemplaza: aquel usaba unstack(fill_value=0) y contaba esos meses como 0% de retención, lo que arrastraba la cola de la curva promedio hacia abajo por censura y no por comportamiento. Por eso la curva promedio publica también el número de cohortes que entran en cada punto.
Segmentación RFM
RFM puntúa a cada cliente en tres dimensiones y combina los puntajes en segmentos accionables. Se calcula sobre el estado final de cada cliente, no por su mes de adquisición — razón por la cual el filtro de cohortes no mueve esta página.
| Dimensión | Qué mide | Dirección |
|---|---|---|
| R — Recencia | Días desde la última compra al corte del dataset | Menor es mejor |
| F — Frecuencia | Número total de pedidos entregados | Mayor es mejor |
| M — Monto | Ingreso total generado por el cliente | Mayor es mejor |
rfm["R_score"] = pd.qcut(rfm["recency_days"], q=5, labels=[5, 4, 3, 2, 1])
rfm["F_score"] = pd.qcut(rfm["total_orders"].rank(method="first"), q=5, labels=[1, 2, 3, 4, 5])
rfm["M_score"] = pd.qcut(rfm["total_revenue"], q=5, labels=[1, 2, 3, 4, 5])La frecuencia se rankea antes de cortar en quintiles porque su distribución es casi degenerada: con un 97% de clientes en un solo pedido, qcut sobre los valores crudos no encuentra cinco cortes distintos y falla. Rankear rompe los empates de forma arbitraria pero estable, que es lo mejor disponible cuando la variable casi no varía — y conviene saberlo al leer los segmentos: la F aporta poca información real en este dataset.
Supervivencia Kaplan-Meier
El estimador Kaplan-Meier responde: ¿cuál es la probabilidad de que un cliente todavía no haya hecho su segunda compra, t días después de la primera? Es la herramienta correcta aquí porque el dato está censurado por la derecha: de un cliente que aún no recompró no sabemos que nunca lo hará, sólo que no lo ha hecho todavía. Tratar esos casos como «no recompra» subestimaría la retención de las cohortes recientes.
| Término | En este contexto |
|---|---|
duration_days | Días de la primera a la segunda compra, o al corte del dataset si no hubo segunda |
event_observed | 1 si hubo segunda compra, 0 si el caso está censurado |
| S(t) | Probabilidad de no haber recomprado hasta el día t |
La curva se estima en el pipeline, no en el navegador, y se muestrea en una malla semanal hasta los 720 días. Los intervalos usan la transformación log-log (Greenwood exponencial), que es la que produce lifelines por omisión; la forma directa sobre S(t) puede salirse de [0, 1], que para una probabilidad no es dibujable. El producto se acumula sobre cada tiempo de evento distinto y sólo después se muestrea, porque hacerlo al revés calcularía mal el conjunto en riesgo.
La mediana no existe. S(t) se estabiliza cerca del 95% y nunca llega al 50%, así que no hay un día en el que la mitad de los clientes haya recomprado. El tablero informa «sin mediana» en lugar de un número: con ~97% de compradores únicos, el evento que la mediana mediría casi nunca ocurre.
Factores de activación
Una regresión logística sobre la probabilidad de una segunda compra, usando sólo variables conocidas en el primer pedido: su valor, su reseña, el número de artículos, el medio de pago y la categoría. Los coeficientes se presentan como odds ratios en escala log₂, donde duplicar y reducir a la mitad quedan a la misma distancia del cero.
El gráfico colorea una variable sólo cuando su intervalo al 95% no cruza el cero. Un coeficiente cuyo intervalo incluye «sin efecto» no tiene dirección que afirmar, aunque su estimación puntual caiga a un lado. Y todo esto es observacional: las asociaciones describen a quién conviene buscar, no qué pasaría si se cambiara la variable.
Fuente de datos
Muestra pública del marketplace brasileño Olist, publicada en Kaggle: nueve CSV con pedidos anonimizados. Este análisis usa 96,478 pedidos entregados de 93,358 clientes únicos, entre 2016-09-15 y 2018-08-29, en 27 estados.
| Métrica | Valor |
|---|---|
| Pedidos entregados | 96,478 |
| Clientes únicos | 93,358 |
| Tasa de recompra | 3% |
| Ingresos totales | R$ 15,419,774 |
| LTV promedio | R$ 165 |
| Estados | 27 |
customer_unique_id, nunca customer_id. Olist emite un customer_id nuevo por pedido, así que agrupar por él contaría cada compra como un cliente distinto: inflaría el número de clientes y haría desaparecer la recompra por construcción — el hallazgo central del análisis sería un artefacto de la llave elegida.
# Correcto: la identidad real del cliente
customers = orders.merge(
raw_customers[["customer_id", "customer_unique_id"]], on="customer_id"
)
customers.groupby("customer_unique_id") # una fila por persona
# Incorrecto: una "persona" por pedido
# orders.groupby("customer_id")De Streamlit a estático
Este tablero fue una aplicación Streamlit que leía 36 MB de parquet en cada interacción y por eso necesitaba un contenedor corriendo. Sus tres filtros —rango de cohortes, tamaño mínimo de cohorte y segmento RFM— resultaron ser subconjuntos, nunca recálculos: seleccionan filas de una matriz ya agregada. Nada volvía a calcularse desde los 96,478 pedidos.
Así que la misma interactividad sobrevive a un export estático, siempre que el JSON esté preagregado en los ejes por los que cortan los filtros: mes de cohorte, segmento y estado. Los dos conjuntos por cliente (93,358 filas cada uno) no se publican: el mapa RFM sale como rejilla agrupada y la curva de supervivencia como puntos ya estimados. El resultado son 276 KB de JSON —unos 38 KB comprimidos— en lugar de un servicio con estado.
Que los números no cambiaran es una afirmación verificable, así que está verificada: data-pipeline/06_verify_parity.py vuelve a calcular cada cifra desde el parquet con las mismas expresiones que usaban las páginas de Streamlit y las compara con el JSON publicado. La curva Kaplan-Meier y sus dos intervalos se comparan contra lifelines, la implementación de referencia, en toda la malla.
Limitaciones
- Censura. El dataset termina en 2018-08-29. Las cohortes más recientes han tenido menos tiempo para recomprar, así que su retención observada es un piso, no una estimación.
- Es un marketplace. Olist es intermediario: la experiencia depende también del vendedor, que este análisis no puede separar de la plataforma.
- Observacional. Ninguna cifra aquí identifica un efecto causal. La correlación entre entrega y recompra se calcula sobre 24 estados como unidades, y São Paulo difiere del norte del país en mucho más que la logística.
- Datos faltantes. Cerca del 8% de los pedidos no tiene reseña. Los promedios de reseña y de entrega se calculan sobre los pedidos que sí la tienen, con numerador y denominador propios en lugar de rellenar.
- Frecuencia casi constante. Con un 97% de clientes de un solo pedido, la F de RFM aporta poca información y los segmentos los separa sobre todo la recencia y el monto.