Analisis RFM para la segmentacion de clientes

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!!.

Hernan Hernandez https://example.com/norajones
2022-08-27

Introducción 🚀

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:

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:

Exploración de los datos 🧐

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.

Show code
summarytools::st_options(lang = 'es')
summarytools::dfSummary(df, plain.ascii  = FALSE, 
                                           style        = "grid", 
                                           graph.magnif = 0.75, 
                                           valid.col    = FALSE,
                                           tmp.img.dir  = "/tmp")

Tabla resumen

df

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.

Show code
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.

Implementamos el modelo 💡

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.

Normalización de la tabla ✂️

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.

Show code
df <- normalize_table(df = df,
                date = "Date",
                id_costumer = "CustomerID",
                cantidad = "Quantity",
                monto = "Monetary")

DT::datatable(df,filter= 'top',options = list(pageLength = 5, dom = 'tip'))

Creamos los puntajes RFM 🛍️

La función rfm_category, admite 5 parámetros:

Show code
tabla_rfm <- df %>%
  mutate(email= "cliente@rfm.com")%>%
  split(.$Country) %>%
  map(~ rfm_category(df = .,
                     fecha_analisis = '2011-12-09',
                     bins = 4,
                     group_by = "Country",
                     inherits.threshold = NULL))
Show code
xaringanExtra::use_panelset()
Show code
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")
}

France

Table 1: Puntos de corte scoring. France.
Recency
Frequency
Monetary
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

Germany

Table 1: Puntos de corte scoring. Germany.
Recency
Frequency
Monetary
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

United Kingdom

Table 1: Puntos de corte scoring. United Kingdom.
Recency
Frequency
Monetary
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.

Show code
DT::datatable(tabla_rfm[[3]]$resultado_rfm %>% head(10),filter= 'top',options = list(pageLength = 5, dom = 'tip'))

¿Cómo se asignan los puntajes? ¿Que representan esos valores? 🤔

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.

Creamos los segmentos ✏️

Para la creación de los segmentos utilizaremos la función segment_rfm, a la cuál debemos pasarle 3 grupos de parámetros:

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.

Comenzamos por definir los segmentos y los umbrales 📝

Como se mencionó anteriormente trabajaremos con los siguientes segmentos:

Asignamos los umbrales

Show code
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
  )
}
Show code
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")

}

France

Distribución de los clientes según el scoring (RFM

Composición de los segmentos

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

Gráfico composición de los segmentos

Impacto de los segmentos en las Ventas y el Monto

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

Germany

Distribución de los clientes según el scoring (RFM

Composición de los segmentos

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

Gráfico composición de los segmentos

Impacto de los segmentos en las Ventas y el Monto

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

United Kingdom

Distribución de los clientes según el scoring (RFM

Composición de los segmentos

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

Gráfico composición de los segmentos

Impacto de los segmentos en las Ventas y el Monto

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

¿Que podemos decir de los segmentos? 🔈

👉🏼 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.

Comentarios finales 😉

🔦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✈️.


  1. 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%.↩︎