Los modelos RFM (recency, frequency, monetary) son ampliamente utilizados en las areas de marketing para la segmentación de sus clientes, pudiendo identificar los clientes más leales, o aquéllos que no debería perder. En este post voy a mostrarles como implementar un modelo RFM, asi que ¡¡allá vamos!!.
Como decíamos en la preview de este post, los modelos RFM son ampliamente utilizados en las áreas de marketing para segmentar sus carteras de clientes. Esta segmentación se realiza a partir de un scoring (que veremos más adelante), sobre tres parámetros:
recency: identifica el tiempo transcurrido (generalmente en días), desde la última actividad del usuario y hasta la fecha de establecida para el modelo.
frequency: refiere a con qué frecuencia realiza movimientos (de compra o venta) el usuario..
monetary: refiere al valor o monto de los movimientos del usuario.
El modelo RFM descansa sobre el siguiente principio :
El 80% de tu negocio proviene del 20% de tus clientes .
En este post, iremos mostrando una serie de funciones que he creado para procesar los datos y obtener los segmentos:
normalize_table: con esta función normalizamos los nombres del dataset para que puedan ser procesados por la función que genera el scoring.
rfm_category: con esta función obtenemos la tabla RFM, la cuál contiene los puntajes para los parámetros (recency, frequency, monetary). Además, nos devuelve una serie de métricas que utilizaremos para explorar nuestros datos.
segment_rfm: esta función nos permite crear los segmentos a partir del scoring de la tabla anterior (tabla RFM), más la definición de los threshold. También nos devuelve algunas métricas que nos permiten explorar nuestros resultados.
Utilizaremos un dataset tomado de Kaggle que cuenta con información de ventas de una tienda minorista en línea registrada y con sede en el Reino Unido. El dataset muestra las operaciones en distintos países lo cuál resulta muy atractivo a la hora de implementar un modelo RFM que contemple los datos a este nivel de agregación.
Vamos la usar el paquete summarytools para obtener una tabla resumen con los valores para todas las variables.
summarytools::st_options(lang = 'es')
summarytools::dfSummary(df, plain.ascii = FALSE,
style = "grid",
graph.magnif = 0.75,
valid.col = FALSE,
tmp.img.dir = "/tmp")
Dimensiones: 541909 x 8
Duplicados: 5268
| No | Variable | Estadísticas / Valores | Frec. (% sobre válidos) | Gráfico | Perdidos |
|---|---|---|---|---|---|
| 1 | InvoiceNo [character] |
1. 573585 2. 581219 3. 581492 4. 580729 5. 558475 6. 579777 7. 581217 8. 537434 9. 580730 10. 538071 [ 25890 otros ] |
1114 ( 0.2%) 749 ( 0.1%) 731 ( 0.1%) 721 ( 0.1%) 705 ( 0.1%) 687 ( 0.1%) 676 ( 0.1%) 675 ( 0.1%) 662 ( 0.1%) 652 ( 0.1%) 534537 (98.6%) |
![]() |
0 (0.0%) |
| 2 | StockCode [character] |
1. 85123A 2. 22423 3. 85099B 4. 47566 5. 20725 6. 84879 7. 22720 8. 22197 9. 21212 10. 20727 [ 4060 otros ] |
2313 ( 0.4%) 2203 ( 0.4%) 2159 ( 0.4%) 1727 ( 0.3%) 1639 ( 0.3%) 1502 ( 0.3%) 1477 ( 0.3%) 1476 ( 0.3%) 1385 ( 0.3%) 1350 ( 0.2%) 524678 (96.8%) |
![]() |
0 (0.0%) |
| 3 | Description [character] |
1. WHITE HANGING HEART T-LIG 2. REGENCY CAKESTAND 3 TIER 3. JUMBO BAG RED RETROSPOT 4. PARTY BUNTING 5. LUNCH BAG RED RETROSPOT 6. ASSORTED COLOUR BIRD ORNA 7. SET OF 3 CAKE TINS PANTRY 8. (Cadena vacía) 9. PACK OF 72 RETROSPOT CAKE 10. LUNCH BAG BLACK SKULL. [ 4214 otros ] |
2369 ( 0.4%) 2200 ( 0.4%) 2159 ( 0.4%) 1727 ( 0.3%) 1638 ( 0.3%) 1501 ( 0.3%) 1473 ( 0.3%) 1454 ( 0.3%) 1385 ( 0.3%) 1350 ( 0.2%) 524653 (96.8%) |
![]() |
0 (0.0%) |
| 4 | Quantity [integer] |
Media (d-s) : 9.6 (218.1) min < mediana < max: -80995 < 3 < 80995 RI (CV) : 9 (22.8) |
722 valores distintos | ![]() |
0 (0.0%) |
| 5 | InvoiceDate [character] |
1. 10/31/2011 14:41 2. 12/8/2011 9:28 3. 12/9/2011 10:03 4. 12/5/2011 17:24 5. 6/29/2011 15:58 6. 11/30/2011 15:13 7. 12/8/2011 9:20 8. 12/6/2010 16:57 9. 12/5/2011 17:28 10. 12/9/2010 14:09 [ 23250 otros ] |
1114 ( 0.2%) 749 ( 0.1%) 731 ( 0.1%) 721 ( 0.1%) 705 ( 0.1%) 687 ( 0.1%) 676 ( 0.1%) 675 ( 0.1%) 662 ( 0.1%) 652 ( 0.1%) 534537 (98.6%) |
![]() |
0 (0.0%) |
| 6 | UnitPrice [numeric] |
Media (d-s) : 4.6 (96.8) min < mediana < max: -11062.1 < 2.1 < 38970 RI (CV) : 2.9 (21) |
1630 valores distintos | ![]() |
0 (0.0%) |
| 7 | CustomerID [integer] |
Media (d-s) : 15287.7 (1713.6) min < mediana < max: 12346 < 15152 < 18287 RI (CV) : 2838 (0.1) |
4372 valores distintos | ![]() |
135080 (24.9%) |
| 8 | Country [character] |
1. United Kingdom 2. Germany 3. France 4. EIRE 5. Spain 6. Netherlands 7. Belgium 8. Switzerland 9. Portugal 10. Australia [ 28 otros ] |
495478 (91.4%) 9495 ( 1.8%) 8557 ( 1.6%) 8196 ( 1.5%) 2533 ( 0.5%) 2371 ( 0.4%) 2069 ( 0.4%) 2002 ( 0.4%) 1519 ( 0.3%) 1259 ( 0.2%) 8430 ( 1.6%) |
![]() |
0 (0.0%) |
En la tabla resumen puede verse que la mayoría (91,4%) de las operaciones se concentran en UK,y en menos medida en Alemania y Francia. Además, podemos ver la que la fecha de la factura (InvoiceDate) es una variable texto con formato m/dd/yyyy hh:mm. También debe destacarse el hecho de que existen cantidades vendidas con valores menores a 0.
Considerando lo anterior transformamos la variable InvoiceDate a tipo Date (yyyy-mm-dd), excluimos cualquier valor númerico menor a 0 y seleccionamos los datos para los primeros 3 países, es decir UK, Alemania y Francia.
Luego, agrupamos por pais, Date y CustomerID las cantidades vendidas y el monto de las mismas.
df <- df %>%
mutate(Date= as_date(lubridate::mdy_hm(InvoiceDate)))%>%
group_by(Country,Date,CustomerID)%>%
summarise(Quantity= sum(Quantity),
Monetary= sum(UnitPrice)) %>%
filter(Country %in% c("United Kingdom","Germany","France")) %>%
filter_if(is.numeric, ~ .x > 0)
DT::datatable(df,filter= 'top',options = list(pageLength = 5, dom = 'tip'))
En la tabla podemos visualizar como nos quedó conformado el dataset.
Esta etapa está compuesta por tres subetapas: a) normalización de la tabla, 2) creación de los puntajes RFM, 3) creación de los segmentos.
Vamos a normalizar los nombres de nuestro dataset para hacerlos coincidir con los parámetros con los que la función rfm_category asigna los puntajes. Para ello usaremos la normalize_table que recibe el dataset con los actuales nombres de los parámetros.
La función rfm_category, admite 5 parámetros:
df: el dataset a procesar
fecha_analisis: representa la fecha de implementación del modelo y es útil para determinar los días desde la última venta (recency).
bins: nos permite indicar el número de segmentos en los cuáles queremos cortar nuestra población.
group_by: nos permite utilizar distintos niveles de agregación. Como el CostumerID puede repetirse entre los países, con este parámetro nos aseguramos que ese valor sea único.
inherits.threshold: por defecto es NULL. Este parámetro nos permite customizar los umbrales para la asignación de puntajes. Por ejemplo podríamos utilizar para los comercios los umbrales del país y de esa forma estandarizarlos para su comparación.
A su vez la función nos devuelve:
resultado_rfm: una tabla que contiene el scoring
heatmap: un gráfico que muestra el promedio del monto para las combinaciones de recency y de frequency.
threshold: una tabla con los umbrales para recency, frequency y monetary.
Sin más preámbulos, miremos el resultado de la función.
xaringanExtra::use_panelset()
for(i in 1:length(tabla_rfm)){
cat("::: {.panel}\n")
cat("##", unique(tabla_rfm[[i]]$resultado_rfm$Country) , "{.panel-name}\n")
print(tabla_rfm[[i]]$heatmap)
cat("\n")
print(kableExtra::kbl(tabla_rfm[[i]]$threshold,
format.args = list(decimal.mark = ',', big.mark = "."), col.names = rep(c("lower","upper"),3),
caption = paste0("Puntos de corte scoring. ",unique(tabla_rfm[[i]]$resultado_rfm$Country),"."))%>%
kableExtra::add_header_above(c("Recency" = 2, "Frequency" = 2, "Monetary"= 2),color = "#191C3C", bold = T,align = "center") %>%
kableExtra::kable_paper())
cat("\n")
cat(":::\n")
}
| lower | upper | lower | upper | lower | upper |
|---|---|---|---|---|---|
| 0,0 | 11,5 | 1,0 | 211,0 | 0,010 | 79,125 |
| 11,5 | 32,0 | 211,0 | 446,0 | 79,125 | 186,770 |
| 32,0 | 111,0 | 446,0 | 1.674,5 | 186,770 | 350,815 |
| 111,0 | 372,0 | 1.674,5 | 10.924,0 | 350,815 | 2.030,560 |
| lower | upper | lower | upper | lower | upper |
|---|---|---|---|---|---|
| 0,00 | 17,25 | 1,0 | 260,0 | 0,0100 | 84,7925 |
| 17,25 | 32,00 | 260,0 | 610,0 | 84,7925 | 185,5250 |
| 32,00 | 93,00 | 610,0 | 1.629,5 | 185,5250 | 496,5550 |
| 93,00 | 373,00 | 1.629,5 | 8.213,0 | 496,5550 | 2.431,2800 |
| lower | upper | lower | upper | lower | upper |
|---|---|---|---|---|---|
| 0 | 18 | 1 | 155 | 0,0100 | 49,8050 |
| 18 | 51 | 155 | 366 | 49,8050 | 123,4400 |
| 51 | 144 | 366 | 946 | 123,4400 | 285,5625 |
| 144 | 374 | 946 | 69.982 | 285,5625 | 41.377,3300 |
Aquí podemos ver los primeros 10 registros de la tabla RFM para UK.
Cómo se puede observar en la tabla anterior, al dataset se le han añadido 3 columnas (recency_cut, frequency_cut y monetary_cut), con valores de 1 a 4 1(según la cantidad de cortes o bins elegidos podría ser 5), que deben interpretarse del siguiente modo:
recency: la puntuación se genera asignando a los clientes con las fechas de ventas más recientes la máxima puntuación (4 en este caso), y aquellos con fechas de ventas más distante reciben una clasificación de actualidad de 1.
frequency: a los usuarios con mayor cantidad de unidades vendidas se les asigna una puntuación más alta (4) y a los de menor cantidad una puntuación de 1.
monetary: se asigna sobre la base del monto total de las ventas al usuario en el período considerado para el análisis. A los clientes con mayores montos de venta se les asigna una puntuación más alta, mientras que a los que tienen montos de venta más bajos se les asigna una puntuación de 1.
Los heatmap, confirman -aunque en menor medida para Francia- que los montos promedios más altos povienen que los usuarios con mayor cantidad de bienes vendidos y con recientes operaciones.
Cuando se implementó el modelo con bins= 5, para Francia y Alemania se obtuvo un gráfico inconsistente (no completo todos sus casilleros), por lo que se decidió utilizar 4 cortes.
Para la creación de los segmentos utilizaremos la función segment_rfm, a la cuál debemos pasarle 3 grupos de parámetros:
tabla_rfm: es la tabla resultante de la función anterior y contiene cada uno de nuestros costumer con un puntaje de recency, frequency y monetary asignado.
nombres_segmentos: es un vector de datos con la cantidad de nombres comos segmentos queramos utilizar. En nuestro caso utilizaremos 7 segmentos (incluyendo los sin clasificar) para hacerlo más comprensible.
umbrales de los segmentos: llevan los nombres recency_lower, recency_upper, frequency_lower, frequency_upper, monetary_lower, monetary_upper y son vectores que contienen los umbrales con los cuáles agrupamos en segmentos los scoring rfm.
Esta función nos retorna:
tabla_rfm: que contiene cada uno de nuestros customer segmentados
bar_chart: gráfico de barras que contabiliza los customer según el puntaje de recency, frequency y monetary. Representa una estrategia visual para observar la composición de nuestros clientes.
Composicion_segmento: muestra la frecuencia absoluta y relativa de los customer según los segmentos.
treemap_segmentos : gráfico treemap con la composición por segmentos.
impact_segment:contabiliza en términos absolutos y relativos el impacto que tienen los segmentos sobre las ventas y el monto en el país.
Como se mencionó anteriormente trabajaremos con los siguientes segmentos:
Champions: es el segmento que reúne el máximo puntaje en los tres parámetros. Para las ventas en UK este segmento concentra el 43% de las mismas y el 40% del monto facturado.
Loyalist: son los clientes que tienen el máximo puntaje en la cantidad de productos comprados. Representan el segundo segmento en términos de monto facturado.
Big Spenders: este segmento tiene el máximo puntaje en los montos de compras. Si bien en término de cantidad adquirida no son influyente, respresentan el tercer segmento en monto facturado.
Promising: este segmento muestra puntajes de RFM por encima del resto, pero por debajo de los champions. Están tímidos pero son sensibles de estimular.
New costumer: tienen alto puntaje en recency, es decir han realizado compras recientes aunque no por mucha cantidad ni de montos altos. Debemos estimularlos.
Hibernating: muestran los puntajes más bajos en las tres categorías.
Asignamos los umbrales
recency_lower : 4,1,1,2,4,1
recency_upper: 4,4,4,4,4,1
frequency_lower: 4,4,1,2,1,1
frequency_upper: 4,4,4,4,4,1
monetary_lower: 4,1,4,2,1,1
monetary_upper: 4,4,4,4,4,1
nombres_segmentos <- c("Champions","Loyalist","Big Spenders",
"Promising","New Customers","Hibernating")
recency_lower <- c(4,1,1,2,4,1)
recency_upper <- c(4,4,4,4,4,1)
frequency_lower <- c(4,4,1,2,1,1)
frequency_upper <- c(4,4,4,4,4,1)
monetary_lower <- c(4,1,4,2,1,1)
monetary_upper <- c(4,4,4,4,4,1)
rfm <- list()
for(i in 1:length(tabla_rfm)){
rfm[[i]] <- segment_rfm(tabla_rfm = tabla_rfm[[i]],
nombres_segmentos = nombres_segmentos,
recency_lower,
recency_upper,
frequency_lower,
frequency_upper,
monetary_lower,
monetary_upper
)
}
for(i in 1:length(rfm)){
cat("::: {.panel}\n")
cat("###", unique(rfm[[i]]$tabla_rfm$Country), "{.panel-name}\n")
cat("\n")
cat("#### Distribución de los clientes según el scoring (RFM")
print(rfm[[i]]$bar_chart)
cat("\n")
cat("#### Composición de los segmentos")
cat("\n")
print(kableExtra::kbl(rfm[[i]]$Composicion_segmento,
format.args = list(decimal.mark = ',', big.mark = ".")) %>%
kableExtra::kable_paper())
cat("\n")
cat("#### Gráfico composición de los segmentos")
print(rfm[[i]]$treemap_segmentos)
cat("\n")
cat("#### Impacto de los segmentos en las Ventas y el Monto")
cat("\n")
print(kableExtra::kbl(rfm[[i]]$impact_segment,
col.names = c("Segmentos","Ventas","%","Monto","%"),
format.args = list(decimal.mark = ',', big.mark = ".")) %>%
kableExtra::kable_paper())
cat("\n")
cat(":::\n")
}

