Sistema de recomendación

7 minute read


Autumn on the Seine at Argenteuil

Claude Monet, Autumn on the Seine at Argenteuil (1873)

El código completo lo encontraras aquí


En el anterior post, hablábamos de un algoritmo que se encuentra en la categoría de algoritmos supervisados, ya que cada observación tiene una etiqueta que indica la clase correspondiente, ahora toca el turno de un ejemplo de algoritmo no supervisado. Crearemos un sistema de recomendaciones para visitar nuevos negocios.

Después de completar este tutorial, sabrás:

  • Que es el coeficiente jaccard
  • Crear una matriz one hot
  • Como crear un sistema de recomendaciones

Nos apoyaremos del dataset el cual incluye los ids de personas y negocios, y las visitas que hicieron las personas a dichos negocios tomado de foursquare

#cargamos los paquetes necesarios
library(tidyverse) #Manipulación
library(knitr)     #Presentación de tablas

Cargamos la base de datos

data <- read_csv("https://raw.githubusercontent.com/alonmar/sistema-de-recomendacion/master/data/checkins.csv")
#Nombramos las variables
names(data) <- c("user_id","negocio_id")
head(data,10) %>% kable()
user_id negocio_id
35443 899
24973 42406
14860 177
222505 177
63524 609
51957 435580
7860 68829
8952 12790
42283 15071
14506 25864

Veamos cuantas veces visito cada persona a cada negocio

data %>% group_by(user_id,negocio_id) %>%
  summarise(n = n()) %>% arrange(desc(n)) %>%
  head(15) %>% kable()
user_id negocio_id n
51957 435580 149
51445 64971 92
17238 9209 86
8952 39242 74
133970 41096 73
39836 15743 72
38456 7999 69
59959 5222 63
17238 82634 55
49029 139117 55
54405 41096 55
56139 86250 54
18345 42705 53
2033 14327 51
91070 35085 51

Como vimos si una persona visita mas de una vez un mismo lugar esa observación aparecerá dos veces. Veamos cuantas personas y negocios únicos tenemos

## Usuarios unicos:

## 2060

## Negocios unicos:

## 2876

Para este tutorial elimináramos las observaciones que solo tienen una visita, ya que nos puede indicar que no se tomo la molestia de regresar a ese negocio, en cambio nos quedaremos con aquellos que regresaron una o mas veces.

filter_data <- data %>% group_by(user_id,negocio_id) %>%
  summarise(n = n()) %>% arrange(desc(n)) %>% filter(n > 1)

filter_data %>% head(5) %>%  kable()
user_id negocio_id n
51957 435580 149
51445 64971 92
17238 9209 86
8952 39242 74
133970 41096 73
filter_data %>% tail(5) %>%  kable()
user_id negocio_id n
266532 4310 2
266532 10604 2
266532 22768 2
266712 4889 2
266712 9953 2

One Hot Coding

Ahora lo que haremos es lo que se conoce como one hot coding. A continuación explicare brevemente como crear una matriz en formato one hot coding. Básicamente se trata de crear una matriz donde si un valor corresponde tendremos un 1 y de lo contrario tendremos un 0

Veamos esta tabla:

Author Nacionalidad
Juan Rulfo MEX
Gabriel García Márquez COL
Octavio Paz MEX

En la anterior tabla se observa como que tanto Juan Rulfo como Octavio Paz son mexicanos, por lo tanto al transformar en la columna Mex le corresponde 1 y en la columna Col un 0 Ahora transformada en one-hot se vería así

Author COL MEX
Gabriel García Márquez 1 0
Juan Rulfo 0 1
Octavio Paz 0 1

(Si quieres saber mas sobre one-hoy te recomiendo este link ) Sin embargo para nuestro ejemplo colocaremos NAs en lugar de ceros, ya que nos apoyaremos de la función drop_na().

De tal manera que esta sería nuestra nueva matriz, donde las filas corresponden a cada ID de negocio, y las columnas a cada ID de usuario.

data_matrix <- filter_data %>% select(-n) %>%
  mutate(count= 1) %>%
  spread(user_id,count)

data_matrix[1:10,1:10] %>% kable()
negocio_id 6 15 72 102 104 105 132 133 134
15 NA NA NA NA NA NA NA NA NA
20 NA NA NA NA NA NA NA NA NA
25 NA NA NA NA NA NA NA NA NA
36 NA NA NA NA NA NA NA NA NA
39 NA NA NA NA NA NA NA NA NA
40 NA NA NA NA NA NA NA NA NA
42 NA NA NA NA NA NA NA NA NA
46 NA NA NA NA NA NA NA NA NA
47 NA NA NA NA NA NA NA NA NA
52 NA NA NA NA NA NA NA NA NA

Solo vemos NAs, ya que para esos negocios y esos usuario no existe interacción, sin embargo veamos un caso concreto

Seleccionemos un id de usuario y eliminaremos los NA

Índice de Jaccard

Haremos uso del coeficiente de Jaccard, siendo el cociente de la intersección de dos conjuntos entre la unión de dichos conjuntos.

En otras palabras dividiremos el numero de elementos que se encuentran en ambos conjuntos entre el numero de elementos únicos de ambos conjuntos.

Ejemplo: conjunto_A = {a,b,c,c,d} conjunto_B = {b,d,e}

AB = (b,d) es decir solo tenemos 2 elementos que se encuentran en A y B AB = (a,b,c,d,e) lo cual significa existen 5 elementos únicos en ambos conjuntos

