Sistema de Recomendación

22 minute read

RECOMENDADOR DE PELÍCULAS

Uno de las aplicaciones más utilizadas que ha aportado el Machine Learning son los sistemas de Recomendación. Debido al grado de efectividad que tienen podemos verlos cada día en una multitud de de servicios que consumimos: Netflix recomendando películas y series, Spotify sugiriendo canciones y artistas, o Amazón recomendandote nuevos artículos para comprar. También podemos ver esto en pequeños blogs que nos ofrecen lecturas recomendadas, o los propios periodicos digitales.

En estos ejemplos se podía intuir con claridad que detrás de las recomendaciones hay un algoritmo funcionando, pero en otras ocasiones no es tan claro. Por ejemplo, Netflix no solo recomienda películas y series, sino que para la mayoría de estas tiene varias carátulas para mostrarte y según el análisis de tu usuario te mostrará una u otra para hacerte más atractiva su recomendación.

Un ejemplo de lo anterior que me ha pasado recientemente: hace unos meses vi la serie Gambito de Dama, la cual protagoniza Anya Taylor-Joy haciendo un papel soberbio. Pues desde entonces en la serie Peaky Blinders (que no he visto de momento), me aparece la carátula con su cara. Entiendo que ella aparece en esta serie e intentan aprovechar el tirón con esta actriz.

Los sistemas de recomendación, a veces llamados en inglés “recommender systems” son algoritmos que intentan “predecir” los siguientes ítems (productos, canciones, etc.) que querrá adquirir un usuario en particular.

Antes del Machine Learning, lo más común era usar “rankings” ó listas con lo más votado, ó más popular de entre todos los productos. Entonces a todos los usuarios se les recomendaba lo mismo. Es una técnica que aún se usa y en muchos casos funciona bien, por ejemplo, en librerías ponen apartados con los libros más vendidos, best sellers.

Tipos de motores

Estos son algunas de los métodos de recomendación más utilizados:

Popularity: Aconseja por la “popularidad” de los productos. Por ejemplo, “los más vendidos” globalmente, se ofrecerán a todos los usuarios por igual sin aprovechar la personalización. Es fácil de implementar y en algunos casos es efectiva. Esto podemos verlo por ejemplo en la Casa del libro, donde siempre tenemos al entrar en la tienda el top 10 en ventas.

Content-based: A partir de productos visitados por el usuario, se intenta “adivinar” qué busca el usuario y ofrecer mercancías similares. Un ejemplo clásico es Amazón, en la que guiado por nuestras visitas (no hace falta que se compre) nos ofrece productos similares hasta que el algoritmo detecta nuestro cambio de “necesidad”.

Collaborative: Es el más novedoso, pues utiliza la información de “masas” para identificar perfiles similares y aprender de los datos para recomendar productos de manera individual. Este tipo de sistema de recomendación es el que vamos a ver en detalle.

Se basa en el supuesto de que si las personas coinciden en gustos en el pasado también lo harán en el futuro. PROS: Fácil de implementar con resultado acertado. CONTRAS: Sin un ranking inicial no es posible tener una recomendación.

Comenzamos a contruir nuestro motor

Existen varias formas de construir un sistema de recomendación collaborative.

En nuestro caso vamos a probar varios métodos, en primer lugar correlaremos nuestra matriz con el método de Pearson, y en segundo lugar utilizaremos la librería Surprise (Surprise es una herramienta de Python para construir y analizar sistemas de recomendación que tratan con datos de calificación explícitos) y probaremos algunos de sus algoritmos para ver cual arroja el menor error posible.

OBTENEMOS LOS DATOS

Obtenemos el archivo de películas de la página https://grouplens.org/datasets/movielens/. En este caso, y para facilitar los cálculos, utilizaremos el pequeño que tiene 100.000 valoraciones (el completo tiene 27 millones!!!)

Los datos consisten en: - 100,000 valoraciones entre 1 y 5 de 943 usuarios sobre 1682 películas clásicas. - Cada usuarios valora al menos 20 películas. - Se completa el archivo con datos demográficos básicos como edad, genéro, ocupación,…

# Importamos pandas
import pandas as pd
import numpy as np

