0%

Movie Recommendation Engine With Content Based Filtering (Implementation)

We can define a Recommendation system as a collection of a bunch of algorithms that are used to recommend items to the users based on the information taken from the user. These systems have become very common and are widely used and can be commonly seen in online stores, movies databases and job finders.

A very simple representation of a content based recommendation system for movies looks like this

In this blog, we will explore Content-based recommendation systems and implement a simple version of one using Python and the Pandas library.

Step 1: Acquiring the Data

In [2]:

Please run these 2 commands to download and unzip the data

!wget -O moviedataset.zip https://s3-api.us-geo.objectstorage.softlayer.net/cf-courses-data/CognitiveClass/ML0101ENv3/labs/moviedataset.zip
('and the command 2 is ...')
!unzip -o -j moviedataset.zip 
--2020-05-11 22:08:49--  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  21.5MB/s    in 7.8s    

2020-05-11 22:08:57 (19.5 MB/s) - ‘moviedataset.zip’ saved [160301210/160301210]

unziping ...
Archive:  moviedataset.zip
  inflating: links.csv               
  inflating: movies.csv              
  inflating: rating_scores.csv             
  inflating: README.txt              
  inflating: tags.csv                

Now you're ready to start working with the data!

Data Preprocessing

First, let's get all of the imports out of the way:

In [3]:
# Pandas - Dataframe manipulation library
import pandas as pd
# Math - Math functions, we'll only need the sqrt function so let's import only that
from math import sqrt
# numpy - for all numeric operations
import numpy as np
# matplotlib - to plot charts

import matplotlib.pyplot as plt
%matplotlib inline

Now let's read each file into their Dataframes:

In [4]:
# Storing the movie information into a pandas dataframe
dataframe_movies = pd.read_csv('movies.csv')
# Storing the user information into a pandas dataframe
rating_scores_df = pd.read_csv('rating_scores.csv')
# Head is a function that gets the first N rows of a dataframe. N's default is 5.
dataframe_movies.head()
Out[4]:
Id_movie name categorys_class
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

From the name we also need to remove the column "year". We will use pandas' replace function & store it in a new column

In [5]:

dataframe_movies['year'] = dataframe_movies.name.str.extract('(\(\d\d\d\d\))',expand=False)
# Removing paranthesis{}
dataframe_movies['year'] = dataframe_movies.year.str.extract('(\d\d\d\d)',expand=False)
# Removing the years from the 'name' column
dataframe_movies['name'] = dataframe_movies.name.str.replace('(\(\d\d\d\d\))', '')
#Applying the strip function to get rid of any ending whitespace characters that may have appeared
dataframe_movies['name'] = dataframe_movies['name'].apply(lambda x: x.strip())
dataframe_movies.head()
Out[5]:
Id_movie name categorys_class 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

Now as we have performed some preprocessing on our data, let us also split the values according to categorys_class column into a list of categorys_class to simplify future use. This can be achieved by applying Python's split string function on the correct column.

In [6]:
#Every category is separated by a | so we simply have to call the split function on |
dataframe_movies['categorys_class'] = dataframe_movies.categorys_class.str.split('|')
dataframe_movies.head()
Out[6]:
Id_movie name categorys_class 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

We will use a technique named One Hot Encoding to convert the list of categorys_class to a vector where each column corresponds to one possible value of the feature because keeping categorys_class in a list format isn't optimal for a recommendation system which is content-based. We will input the categorical data to one hot encoding like we will our data of categorys_class in columns which represents either 0 or . 0 represents that a movie does not have that category and 1 represents that it does.

In [7]:
#Copying the movie dataframe into a new one since we won't need to use the category information in our first case.
moviesWithcategorys_class_df = dataframe_movies.copy()

#For every row in the dataframe, iterate through the list of categorys_class and place a 1 into the corresponding column
for index, row in dataframe_movies.iterrows():
    for category in row['categorys_class']:
        moviesWithcategorys_class_df.at[index, category] = 1
