Cómo vectorizar el cálculo de la Recencia
Calcular la Recencia usando dplyr
Tenemos un practicante en mi pega que está trabajando en su Tesis. El tema es que dentro de un modelo que está haciendo necesita calcular la Recencia de un cliente, es decir, el número de meses que han tranascurrido desde su última compra. Me pareció un desafío bastante interesante, porque calcularlo de manera individual para un cliente no es dificil, pero ser capaz de vectorizarlo me pareció desafiante, en especial porque está trabajando en R.
El problema
Este es el problema, con la solución esperada (Recency
):
(data <- tibble::tribble(
~Client_ID, ~Date_ID, ~Purchase_Amount, ~Recency,
1, 1, 2344, 0,
1, 2, 0, 1,
1, 3, 0, 2,
1, 4, 5676, 0,
1, 5, 4587, 0,
1, 6, 0, 1,
1, 7, 0, 2,
1, 8, 0, 3,
2, 1, 2500, 0,
2, 2, 2634, 0,
2, 3, 0, 1,
2, 4, 0, 2,
2, 5, 0, 3,
2, 6, 4578, 0,
2, 7, 4562, 0,
2, 8, 0, 1
)
)
Como se puede ver, cada vez que tengo una compra el contador de Recencia necesita reiniciarse en cero y comenzar a contar hasta la última compra de cada cliente. Adicionalmente el contador tiene que reiniciarse también cuando hay un cambio de Cliente, según Client_ID
La solución
En este caso particular voy a detallar los distintos pasos de la solución porque puede ser medio complicado de seguir.
Primero, creo una variable auxiliar llamada has_purchased
y date_group
. Estas variables se tienen que crear al nivel de cliente. cumsum()
irá creando grupos que permitirán reiniciar el cálculo de la recencia.
data %>%
filter(Client_ID == 1) %>%
mutate(has_purchased = as.numeric(Purchase_Amount > 0),
date_group = cumsum(has_purchased))
Luego no es nada más que calcular el número de fila y restarle uno, el tema es que esta operación se debe realizar de manera agrupada date_group
.
data %>%
filter(Client_ID == 1) %>%
mutate(
has_purchased = as.numeric(Purchase_Amount > 0),
date_group = cumsum(has_purchased)
) %>%
group_by(date_group) %>%
mutate(calculated_recency = row_number() - 1)
Ahora para hacer esta operación más eficiente computacionalmente y poder generalizar la solución para todos los clientes, hay que generar esto para cada Client_ID
. Para eso se utilizará group_modify()
. Esta función es muy similar a los map_
de purrr, pero para datos agrupados.
data %>%
#grouped by client
group_by(Client_ID) %>%
group_modify(
#This is the same pipeline showed before but applied to element .x that represents each group
~ .x %>%
mutate(
has_purchased = as.numeric(Purchase_Amount > 0),
date_group = cumsum(has_purchased)
) %>%
group_by(date_group) %>%
mutate(calculated_recency = row_number() - 1)
) %>%
select(-has_purchased, -date_group)
Se puede ver que calculated_recency
es igual al resultado esperado al principio del artículo, y puede ser fácilmente logrado por medio de group_by
. Creo que la gracia de esta solución es que permite generar una solución vectorizada evitando una solución que normalmente requeriría de 2 for
loops.