# Tenemos los datos divididos en varios archivos. Nosotros valomos a utilizar en principio los archivos "u.data" 
# con la información del usuario, el id de la película y la valoración, y el archivo "u.item" del que únicamente 
# rescataremos los datos de id de la película y el título.

colum_usu = ['usuario_id', 'pelicula_id', 'valoracion']  
valora_usu = pd.read_csv('C:/Users/hesca/Documents/DataSets/ml-100k/u.data',
                      sep='\t', names=colum_usu, usecols=range(3), encoding="ISO-8859-1")

colum_pelis = ['pelicula_id', 'titulo']  
peliculas = pd.read_csv('C:/Users/hesca/Documents/DataSets/ml-100k/u.item', sep='|',
                     names=colum_pelis, usecols=range(2), encoding="ISO-8859-1")

# Combinamos ambos datasets ...
valoraciones = pd.merge(peliculas, valora_usu)
# Ahora voy a votar yo mismo algunas películas, y utilizaré el recomendador para descubrir nuevas películas
colum_vot = ['usuario_id', 'titulo', 'valoracion'] 
misValoraciones = pd.read_csv('C:/Users/hesca/Documents/DataSets/ml-100k/misVotaciones.csv',
                            sep=';', names=colum_vot, usecols=range(3), encoding="ISO-8859-1")

# Estas son las películas que he votado
misValoraciones
usuario_id titulo valoracion
0 999 Aladdin (1992) 3.0
1 999 Braveheart (1995) 3.0
2 999 Clockwork Orange, A (1971) 4.0
3 999 Dances with Wolves (1990) 3.0
4 999 English Patient, The (1996) 3.0
5 999 Face/Off (1997) 2.0
6 999 Forrest Gump (1994) 4.0
7 999 Game, The (1997) 3.0
8 999 Godfather, The (1972) 5.0
9 999 Jurassic Park (1993) 2.5
10 999 Lion King, The (1994) 2.5
11 999 Pulp Fiction (1994) 5.0
12 999 Reservoir Dogs (1992) 4.5
13 999 Return of the Jedi (1983) 2.0
14 999 Rock, The (1996) 3.0
15 999 Scream (1996) 1.0
16 999 Seven (Se7en) (1995) 4.0
17 999 Silence of the Lambs, The (1991) 4.0
18 999 Star Wars (1977) 1.0
19 999 Terminator 2: Judgment Day (1991) 3.0
20 999 Titanic (1997) 1.5
21 999 Trainspotting (1996) 5.0
22 999 Toy Story (1995) 2.5
23 999 Good Will Hunting (1997) 5.0
24 999 Schindler's List (1993) 4.0
25 999 Fargo (1996) 4.0
# Unimos nuestra votación al total de datos
valoraciones = pd.concat([valoraciones[['titulo','usuario_id','valoracion']], misValoraciones],sort=False, axis=0)
valoraciones
titulo usuario_id valoracion
0 Toy Story (1995) 308 4.0
1 Toy Story (1995) 287 5.0
2 Toy Story (1995) 148 4.0
3 Toy Story (1995) 280 4.0
4 Toy Story (1995) 66 3.0
... ... ... ...
21 Trainspotting (1996) 999 5.0
22 Toy Story (1995) 999 2.5
23 Good Will Hunting (1997) 999 5.0
24 Schindler's List (1993) 999 4.0
25 Fargo (1996) 999 4.0

100026 rows × 3 columns

