Recommending Movies to a User
We will explore content-based and colaborative filtering recommendation systems.
Recommendation systems are a collection of algorithms used to recommend items to users based on information taken from the user. These systems have become ubiquitous, and can commonly be seen in online stores, movie databases, and job finders. In this blog post, we will explore content-based and colaborative filtering recommendation systems.
The dataset we'll be working on has been acquired from GroupLens. It consists of 27 million ratings and 1.1 million tag applications applied to 58,000 movies by 280,000 users.
import pandas as pd
from math import sqrt
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
movies_df = pd.read_csv('movies1.csv')
# store the ratings information into a pandas dataframe
ratings_df = pd.read_csv('ratings.csv')
movies_df.head()
Each movie has a unique ID, a title with its release year along with it (which may contain unicode characters) and several different genres in the same field.
print(movies_df.shape)
print(ratings_df.shape)
# we specify the parantheses so we don't conflict with movies that have years in their titles
movies_df['year'] = movies_df.title.str.extract('(\(\d\d\d\d\))', expand=False)
# remove the parentheses
movies_df['year'] = movies_df.year.str.extract('(\d\d\d\d)', expand=False)
# remove the years from the 'title' column
movies_df['title'] = movies_df.title.str.replace('(\(\d\d\d\d\))', '')
# apply the strip finction to get rid of any ending whitespace characters that may have appeared
movies_df['title'] = movies_df['title'].apply(lambda x: x.strip())
movies_df.head()
Let's also split the values in the 'genres' column into a 'list of genres' to simplify future use. Apply Python's split string function on the genres column.
movies_df['genres'] = movies_df.genres.str.split('|')
movies_df.head()
Since keeping genres in a list format isn't optimal for the content-based recommendation system technique, we will use the One Hot Encoding technique to convert the list of genres to a vector where each column corresponds to one possible value of the feature. This encoding is needed for feeding categorical data.
In this case, we store every differrent genre in columns that contain either 1 or 0. 1 shows that a movie has that genre and 0 shows that it doesn't. Let's also store this dataframe in another variable since genres won't be important for our first recommendation system.
moviesWithGenres_df = movies_df.copy()
# for every row in the dataframe, iterate through the list of genres and place a 1 in the corresponding column
for index, row in movies_df.iterrows():
for genre in row['genres']:
moviesWithGenres_df.at[index, genre] = 1
# fill in the NaN values with 0 to show that a movie doesn't have that column's genre
moviesWithGenres_df = moviesWithGenres_df.fillna(0)
moviesWithGenres_df.head()
Now, let's focus on the ratings dataframe.
ratings_df.head()
Every row in the ratings dataframe has a userId associated with at least one movie, a rating and a timestamp showing when they reviewed it. We won't be needing the timestamp column, so let's drop it.
ratings_df = ratings_df.drop('timestamp',1)
ratings_df.head()
--
Content-based recommendation system
This technique attempts to figure out what a user's favorite aspects of an item are, and then recommends items that present those aspects. In our case, we're going to try to figure out the input's favorite genres from the movies and ratings given.
Advantages of content-based filtering:
- it learns the user's preferences.
- it's highly personalized for the user.
Disadvantages of content-based filtering:
- it doesn't take into account what others think of the item, so low quality item recommendations might happen.
- Extracting data is not always intuitive.
- Determining what characteristics of the item the user dislikes or likes is not always obvious.
Create an input to recommend movies to.
userInput = [
{'title':'Mission: Impossible - Fallout', 'rating':5},
{'title':'Top Gun', 'rating':4.5},
{'title':'Jerry Maguire', 'rating':3},
{'title':'Vanilla Sky', 'rating':2.5},
{'title':'Minority Report', 'rating':4},
]
inputMovies = pd.DataFrame(userInput)
inputMovies
Add movieId to input user.
Extract the input movie's ID from the movies dataframe and add it to the input.
inputId = movies_df[movies_df['title'].isin(inputMovies['title'].tolist())]
# merge it to get the movieId
inputMovies = pd.merge(inputId, inputMovies)
# drop information we won't use from the input dataframe
inputMovies = inputMovies.drop('genres', 1).drop('year', 1)
# final input dataframe
inputMovies
We will learn the input's preferences. So let's get the subset of movies that the input has watched from the dataframe containing genres defined with binary values.
userMovies = moviesWithGenres_df[moviesWithGenres_df['movieId'].isin(inputMovies['movieId'].tolist())]
userMovies
We only need the actual genre table. Reset the index and drop the unnecessary columns.
userMovies = userMovies.reset_index(drop=True)
# drop unnecessary columns
userGenreTable = userMovies.drop('movieId', 1).drop('title', 1).drop('genres', 1).drop('year', 1)
userGenreTable
Now we learn the input preferences.
We turn each genre into weights using the input's reviews and multiplying them into the input's genre table, and then summing up the resulting table by column.
inputMovies['rating']
userProfile = userGenreTable.transpose().dot(inputMovies['rating'])
# the user profile
userProfile
Now we have the weights for each of the user's preferences. This is the User Profile. Using this, we can recommend movies that satisfy the user's preferences.
Let's start by extracting the genre table from the original dataframe.
genreTable = moviesWithGenres_df.set_index(moviesWithGenres_df['movieId'])
# drop unnecessary columns
genreTable = genreTable.drop('movieId', 1).drop('title', 1).drop('genres', 1).drop('year', 1)
genreTable.head()
genreTable.shape
With the input's profile and the complete list of movies and their genres in hand, we're going to take the weighted average of every movie based on the input profile and recommend the top twenty movies that most satisfy it.
recommendationTable_df = ((genreTable*userProfile).sum(axis=1)) / (userProfile.sum())
recommendationTable_df.head()
Here is the recommendation table.
movies_df.loc[movies_df['movieId'].isin(recommendationTable_df.head(20).keys())]
These are the top 20 movies to recommend to the user based on a content-based recommendation system.
Collaborative Filtering
This technique uses other users to recommend items to the input user. It attempts to find users that have similar preferences and opinions as the input and then recommends items that they have liked to the input. there are several methods of finding similar users, and the one we will be using here is going to be based on the Pearson Correlation Function.
The process for creating a user-based recommendation system is as follows:
- Select a user with the movies the user has watched.
- Based on his ratings of movies, find the top X neighbours.
- Get the watched movie record of the user for each neighbour.
- Calculate a similarity score using some formula.
- Recommend the items with the highest score.
Advantages of collaborative filtering:
- It takes other user's ratings into consideration
- It doesn't need to study or extract information from the recommended item
- It adapts to the user's interestes which might change over time
Disadvantages of collaborative filtering:
- The approximation function can be slow.
- There might be a low amount of users to approximate
- There might be privacy issues when trying to learn the user's experiences.
Let's create an input user to recommend movies to.
userInput = [
{'title':'Mission: Impossible - Fallout', 'rating':5},
{'title':'Top Gun', 'rating':4.5},
{'title':'Jerry Maguire', 'rating':3},
{'title':'Vanilla Sky', 'rating':2.5},
{'title':'Minority Report', 'rating':4},
]
inputMovies = pd.DataFrame(userInput)
inputMovies
inputId = movies_df[movies_df['title'].isin(inputMovies['title'].tolist())]
# merge it to get the movieId
inputMovies = pd.merge(inputId, inputMovies)
# drop information we won't use from the input dataframe
inputMovies = inputMovies.drop('genres', 1).drop('year', 1)
# final input dataframe
inputMovies
userSubset = ratings_df[ratings_df['movieId'].isin(inputMovies['movieId'].tolist())]
userSubset.head()
Group the rows by userId.
userSubsetGroup = userSubset.groupby(['userId'])
Let's look at one of these users - userId = 4
userSubsetGroup.get_group(4)
Let's sort these groups so the users that share the most movies in common with the input have higher priority. This provides a richer recommendation since we won't go through every single user.
userSubsetGroup = sorted(userSubsetGroup, key=lambda x: len(x[1]), reverse=True)
Now let's look at the first user.
userSubsetGroup[0:3]
Next, we are going to compare users to our specified user and find the one that is most similar.
We're going to find out how similar each user is to the input through the Pearson Correlation Coefficient. It is used to measure the strength of a linear association between two variables.
We will select a subset of users to iterate through. The limit is imposed because we don't want to waste too much time going through every single user.
userSubsetGroup = userSubsetGroup[0:100]
Calculate the Pearson Correlation between the input user and the subset group, and store it in a dictionary, where the key is the userId and the value is the coefficient.
pearsonCorrelationDict = {}
#For every user group in our subset
for name, group in userSubsetGroup:
#Let's start by sorting the input and current user group so the values aren't mixed up later on
group = group.sort_values(by='movieId')
inputMovies = inputMovies.sort_values(by='movieId')
#Get the N for the formula
nRatings = len(group)
#Get the review scores for the movies that they both have in common
temp_df = inputMovies[inputMovies['movieId'].isin(group['movieId'].tolist())]
#And then store them in a temporary buffer variable in a list format to facilitate future calculations
tempRatingList = temp_df['rating'].tolist()
#Let's also put the current user group reviews in a list format
tempGroupList = group['rating'].tolist()
#Now let's calculate the pearson correlation between two users, so called, x and y
Sxx = sum([i**2 for i in tempRatingList]) - pow(sum(tempRatingList),2)/float(nRatings)
Syy = sum([i**2 for i in tempGroupList]) - pow(sum(tempGroupList),2)/float(nRatings)
Sxy = sum( i*j for i, j in zip(tempRatingList, tempGroupList)) - sum(tempRatingList)*sum(tempGroupList)/float(nRatings)
#If the denominator is different than zero, then divide, else, 0 correlation.
if Sxx != 0 and Syy != 0:
pearsonCorrelationDict[name] = Sxy/sqrt(Sxx*Syy)
else:
pearsonCorrelationDict[name] = 0
pearsonCorrelationDict.items()
pearsonDF = pd.DataFrame.from_dict(pearsonCorrelationDict, orient='index')
pearsonDF.columns = ['similarityIndex']
pearsonDF['userId'] = pearsonDF.index
pearsonDF.index = range(len(pearsonDF))
pearsonDF.head()
topUsers = pearsonDF.sort_values(by='similarityIndex', ascending=False)[0:50]
topUsers.head()
Now let's start recommending movies to the input user.
Rating of selected users to all movies
We're going to do this by taking the weighted average of the ratings of the movies using the Pearson Correlation as the weight. But to do this, we first need to get the movies watched by the users in our pearsonDF from the ratings dataframe, and then store their correlation in a new column called 'similarityIndex'.
topUsersRating=topUsers.merge(ratings_df, left_on='userId', right_on='userId', how='inner')
topUsersRating.head()
Now we multiply the movie rating by its weight (the similarity index), then sum up the new ratings and divide it by the sum of the weights.
We can easily do this by simply multiplying two columns, then grouping up the dataframe by movieId and then dividing two columns.
It shows the idea of all similar users to candidate movies for the input user.
topUsersRating['weightedRating'] = topUsersRating['similarityIndex']*topUsersRating['rating']
topUsersRating.head()
tempTopUsersRating = topUsersRating.groupby('movieId').sum()[['similarityIndex','weightedRating']]
tempTopUsersRating.columns = ['sum_similarityIndex','sum_weightedRating']
tempTopUsersRating.head()
recommendation_df = pd.DataFrame()
# take the weighted average
recommendation_df['weighted average recommendation score'] = tempTopUsersRating['sum_weightedRating']/tempTopUsersRating['sum_similarityIndex']
recommendation_df['movieId'] = tempTopUsersRating.index
recommendation_df.head()
Let's sort this and see the top 20 movies that the algorithm recommended.
recommendation_df = recommendation_df.sort_values(by='weighted average recommendation score', ascending=False)
recommendation_df.head()
movies_df.loc[movies_df['movieId'].isin(recommendation_df.head(20)['movieId'].tolist())]
These are the top 20 movies to recommend to the user based on a collaborative filtering recommendation system.