Skip to the content.

Project - Recommendation Systems: Movie Recommendation System

Marks: 40


Context


Online streaming platforms like Netflix have plenty of movies in their repository and if we can build a Recommendation System to recommend relevant movies to users, based on their historical interactions, this would improve customer satisfaction and hence, it will also improve the revenue of the platform. The techniques that we will learn here will not only be limited to movies, it can be any item for which you want to build a recommendation system.


Objective


In this project we will be building various recommendation systems:

we are going to use the ratings dataset.


Dataset


The ratings dataset contains the following attributes:

Sometimes, the installation of the surprise library, which is used to build recommendation systems, faces issues in Jupyter. To avoid any issues, it is advised to use Google Colab for this case study.

Let’s start by mounting the Google drive on Colab.

# uncomment if you are using google colab

from google.colab import drive
drive.mount('/content/drive')
Mounted at /content/drive

Installing surprise library

# Installing surprise library, only do it for first time
!pip install surprise
Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/
Collecting surprise
  Downloading surprise-0.1-py2.py3-none-any.whl (1.8 kB)
Collecting scikit-surprise
  Downloading scikit-surprise-1.1.3.tar.gz (771 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 772.0/772.0 kB 12.4 MB/s eta 0:00:00
[?25h  Preparing metadata (setup.py) ... [?25l[?25hdone
Requirement already satisfied: joblib>=1.0.0 in /usr/local/lib/python3.9/dist-packages (from scikit-surprise->surprise) (1.2.0)
Requirement already satisfied: numpy>=1.17.3 in /usr/local/lib/python3.9/dist-packages (from scikit-surprise->surprise) (1.22.4)
Requirement already satisfied: scipy>=1.3.2 in /usr/local/lib/python3.9/dist-packages (from scikit-surprise->surprise) (1.10.1)
Building wheels for collected packages: scikit-surprise
  Building wheel for scikit-surprise (setup.py) ... [?25l[?25hdone
  Created wheel for scikit-surprise: filename=scikit_surprise-1.1.3-cp39-cp39-linux_x86_64.whl size=3195832 sha256=8f3babac20ecd6bc40c9d8f4d366c5014960af1be5c00488b81dcb0a28c0888b
  Stored in directory: /root/.cache/pip/wheels/c6/3a/46/9b17b3512bdf283c6cb84f59929cdd5199d4e754d596d22784
Successfully built scikit-surprise
Installing collected packages: scikit-surprise, surprise
Successfully installed scikit-surprise-1.1.3 surprise-0.1

Importing the necessary libraries and overview of the dataset

# Used to ignore the warning given as output of the code
import warnings                                 
warnings.filterwarnings('ignore')

# Basic libraries of python for numeric and dataframe computations
import numpy as np                              
import pandas as pd

# Basic library for data visualization
import matplotlib.pyplot as plt     

# Slightly advanced library for data visualization            
import seaborn as sns                           

# A dictionary output that does not raise a key error
from collections import defaultdict             

# A performance metrics in surprise
from surprise import accuracy

# Class is used to parse a file containing ratings, data should be in structure - user ; item ; rating
from surprise.reader import Reader

# Class for loading datasets
from surprise.dataset import Dataset

# For model tuning model hyper-parameters
from surprise.model_selection import GridSearchCV

# For splitting the rating data in train and test dataset
from surprise.model_selection import train_test_split

# For implementing similarity based recommendation system
from surprise.prediction_algorithms.knns import KNNBasic

# For implementing matrix factorization based recommendation system
from surprise.prediction_algorithms.matrix_factorization import SVD

# For implementing cross validation
from surprise.model_selection import KFold

Loading the data

# Import the dataset
#rating = pd.read_csv('ratings.csv')
rating = pd.read_csv('/content/drive/MyDrive/ratings.csv') # Uncomment this line code  and comment above line of code if you are using google colab.

Let’s check the info of the data

rating.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 100004 entries, 0 to 100003
Data columns (total 4 columns):
 #   Column     Non-Null Count   Dtype  
---  ------     --------------   -----  
 0   userId     100004 non-null  int64  
 1   movieId    100004 non-null  int64  
 2   rating     100004 non-null  float64
 3   timestamp  100004 non-null  int64  
dtypes: float64(1), int64(3)
memory usage: 3.1 MB
# Dropping timestamp column
rating = rating.drop(['timestamp'], axis=1)

Question 1: Exploring the dataset (7 Marks)

Let’s explore the dataset and answer some basic data-related questions:

###Q 1.1 Print the top 5 rows of the dataset (1 Mark)

# Printing the top 5 rows of the dataset Hint: use .head()

# Remove _______and complete the code
rating.head()
userId movieId rating
0 1 31 2.5
1 1 1029 3.0
2 1 1061 3.0
3 1 1129 2.0
4 1 1172 4.0

Q 1.2 Describe the distribution of ratings. (1 Mark)

plt.figure(figsize = (12, 4))

# Remove _______and complete the code
sns.countplot(x="rating", data=rating)

plt.tick_params(labelsize = 10)
plt.title("Distribution of Ratings ", fontsize = 10)
plt.xlabel("Ratings", fontsize = 10)
plt.ylabel("Number of Ratings", fontsize = 10)
plt.show()

png

Write your Answer here:

The highest counts of ratings are for integer ratings. The highest is for rating 4.0 (>25k), followed by rating 3.0 (∼20k) and 5.0 (∼15k). The distribution is left-skewed. There are more high ratings than very low ratings.

Q 1.3 What is the total number of unique users and unique movies? (1 Mark)

# Finding number of unique users
#remove _______ and complete the code
rating['userId'].nunique()
671

Write your answer here:

There are 671 unique users.

# Finding number of unique movies
# Remove _______ and complete the code

rating['movieId'].nunique()
9066

Write your answer here:

There are 9066 unique movies.

Q 1.4 Is there a movie in which the same user interacted with it more than once? (1 Mark)

rating.groupby(['userId', 'movieId']).count()
rating
userId movieId
1 31 1
1029 1
1061 1
1129 1
1172 1
... ... ...
671 6268 1
6269 1
6365 1
6385 1
6565 1

100004 rows × 1 columns

rating.groupby(['userId', 'movieId']).count()['rating'].sum()
100004

Write your Answer here:

No, there is no movie where the same user interact with it more than once, because the sum of ratings (100004) is the same as the total number of user-movie interactions.

Q 1.5 Which is the most interacted movie in the dataset? (1 Mark)

# Remove _______ and complete the code
rating['movieId'].value_counts()
356       341
296       324
318       311
593       304
260       291
         ... 
98604       1
103659      1
104419      1
115927      1
6425        1
Name: movieId, Length: 9066, dtype: int64

Write your Answer here:

The movie with id 356 is the most interacted movie in the dataset with 341 ratings. This number is just a fraction of the 671 unique users. 671-341= 330 users could still interact with that movie.

# Plotting distributions of ratings for 341 interactions with movieid 356 
plt.figure(figsize=(7,7))

rating[rating['movieId'] == 356]['rating'].value_counts().plot(kind='bar')

plt.xlabel('Rating')

plt.ylabel('Count')

plt.show()

png

Write your Answer here:______

Q 1.6 Which user interacted the most with any movie in the dataset? (1 Mark)

# Remove _______ and complete the code
rating['userId'].value_counts()
547    2391
564    1868
624    1735
15     1700
73     1610
       ... 
296      20
289      20
249      20
221      20
1        20
Name: userId, Length: 671, dtype: int64

Write your Answer here:

The user with id 547 interacted the most with 2391 movies in the dataset.

Q 1.7 What is the distribution of the user-movie interactions in this dataset? (1 Mark)

# Finding user-movie interactions distribution
count_interactions = rating.groupby('userId').count()['movieId']
count_interactions
userId
1       20
2       76
3       51
4      204
5      100
      ... 
667     68
668     20
669     37
670     31
671    115
Name: movieId, Length: 671, dtype: int64
# Plotting user-movie interactions distribution

plt.figure(figsize=(15,7))
# Remove _______ and complete the code

sns.histplot(count_interactions)

plt.xlabel('Number of Interactions by Users')

plt.show()

png

Write your Answer here:

The distribution of user-movie interactions is very right-skewed. Most of the users interacted with less than 200 movies and very few with more than 500 movies.

As we have now explored the data, let’s start building Recommendation systems

Question 2: Create Rank-Based Recommendation System (3 Marks)

Model 1: Rank-Based Recommendation System

Rank-based recommendation systems provide recommendations based on the most popular items. This kind of recommendation system is useful when we have cold start problems. Cold start refers to the issue when we get a new user into the system and the machine is not able to recommend movies to the new user, as the user did not have any historical interactions in the dataset. In those cases, we can use rank-based recommendation system to recommend movies to the new user.

To build the rank-based recommendation system, we take average of all the ratings provided to each movie and then rank them based on their average rating.

# Remove _______ and complete the code

# Calculating average ratings
average_rating = rating.groupby('movieId').mean()['rating']

# Calculating the count of ratings
count_rating = rating.groupby('movieId').count()['rating']

# Making a dataframe with the count and average of ratings
final_rating = pd.DataFrame({'avg_rating':average_rating, 'rating_count':count_rating})
final_rating.head()
avg_rating rating_count
movieId
1 3.872470 247
2 3.401869 107
3 3.161017 59
4 2.384615 13
5 3.267857 56

Now, let’s create a function to find the top n movies for a recommendation based on the average ratings of movies. We can also add a threshold for a minimum number of interactions for a movie to be considered for recommendation.

def top_n_movies(data, n, min_interaction=100):
    
    #Finding movies with minimum number of interactions
    recommendations = data[data['rating_count'] >= min_interaction]
    
    #Sorting values w.r.t average rating 
    recommendations = recommendations.sort_values(by='avg_rating', ascending=False)
    
    return recommendations.index[:n]

We can use this function with different n’s and minimum interactions to get movies to recommend

Recommending top 5 movies with 50 minimum interactions based on popularity

# Remove _______ and complete the code
list(top_n_movies(final_rating, 5, 50))
[858, 318, 969, 913, 1221]

Recommending top 5 movies with 100 minimum interactions based on popularity

# Remove _______ and complete the code
list(top_n_movies(final_rating, 5, 100))
[858, 318, 1221, 50, 527]

Recommending top 5 movies with 200 minimum interactions based on popularity

# Remove _______ and complete the code
list(top_n_movies(final_rating, 5, 200))
[858, 318, 50, 527, 608]

Now that we have seen how to apply the Rank-Based Recommendation System, let’s apply the Collaborative Filtering Based Recommendation Systems.

Model 2: User based Collaborative Filtering Recommendation System (7 Marks)

collaborative_filtering.PNG

In the above interactions matrix, out of users B and C, which user is most likely to interact with the movie, “The Terminal”?

In this type of recommendation system, we do not need any information about the users or items. We only need user item interaction data to build a collaborative recommendation system. For example -

  1. Ratings provided by users. For example - ratings of books on goodread, movie ratings on imdb etc
  2. Likes of users on different facebook posts, likes on youtube videos
  3. Use/buying of a product by users. For example - buying different items on e-commerce sites
  4. Reading of articles by readers on various blogs

Types of Collaborative Filtering

Building Similarity/Neighborhood based Collaborative Filtering

test_image

Building a baseline user-user similarity based recommendation system

Below we are loading the rating dataset, which is a pandas DataFrame, into a different format called surprise.dataset.DatasetAutoFolds, which is required by this library. To do this, we will be using the classes Reader and Dataset. Finally splitting the data into train and test set.

Making the dataset into surprise dataset and splitting it into train and test set

# Instantiating Reader scale with expected rating scale
reader = Reader(rating_scale=(0, 5))

# Loading the rating dataset
data = Dataset.load_from_df(rating[['userId', 'movieId', 'rating']], reader)

# Splitting the data into train and test dataset
trainset, testset = train_test_split(data, test_size=0.2, random_state=42)

Build the first baseline similarity based recommendation system using cosine similarity and KNN

# Remove _______ and complete the code

# Defining Nearest neighbour algorithm

sim_options = {'name': 'cosine', 'user_based': True}
algo_knn_user = KNNBasic(sim_options=sim_options, verbose=False)

# Train the algorithm on the trainset or fitting the model on train dataset 
algo_knn_user.fit(trainset)

# Predict ratings for the testset
predictions = algo_knn_user.test(testset)

# Then compute RMSE
accuracy.rmse(predictions)
RMSE: 0.9925





0.9924509041520163

Q 3.1 What is the RMSE for baseline user based collaborative filtering recommendation system? (1 Mark)

Write your Answer here:

On test set the baseline user based collaborative filtering recommandation system has RMSE≈0.99.

Q 3.2 What is the Predicted rating for an user with userId=4 and for movieId=10 and movieId=3? (1 Mark)

Let’s us now predict rating for an user with userId=4 and for movieId=10

# Remove _______ and complete the code
algo_knn_user.predict(4, 10, r_ui=4, verbose=True)
user: 4          item: 10         r_ui = 4.00   est = 3.62   {'actual_k': 40, 'was_impossible': False}





Prediction(uid=4, iid=10, r_ui=4, est=3.6244912065910952, details={'actual_k': 40, 'was_impossible': False})

Write your Answer here:

User with id 4 has already interacted with movie with id 10 with a rating of 4.0. The estimated rating from this recommendation system is 3.62.

Let’s predict the rating for the same userId=4 but for a movie which this user has not interacted before i.e. movieId=3

# Remove _______ and complete the code
algo_knn_user.predict(4, 3, verbose=True)
user: 4          item: 3          r_ui = None   est = 3.20   {'actual_k': 40, 'was_impossible': False}





Prediction(uid=4, iid=3, r_ui=None, est=3.202703552548654, details={'actual_k': 40, 'was_impossible': False})

Write your Answer here:

User with id 4 has not interacted yet with movie with id 3. The estimated rating coming from this recommendation system is 3.20.

Improving user-user similarity based recommendation system by tuning its hyper-parameters

Below we will be tuning hyper-parmeters for the KNNBasic algorithms. Let’s try to understand different hyperparameters of KNNBasic algorithm -

For more details please refer the official documentation https://surprise.readthedocs.io/en/stable/knn_inspired.html

Q 3.3 Perform hyperparameter tuning for the baseline user based collaborative filtering recommendation system and find the RMSE for tuned user based collaborative filtering recommendation system? (3 Marks)

# Remove _______ and complete the code

# Setting up parameter grid to tune the hyperparameters
param_grid = {'k': [20,30,40], 'min_k': [1,3,6,9], 'sim_options': {'name':['msd','cosine'], 'user_based':[True]}}

# Performing 3-fold cross validation to tune the hyperparameters
grid_obj = GridSearchCV(KNNBasic, param_grid, measures=['rmse', 'mae'], cv=3, n_jobs=-1)

# Fitting the data
grid_obj.fit(data)

# Best RMSE score
print(grid_obj.best_score['rmse'])

# Combination of parameters that gave the best RMSE score
print(grid_obj.best_params['rmse'])
0.965646132311634
{'k': 20, 'min_k': 3, 'sim_options': {'name': 'msd', 'user_based': True}}

Once the grid search is complete, we can get the optimal values for each of those hyperparameters as shown above.

Below we are analysing evaluation metrics - RMSE and MAE at each and every split to analyze the impact of each value of hyperparameters

results_df = pd.DataFrame.from_dict(grid_obj.cv_results)
results_df.head()
split0_test_rmse split1_test_rmse split2_test_rmse mean_test_rmse std_test_rmse rank_test_rmse split0_test_mae split1_test_mae split2_test_mae mean_test_mae std_test_mae rank_test_mae mean_fit_time std_fit_time mean_test_time std_test_time params param_k param_min_k param_sim_options
0 0.978240 0.975281 0.975510 0.976344 0.001344 8 0.750479 0.747570 0.748425 0.748825 0.001221 6 0.104516 0.002837 2.726556 0.035860 {'k': 20, 'min_k': 1, 'sim_options': {'name': ... 20 1 {'name': 'msd', 'user_based': True}
1 1.005711 1.003892 1.002947 1.004183 0.001147 24 0.775955 0.774435 0.773293 0.774561 0.001091 24 0.214824 0.018782 2.719341 0.030162 {'k': 20, 'min_k': 1, 'sim_options': {'name': ... 20 1 {'name': 'cosine', 'user_based': True}
2 0.963572 0.965565 0.967802 0.965646 0.001728 1 0.742218 0.741581 0.743573 0.742457 0.000831 1 0.212198 0.051202 4.262047 1.046854 {'k': 20, 'min_k': 3, 'sim_options': {'name': ... 20 3 {'name': 'msd', 'user_based': True}
3 0.990955 0.994063 0.995241 0.993420 0.001808 17 0.767455 0.768192 0.768272 0.767973 0.000368 15 0.247518 0.017589 2.738646 0.015598 {'k': 20, 'min_k': 3, 'sim_options': {'name': ... 20 3 {'name': 'cosine', 'user_based': True}
4 0.967522 0.968595 0.970320 0.968812 0.001153 3 0.745046 0.743944 0.745736 0.744909 0.000738 2 0.128241 0.026238 3.353801 0.889977 {'k': 20, 'min_k': 6, 'sim_options': {'name': ... 20 6 {'name': 'msd', 'user_based': True}

Now, let’s build the final model by using tuned values of the hyperparameters, which we received by using grid search cross-validation.

# Remove _______ and complete the code

# Using the optimal similarity measure for user-user based collaborative filtering
# Creating an instance of KNNBasic with optimal hyperparameter values

sim_options = {'name': 'msd', 'user_based': True}
similarity_algo_optimized_user = KNNBasic(sim_options=sim_options, k=20, min_k=3,verbose=False)

# Training the algorithm on the trainset
similarity_algo_optimized_user.fit(trainset)

# Predicting ratings for the testset
predictions = similarity_algo_optimized_user.test(testset)

# Computing RMSE on testset
accuracy.rmse(predictions)
RMSE: 0.9571





0.9571445417153293

Write your Answer here:

The tuned user based collaborative filtering recommandation system has RMSE≈0.96. Compared to the baseline system (RMSE≈0.99) the RMSE has decreased. Therefore, after hyperparameter tuning the system has been improved.

Q 3.4 What is the Predicted rating for an user with userId =4 and for movieId= 10 and movieId=3 using tuned user based collaborative filtering? (1 Mark)

Let’s us now predict rating for an user with userId=4 and for movieId=10 with the optimized model

# Remove _______ and complete the code
similarity_algo_optimized_user.predict(4,10, r_ui=4, verbose=True)
user: 4          item: 10         r_ui = 4.00   est = 3.74   {'actual_k': 20, 'was_impossible': False}





Prediction(uid=4, iid=10, r_ui=4, est=3.740028692988536, details={'actual_k': 20, 'was_impossible': False})

Write your Answer here:

User with id 4 has already interacted with movie with id 10 with a rating of 4.0. The estimated rating from this tuned user based recommendation system is 3.74, better than the estimated rating from the baseline system (3.62).

Below we are predicting rating for the same userId=4 but for a movie which this user has not interacted before i.e. movieId=3, by using the optimized model as shown below -

# # Remove _______ and complete the code
similarity_algo_optimized_user.predict(4,3, verbose=True)
user: 4          item: 3          r_ui = None   est = 3.72   {'actual_k': 20, 'was_impossible': False}





Prediction(uid=4, iid=3, r_ui=None, est=3.7228745701935386, details={'actual_k': 20, 'was_impossible': False})

Write your Answer here:

User with id 4 has not interacted yet with movie with id 3. The estimated rating coming from the tuned user based recommendation system is 3.72. With the baseline system the estimated rating is 3.20.

Identifying similar users to a given user (nearest neighbors)

We can also find out the similar users to a given user or its nearest neighbors based on this KNNBasic algorithm. Below we are finding 5 most similar user to the userId=4 based on the msd distance metric

similarity_algo_optimized_user.get_neighbors(4, k=5)
[665, 417, 647, 654, 260]

Implementing the recommendation algorithm based on optimized KNNBasic model

Below we will be implementing a function where the input parameters are -

def get_recommendations(data, user_id, top_n, algo):
    
    # Creating an empty list to store the recommended movie ids
    recommendations = []
    
    # Creating an user item interactions matrix 
    user_item_interactions_matrix = data.pivot(index='userId', columns='movieId', values='rating')
    
    # Extracting those movie ids which the user_id has not interacted yet
    non_interacted_movies = user_item_interactions_matrix.loc[user_id][user_item_interactions_matrix.loc[user_id].isnull()].index.tolist()
    
    # Looping through each of the movie id which user_id has not interacted yet
    for item_id in non_interacted_movies:
        
        # Predicting the ratings for those non interacted movie ids by this user
        est = algo.predict(user_id, item_id).est
        
        # Appending the predicted ratings
        recommendations.append((item_id, est))

    # Sorting the predicted ratings in descending order
    recommendations.sort(key=lambda x: x[1], reverse=True)

    return recommendations[:top_n] # returing top n highest predicted rating movies for this user

Predicted top 5 movies for userId=4 with similarity based recommendation system

#remove _______ and complete the code
recommendations = get_recommendations(rating, 4, 5, similarity_algo_optimized_user)

Q 3.5 Predict the top 5 movies for userId=4 with similarity based recommendation system (1 Mark)

recommendations
[(309, 5),
 (3038, 5),
 (6273, 4.928202652354184),
 (98491, 4.863224466679252),
 (2721, 4.845513973527148)]

Model 3: Item based Collaborative Filtering Recommendation System (7 Marks)

# Remove _______ and complete the code

# Definfing similarity measure
sim_options = {'name': 'cosine',
               'user_based': False}

# Defining Nearest neighbour algorithm
algo_knn_item = KNNBasic(sim_options=sim_options,verbose=False)

# Train the algorithm on the trainset or fitting the model on train dataset 
algo_knn_item.fit(trainset)

# Predict ratings for the testset
predictions = algo_knn_item.test(testset)

# Then compute RMSE
accuracy.rmse(predictions)
RMSE: 1.0032





1.003221450633729

Q 4.1 What is the RMSE for baseline item based collaborative filtering recommendation system ?(1 Mark)

Write your Answer here:

On the test set the baseline item based collaborative filtering recommendation system has RMSE≈1.00.

Let’s us now predict rating for an user with userId=4 and for movieId=10

Q 4.2 What is the Predicted rating for an user with userId =4 and for movieId= 10 and movieId=3? (1 Mark)

# Remove _______ and complete the code
algo_knn_item.predict(4,10, r_ui=4, verbose=True)
user: 4          item: 10         r_ui = 4.00   est = 4.37   {'actual_k': 40, 'was_impossible': False}





Prediction(uid=4, iid=10, r_ui=4, est=4.373794871885004, details={'actual_k': 40, 'was_impossible': False})

Write your Answer here:

User with id 4 has already interacted with movie with id 10 with a rating of 4.0. The estimated rating from this recommendation system is 4.37.

Let’s predict the rating for the same userId=4 but for a movie which this user has not interacted before i.e. movieId=3

# Remove _______ and complete the code
algo_knn_item.predict(4,3, verbose=True)
user: 4          item: 3          r_ui = None   est = 4.07   {'actual_k': 40, 'was_impossible': False}





Prediction(uid=4, iid=3, r_ui=None, est=4.071601862880049, details={'actual_k': 40, 'was_impossible': False})

Write your Answer here:

User with id 4 has not interacted yet with movie with id 3. The estimated rating from this recommendation system is 4.07.

Q 4.3 Perform hyperparameter tuning for the baseline item based collaborative filtering recommendation system and find the RMSE for tuned item based collaborative filtering recommendation system? (3 Marks)

# Remove _______ and complete the code

# Setting up parameter grid to tune the hyperparameters
param_grid = {'k': [20,30,40], 'min_k': [1,3,6,9], 'sim_options': {'name':['msd','cosine'], 'user_based':[False]}}

# Performing 3-fold cross validation to tune the hyperparameters
grid_obj = GridSearchCV(KNNBasic, param_grid, measures=['rmse', 'mae'], cv=3, n_jobs=-1)

# Fitting the data
grid_obj.fit(data)

# Best RMSE score
print(grid_obj.best_score['rmse'])

# Combination of parameters that gave the best RMSE score
print(grid_obj.best_params['rmse'])
0.9396898494196343
{'k': 40, 'min_k': 3, 'sim_options': {'name': 'msd', 'user_based': False}}

Once the grid search is complete, we can get the optimal values for each of those hyperparameters as shown above

Below we are analysing evaluation metrics - RMSE and MAE at each and every split to analyze the impact of each value of hyperparameters

results_df = pd.DataFrame.from_dict(grid_obj.cv_results)
results_df.head()
split0_test_rmse split1_test_rmse split2_test_rmse mean_test_rmse std_test_rmse rank_test_rmse split0_test_mae split1_test_mae split2_test_mae mean_test_mae std_test_mae rank_test_mae mean_fit_time std_fit_time mean_test_time std_test_time params param_k param_min_k param_sim_options
0 0.948193 0.951402 0.952476 0.950690 0.001819 10 0.731816 0.734507 0.734481 0.733601 0.001263 9 4.235982 0.619689 15.547943 2.610945 {'k': 20, 'min_k': 1, 'sim_options': {'name': ... 20 1 {'name': 'msd', 'user_based': False}
1 1.011516 1.014836 1.012522 1.012958 0.001390 22 0.789859 0.791114 0.789257 0.790077 0.000774 21 7.637552 1.670468 16.183732 3.388851 {'k': 20, 'min_k': 1, 'sim_options': {'name': ... 20 1 {'name': 'cosine', 'user_based': False}
2 0.948333 0.950857 0.952367 0.950519 0.001664 9 0.732038 0.734334 0.734687 0.733686 0.001174 10 4.401310 0.473360 15.770335 1.387262 {'k': 20, 'min_k': 3, 'sim_options': {'name': ... 20 3 {'name': 'msd', 'user_based': False}
3 1.011642 1.014368 1.012477 1.012829 0.001140 21 0.790074 0.790992 0.789489 0.790185 0.000619 22 6.292680 0.540871 12.983727 0.218869 {'k': 20, 'min_k': 3, 'sim_options': {'name': ... 20 3 {'name': 'cosine', 'user_based': False}
4 0.948333 0.951057 0.953210 0.950867 0.001996 11 0.732084 0.734551 0.735015 0.733883 0.001287 11 4.428361 1.679174 19.724888 3.229316 {'k': 20, 'min_k': 6, 'sim_options': {'name': ... 20 6 {'name': 'msd', 'user_based': False}

Now let’s build the final model by using tuned values of the hyperparameters which we received by using grid search cross-validation.

# Remove _______ and complete the code
# Creating an instance of KNNBasic with optimal hyperparameter values
similarity_algo_optimized_item = KNNBasic(sim_options={'name': 'msd', 'user_based': False}, k=40, min_k=3,verbose=False)

# Training the algorithm on the trainset
similarity_algo_optimized_item.fit(trainset)

# Predicting ratings for the testset
predictions = similarity_algo_optimized_item.test(testset)

# Computing RMSE on testset
accuracy.rmse(predictions)
RMSE: 0.9433





0.9433184999641279

Write your Answer here:

The tuned item based collaborative filtering recommandation system has RMSE≈0.94. Compared to the baseline system (RMSE≈1.00) the RMSE has decreased. Therefore, after hyperparameter tuning the system has been improved.

Q 4.4 What is the Predicted rating for an item with userId =4 and for movieId= 10 and movieId=3 using tuned item based collaborative filtering? (1 Mark)

Let’s us now predict rating for an user with userId=4 and for movieId=10 with the optimized model as shown below

# Remove _______ and complete the code
similarity_algo_optimized_item.predict(4,10, r_ui=4, verbose=True)
user: 4          item: 10         r_ui = 4.00   est = 4.26   {'actual_k': 40, 'was_impossible': False}





Prediction(uid=4, iid=10, r_ui=4, est=4.255054787154994, details={'actual_k': 40, 'was_impossible': False})

Write your Answer here:

The estimated rating from this tuned item based recommendation system is 4.26, better than the estimated rating from the baseline system (4.37).

Let’s predict the rating for the same userId=4 but for a movie which this user has not interacted before i.e. movieId=3, by using the optimized model:

# Remove _______ and complete the code
similarity_algo_optimized_item.predict(4, 3, verbose=True)
user: 4          item: 3          r_ui = None   est = 3.87   {'actual_k': 40, 'was_impossible': False}





Prediction(uid=4, iid=3, r_ui=None, est=3.865175609312417, details={'actual_k': 40, 'was_impossible': False})

Write your Answer here:

The estimated rating coming from the tuned item based recommendation system is 3.87. With the baseline system the estimated rating is 4.07.

Identifying similar items to a given item (nearest neighbors)

We can also find out the similar items to a given item or its nearest neighbors based on this KNNBasic algorithm. Below we are finding 5 most similar items to the movieId=3 based on the msd distance metric

# Remove _______ and complete the code
similarity_algo_optimized_item.get_neighbors(3, k=5)
[31, 37, 42, 48, 73]

Predicted top 5 movies for userId=4 with similarity based recommendation system

# Remove _______ and complete the code
recommendations = get_recommendations(rating, 4, 5, similarity_algo_optimized_item)

Q 4.5 Predict the top 5 movies for userId=4 with similarity based recommendation system (1 Mark)

recommendations
[(84, 5), (1040, 5), (2481, 5), (3078, 5), (3116, 5)]

Model 4: Based Collaborative Filtering - Matrix Factorization using SVD (7 Marks)

Model-based Collaborative Filtering is a personalized recommendation system, the recommendations are based on the past behavior of the user and it is not dependent on any additional information. We use latent features to find recommendations for each user.

Latent Features: The features that are not present in the empirical data but can be inferred from the data. For example:

test_image

Now if we notice the above movies closely:

test_image

Here Action, Romance, Suspense and Comedy are latent features of the corresponding movies. Similarly, we can compute the latent features for users as shown below:

test_image

Singular Value Decomposition (SVD)

SVD is used to compute the latent features from the user-item matrix. But SVD does not work when we miss values in the user-item matrix.

First we need to convert the below movie-rating dataset:

test_image

into an user-item matrix as shown below:

test_image

We have already done this above while computing cosine similarities.

SVD decomposes this above matrix into three separate matrices:

U-matrix

test_image

the above matrix is a n x k matrix, where:

Sigma-matrix

test_image

the above matrix is a k x k matrix, where:

V-transpose matrix

test_image

the above matrix is a kxn matrix, where:

Build a baseline matrix factorization recommendation system

# Remove _______ and complete the code

# Using SVD matrix factorization
algo_svd = SVD()

# Training the algorithm on the trainset
algo_svd.fit(trainset)

# Predicting ratings for the testset
predictions = algo_svd.test(testset)

# Computing RMSE on the testset
accuracy.rmse(predictions)
RMSE: 0.9029





0.9029014689887866

Q 5.1 What is the RMSE for baseline SVD based collaborative filtering recommendation system? (1 Mark)

Write your Answer here:

The baseline SVD based collaborative filtering recommendation system has a RMSE≈0.90 on the test set. This is the lowest RMSE that we have encountered so far: it is less than the tuned user based collaborative filtering recommendation system (RMSE≈0.96) and the tuned item based collaborative filtering recommendation system (RMSE≈0.94).

Q 5.2 What is the Predicted rating for an user with userId =4 and for movieId= 10 and movieId=3? (1 Mark)

Let’s us now predict rating for an user with userId=4 and for movieId=10

# Remove _______ and complete the code
algo_svd.predict(4, 10, r_ui=4, verbose=True)
user: 4          item: 10         r_ui = 4.00   est = 4.00   {'was_impossible': False}





Prediction(uid=4, iid=10, r_ui=4, est=4.004509633308034, details={'was_impossible': False})

Write your Answer here:

The true rating is 4.0. The estimated one is basically the same as the true one (4.0).

Let’s predict the rating for the same userId=4 but for a movie which this user has not interacted before i.e. movieId=3:

# Remove _______ and complete the code
algo_svd.predict(4, 3, verbose=True)
user: 4          item: 3          r_ui = None   est = 3.96   {'was_impossible': False}





Prediction(uid=4, iid=3, r_ui=None, est=3.9616820359367093, details={'was_impossible': False})

Write your Answer here:

The estimated rating for movie with id 3 is 3.96 with the use of the baseline SVD based collaborative filtering recommendation system.

Improving matrix factorization based recommendation system by tuning its hyper-parameters

In SVD, rating is predicted as -

\[\hat{r}_{u i}=\mu+b_{u}+b_{i}+q_{i}^{T} p_{u}\]

If user $u$ is unknown, then the bias $b_{u}$ and the factors $p_{u}$ are assumed to be zero. The same applies for item $i$ with $b_{i}$ and $q_{i}$.

To estimate all the unknown, we minimize the following regularized squared error:

\[\sum_{r_{u i} \in R_{\text {train }}}\left(r_{u i}-\hat{r}_{u i}\right)^{2}+\lambda\left(b_{i}^{2}+b_{u}^{2}+\left\|q_{i}\right\|^{2}+\left\|p_{u}\right\|^{2}\right)\]

The minimization is performed by a very straightforward stochastic gradient descent:

\[\begin{aligned} b_{u} & \leftarrow b_{u}+\gamma\left(e_{u i}-\lambda b_{u}\right) \\ b_{i} & \leftarrow b_{i}+\gamma\left(e_{u i}-\lambda b_{i}\right) \\ p_{u} & \leftarrow p_{u}+\gamma\left(e_{u i} \cdot q_{i}-\lambda p_{u}\right) \\ q_{i} & \leftarrow q_{i}+\gamma\left(e_{u i} \cdot p_{u}-\lambda q_{i}\right) \end{aligned}\]

There are many hyperparameters to tune in this algorithm, you can find a full list of hyperparameters here

Below we will be tuning only three hyperparameters -

Q 5.3 Perform hyperparameter tuning for the baseline SVD based collaborative filtering recommendation system and find the RMSE for tuned SVD based collaborative filtering recommendation system? (3 Marks)

# Remove _______ and complete the code

# Set the parameter space to tune
param_grid = {'n_epochs': [10, 20, 30], 'lr_all': [0.001, 0.005, 0.01],
              'reg_all': [0.2, 0.4, 0.6]}

# Performing 3-fold gridsearch cross validation
gs = GridSearchCV(SVD, param_grid, measures=['rmse', 'mae'], cv=3, n_jobs=-1)

# Fitting data
gs.fit(data)

# Best RMSE score
print(gs.best_score['rmse'])

# Combination of parameters that gave the best RMSE score
print(gs.best_params['rmse'])
0.8943041060540651
{'n_epochs': 30, 'lr_all': 0.01, 'reg_all': 0.2}

Once the grid search is complete, we can get the optimal values for each of those hyperparameters, as shown above.

Below we are analysing evaluation metrics - RMSE and MAE at each and every split to analyze the impact of each value of hyperparameters

results_df = pd.DataFrame.from_dict(gs.cv_results)
results_df.head()
split0_test_rmse split1_test_rmse split2_test_rmse mean_test_rmse std_test_rmse rank_test_rmse split0_test_mae split1_test_mae split2_test_mae mean_test_mae std_test_mae rank_test_mae mean_fit_time std_fit_time mean_test_time std_test_time params param_n_epochs param_lr_all param_reg_all
0 0.947087 0.935991 0.947257 0.943445 0.005271 25 0.739996 0.734454 0.740324 0.738258 0.002693 25 1.040736 0.298807 0.651955 0.134450 {'n_epochs': 10, 'lr_all': 0.001, 'reg_all': 0.2} 10 0.001 0.2
1 0.951797 0.940151 0.951333 0.947760 0.005384 26 0.745714 0.739233 0.745168 0.743372 0.002935 26 1.590924 0.074432 0.766977 0.223881 {'n_epochs': 10, 'lr_all': 0.001, 'reg_all': 0.4} 10 0.001 0.4
2 0.956860 0.945245 0.956954 0.953020 0.005498 27 0.751177 0.744883 0.751591 0.749217 0.003069 27 0.853958 0.054258 0.439016 0.034506 {'n_epochs': 10, 'lr_all': 0.001, 'reg_all': 0.6} 10 0.001 0.6
3 0.909925 0.900115 0.912273 0.907437 0.005266 10 0.704079 0.699159 0.704628 0.702622 0.002459 9 0.848723 0.064738 0.461821 0.033696 {'n_epochs': 10, 'lr_all': 0.005, 'reg_all': 0.2} 10 0.005 0.2
4 0.917267 0.906880 0.919373 0.914507 0.005461 15 0.712158 0.706661 0.712466 0.710428 0.002667 15 0.825620 0.024862 0.436168 0.013312 {'n_epochs': 10, 'lr_all': 0.005, 'reg_all': 0.4} 10 0.005 0.4

Now, we will the build final model by using tuned values of the hyperparameters, which we received using grid search cross-validation above.

# Remove _______ and complete the code

# Building the optimized SVD model using optimal hyperparameter search
svd_algo_optimized = SVD(n_epochs=30, lr_all=0.01, reg_all=0.2)

# Training the algorithm on the trainset
svd_algo_optimized.fit(trainset)

# Predicting ratings for the testset
predictions = svd_algo_optimized.test(testset)

# Computing RMSE
accuracy.rmse(predictions)
RMSE: 0.8955





0.895453182598101

Q 5.4 What is the Predicted rating for an user with userId =4 and for movieId= 10 and movieId=3 using SVD based collaborative filtering? (1 Mark)

Let’s us now predict rating for an user with userId=4 and for movieId=10 with the optimized model

# Remove _______ and complete the code
svd_algo_optimized.predict(4, 10, r_ui=4, verbose=True)
user: 4          item: 10         r_ui = 4.00   est = 3.99   {'was_impossible': False}





Prediction(uid=4, iid=10, r_ui=4, est=3.986677913286987, details={'was_impossible': False})

Write your Answer here:

The estimated rating for movie with id 10 is 3.99. It is basically the same as the true one (4.0).

Let’s predict the rating for the same userId=4 but for a movie which this user has not interacted before i.e. movieId=3:

# Remove _______ and complete the code
svd_algo_optimized.predict(4, 3, verbose=True)
user: 4          item: 3          r_ui = None   est = 3.63   {'was_impossible': False}





Prediction(uid=4, iid=3, r_ui=None, est=3.6287192073193566, details={'was_impossible': False})

Q 5.5 Predict the top 5 movies for userId=4 with SVD based recommendation system?(1 Mark)

# Remove _______ and complete the code
get_recommendations(rating, 4, 5, svd_algo_optimized)
[(1192, 5),
 (116, 4.951504150667473),
 (926, 4.9486031091710005),
 (5114, 4.943015803442837),
 (3310, 4.9267735930685035)]

Predicting ratings for already interacted movies

Below we are comparing the rating predictions of users for those movies which has been already watched by an user. This will help us to understand how well are predictions are as compared to the actual ratings provided by users

def predict_already_interacted_ratings(data, user_id, algo):
    
    # Creating an empty list to store the recommended movie ids
    recommendations = []
    
    # Creating an user item interactions matrix 
    user_item_interactions_matrix = data.pivot(index='userId', columns='movieId', values='rating')
    
    # Extracting those movie ids which the user_id has interacted already
    interacted_movies = user_item_interactions_matrix.loc[user_id][user_item_interactions_matrix.loc[user_id].notnull()].index.tolist()
    
    # Looping through each of the movie id which user_id has interacted already
    for item_id in interacted_movies:
        
        # Extracting actual ratings
        actual_rating = user_item_interactions_matrix.loc[user_id, item_id]
        
        # Predicting the ratings for those non interacted movie ids by this user
        predicted_rating = algo.predict(user_id, item_id).est
        
        # Appending the predicted ratings
        recommendations.append((item_id, actual_rating, predicted_rating))

    # Sorting the predicted ratings in descending order
    recommendations.sort(key=lambda x: x[1], reverse=True)

    return pd.DataFrame(recommendations, columns=['movieId', 'actual_rating', 'predicted_rating']) # returing top n highest predicted rating movies for this user

Here we are comparing the predicted ratings by similarity based recommendation system against actual ratings for userId=7

predicted_ratings_for_interacted_movies = predict_already_interacted_ratings(rating, 7, similarity_algo_optimized_item)
df = predicted_ratings_for_interacted_movies.melt(id_vars='movieId', value_vars=['actual_rating', 'predicted_rating'])
sns.displot(data=df, x='value', hue='variable', kde=True);

png

Write your Answer here:

For user 7 the distribution of the predicted ratings by the tuned item based recommendation system is more centered on the most common actual ratings (3.0 and 4.0). Most of the predicted ratings are between 3.0 and 4.0. They are in a continuous range while the actual ratings are discreet. This is because the predicted values are obtained by aggregating the ratings from the nearest neighbors of user 7.

Below we are comparing the predicted ratings by matrix factorization based recommendation system against actual ratings for userId=7

predicted_ratings_for_interacted_movies = predict_already_interacted_ratings(rating, 7, svd_algo_optimized)
df = predicted_ratings_for_interacted_movies.melt(id_vars='movieId', value_vars=['actual_rating', 'predicted_rating'])
sns.displot(data=df, x='value', hue='variable', kde=True);

png

# Instantiating Reader scale with expected rating scale
reader = Reader(rating_scale=(0, 5))

# Loading the rating dataset
data = Dataset.load_from_df(rating[['userId', 'movieId', 'rating']], reader)

# Splitting the data into train and test dataset
trainset, testset = train_test_split(data, test_size=0.2, random_state=42)

Precision and Recall @ k

RMSE is not the only metric we can use here. We can also examine two fundamental measures, precision and recall. We also add a parameter k which is helpful in understanding problems with multiple rating outputs.

Precision@k - It is the fraction of recommended items that are relevant in top k predictions. Value of k is the number of recommendations to be provided to the user. One can choose a variable number of recommendations to be given to a unique user.

Recall@k - It is the fraction of relevant items that are recommended to the user in top k predictions.

Recall - It is the fraction of actually relevant items that are recommended to the user i.e. if out of 10 relevant movies, 6 are recommended to the user then recall is 0.60. Higher the value of recall better is the model. It is one of the metrics to do the performance assessment of classification models.

Precision - It is the fraction of recommended items that are relevant actually i.e. if out of 10 recommended items, 6 are found relevant by the user then precision is 0.60. The higher the value of precision better is the model. It is one of the metrics to do the performance assessment of classification models.

See the Precision and Recall @ k section of your notebook and follow the instructions to compute various precision/recall values at various values of k.

To know more about precision recall in Recommendation systems refer to these links :

https://surprise.readthedocs.io/en/stable/FAQ.html

https://medium.com/@m_n_malaeb/recall-and-precision-at-k-for-recommender-systems-618483226c54

Question6: Compute the precision and recall, for each of the 6 models, at k = 5 and 10. This is 6 x 2 = 12 numerical values? (4 marks)

# Function can be found on surprise documentation FAQs
def precision_recall_at_k(predictions, k=10, threshold=3.5):
    """Return precision and recall at k metrics for each user"""

    # First map the predictions to each user.
    user_est_true = defaultdict(list)
    for uid, _, true_r, est, _ in predictions:
        user_est_true[uid].append((est, true_r))

    precisions = dict()
    recalls = dict()
    for uid, user_ratings in user_est_true.items():

        # Sort user ratings by estimated value
        user_ratings.sort(key=lambda x: x[0], reverse=True)

        # Number of relevant items
        n_rel = sum((true_r >= threshold) for (_, true_r) in user_ratings)

        # Number of recommended items in top k
        n_rec_k = sum((est >= threshold) for (est, _) in user_ratings[:k])

        # Number of relevant and recommended items in top k
        n_rel_and_rec_k = sum(((true_r >= threshold) and (est >= threshold))
                              for (est, true_r) in user_ratings[:k])

        # Precision@K: Proportion of recommended items that are relevant
        # When n_rec_k is 0, Precision is undefined. We here set it to 0.

        precisions[uid] = n_rel_and_rec_k / n_rec_k if n_rec_k != 0 else 0

        # Recall@K: Proportion of relevant items that are recommended
        # When n_rel is 0, Recall is undefined. We here set it to 0.

        recalls[uid] = n_rel_and_rec_k / n_rel if n_rel != 0 else 0

    return precisions, recalls


# A basic cross-validation iterator.
kf = KFold(n_splits=5)

# Make list of k values
K = [5, 10]

# Remove _______ and complete the code
# Make list of models
models = [algo_knn_user, similarity_algo_optimized_user, algo_knn_item, similarity_algo_optimized_item, algo_svd, svd_algo_optimized]

for k in K:
    for model in models:
        print('> k={}, model={}'.format(k,model.__class__.__name__))
        p = []
        r = []
        for trainset, testset in kf.split(data):
            model.fit(trainset)
            predictions = model.test(testset, verbose=False)
            precisions, recalls = precision_recall_at_k(predictions, k=k, threshold=3.5)

            # Precision and recall can then be averaged over all users
            p.append(sum(prec for prec in precisions.values()) / len(precisions))
            r.append(sum(rec for rec in recalls.values()) / len(recalls))
        
        print('-----> Precision: ', round(sum(p) / len(p), 3))
        print('-----> Recall: ', round(sum(r) / len(r), 3))
> k=5, model=KNNBasic
-----> Precision:  0.767
-----> Recall:  0.412
> k=5, model=KNNBasic
-----> Precision:  0.774
-----> Recall:  0.418
> k=5, model=KNNBasic
-----> Precision:  0.604
-----> Recall:  0.323
> k=5, model=KNNBasic
-----> Precision:  0.679
-----> Recall:  0.357
> k=5, model=SVD
-----> Precision:  0.748
-----> Recall:  0.384
> k=5, model=SVD
-----> Precision:  0.747
-----> Recall:  0.385
> k=10, model=KNNBasic
-----> Precision:  0.75
-----> Recall:  0.55
> k=10, model=KNNBasic
-----> Precision:  0.751
-----> Recall:  0.558
> k=10, model=KNNBasic
-----> Precision:  0.59
-----> Recall:  0.475
> k=10, model=KNNBasic
-----> Precision:  0.661
-----> Recall:  0.504
> k=10, model=SVD
-----> Precision:  0.733
-----> Recall:  0.517
> k=10, model=SVD
-----> Precision:  0.729
-----> Recall:  0.525

Question 7 ( 5 Marks)

7.1 Compare the results from the base line user-user and item-item based models.

7.2 How do these baseline models compare to each other with respect to the tuned user-user and item-item models?

7.3 The matrix factorization model is different from the collaborative filtering models. Briefly describe this difference. Also, compare the RMSE and precision recall for the models.

7.4 Does it improve? Can you offer any reasoning as to why that might be?

Write your Answer here:

Conclusions

In this case study, we saw three different ways of building recommendation systems:

We also understood advantages/disadvantages of these recommendation systems and when to use which kind of recommendation systems. Once we build these recommendation systems, we can use A/B Testing to measure the effectiveness of these systems.

Here is an article explaining how Amazon use A/B Testing to measure effectiveness of its recommendation systems.