# Pivotamos la tabla para crear una matriz con una fila por usuario, una columna por película y la votación 
# que se le dio a la misma.  
ValoracionPeliculas = valoraciones.pivot_table(index=['usuario_id'],columns=['titulo'],values='valoracion')  
ValoracionPeliculas
titulo 'Til There Was You (1997) 1-900 (1994) 101 Dalmatians (1996) 12 Angry Men (1957) 187 (1997) 2 Days in the Valley (1996) 20,000 Leagues Under the Sea (1954) 2001: A Space Odyssey (1968) 3 Ninjas: High Noon At Mega Mountain (1998) 39 Steps, The (1935) ... Yankee Zulu (1994) Year of the Horse (1997) You So Crazy (1994) Young Frankenstein (1974) Young Guns (1988) Young Guns II (1990) Young Poisoner's Handbook, The (1995) Zeus and Roxanne (1997) unknown Á köldum klaka (Cold Fever) (1994)
usuario_id
1 NaN NaN 2.0 5.0 NaN NaN 3.0 4.0 NaN NaN ... NaN NaN NaN 5.0 3.0 NaN NaN NaN 4.0 NaN
2 NaN NaN NaN NaN NaN NaN NaN NaN 1.0 NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
3 NaN NaN NaN NaN 2.0 NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
4 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
5 NaN NaN 2.0 NaN NaN NaN NaN 4.0 NaN NaN ... NaN NaN NaN 4.0 NaN NaN NaN NaN 4.0 NaN
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
940 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
941 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
942 NaN NaN NaN NaN NaN NaN NaN 3.0 NaN 3.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
943 NaN NaN NaN NaN NaN 2.0 NaN NaN NaN NaN ... NaN NaN NaN NaN 4.0 3.0 NaN NaN NaN NaN
999 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

944 rows × 1664 columns

Perfecto, ya tenemos nuestra matriz con la valoracón de cada película que ha puesto cada usuario. Antes de comenzar con nuestro recomendador, vamos a probar a correlar una película con el resto, según las valoraciones, para ver películas parecidas a esta.

# Vamos a probar con la película Fou Rooms:
FouRoomsValoracion = ValoracionPeliculas['Four Rooms (1995)']

# Correlamos el resto de películas (columnas) con la seleccionada (Four Rooms)  
pelisParecidas = ValoracionPeliculas.corrwith(FouRoomsValoracion)  
pelisParecidas = pelisParecidas.dropna()  
df = pd.DataFrame(pelisParecidas)

# Las ordenamos por el valor de score que hemos generado, de forma descendente  
pelisParecidas.sort_values(ascending=False)
C:\Users\hesca\Anaconda3\lib\site-packages\numpy\lib\function_base.py:2526: RuntimeWarning: Degrees of freedom <= 0 for slice
  c = cov(x, y, rowvar)
C:\Users\hesca\Anaconda3\lib\site-packages\numpy\lib\function_base.py:2455: RuntimeWarning: divide by zero encountered in true_divide
  c *= np.true_divide(1, fact)





titulo
Purple Noon (1960)                        1.0
Roseanna's Grave (For Roseanna) (1997)    1.0
Man of the House (1995)                   1.0
Little Princess, A (1995)                 1.0
Bushwhacked (1995)                        1.0
                                         ... 
Inspector General, The (1949)            -1.0
Kissed (1996)                            -1.0
Man of No Importance, A (1994)           -1.0
Mark of Zorro, The (1940)                -1.0
Little Odessa (1994)                     -1.0
Length: 1137, dtype: float64

Como podemos ver, hay muchas películas con el nivel de parecido máximo (1.0), pero sin embargo, son muy desconocidas. Esto puede deberse a que haya películas con muy pocas valoraciones pero que se de la casualidad de que dos o tres usuarios hayan valorado a Four Rooms y a estas películas con la misma puntuación. Vamos a comprobar el número de votaciones de Purple Noon por ejemplo:

ValoracionPeliculas['Purple Noon (1960)'].count()
7

En efecto tiene solo 7 valoraciones.

Para solucionar el hecho de que películas poco votadas tengan tanto peso en nuestro recomendador y pierda eficacia, lo que haremos será agregar las votaciones por película para coger solo aquellas películas que tengan al menos 50 valoraciones de usuarios distintos.

# Agregamos por título y devolvemos el número de veces que se puntuó, y la media de la puntuación  
peliculasVotadas = valoraciones.groupby('titulo').agg({'valoracion': [np.size, np.mean]})

peliculasVotadas

valoracion
size mean
titulo
'Til There Was You (1997) 9.0 2.333333
1-900 (1994) 5.0 2.600000
101 Dalmatians (1996) 109.0 2.908257
12 Angry Men (1957) 125.0 4.344000
187 (1997) 41.0 3.024390
... ... ...
Young Guns II (1990) 44.0 2.772727
Young Poisoner's Handbook, The (1995) 41.0 3.341463
Zeus and Roxanne (1997) 6.0 2.166667
unknown 9.0 3.444444
Á köldum klaka (Cold Fever) (1994) 1.0 3.000000