| segmento | cantidad | % |
|---|---|---|
| Promising | 31 | 35,6 |
| Usuals | 18 | 20,7 |
| Champions | 11 | 12,6 |
| Hibernating | 11 | 12,6 |
| Loyalist | 11 | 12,6 |
| Big Spenders | 3 | 3,4 |
| New Customers | 2 | 2,3 |

| Segmentos | Ventas | % | Monto | % |
|---|---|---|---|---|
| Loyalist | 47.400 | 42,9 | 9.095,71 | 33,0 |
| Champions | 35.434 | 32,1 | 8.474,69 | 30,7 |
| Promising | 16.877 | 15,3 | 5.988,87 | 21,7 |
| Usuals | 5.384 | 4,9 | 1.651,01 | 6,0 |
| Big Spenders | 4.012 | 3,6 | 1.877,45 | 6,8 |
| Hibernating | 979 | 0,9 | 419,30 | 1,5 |
| New Customers | 381 | 0,3 | 88,94 | 0,3 |

| segmento | cantidad | % |
|---|---|---|
| Usuals | 30 | 31,9 |
| Promising | 24 | 25,5 |
| Loyalist | 13 | 13,8 |
| Champions | 11 | 11,7 |
| Big Spenders | 7 | 7,4 |
| Hibernating | 7 | 7,4 |
| New Customers | 2 | 2,1 |

