Sistema de Recomendación
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.