1664 rows × 2 columns

# Nos quedamos con todas las que tengan mas de 50 puntuaciones de distintos usuarios  
peliculasPopulares = peliculasVotadas['valoracion']['size'] >= 50
# Ordenamos por la puntuación asignada  
peliculasVotadas[peliculasPopulares].sort_values([('valoracion', 'mean')], ascending=False)
valoracion
size mean
titulo
Close Shave, A (1995) 112.0 4.491071
Wrong Trousers, The (1993) 118.0 4.466102
Schindler's List (1993) 299.0 4.464883
Casablanca (1942) 243.0 4.456790
Wallace & Gromit: The Best of Aardman Animation (1996) 67.0 4.447761
... ... ...
Cable Guy, The (1996) 106.0 2.339623
Beautician and the Beast, The (1997) 86.0 2.313953
Striptease (1996) 67.0 2.238806
McHale's Navy (1997) 69.0 2.188406
Island of Dr. Moreau, The (1996) 57.0 2.157895

605 rows × 2 columns

Podemos ver todas aquellas películas que tienen más de 50 valoraciones de distintos usuarios, ordenadas por su puntuación media. Si ahora hacemos un “join”; con la tabla de valoraciones original, nos quedaremos solo con estas peliculas, descartando aquellas que solo valoraron unos pocos usuarios:

# Hacemos el join  
df = peliculasVotadas[peliculasPopulares].join(pd.DataFrame(pelisParecidas, columns=['similitud']))

# Ordenamos el dataframe por similitud, y vemos los primeros 10 resultados  
df.sort_values(['similitud'], ascending=False)[:10]  

C:\Users\hesca\Anaconda3\lib\site-packages\pandas\core\reshape\merge.py:617: UserWarning: merging between different levels can give an unintended result (2 levels on the left, 1 on the right)
  warnings.warn(msg, UserWarning)
(valoracion, size) (valoracion, mean) similitud
titulo
Apostle, The (1997) 55.0 3.654545 1.000000
Four Rooms (1995) 90.0 3.033333 1.000000
Notorious (1946) 52.0 4.115385 0.870388
Philadelphia Story, The (1940) 104.0 4.115385 0.834726
Excess Baggage (1997) 52.0 2.538462 0.771744
Mrs. Brown (Her Majesty, Mrs. Brown) (1997) 96.0 3.947917 0.743161
Mary Shelley's Frankenstein (1994) 59.0 3.067797 0.729866
Seven Years in Tibet (1997) 155.0 3.458065 0.724176
Life Less Ordinary, A (1997) 53.0 3.075472 0.704779
Nosferatu (Nosferatu, eine Symphonie des Grauens) (1922) 54.0 3.555556 0.690066

Vemos que tenemos 2 películas con similitud perfecta, la propia Four Rooms, y The Apostle.Con esto hemos comprobado cómo podemos encontrar películas similares a la propuesta según las votaciones de los distintos usuarios. Pasamos a construir nuestro modelo.

CONSTRUYENDO EL MOTOR DE RECOMENDACIÓN POR CORRELACIÓN

Ahora que hemos visto un ejemplo de como encontrar similitudes entre pelÍculas, podemos avanzar y tratar de generar recomendaciones para un usuario basadas en su actividad anterior (en su histórico de puntuaciones). Es muy parecido a lo que hemos hecho hasta ahora… esta vez lo que haremos será, en lugar de correlar una película con las demás, correlar todas con todas, del siguiente modo:

# Correlamos todas las columnas con todas las demás usando el metodo pearson habiendo descartado todas aquellas
# que no tengan al menos 50 valoraciones de usuarios  
corrMatrix = ValoracionPeliculas.corr(method='pearson', min_periods=50)  
corrMatrix.head(20) 
titulo 'Til There Was You (1997) 1-900 (1994) 101 Dalmatians (1996) 12 Angry Men (1957) 187 (1997) 2 Days in the Valley (1996) 20,000 Leagues Under the Sea (1954) 2001: A Space Odyssey (1968) 3 Ninjas: High Noon At Mega Mountain (1998) 39 Steps, The (1935) ... Yankee Zulu (1994) Year of the Horse (1997) You So Crazy (1994) Young Frankenstein (1974) Young Guns (1988) Young Guns II (1990) Young Poisoner's Handbook, The (1995) Zeus and Roxanne (1997) unknown Á köldum klaka (Cold Fever) (1994)
titulo
'Til There Was You (1997) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
1-900 (1994) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
101 Dalmatians (1996) NaN NaN 1.0 NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
12 Angry Men (1957) NaN NaN NaN 1.000000 NaN NaN NaN 0.178848 NaN NaN ... NaN NaN NaN 0.096546 NaN NaN NaN NaN NaN NaN
187 (1997) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2 Days in the Valley (1996) NaN NaN NaN NaN NaN 1.0 NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
20,000 Leagues Under the Sea (1954) NaN NaN NaN NaN NaN NaN 1.000000 0.259308 NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2001: A Space Odyssey (1968) NaN NaN NaN 0.178848 NaN NaN 0.259308 1.000000 NaN NaN ... NaN NaN NaN -0.001307 -0.174918 NaN NaN NaN NaN NaN
3 Ninjas: High Noon At Mega Mountain (1998) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
39 Steps, The (1935) NaN NaN NaN NaN NaN NaN NaN NaN NaN 1.0 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
8 1/2 (1963) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
8 Heads in a Duffel Bag (1997) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
8 Seconds (1994) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
A Chef in Love (1996) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
Above the Rim (1994) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
Absolute Power (1997) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
Abyss, The (1989) NaN NaN NaN NaN NaN NaN NaN 0.089206 NaN NaN ... NaN NaN NaN 0.140161 0.384703 NaN NaN NaN NaN NaN
Ace Ventura: Pet Detective (1994) NaN NaN NaN NaN NaN NaN NaN 0.138417 NaN NaN ... NaN NaN NaN 0.221837 0.360457 NaN NaN NaN NaN NaN
Ace Ventura: When Nature Calls (1995) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
Across the Sea of Time (1995) NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

20 rows × 1664 columns

En esta tabla podemos ver la correlación entre unas películas y otras. Vemos que hay gran cantidad de Nan, esto es debido a que no se cumple la condición de que haya 50 valoraciones para alguna de las películas que forman la celda de nuestra matriz. A estas matrices que tienen tantos valores nulos se las denomina matrices “sparse” o “dispersas”.

Ahora sí, vamos a utilizar nuestro usuario (id=999), y según sus votaciones utilizaremos nuestro recomendador.

# Seleccionamos el usuario 999 y eliminamos todas las columnas que tengan nulo (películas no vistas)  
misValoraciones = ValoracionPeliculas.loc[999].dropna()

# Recordamos las pelítuclas valoradas  
misValoraciones
titulo
Aladdin (1992)                       3.0
Braveheart (1995)                    3.0
Clockwork Orange, A (1971)           4.0
Dances with Wolves (1990)            3.0
English Patient, The (1996)          3.0
Face/Off (1997)                      2.0
Fargo (1996)                         4.0
Forrest Gump (1994)                  4.0
Game, The (1997)                     3.0
Godfather, The (1972)                5.0
Good Will Hunting (1997)             5.0
Jurassic Park (1993)                 2.5
Lion King, The (1994)                2.5
Pulp Fiction (1994)                  5.0
Reservoir Dogs (1992)                4.5
Return of the Jedi (1983)            2.0
Rock, The (1996)                     3.0
Schindler's List (1993)              4.0
Scream (1996)                        1.0
Seven (Se7en) (1995)                 4.0
Silence of the Lambs, The (1991)     4.0
Star Wars (1977)                     1.0
Terminator 2: Judgment Day (1991)    3.0
Titanic (1997)                       1.5
Toy Story (1995)                     2.5
Trainspotting (1996)                 5.0
Name: 999, dtype: float64

Hemos valorado 25 películas. Con esta información y correlando estas películas con el resto vamos a comprobar cómo funciona nuestro recomendador.

posiblesSimilares = pd.Series()

# Creamos un bucle para recorrer cada película que he votado
for i in range(0, len(misValoraciones.index)):  
    
# Obtenemos el grado de similitud de las películas bajo la premisa de haber sido puntuadas más de 50 veces. 
    similares = corrMatrix[misValoraciones.index[i]].dropna()

