Los Sistemas Recomendadores son una colección de algoritmos utilizados para sugerir temas a los usuarios basados en información tomada desde el punto de vista del usuario. Estos sistemas ubicuos pueden ser comúnmente vistos en tiendas online, bases de datos de películas y buscadores de empleos. Vamos a ver los sistemas de recomendación basados en Contenido e implementaremos una versión de filtrado colaborativo en Python y la librería Pandas.
Primero, importemos las librerías necesarias:
#librería de manipulación de dataframes
import pandas as pd
#Funciones matemáticas, necesitaremos sólo importar la función sqrt
from math import sqrt
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
Adquisición de Datos
Para adquirir y extraer los datos, simplemente ejecuta los siguientes scripts Bash:
Ahora carguemos cada archivo dentro de su dataframe:
#Guardando la información de la película dentro de un dataframe de panda
movies_df = pd.read_csv('movies.csv')
#Guardando información del usuario dentro de un dataframe de panda
ratings_df = pd.read_csv('ratings.csv')
Miremos cada uno de ellos a ver cómo están organizados:
#Head es una función que obtiene las primeras N filas de un dataframe. El valor por omisión de N es 5.
movies_df.head()
movieId
title
genres
0
1
Toy Story (1995)
Adventure|Animation|Children|Comedy|Fantasy
1
2
Jumanji (1995)
Adventure|Children|Fantasy
2
3
Grumpier Old Men (1995)
Comedy|Romance
3
4
Waiting to Exhale (1995)
Comedy|Drama|Romance
4
5
Father of the Bride Part II (1995)
Comedy
Cada película tiene un único ID, un título con su año de estreno (El cual puede contener caracteres unicode) y muchos géneros diferentes en el mismo campo. Saquemos el año de la columna del título y ubiquemoslo en su propia columna con la función extract que viene con Pandas.
Saquemos el año de la columna title utilizando la función de pandas replace y guardémoslo en la nueva columna year.
#Utilizar expresiones regulares para encontrar un año guardado entre paréntesis
#Especificamos los paréntesis de forma tal de que no haya problemas con las películas que tiene el año en sus títulos
movies_df['year'] = movies_df.title.str.extract('()',expand=False)
#Sacando los paréntesis
movies_df['year'] = movies_df.year.str.extract('(\d\d\d\d)',expand=False)
#Sacando los años de la columna 'title'
movies_df['title'] = movies_df.title.str.replace('()', '')
#Aplicando la función strip para sacar los espacios finales que pudiera haber
movies_df['title'] = movies_df['title'].apply(lambda x: x.strip())
Veamos el resultado
movies_df.head()
movieId
title
genres
year
0
1
Toy Story
Adventure|Animation|Children|Comedy|Fantasy
1995
1
2
Jumanji
Adventure|Children|Fantasy
1995
2
3
Grumpier Old Men
Comedy|Romance
1995
3
4
Waiting to Exhale
Comedy|Drama|Romance
1995
4
5
Father of the Bride Part II
Comedy
1995
Luego, saquemos la columna de los géneros ya que no los necesitaremos para este sistema recomendador.
#Eliminando la columna géneros
movies_df = movies_df.drop('genres', 1)
Aquí está el dataframe final:
movies_df.head()
movieId
title
year
0
1
Toy Story
1995
1
2
Jumanji
1995
2
3
Grumpier Old Men
1995
3
4
Waiting to Exhale
1995
4
5
Father of the Bride Part II
1995
Ahora, veamos el dataframe de los ratings.
ratings_df.head()
userId
movieId
rating
timestamp
0
1
169
2.5
1204927694
1
1
2471
3.0
1204927438
2
1
48516
5.0
1204927435
3
2
2571
3.5
1436165433
4
2
109487
4.0
1436165496
Cada fila en el dataframe ratings tiene un id de usuario asociado con al menos una película, un rating y una marca de tiempo que muestra cuando se revisó. No se necesitará la columna timestamp, por lo que se eliminará para ahorrar memoria.
#Drop elimina una fila en particular o columna dentro de un dataframe
ratings_df = ratings_df.drop('timestamp', 1)
Asi es cómo el Dataframe definitivo se ve:
ratings_df.head()
userId
movieId
rating
0
1
169
2.5
1
1
2471
3.0
2
1
48516
5.0
3
2
2571
3.5
4
2
109487
4.0
Filtrado Colaborativo en python
Ahora, comenzaremos el trabajo con los sistemas recomendadores.
La primer técnica que vas a ver se llama Filtrado Colaborativo, también conocido como Filtrado de Usuario a Usuario. Como lo indica su nombre alternativo, esta técnica utiliza otros usuarios para recomendar items al ingreso de datos. Se intenta encontrar usuarios que tengan preferencias y opiniones parecidas para entonces recomendar items que se hayan parecido al ingreso anterior. Existen varios métodos para encontrar usuarios parecidos (Incluso los que utilizan Machine Learning), y el método que vamos a utilizar estará basado en la Función de Correlación de Pearson.
El proceso para crear un sistema de recomendación Basado en el Usuario es el siguiente:
Elegir un usuario con las películas que el usuario ha mirado
Basado en su indice de selección de películas, encuentra a los primeros X vecinos
Obtener el registro de la película que miró el usuario para cada vecino.
Calcular un puntaje de similitud utilizando alguna fórmula
Recomendar los ítems con los puntajes más altos
Comencemos creando un usuario a quien recomendar películas:
Nota: Para agregar más películas, aumenta la catidad de elementos en userInput. Agrega tantos como desees! Solo asegúrate de escribir en letras mayúsculas y si una película comienza con un «The», como «The Matrix» entonces escríbelo así: ‘Matrix, The’.
Agregar movieId al ingreso del usuario: Con las datos ingresados completos, extraigamos los ID de las películas del dataframe de películas y agreguémosla.
Esto se logra primero sacando las filas que tienen que tienen títulos de películas y luego une este subconjunto con el dataframe de entrada. También sacamos columnas que no se necesitan para ahorrar espacio de memoria.
#Filtrar las películas por título
inputId = movies_df[movies_df['title'].isin(inputMovies['title'].tolist())]
#Luego juntarlas para obtener el movieId. Implícitamente, lo está uniendo por título.
inputMovies = pd.merge(inputId, inputMovies)
#Eliminando información que no utilizaremos del dataframe de entrada
inputMovies = inputMovies.drop('year', 1)
#Dataframe de entrada final
#Si una película que se agregó no se encuentra, entonces podría no estar en el dataframe
#original o podría estar escrito de otra forma, por favor revisar mayúscula o minúscula.
inputMovies
movieId
title
rating
0
1
Toy Story
3.5
1
2
Jumanji
2.0
2
296
Pulp Fiction
5.0
3
1274
Akira
4.5
4
1968
Breakfast Club, The
5.0
Usuarios que han visto las mismas películas: Ahora, que el ID de la película está como entrada, podemos obtener el subconjunto de usuarios que han visto o revisado las películas en nuestra entrada.
#Filtrando los usuarios que han visto las películas y guardándolas
userSubset = ratings_df[ratings_df['movieId'].isin(inputMovies['movieId'].tolist())]
userSubset.head()
userId
movieId
rating
19
4
296
4.0
441
12
1968
3.0
479
13
2
2.0
531
13
1274
5.0
681
14
296
2.0
Ahora agrupamos las filas por el ID del usuario.
#Groupby crea varios dataframes donde todos tienen el mismo valor para la columna especificada como parámetro
userSubsetGroup = userSubset.groupby(['userId'])
Miremos a uno de los usuarios, por ejemlo, el userID=1130
userSubsetGroup.get_group(1130)
userId
movieId
rating
104167
1130
1
0.5
104168
1130
2
4.0
104214
1130
296
4.0
104363
1130
1274
4.5
104443
1130
1968
4.5
Ahora hagamos ordenamiento dentro de cada grupo de forma tal que los usuarios que compartan la mayor cantidad de películas tengan prioridad. Esto brinda una recomendación mejorada ya que no será necesario pasar por todos los usuarios.
#Ordenamiento de forma tal de que los usuarios con más películas en común tengan prioridad
userSubsetGroup = sorted(userSubsetGroup, key=lambda x: len(x[1]), reverse=True)
Luego, compararemos a todos los usuarios (casi todos) con nuestro usuario indicado y encontraremos el que más se parece.
Encontraremos cómo cada usuario similar se relacionan entre si a través del Coeficiente de Correlación de Pearson. Se utiliza para medir la fuerza de una asociación lineal entre dos variables. La fórmula para encontrar este coeficiente entre los conjuntos X e Y con los valores de N se puede ver en la fórmula:
¿Por qué la Correlación Pearson?
La correlación Pearson no varía con la escala, ejemplo: si se multiplican todos los elementos por una constante distinta a cero o si se agrega cualquier constante a todos los elementos. Por ejemplo, si tienes dos vectores e , entonces, . Esta es una propiedad muy importante en los sistemas recomendadores por que dos usuarios podrían putnuar dos series de elementos de manera completamente diferente, pero serían usuarios parecidos (ejemplo: con ideas similares) con puntuaciones parecidas en escalas variadas .
Los valores brindados por la fórumula puede variar de a , donde 1 se correlaciona directamente entre las dos entidades (esto sería una correlación positiva perfecta) y -1 forma una correlación negativa perfecta. En nuestro caso, un 1 se refiere a que dos usuarios tiene gustos parecidos, mientras que -1 es lo opuesto.
Elegiremos un subconjunto de usuarios para hacer las iteraciones. Este limite existe porque no queremos desperdiciar mucho tiempo pasando por cada usuario.
userSubsetGroup = userSubsetGroup[0:100]
Ahora, calculemos la Correlación Pearson entre la entrada del usuario el grupo, para almacenarlo en el diccionario, donde la clave es el Id del usuario y el valor es el coeficiente
#Guardar la Correlación Pearson en un diccionario, donde la clave es el Id del usuario y el valor es el coeficiente
pearsonCorrelationDict = {}
#Para cada grupo de usuarios en nuestro subconjunto
for name, group in userSubsetGroup:
#Comencemos ordenando el usuario actual y el ingresado de forma tal que los valores no se mezclen luego
group = group.sort_values(by='movieId')
inputMovies = inputMovies.sort_values(by='movieId')
#Obtener el N para la fórmula
nRatings = len(group)
#Obtener los puntajes de revisión para las películas en común
temp_df = inputMovies[inputMovies['movieId'].isin(group['movieId'].tolist())]
#Guardarlas en una variable temporal con formato de lista para facilitar cálculos futuros
tempRatingList = temp_df['rating'].tolist()
#Pongamos también las revisiones de grupos de usuarios en una lista
tempGroupList = group['rating'].tolist()
#Calculemos la Correlación Pearson entre dos usuarios, x e y
Sxx = sum([i**2 for i in tempRatingList]) - pow(sum(tempRatingList),2)/float(nRatings)
Syy = sum([i**2 for i in tempGroupList]) - pow(sum(tempGroupList),2)/float(nRatings)
Sxy = sum( i*j for i, j in zip(tempRatingList, tempGroupList)) - sum(tempRatingList)*sum(tempGroupList)/float(nRatings)
#Si el denominador es diferente a cero, entonces dividir, sino, la correlación es 0.
if Sxx != 0 and Syy != 0:
pearsonCorrelationDict[name] = Sxy/sqrt(Sxx*Syy)
else:
pearsonCorrelationDict[name] = 0
Recomendemos películas al usuario de entrada puntuando a los usuarios elegidos para todas las películas
Haremos esto tomando el peso promedio de los ratings de las películas utilizando la Correlación Pearson. Pero para hacer esto, primero necesitamos que los usuarios vean las películas en nuestro pearsonDF a partir del dataframe de puntajes y luego guardar su correlación en una nueva columna llamada _similarityIndex». Estos se logra juntando estas dos tablas de debajo.
Ahora todo lo que se necesita hacer es multiplicar el puntaje de la película por su peso (El índice de similitud), luego se suman los nuevos puntajes y dividen por la suma de los pesos.
Esto se logra sencillamente multiplicando dos columnas, luego agrupando el dataframe por la columna movieId y luego dividiendo dos columnas:
Aqui se muestra la idea de todos los usuarios similares respecto de las películas candidatas para el usuario ingresado:
#Se multiplica la similitud de los puntajes de los usuarios
topUsersRating['weightedRating'] = topUsersRating['similarityIndex']*topUsersRating['rating']
topUsersRating.head()
similarityIndex
userId
movieId
rating
weightedRating
0
0.961678
12325
1
3.5
3.365874
1
0.961678
12325
2
1.5
1.442517
2
0.961678
12325
3
3.0
2.885035
3
0.961678
12325
5
0.5
0.480839
4
0.961678
12325
6
2.5
2.404196
#Se aplica una suma a los topUsers luego de agruparlos por userId
tempTopUsersRating = topUsersRating.groupby('movieId').sum()[['similarityIndex','weightedRating']]
tempTopUsersRating.columns = ['sum_similarityIndex','sum_weightedRating']
tempTopUsersRating.head()
sum_similarityIndex
sum_weightedRating
movieId
1
38.376281
140.800834
2
38.376281
96.656745
3
10.253981
27.254477
4
0.929294
2.787882
5
11.723262
27.151751
#Se crea un dataframe vacío
recommendation_df = pd.DataFrame()
#Ahora se toma el promedio ponderado
recommendation_df['weighted average recommendation score'] = tempTopUsersRating['sum_weightedRating']/tempTopUsersRating['sum_similarityIndex']
recommendation_df['movieId'] = tempTopUsersRating.index
recommendation_df.head()
weighted average recommendation score
movieId
movieId
1
3.668955
1
2
2.518658
2
3
2.657941
3
4
3.000000
4
5
2.316058
5
Luego, ordenémoslo y veamos las primeras 20 películas que el algoritmo recomendó!
recommendation_df = recommendation_df.sort_values(by='weighted average recommendation score', ascending=False)
recommendation_df.head(10)