#Filling in the NaN values with 0 to show that a movie doesn't have that column's category
moviesWithcategorys_class_df = moviesWithcategorys_class_df.fillna(0)
moviesWithcategorys_class_df.head()
Out[7]:
Id_movie name categorys_class year Adventure Animation Children Comedy Fantasy Romance ... Horror Mystery Sci-Fi IMAX Documentary War Musical Western Film-Noir (no categorys_class 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

5 rows × 24 columns

Next, let's look at the rating_scores dataframe.

In [8]:
rating_scores_df.head()
Out[8]:
userId Id_movie rating_score 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

As we do not require it, we will be removing timestamp from df

In [9]:
#Drop removes a specified row or column from a dataframe
rating_scores_df = rating_scores_df.drop('timestamp', 1)
rating_scores_df.head()
Out[9]:
userId Id_movie rating_score
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

Content-Based recommendation system

Now, let's take a look at how to implement Content-Based or Item-Item recommendation systems. This technique attempts to figure out what a user's favourite aspects of an item is, and then recommends items that present those aspects. In our case, we're going to try to figure out the input's favorite categorys_class from the movies and rating_scores given.

Let's begin by creating an input user to recommend movies to:

Notice: To add more movies, simply increase the amount of elements in the userInput. Feel free to add more in! Just be sure to write it in with capital letters and if a movie starts with a "The", like "The Matrix" then write it in like this: 'Matrix, The' .

In [10]:
userInput = [
            {'name':'Dinner', 'rating_score':6},
            {'name':'Titanic', 'rating_score':5},
            {'name':'Notebook', 'rating_score':2},
            {'name':"Inception", 'rating_score':5},
            {'name':'Fast2Furious', 'rating_score':4}
         ] 
movie_input = pd.DataFrame(userInput)
movie_input
Out[10]:
name rating_score
0 Breakfast Club, The 5.0
1 Toy Story 3.5
2 Jumanji 2.0
3 Pulp Fiction 5.0
4 Akira 4.5

Add Id_movie to input user

With the input finalised, let us extract the ID of the input movie from the dataframe df and add the ID into it.

We can now achieve this by 1st filtering out the rows which contain the input_movie's name and then merging this subset with the input df dataframe. Also, we can drop not-required columns from the input to save some memory.

In [11]:
# searching movies by name
inputId = dataframe_movies[dataframe_movies['name'].isin(movie_input['name'].tolist())]
# merging it to get the Id_movie.
movie_input = pd.merge(inputId, movie_input)
# Deleting columns that are not required
movie_input = movie_input.drop('categorys_class', 1).drop('year', 1)
#Final input dataframe
movie_input
Out[11]:
Id_movie name rating_score
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

We're going to start by learning the input's preferences, so let's get the subset of movies that the input has watched from the Dataframe containing categorys_class defined with binary values.

In [12]:
#Filtering out the movies from the input
userMovies = moviesWithcategorys_class_df[moviesWithcategorys_class_df['Id_movie'].isin(movie_input['Id_movie'].tolist())]
userMovies
Out[12]:
Id_movie name categorys_class year Adventure Animation Children Comedy Fantasy Romance ... Horror Mystery Sci-Fi IMAX Documentary War Musical Western Film-Noir (no categorys_class 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

5 rows × 24 columns

We'll only need the actual category table, so let's clean this up a bit by resetting the index and dropping the Id_movie, name, categorys_class and year columns.

In [13]:
#Resetting the index to avoid future issues
userMovies = userMovies.reset_index(drop=True)
#Dropping unnecessary issues due to save memory and to avoid issues
usertable_category = userMovies.drop('Id_movie', 1).drop('name', 1).drop('categorys_class', 1).drop('year', 1)
usertable_category
Out[13]:
Adventure Animation Children Comedy Fantasy Romance Drama Action Crime Thriller Horror Mystery Sci-Fi IMAX Documentary War Musical Western Film-Noir (no categorys_class 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

Now we're ready to start learning the input's preferences!

To do this, we're going to transform all category into respective weights. We can do this by using the input's reviews and multiplying them into the input's category table and then summing up the resulting table by column. This operation is actually a dot product between a matrix and a vector, so we can simply accomplish by calling Pandas's "dot" function.

In [14]:
movie_input['rating_score']
Out[14]:
0    3.5
1    2.0
2    5.0
3    4.5
4    5.0
Name: rating_score, dtype: float64
In [15]:
#Dot produt to get weights
userProfile = usertable_category.transpose().dot(movie_input['rating_score'])
#The user profile
userProfile
Out[15]:
Drama                 9.99
Adventure             10.0
Children               5.5
Comedy                13.5
Animation              8.0
Fantasy                5.5
Romance                0.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 categorys_class listed)     0.0
dtype: float64

After this step, we have weights of each of the user's movie choices, we term this as the User's Profile. By utilizing this information, we can suggest movies that satisfy the user's preferences.

Let's start by extracting the category table from the original dataframe:

In [16]:
#Now let's get the categorys_class of every movie in our original dataframe
table_category = moviesWithcategorys_class_df.set_index(moviesWithcategorys_class_df['Id_movie'])
#And drop the unnecessary information
table_category = table_category.drop('Id_movie', 1).drop('name', 1).drop('categorys_class', 1).drop('year', 1)
table_category.head()
Out[16]:
Adventure Animation Children Comedy Fantasy Romance Drama Action Crime Thriller Horror Mystery Sci-Fi IMAX Documentary War Musical Western Film-Noir (no categorys_class listed)
Id_movie
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
In [17]:
table_category.shape
Out[17]:
(34208, 20)

As we have the profile of the input and the complete list of movies and their categorys_class in hand, we are not going to take the weighted average of each movie with respect to the input profile and recommend the top twenty movies that most satisfy it.

In [18]:
#Multiply the categorys_class by the weights and then take the weighted average
table_df_recomendation = ((table_category*userProfile).sum(axis=1))/(userProfile.sum())
table_df_recomendation.head()
Out[18]:
Id_movie
1    0.594406
2    0.293706
3    0.188811
4    0.328671
5    0.188811
dtype: float64
In [19]:
# Now we will sort our recommendations in descending order
table_df_recomendation = table_df_recomendation.sort_values(ascending=False)
#Just a peek at the values
table_df_recomendation.head()
Out[19]:
Id_movie
5018      0.748252
26093     0.734266
27344     0.720280
148775    0.685315
6902      0.678322
dtype: float64

Now here's the recommendation table!

In [20]:
#The final recommendation table
dataframe_movies.loc[dataframe_movies['Id_movie'].isin(table_df_recomendation.head(20).keys())]
Out[20]:
Id_movie name categorys_class 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

Advantages and Disadvantages of Content-Based Filtering

Advantages
  • Learns user's preferences
  • Highly personalized for the user
Disadvantages
  • 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