| Segmentos | Ventas | % | Monto | % |
|---|---|---|---|---|
| Champions | 43.710 | 36,7 | 11.334,46 | 32,7 |
| Loyalist | 38.720 | 32,5 | 9.612,45 | 27,8 |
| Promising | 15.851 | 13,3 | 4.867,00 | 14,1 |
| Usuals | 11.791 | 9,9 | 3.103,82 | 9,0 |
| Big Spenders | 7.655 | 6,4 | 5.212,01 | 15,1 |
| Hibernating | 1.020 | 0,9 | 351,03 | 1,0 |
| New Customers | 430 | 0,4 | 147,49 | 0,4 |

| segmento | cantidad | % |
|---|---|---|
| Usuals | 1.205 | 30,8 |
| Promising | 963 | 24,6 |
| Loyalist | 603 | 15,4 |
| Champions | 375 | 9,6 |
| Big Spenders | 345 | 8,8 |
| Hibernating | 289 | 7,4 |
| New Customers | 136 | 3,5 |

| Segmentos | Ventas | % | Monto | % |
|---|---|---|---|---|
| Champions | 1.768.772 | 43,6 | 439.585,40 | 39,9 |
| Loyalist | 1.372.598 | 33,8 | 217.268,88 | 19,7 |
| Promising | 422.793 | 10,4 | 136.777,67 | 12,4 |
| Usuals | 259.241 | 6,4 | 87.422,87 | 7,9 |
| Big Spenders | 189.988 | 4,7 | 205.524,46 | 18,7 |
| New Customers | 23.664 | 0,6 | 8.008,58 | 0,7 |
| Hibernating | 21.020 | 0,5 | 6.672,41 | 0,6 |
👉🏼 Los patrones y tendencias son más fácil de obervar en UK en razón de una mayor cantidad de observaciones. El bar_chart que combina los scoring de los tres parámetros muestra que los clientes que realizan compras de mayor valor además tienen altos puntajes de recency y de frequency (esquina superior derecha). En el opuesto, los que tienen menores puntajes de monetary coincide con los menores también de recency y frequency. Esto se da también en los clientes de Alemania y Francia pero con menos nitidez.
👉🏼Un punto interesante es el segmento Usuals, que representa a los clientes no clasificados en los umbrales y que es el principal segmento de UK y Alemania. El porcentaje de este segmento está directamente vinculado a los objetivos de la compañía en relación a las políticas de fidelización y en términos estadísticos a la sensibilidad y especificidad con la que creamos los umbrales. En nuestro caso. si tomáramos umbrales más amplios lograríamos diminuir ese conjunto de cliente (usuals) pero corremos el riesgo de por ejemplo clasificar como Champions clientes que no lo son o viceversa, como Hibernating a clientes que están activos. También podríamos generar nuevos segmentos para incluir este grupo de clientes. Esta es una discusión por demás interesante que requiere de un dialogo fluido entre la estadística y el marketing.
👉🏼Salvo para Francia, los segmentos Champions y Loyalist concentran la mayor parte de las ventas y la facturación (Monto). Los Big Spenders en estos países representan el tercer segmento en cuanto a facturación. Los Promising son una oportunidad para el negocio ya que son sensibles a promociones y estrategias de marketing.
🔦Son varias cosas las que me surgen para este apartado final pero voy a abocarme sólo a algunas de ellas. Por un lado, la oportunidad que representan los análisis RFM para el diálogo entre la estadística, los datos y el apoyo a los decision makers.
🔦Por otro lado,en este post he mostrado 3 funciones desarrolladas íntegramente en R que pueden adaptarse para su uso en distintos escenarios y que cuentan con la posibilidad de ajustar los parámetros centrales para lograr robustez en los resultados.
Sin dudas hay mucho camino por recorrer aún en el mundo de los datos., así que sigamos viajando✈️.
la cantidad de cortes seleccionados divide a la población en partes iguales. Si seleccionamos 4 cortes, dividimos a la población en cuartiles cada uno de los cuáles representa el 25%. Si decidiéramos utilizar 5 cortes, dividiríamos la población en quintiles, representando cada uno un 20%.↩︎