Los sistemas recomendadores son colecciones de algoritmos utilizados para sugerir elementos a los usuarios basados en información del usuario. Estos sistemas suelen verse en tiendas online, base datos de películas y buscadores de trabajos. Vamos a explorar los sistemas Content-based de recomendadores basados en contenidos e implementar una simple versión de uno utilizando Python con las librerías Pandas.
Primero, importemos las librerías necesarias:
#Librería de manipulación del marco de datos
import pandas as pd
#Funciones matemáticas, necesitaremos la función sqrt para importar sólo lo necesario
from math import sqrt
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
Obteniendo los Datos
Para obtener y extraer los datos, solo corre los siguientes scripts Bash:
Descargamos el set de datos utilizando !wget.
Ahora carguemos cada archivo en su propio marco de datos:
#Guardando la información de las películas dentro del marco de datos pandas
movies_df = pd.read_csv('movies.csv')
#Guardando la información del usuario dentro del marco de datos pandas
ratings_df = pd.read_csv('ratings.csv')
#Head es una función que obtiene los primeros N registros de un marco de datos. El valor por omision 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
Ahora saquemos el año de la columna title utilizando la función replace y la guardamos en una nueva columna llamada year.
#Utilizando expresiones regulares para encontrar un año guardado entre paréntesis
#Especificamos los paréntesis para no tener conflicto con las películas que tienen años como parte de su título
movies_df['year'] = movies_df.title.str.extract('()',expand=False)
#Eliminando los paréntesis
movies_df['year'] = movies_df.year.str.extract('(\d\d\d\d)',expand=False)
#Eliminando los años de la columna 'title'
movies_df['title'] = movies_df.title.str.replace('()', '')
#Aplicando la función strip para eliminar los caracteres blancos finales
movies_df['title'] = movies_df['title'].apply(lambda x: x.strip())
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
Con ello, separemos los valores de la columna Genres y pongámoslo todos en list of Genres para simplificar una utilización que haremos después. Esto también se puede lograr la función split string de Python dentro de la columna que corresponde.
#Cada género está separado por un | para simplificar la llamada que se haga solo a |
movies_df['genres'] = movies_df.genres.str.split('|')
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
Ya que poner los géneros en una lista no es lo más óptimo para la técnica de sistemas recomendadores basados en contenidos, usaremos la técnica One Hot Encoding para convertir la lista de géneros en un vector donde cada colunna corresponde a un valor de la característica mencionada. Esta codificación se necesita para alimentar datos categóricos. En este caso, guardamos cada género distinto en colunnas que tienen 1 ó 0. 1 quiere decir que una película tiene género y 0 que no lo tiene. Guardames también un marco de datos en otra variable ya que los géneros no serán importante en esta primera instancia de sistemas recomendadores.
#Copiando el marco de datos de la pelicula en uno nuevo ya que no necesitamos la información del género por ahora.
moviesWithGenres_df = movies_df.copy()
#Para cada fila del marco de datos, iterar la lista de géneros y colocar un 1 en la columna que corresponda
for index, row in movies_df.iterrows():
for genre in row['genres']:
moviesWithGenres_df.at[index, genre] = 1
#Completar los valores NaN con 0 para mostrar que una película no tiene el género de la columna
moviesWithGenres_df = moviesWithGenres_df.fillna(0)
moviesWithGenres_df.head()
movieId
title
genres
year
Adventure
Animation
Children
Comedy
Fantasy
Romance
…
Horror
Mystery
Sci-Fi
IMAX
Documentary
War
Musical
Western
Film-Noir
(no genres listed)
0
1
Toy Story
[Adventure, Animation, Children, Comedy, Fantasy]
1995
1.0
1.0
1.0
1.0
1.0
0.0
…
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
1
2
Jumanji
[Adventure, Children, Fantasy]
1995
1.0
0.0
1.0
0.0
1.0
0.0
…
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
2
3
Grumpier Old Men
[Comedy, Romance]
1995
0.0
0.0
0.0
1.0
0.0
1.0
…
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
3
4
Waiting to Exhale
[Comedy, Drama, Romance]
1995
0.0
0.0
0.0
1.0
0.0
1.0
…
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
4
5
Father of the Bride Part II
[Comedy]
1995
0.0
0.0
0.0
1.0
0.0
0.0
…
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
A continuación, miremos los ratings de los marcos de datos.
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 dentro de los ratings tienen un user id asociado con al menos una película, un rating y una marca de tiempo que muestra cuándo se revisó la película. No se necesitará la columna de la marca de tiempo, por lo que la podemos eliminar para ahorrar espacio de memoria.
#La sentencia Drop elimina la fila o columna señalada del marco de datos
ratings_df = ratings_df.drop('timestamp', 1)
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
Sistema de recomendación Basado en Contenido
Ahora miremos cómo implementar Content-Based o Item-Item recommendation systems. Esta técnica intenta descubrir los aspectos favoritos del usuario para algún elemento, y luego recomienda elementos que presentan esos aspectos. En nuestro caso, vamos a intentar descubrir los géneros favoritos a partir de las películas y los ratings.
Comencemos por crear una entrada para el usuario para que recomiende películas
Nota: Para agregar más películas, aumenta la cantidad 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’ .
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('genres', 1).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
Ahora vamos a comenzar aprendiendo las preferencias del ingreso de datos, por lo que obtendremos el subconjunto de películas que se vieron a partir del marco de datos que contienen los géneros definidos con valores binarios.
#Descartando las películas de la entrada de datos
userMovies = moviesWithGenres_df[moviesWithGenres_df['movieId'].isin(inputMovies['movieId'].tolist())]
userMovies
movieId
title
genres
year
Adventure
Animation
Children
Comedy
Fantasy
Romance
…
Horror
Mystery
Sci-Fi
IMAX
Documentary
War
Musical
Western
Film-Noir
(no genres listed)
0
1
Toy Story
[Adventure, Animation, Children, Comedy, Fantasy]
1995
1.0
1.0
1.0
1.0
1.0
0.0
…
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
1
2
Jumanji
[Adventure, Children, Fantasy]
1995
1.0
0.0
1.0
0.0
1.0
0.0
…
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
293
296
Pulp Fiction
[Comedy, Crime, Drama, Thriller]
1994
0.0
0.0
0.0
1.0
0.0
0.0
…
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
1246
1274
Akira
[Action, Adventure, Animation, Sci-Fi]
1988
1.0
1.0
0.0
0.0
0.0
0.0
…
0.0
0.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
1885
1968
Breakfast Club, The
[Comedy, Drama]
1985
0.0
0.0
0.0
1.0
0.0
0.0
…
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
Necesitaremos solamente la tabla actual de géneros, por lo que ordenaremos un poco inicializando el índice y eliminando las columnas movieId, title, genres e year.
#Inicializando el índice para evitar problemas a futuro
userMovies = userMovies.reset_index(drop=True)
#Eliminando problemas innecesarios para ahorrar memoria y evitar conflictos
userGenreTable = userMovies.drop('movieId', 1).drop('title', 1).drop('genres', 1).drop('year', 1)
userGenreTable
Adventure
Animation
Children
Comedy
Fantasy
Romance
Drama
Action
Crime
Thriller
Horror
Mystery
Sci-Fi
IMAX
Documentary
War
Musical
Western
Film-Noir
(no genres listed)
0
1.0
1.0
1.0
1.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
1
1.0
0.0
1.0
0.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
2
0.0
0.0
0.0
1.0
0.0
0.0
1.0
0.0
1.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
3
1.0
1.0
0.0
0.0
0.0
0.0
0.0
1.0
0.0
0.0
0.0
0.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
4
0.0
0.0
0.0
1.0
0.0
0.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
Ahora si estamos listos para comenzar a aprender las preferencias recibidas!
Para lograrlo, ponderaremos cada género. Esto se puede lograr utilizando las revisiones y multiplicádolas dentro de la tabla de ingreso de género para luego juntar la tabla resultante por columna. Esta operación en realidad es un producto escalar entre una matriz y un vector. Esto se logra invocando la función de Panda llamada «dot».
#Producto escalar para obtener los pesos
userProfile = userGenreTable.transpose().dot(inputMovies['rating'])
#Perfil del usuario
userProfile
Adventure 10.0
Animation 8.0
Children 5.5
Comedy 13.5
Fantasy 5.5
Romance 0.0
Drama 10.0
Action 4.5
Crime 5.0
Thriller 5.0
Horror 0.0
Mystery 0.0
Sci-Fi 4.5
IMAX 0.0
Documentary 0.0
War 0.0
Musical 0.0
Western 0.0
Film-Noir 0.0
(no genres listed) 0.0
dtype: float64
Ahora, tenemos los pesos para cada preferencia del usuario. Esto se conoce como Perfil del Usuario. Utilizando esto, podemos sugerir películas que satisfagan las preferencias del usuario.
Comencemos extrayendo la tabla de géner del marco de datos original:
#Ahora llevemos los géneros de cada película al marco de datos original
genreTable = moviesWithGenres_df.set_index(moviesWithGenres_df['movieId'])
#Y eliminemos información innecesaria
genreTable = genreTable.drop('movieId', 1).drop('title', 1).drop('genres', 1).drop('year', 1)
genreTable.head()
Adventure
Animation
Children
Comedy
Fantasy
Romance
Drama
Action
Crime
Thriller
Horror
Mystery
Sci-Fi
IMAX
Documentary
War
Musical
Western
Film-Noir
(no genres listed)
movieId
1
1.0
1.0
1.0
1.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
2
1.0
0.0
1.0
0.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
3
0.0
0.0
0.0
1.0
0.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
4
0.0
0.0
0.0
1.0
0.0
1.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
5
0.0
0.0
0.0
1.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
0.0
genreTable.shape
(34208, 20)
Habiendo completado el perfil de entrada y la lista completa de películas con sus respectivos géneros, llevaremos el peso promedio de cada película basado en el perfil de ingreso para luego recomendar las primeras veinte películas que más se adecuan a tal perfil.
#Multiplicando los géneros por los pesos para luego calcular el peso promedio
recommendationTable_df = ((genreTable*userProfile).sum(axis=1))/(userProfile.sum())
recommendationTable_df.head()
#Ordena nuestra recomendación en orden descendente
recommendationTable_df = recommendationTable_df.sort_values(ascending=False)
#Miremos los valores
recommendationTable_df.head()