De tal manera que el índice de jaccard nos indica que el conjunto A y B son similares en 0.4 en una escala de 0 a 1

Regresando a nuestros datos seleccionaremos dos usuarios para determinar que tan similares son

v1 <- data_matrix %>% select(negocio_id,'14860')  %>% drop_na()
v1 %>% kable()
negocio_id 14860
177 1
578 1
837 1
7542 1
11872 1
13630 1
14790 1
16126 1
19954 1
24001 1
76187 1

seleccionaremos otro id de usuario para ver que tan similares son con el anterior

v2 <- data_matrix %>% select(negocio_id,'222505')  %>% drop_na()
v2 %>% kable()
negocio_id 222505
177 1
5867 1
intersect = length(intersect(v1$negocio_id,v2$negocio_id))
union = length(unique(c(v1$negocio_id,v2$negocio_id)))

jaccard = intersect/union
jaccard
## [1] 0.0833

Lo cual significa que ambos usuarios son 0.0833 similares.

A continuación crearemos una función que nos ayudara a calcular el índice de Jaccard

jaccard <- function(index,v2,v1) {
  v2 <- data.frame(index,v2)
  v2 <- v2 %>% drop_na()
  intersect = length(intersect(v1$negocio_id,v2$index))
  union = length(unique(c(v1$negocio_id,v2$index)))
  intersect/union
}

Ahora aplicaremos esta función a toda la matriz antes creada para conocer que similitud mantiene con cada uno de los otros usuarios

#creamos el vector que usaremos para buscar el coeficiente de jaccard
v1 <- data_matrix %>% select(negocio_id,'14860') %>% drop_na()
#guardamos los indices de los negicios
index <- data_matrix$negocio_id
# aplicamos el coeficiente de jaccard para todas las columnas
result <- sapply(data_matrix %>% select(-1,-'14860'),
                 function(x) jaccard(index,x,v1))


# Filtramos los resultados para solo quedarnos con aquellos que tienen un
# coeficiente mayor a cero
result <- result[result > 0]
# guardamos los ids de los usuarios
usuario <- result %>% names()

#observamos solo el top 10
data.frame(usuario = usuario,similitud = c(result)) %>%
   arrange(desc(similitud)) %>% head(10)
##    usuario similitud
## 1   181505    0.1176
## 2     1174    0.0909
## 3    27588    0.0909
## 4    28733    0.0909
## 5    51971    0.0909
## 6    69153    0.0909
## 7     1581    0.0833
## 8     4403    0.0833
## 9     9409    0.0833
## 10   30939    0.0833

Perfecto!

Como vemos el usuarios con el ID 14860 mantiene una mayor similitud con el usuarios 181505, a continuación recomendaremos lugares que ya visito el segundo usuarios y que el primero aun no conoce

v1 <- data_matrix %>% select(negocio_id,'14860') %>% drop_na()
v2 <- data_matrix %>% select(negocio_id,'181505') %>% drop_na()

v2$negocio_id [!v2$negocio_id %in% v1$negocio_id]
## [1] 11193 13307 15176 15647 17675 27315

Ahora metamos todo esto en una sola función donde seleccionemos el ID del usuario que nos interesa recomendarle establecimientos en función del usuarios que mas se le parezca

recomendacion <- function(id,data) {
id <- as.character(id)
#creamos el vector que usaremos para buscar el coeficiente de jaccard
v1 <- data %>% select(negocio_id,id) %>% drop_na()
#guardamos los índices de los negocios
index <- data$negocio_id
# aplicamos el coeficiente de jaccard para todas las columnas excepto la primera
# que es donde se encuentran los índices y la columna del usuario
result <- sapply(data %>% select(-1,-id),
                 function(x) jaccard(index,x,v1))


# Filtramos los resultados para solo quedarnos con aquellos que tienen un
# coeficiente mayor a cero
result <- result[result > 0]
# guardamos los ids de los usuarios
usuario <- result %>% names()

id_recomender <- data.frame(usuario = usuario,similitud = c(result)) %>%
   arrange(desc(similitud))

top_1 <- as.character(id_recomender$usuario[1])
v2 <- data %>% select(negocio_id, top_1 ) %>% drop_na()

cat("El usuario que guarda mayor similitud con",id ,"es:" ,top_1, '\n',
    "con una indice de",id_recomender$similitud[1],"\n\n")
cat("Quizás le interesen estos lugares: \n\n")
data.frame(negocio_id = (v2$negocio_id [!v2$negocio_id %in% v1$negocio_id])) %>%
  kable()

}

¿Que le podemos recomendar al usuario 190?

recomendacion("190",data_matrix)
## El usuario que guarda mayor similitud con 190 es: 258663
##  con una indice de 0.333
##
## Quizás le interesen estos lugares:
negocio_id
102
1101

Conclusiones

Este es nuestro primer ejemplo de algoritmo No supervisado y nuestro primer sistema de recomendación, es un ejemplo excelente para comprender ambos conceptos y como vimos en la medida que obtengamos mas data acerca de los intereses de los usuarios las recomendaciones serán mejor dadas. Te invito a que elijas un usuario y veas que recomendaciones existen para él, y también te invito a que en lugar de lugares visitados mas de una vez sean mas de 5 veces ¿Qué implicaciones tendrá? y ¿mas de 10 veces?.

Fuentes

https://tidyr.tidyverse.org/reference/spread.html https://hackernoon.com/what-is-one-hot-encoding-why-and-when-do-you-have-to-use-it-e3c6186d008f https://platzi.com/clases/data/

Updated: