Filtro basado en contenido

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.

!wget -O moviedataset.zip https://s3-api.us-geo.objectstorage.softlayer.net/cf-courses-data/CognitiveClass/ML0101ENv3/labs/moviedataset.zip
print('descomprimiendo ...')
!unzip -o -j moviedataset.zip 
--2020-03-02 14:22:28--  https://s3-api.us-geo.objectstorage.softlayer.net/cf-courses-data/CognitiveClass/ML0101ENv3/labs/moviedataset.zip
Resolving s3-api.us-geo.objectstorage.softlayer.net (s3-api.us-geo.objectstorage.softlayer.net)... 67.228.254.196
Connecting to s3-api.us-geo.objectstorage.softlayer.net (s3-api.us-geo.objectstorage.softlayer.net)|67.228.254.196|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 160301210 (153M) [application/zip]
Saving to: ‘moviedataset.zip’

moviedataset.zip    100%[===================>] 152.88M  19.3MB/s    in 8.8s    

2020-03-02 14:22:37 (17.4 MB/s) - ‘moviedataset.zip’ saved [160301210/160301210]

descomprimiendo ...
Archive:  moviedataset.zip
  inflating: links.csv               
  inflating: movies.csv              
  inflating: ratings.csv

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('(\d\d\d\d)',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('(\d\d\d\d)', '')
#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’ .

userInput = [
            {'title':'Breakfast Club, The', 'rating':5},
            {'title':'Toy Story', 'rating':3.5},
            {'title':'Jumanji', 'rating':2},
            {'title':"Pulp Fiction", 'rating':5},
            {'title':'Akira', 'rating':4.5}
         ] 
inputMovies = pd.DataFrame(userInput)
inputMovies
title rating
0 Breakfast Club, The 5.0
1 Toy Story 3.5
2 Jumanji 2.0
3 Pulp Fiction 5.0
4 Akira 4.5

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('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”.

inputMovies['rating']
0    3.5
1    2.0
2    5.0
3    4.5
4    5.0
Name: rating, dtype: float64
#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()
movieId
1    0.594406
2    0.293706
3    0.188811
4    0.328671
5    0.188811
dtype: float64
#Ordena nuestra recomendación en orden descendente
recommendationTable_df = recommendationTable_df.sort_values(ascending=False)
#Miremos los valores
recommendationTable_df.head()
movieId
5018      0.748252
26093     0.734266
27344     0.720280
148775    0.685315
6902      0.678322
dtype: float64

Ahora vemos la tabla de recomendación!

#Tabla de recomendaciones final
movies_df.loc[movies_df['movieId'].isin(recommendationTable_df.head(20).keys())]
movieId title genres year
664 673 Space Jam [Adventure, Animation, Children, Comedy, Fanta… 1996
1824 1907 Mulan [Adventure, Animation, Children, Comedy, Drama… 1998
2902 2987 Who Framed Roger Rabbit? [Adventure, Animation, Children, Comedy, Crime… 1988
4923 5018 Motorama [Adventure, Comedy, Crime, Drama, Fantasy, Mys… 1991
6793 6902 Interstate 60 [Adventure, Comedy, Drama, Fantasy, Mystery, S… 2002
8605 26093 Wonderful World of the Brothers Grimm, The [Adventure, Animation, Children, Comedy, Drama… 1962
8783 26340 Twelve Tasks of Asterix, The (Les douze travau… [Action, Adventure, Animation, Children, Comed… 1976
9296 27344 Revolutionary Girl Utena: Adolescence of Utena… [Action, Adventure, Animation, Comedy, Drama, … 1999
9825 32031 Robots [Adventure, Animation, Children, Comedy, Fanta… 2005
11716 51632 Atlantis: Milo’s Return [Action, Adventure, Animation, Children, Comed… 2003
11751 51939 TMNT (Teenage Mutant Ninja Turtles) [Action, Adventure, Animation, Children, Comed… 2007
13250 64645 The Wrecking Crew [Action, Adventure, Comedy, Crime, Drama, Thri… 1968
16055 81132 Rubber [Action, Adventure, Comedy, Crime, Drama, Film… 2010
18312 91335 Gruffalo, The [Adventure, Animation, Children, Comedy, Drama] 2009
22778 108540 Ernest & Célestine (Ernest et Célestine) [Adventure, Animation, Children, Comedy, Drama… 2012
22881 108932 The Lego Movie [Action, Adventure, Animation, Children, Comed… 2014
25218 117646 Dragonheart 2: A New Beginning [Action, Adventure, Comedy, Drama, Fantasy, Th… 2000
26442 122787 The 39 Steps [Action, Adventure, Comedy, Crime, Drama, Thri… 1959
32854 146305 Princes and Princesses [Animation, Children, Comedy, Drama, Fantasy, … 2000
33509 148775 Wizards of Waverly Place: The Movie [Adventure, Children, Comedy, Drama, Fantasy, … 2009

Ventajas y Desventajas del Filtrado Basado en Contenido

  • Ventajas:
    • Se aprende de las preferencias del usuario
    • Es muy personalizado para el usuario
  • Desventajas:
    • No se tiene en cuenta lo que otros piensan del item, por lo que podemos encontrarnos con items de baja calidad
    • La extracción de datos no siempre es intuitiva
    • Determinar que características de las observaciones le gustan o no al usuario no sienpre es obvio