# Multiplicamos el score de correlación por la puntuación asignada por el usuario  
    similares = similares.map(lambda x: x * misValoraciones[i])

# Añadimos la película y la nueva puntuación a nuestra lista de candidatas  
    posiblesSimilares = posiblesSimilares.append(similares)

# Agrupamos los resultados, ya que si una película es muy parecida a varias de las que ha visto el usuario, aparecerá 
# varias veces. En este caso vamos a sumar la "nueva puntuación" cada vez que sale, ya que si aparece muchas veces 
# sumará más puntos y será muy recomendable, y entonces saldrá de las primeras.
posiblesSimilares = posiblesSimilares.groupby(posiblesSimilares.index).sum()

# Finalmente eliminamos todas las peliculas que el usuario ya habia valorado para no incluirlas en la recomendación,  
# le decimos que ignore errores para evitar excepciones si hay problemas con los titulos  
filtered = posiblesSimilares.drop(misValoraciones.index,errors='ignore')  

# Vemos las 5 películas "más" recomendadas
filtered.sort_values(ascending=False).head(10)
Cape Fear (1991)                          20.098064
Field of Dreams (1989)                    18.909795
Shawshank Redemption, The (1994)          18.618572
Kingpin (1996)                            18.456364
Long Kiss Goodnight, The (1996)           17.624929
One Flew Over the Cuckoo's Nest (1975)    17.584799
River Wild, The (1994)                    16.658393
Die Hard (1988)                           16.609444
Shine (1996)                              16.515340
Stand by Me (1986)                        16.499208
dtype: float64

Estas películas son las que me recomienda, la verdad es que no he visto casi ninguna así que tendré que verlas para saber el grado de efectividad del recomendador :-)

LIBRERIA SUPRISE

Ahora vamos a utilizar la librería Surprise (Podéis encontrar más información en http://surpriselib.com/)

Esta librería es bastante completa y muy especializada en este tipo de tarea. De una manera simple vamos a construir nuestro recomendador con los algoritmos:

  • NMF: Un algoritmo de filtrado colaborativo basado en factorización matricial no negativa.
  • SVD: El famoso algoritmo SVD, popularizado por Simon Funk durante el Premio Netflix. Equivalente a la factorización matricial probabilística.
  • SVD++: Una mejora del algoritmo SVD que tiene en cuenta las valoraciones implícitas.
  • KNN with Z-Score: Un algoritmo de filtrado colaborativo básico que tiene en cuenta una calificación de referencia.
  • Co-Clustering: Un algoritmo de filtrado colaborativo basado en la agrupación conjunta.

Hemos visto que cada algoritmo recomendaba películas distintas. Podemos evaluar estos algoritmos dividiendo nuestro conjunto de datos en train y test y medir el rendimiento en el conjunto de datos de prueba. Aplicaremos Cross Validation (k-fold of k = 3) y obtendremos el RMSE promedio.

# Utilizamos nuevamente las películas con más de 50 valoraciones
valoraciones['n_votaciones'] = valoraciones.groupby(['titulo'])['valoracion'].transform('count')
valoraciones= valoraciones[valoraciones.n_votaciones>50][['usuario_id', 'titulo', 'valoracion']]
# Importamos la librería Surprise y sus métodos
from surprise import NMF, SVD, SVDpp, KNNBasic, KNNWithMeans, KNNWithZScore, CoClustering
from surprise.model_selection import cross_validate
from surprise import Reader, Dataset
Instanciamos la clase Reader y la utilizamos para colocar los datos según el orden necesario. Esta clase es usada para analizar los datos del archivo. Se supone que el archivo especifica una calificación por línea, y cada línea debe respetar la siguiente estructura: ** user item rating (timestamp) ** donde el timestamp es opcional.
algo = Reader(rating_scale=(1, 5))
datos = Dataset.load_from_df(valoraciones, algo)

Ahora eliminamos del dataset las películas que hemos visto.

# Obtenemos la lista de películas
listaPeliculas = valoraciones['titulo'].unique()
# Las películas votadas
misVotaciones = valoraciones.loc[valoraciones['usuario_id']==999, 'titulo']
# Eliminamos nuestras películas
peliculas_predecir = np.setdiff1d(listaPeliculas,misVotaciones)

Creamos el recomendador con el algoritmo NMF

nmf = NMF()
nmf.fit(datos.build_full_trainset())
my_recs = []
for iid in peliculas_predecir:
    my_recs.append((iid, nmf.predict(uid=8,iid=iid).est))
    
pd.DataFrame(my_recs, columns=['titulo', 'prediccion']).sort_values('prediccion', ascending=False).head(10)
titulo prediccion
115 Close Shave, A (1995) 5.000000
96 Casablanca (1942) 5.000000
75 Boot, Das (1981) 4.956953
551 Wallace & Gromit: The Best of Aardman Animatio... 4.904733
245 High Noon (1952) 4.860578
545 Vertigo (1958) 4.858045
1 12 Angry Men (1957) 4.808425
513 Thin Man, The (1934) 4.790323
567 Wrong Trousers, The (1993) 4.782324
164 Dr. Strangelove or: How I Learned to Stop Worr... 4.776931

Creamos el recomendador con el algoritmo SVD

svd = SVD()
svd.fit(datos.build_full_trainset())
my_recs = []
for iid in peliculas_predecir:
    my_recs.append((iid, svd.predict(uid=8,iid=iid).est))
    
pd.DataFrame(my_recs, columns=['titulo', 'prediccion']).sort_values('prediccion', ascending=False).head(10)
titulo prediccion
115 Close Shave, A (1995) 4.948758
461 Shawshank Redemption, The (1994) 4.907975
300 Lawrence of Arabia (1962) 4.850240
383 One Flew Over the Cuckoo's Nest (1975) 4.806374
75 Boot, Das (1981) 4.769054
416 Raise the Red Lantern (1991) 4.767982
418 Ran (1985) 4.732029
252 Hoop Dreams (1994) 4.731066
422 Rear Window (1954) 4.729110
567 Wrong Trousers, The (1993) 4.721393

Creamos el recomendador con el algoritmo SVD++

svdpp = SVDpp()
svdpp.fit(datos.build_full_trainset())
my_recs = []
for iid in peliculas_predecir:
    my_recs.append((iid, svdpp.predict(uid=8,iid=iid).est))
    
pd.DataFrame(my_recs, columns=['titulo', 'prediccion']).sort_values('prediccion', ascending=False).head(10)
titulo prediccion
383 One Flew Over the Cuckoo's Nest (1975) 4.967189
1 12 Angry Men (1957) 4.944020
300 Lawrence of Arabia (1962) 4.934206
294 L.A. Confidential (1997) 4.918136
96 Casablanca (1942) 4.897174
525 To Kill a Mockingbird (1962) 4.879085
461 Shawshank Redemption, The (1994) 4.872577
75 Boot, Das (1981) 4.850577
375 North by Northwest (1959) 4.829080
22 Amadeus (1984) 4.819654

Creamos el recomendador con el algoritmo KNN with Z-Score

KNN = KNNWithZScore()
KNN.fit(datos.build_full_trainset())
my_recs = []
for iid in peliculas_predecir:
    my_recs.append((iid, KNN.predict(uid=8,iid=iid).est))
    
pd.DataFrame(my_recs, columns=['titulo', 'prediccion']).sort_values('prediccion', ascending=False).head(10)
Computing the msd similarity matrix...
Done computing similarity matrix.
titulo prediccion
567 Wrong Trousers, The (1993) 4.994374
115 Close Shave, A (1995) 4.931717
461 Shawshank Redemption, The (1994) 4.923373
414 Raiders of the Lost Ark (1981) 4.886318
96 Casablanca (1942) 4.865282
300 Lawrence of Arabia (1962) 4.836160
175 Empire Strikes Back, The (1980) 4.831657
41 As Good As It Gets (1997) 4.818604
383 One Flew Over the Cuckoo's Nest (1975) 4.792024
294 L.A. Confidential (1997) 4.781272

Creamos el recomendador con el algoritmo Co-Clustering

clust = CoClustering()
clust.fit(datos.build_full_trainset())
my_recs = []
for iid in peliculas_predecir:
    my_recs.append((iid, clust.predict(uid=8,iid=iid).est))
    
pd.DataFrame(my_recs, columns=['titulo', 'prediccion']).sort_values('prediccion', ascending=False).head(10)
titulo prediccion
115 Close Shave, A (1995) 4.835049
567 Wrong Trousers, The (1993) 4.810079
96 Casablanca (1942) 4.800768
551 Wallace & Gromit: The Best of Aardman Animatio... 4.791739
461 Shawshank Redemption, The (1994) 4.789207
422 Rear Window (1954) 4.731538
543 Usual Suspects, The (1995) 4.729746
1 12 Angry Men (1957) 4.687978
108 Citizen Kane (1941) 4.681791
515 Third Man, The (1949) 4.677311

Ahora pasamos a evaluar los diferentes algoritmos

cv = []
# Iteramos sobre cada algoritmo
for recsys in [NMF(), SVD(), SVDpp(), KNNWithZScore(), CoClustering()]:
    # Utilizamos cross-validation
    tmp = cross_validate(recsys, datos, measures=['RMSE'], cv=3, verbose=False)
    cv.append((str(recsys).split(' ')[0].split('.')[-1], tmp['test_rmse'].mean()))
pd.DataFrame(cv, columns=['Algoritmo', 'RMSE'])
Computing the msd similarity matrix...
Done computing similarity matrix.
Computing the msd similarity matrix...
Done computing similarity matrix.
Computing the msd similarity matrix...
Done computing similarity matrix.
Algoritmo RMSE
0 NMF 0.953770
1 SVD 0.929137
2 SVDpp 0.913126
3 KNNWithZScore 0.935942
4 CoClustering 0.945715

CONCLUSIÓN

Hemos creado varios sistemas de recomendación de filtrado colaborativo donde teniendo en cuenta el usuario y la película hemos conseguido que el RMSE sea menor que 1. Esto a priori es un buen trabajo, aunque no hay que perder de vista que el RMSE es tan solo una métrica matemática, y que si nos fijamos en el negocio probablemente no siempre queramos minimizar el error ya que esto no siempre será lo óptimo.

Por ejemplo, cuando Spotify nos recomienda canciones si entre ellas hay alguna canción que ya conozcamos y que nos encante esto nos dará confianza en que la selección está bien realizada y que probablemente el resto de canciones también nos pueden gustar.

Por el contrario imaginemos que creamos un sistema que tiene el menor error posible, pero solo nos recomienda cosas nuevas y desconocidas. Si al escuchar las primeras canciones vemos que no nos gustan demasiado pensaremos en que las recomendaciones no sean acertadas, aunque quizá sean las mejores canciones para nosotros y solo nos hace falta escucharlas un par de veces más.

Otro problema que puede surgir es que encierres al usuario en un estilo musical completo. Por ejemplo, si las primeras canciones que escucho fueron todas de rock de los 90´s, puede ser que solo le ofrezcas canciones de rock, o canciones de esa época y se puedan perder canciones muy buenas que no encajen en ese género o década.

Con esto podemos ver que no siempre minimizar el error es lo óptimo, ya que también tiene sus inconvenientes. En este caso si por ejemplo introdujeramos temas populares de otras décadas, aumentaríamos el error, pero probablemente el usuario descubriría más canciones y aumentaría su satisfacción con nuestro motor de recomendación.

Por otro lado, y como complemento a nuestro motor de recomendación por filtrado colaborativo, podemos aplicar otros algoritmos teniendo en cuenta atributos como el género de la película, la fecha de estreno, el director, el actor, el presupuesto, la duración, etc.

En este caso, nos referimos a los recomendadores basados en contenido que tratan la recomendación como un problema de clasificación específico del usuario y aprenden un clasificador para los gustos y disgustos del usuario en función de las características de un elemento. En este sistema, las palabras clave se utilizan para describir los elementos y se crea un perfil de usuario para indicar el tipo de elemento que le gusta a este usuario. Por último, incluso podemos tener en cuenta los atributos del usuario, como sexo, edad, ubicación, idioma, etc.

Con una mezcla de ambos algoritmos probablemente conseguiríamos resultados más completos para nuestro algoritmo.

Por último indicar que hay departamentos enteros dedicados a mejorar el sistema de recomendación de su empresa. Este artículo solo pretende acercar de una manera simple la creación de estos modelos.