Skip to content

Instantly share code, notes, and snippets.

@DICOT4
Last active September 20, 2024 22:14
Show Gist options
  • Select an option

  • Save DICOT4/f99339dd67ae889b8ca85865d0e543d8 to your computer and use it in GitHub Desktop.

Select an option

Save DICOT4/f99339dd67ae889b8ca85865d0e543d8 to your computer and use it in GitHub Desktop.
Recommendation System
Display the source blob
Display the rendered blob
Raw
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": [],
"gpuType": "V28",
"include_colab_link": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
},
"accelerator": "TPU"
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "view-in-github",
"colab_type": "text"
},
"source": [
"<a href=\"https://colab.research.google.com/gist/DICOT4/f99339dd67ae889b8ca85865d0e543d8/recommendation_engine_movies.ipynb\" target=\"_parent\"><img src=\"https://colab.research.google.com/assets/colab-badge.svg\" alt=\"Open In Colab\"/></a>"
]
},
{
"cell_type": "markdown",
"source": [
"# Recommendation Engine\n",
"\n",
"In this notebook, we will develop a comprehensive movie recommendation engine using two rich datasets from The Movie Database (TMDb). The datasets, sourced from [TMDb Movie Metadata](https://www.kaggle.com/datasets/tmdb/tmdb-movie-metadata), [The Movies Dataset](https://www.kaggle.com/datasets/rounakbanik/the-movies-dataset), and [Movie Lens Dataset](https://grouplens.org/datasets/movielens/latest/)provide detailed information on various aspects of movies, including titles, genres, cast, crew, and user ratings.\n",
"\n",
"We will explore two popular approaches for building recommendation systems:\n",
"* Content-based filtering\n",
"* Collaborative filtering\n"
],
"metadata": {
"id": "Fr-YOE1QP-2N"
}
},
{
"cell_type": "code",
"source": [
"from google.colab import drive\n",
"drive.mount('/content/drive')"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "yoUNIw3cqptT",
"outputId": "1ad51b4e-251f-42db-97b0-416ff5e8bfe0"
},
"execution_count": 1,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"Mounted at /content/drive\n"
]
}
]
},
{
"cell_type": "code",
"source": [
"!ls \"/content/drive/My Drive/movie-dataset/ml-latest-small\""
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "V1ea0AGLwz7-",
"outputId": "5f3fff29-6642-4baf-bcf1-5023ba6681c0"
},
"execution_count": 2,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"links.csv movies.csv ratings.csv README.txt\ttags.csv\n"
]
}
]
},
{
"cell_type": "markdown",
"source": [
"## **Visualisation**"
],
"metadata": {
"id": "jsrPnLu4QKJ-"
}
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {
"id": "hPl9Z5-xMhbu"
},
"outputs": [],
"source": [
"import pandas as pd\n",
"import numpy as np"
]
},
{
"cell_type": "code",
"source": [
"df_credits = pd.read_csv('/content/drive/My Drive/movie-dataset/TMDB-5000-Movie-Dataset/tmdb_5000_credits.csv')\n",
"df_credits.info()"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "XUtAVSWTO7X-",
"outputId": "309ba406-e197-4af5-fc23-a3808f2acc97"
},
"execution_count": 4,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 4803 entries, 0 to 4802\n",
"Data columns (total 4 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 movie_id 4803 non-null int64 \n",
" 1 title 4803 non-null object\n",
" 2 cast 4803 non-null object\n",
" 3 crew 4803 non-null object\n",
"dtypes: int64(1), object(3)\n",
"memory usage: 150.2+ KB\n"
]
}
]
},
{
"cell_type": "code",
"source": [
"df_movies = pd.read_csv('/content/drive/My Drive/movie-dataset/TMDB-5000-Movie-Dataset/tmdb_5000_movies.csv')\n",
"df_movies.info()"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "ilbqRSLJOupl",
"outputId": "9afef70c-d0ad-4634-be71-73e9b706d002"
},
"execution_count": 5,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 4803 entries, 0 to 4802\n",
"Data columns (total 20 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 budget 4803 non-null int64 \n",
" 1 genres 4803 non-null object \n",
" 2 homepage 1712 non-null object \n",
" 3 id 4803 non-null int64 \n",
" 4 keywords 4803 non-null object \n",
" 5 original_language 4803 non-null object \n",
" 6 original_title 4803 non-null object \n",
" 7 overview 4800 non-null object \n",
" 8 popularity 4803 non-null float64\n",
" 9 production_companies 4803 non-null object \n",
" 10 production_countries 4803 non-null object \n",
" 11 release_date 4802 non-null object \n",
" 12 revenue 4803 non-null int64 \n",
" 13 runtime 4801 non-null float64\n",
" 14 spoken_languages 4803 non-null object \n",
" 15 status 4803 non-null object \n",
" 16 tagline 3959 non-null object \n",
" 17 title 4803 non-null object \n",
" 18 vote_average 4803 non-null float64\n",
" 19 vote_count 4803 non-null int64 \n",
"dtypes: float64(3), int64(4), object(13)\n",
"memory usage: 750.6+ KB\n"
]
}
]
},
{
"cell_type": "code",
"source": [
"# Join the two dataset on the 'id' column\n",
"df_credits.columns = ['id', 'tittle', 'cast', 'crew']\n",
"df_movies = df_movies.merge(df_credits, on='id')\n",
"df_movies.info()"
],
"metadata": {
"id": "VFXD5sLFBptu",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "38e6cf6e-a82f-4949-ea88-00e1fc006cdb"
},
"execution_count": 6,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 4803 entries, 0 to 4802\n",
"Data columns (total 23 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 budget 4803 non-null int64 \n",
" 1 genres 4803 non-null object \n",
" 2 homepage 1712 non-null object \n",
" 3 id 4803 non-null int64 \n",
" 4 keywords 4803 non-null object \n",
" 5 original_language 4803 non-null object \n",
" 6 original_title 4803 non-null object \n",
" 7 overview 4800 non-null object \n",
" 8 popularity 4803 non-null float64\n",
" 9 production_companies 4803 non-null object \n",
" 10 production_countries 4803 non-null object \n",
" 11 release_date 4802 non-null object \n",
" 12 revenue 4803 non-null int64 \n",
" 13 runtime 4801 non-null float64\n",
" 14 spoken_languages 4803 non-null object \n",
" 15 status 4803 non-null object \n",
" 16 tagline 3959 non-null object \n",
" 17 title 4803 non-null object \n",
" 18 vote_average 4803 non-null float64\n",
" 19 vote_count 4803 non-null int64 \n",
" 20 tittle 4803 non-null object \n",
" 21 cast 4803 non-null object \n",
" 22 crew 4803 non-null object \n",
"dtypes: float64(3), int64(4), object(16)\n",
"memory usage: 863.2+ KB\n"
]
}
]
},
{
"cell_type": "markdown",
"source": [
"## **Content based filtering**\n",
"\n",
"By analyzing metadata such as the plot, cast, director, and keywords, the system will recommend movies that are similar to a given movie based on its features.\n"
],
"metadata": {
"id": "puRGS5GWPd2A"
}
},
{
"cell_type": "markdown",
"source": [
"### Movie's plot based recommendations\n",
"We will calculate similarity scores between each pair of movies using their plot descriptions and provide recommendations based on those scores. The plot descriptions are available in the overview feature of our dataset. Let's examine the data."
],
"metadata": {
"id": "7Eo0_0urQleu"
}
},
{
"cell_type": "code",
"source": [
"df_movies['overview'].head()"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 241
},
"id": "gf8N3DUIPLt4",
"outputId": "6b4137d1-acd6-4eb4-b9bc-2c4b095abf90"
},
"execution_count": 7,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
"0 In the 22nd century, a paraplegic Marine is di...\n",
"1 Captain Barbossa, long believed to be dead, ha...\n",
"2 A cryptic message from Bond’s past sends him o...\n",
"3 Following the death of District Attorney Harve...\n",
"4 John Carter is a war-weary, former military ca...\n",
"Name: overview, dtype: object"
],
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>overview</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>In the 22nd century, a paraplegic Marine is di...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>Captain Barbossa, long believed to be dead, ha...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>A cryptic message from Bond’s past sends him o...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>Following the death of District Attorney Harve...</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>John Carter is a war-weary, former military ca...</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div><br><label><b>dtype:</b> object</label>"
]
},
"metadata": {},
"execution_count": 7
}
]
},
{
"cell_type": "markdown",
"source": [
"To process text data efficiently, we must convert each document's content into a numerical representation. One common method for this is to compute `Term Frequency-Inverse Document Frequency (TF-IDF)` vectors.\n",
"\n",
"`Term Frequency (TF)` measures how often a word appears in a document, calculated as the number of occurrences of the term divided by the total number of terms in the document.\n",
"\n",
"`Inverse Document Frequency (IDF)` quantifies how unique or rare a word is across multiple documents. It is calculated as the logarithm of the total number of documents divided by the number of documents containing that term. The combined measure, TF-IDF, helps assess the importance of a word to a specific document while reducing the weight of frequently occurring words across the dataset.\n",
"\n",
"The result is a matrix where each column corresponds to a word in the vocabulary *(i.e., the words that appear in at least one document)*, and each row represents a document. This approach helps minimize the impact of common words, enhancing the precision of similarity calculations.\n",
"\n",
"Scikit-learn provides a built-in `TfidfVectorizer` class that simplifies this process, allowing you to generate the TF-IDF matrix efficiently.\n",
"\n",
"This method is crucial for accurately evaluating textual similarity while reducing the noise created by frequent, less meaningful words."
],
"metadata": {
"id": "IzUypCOF38C3"
}
},
{
"cell_type": "code",
"source": [
"# Import the TfidfVectorizer class from scikit-learn\n",
"from sklearn.feature_extraction.text import TfidfVectorizer\n",
"\n",
"# Instantiate a TF-IDF Vectorizer object, removing common English stop words such as 'the' and 'a'\n",
"tfidf = TfidfVectorizer(stop_words='english')\n",
"\n",
"# Replace any missing values (NaN) in the 'overview' column with an empty string\n",
"df_movies['overview'] = df_movies['overview'].fillna('')\n",
"\n",
"# Create the TF-IDF matrix by fitting and transforming the text data from the 'overview' column\n",
"tfidf_matrix = tfidf.fit_transform(df_movies['overview'])\n",
"\n",
"# Display the dimensions of the resulting TF-IDF matrix\n",
"tfidf_matrix.shape\n"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "bnfr2Sdk4SZB",
"outputId": "c640a0e6-17a9-4f03-c179-8d315e975a09"
},
"execution_count": 8,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
"(4803, 20978)"
]
},
"metadata": {},
"execution_count": 8
}
]
},
{
"cell_type": "markdown",
"source": [
"The dataset contains over 21,000 unique words used to describe the 4,800 movies.\n",
"\n",
"With the `TF-IDF` matrix ready, we can now compute similarity scores between these movies. There are several possible approaches to calculating similarity, including Euclidean distance, Pearson correlation, and cosine similarity. No single metric is universally the best; different similarity measures perform better in different contexts, so experimenting with multiple methods can be beneficial.\n",
"\n",
"For this task, we will use cosine similarity, which measures the cosine of the angle between two vectors. It is an effective measure because it is independent of magnitude and is computationally efficient.\n",
"\n",
"The formula for **cosine similarity** between two vectors $\\mathbf{A}$ and $\\mathbf{B}$ is:\n",
"\n",
"$$\n",
"\\text{similarity} = \\cos(\\theta) = \\frac{\\mathbf{A} \\cdot \\mathbf{B}}{\\|\\mathbf{A}\\| \\|\\mathbf{B}\\|} = \\frac{\\sum_{i=1}^{n} A_i B_i}{\\sqrt{\\sum_{i=1}^{n} A_i^2} \\cdot \\sqrt{\\sum_{i=1}^{n} B_i^2}}\n",
"$$\n",
"\n",
"Where:\n",
"- $\\mathbf{A} \\cdot \\mathbf{B}$ represents the dot product of the vectors $\\mathbf{A}$ and $\\mathbf{B}$.\n",
"- $\\|\\mathbf{A}\\|$ and $\\|\\mathbf{B}\\|$ are the magnitudes of vectors $\\mathbf{A}$ and $\\mathbf{B}$, respectively.\n",
"- $A_i$ and $B_i$ represent the components of the vectors at position $i$.\n",
"- $n$ is the number of dimensions or features in the vectors.\n",
"\n",
"Since we have employed the `TF-IDF` vectorizer, calculating the dot product will yield the cosine similarity score directly. To optimize performance, we will use `Scikit-learn's` `linear_kernel()` function instead of `cosine_similarity()`, as it is more efficient for this purpose.\n"
],
"metadata": {
"id": "3KY6Zjoo4453"
}
},
{
"cell_type": "code",
"source": [
"# Import the linear_kernel function from Scikit-learn\n",
"from sklearn.metrics.pairwise import linear_kernel\n",
"\n",
"# Calculate the cosine similarity matrix using the dot product of the TF-IDF matrix\n",
"cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)\n"
],
"metadata": {
"id": "EFmEQ6BR6RDt"
},
"execution_count": 9,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"We will define a function that accepts a movie title as input and returns a list of the *10 most similar* movies. To achieve this, we first need to create a reverse mapping between movie titles and their corresponding DataFrame indices. This will allow us to efficiently retrieve the index of a movie based on its title within the metadata DataFrame."
],
"metadata": {
"id": "st0_MXp3-H80"
}
},
{
"cell_type": "code",
"source": [
"# Create a reverse mapping of movie titles to their corresponding indices.\n",
"indices = pd.Series(df_movies.index, index = df_movies['title']).drop_duplicates()"
],
"metadata": {
"id": "P3C1BIti9--0"
},
"execution_count": 10,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"We are now ready to define our recommendation function, following these steps:\n",
"\n",
"1. Retrieve the index of the movie based on its title.\n",
"2. Obtain the list of cosine similarity scores between the selected movie and all other movies. Convert this into a list of tuples, where the first element represents the movie index, and the second represents the similarity score.\n",
"3. Sort the list of tuples in descending order based on the similarity scores *(the second element)*.\n",
"4. Select the top 10 entries from the sorted list. Ignore the first entry, as it refers to the movie itself *(the highest similarity will be with the movie itself)*.\n",
"5. Return the titles corresponding to the indices of the top entries."
],
"metadata": {
"id": "q7AKLfmQ-hy8"
}
},
{
"cell_type": "code",
"source": [
"# Function that takes a movie title as input and returns the most similar movies\n",
"def get_recommendations(title, cosine_sim=cosine_sim):\n",
" # Retrieve the index of the movie that matches the provided title\n",
" idx = indices[title]\n",
"\n",
" # Get the similarity scores for all movies compared to the selected movie\n",
" sim_scores = list(enumerate(cosine_sim[idx]))\n",
"\n",
" # Sort the similarity scores in descending order\n",
" sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)\n",
"\n",
" # Select the top 10 most similar movies, excluding the first (which is the movie itself)\n",
" sim_scores = sim_scores[1:11]\n",
"\n",
" # Extract the movie indices of the top 10 similar movies\n",
" movie_indices = [i[0] for i in sim_scores]\n",
"\n",
" # Return the titles of the top 10 most similar movies\n",
" return df_movies['title'].iloc[movie_indices]\n"
],
"metadata": {
"id": "7znrd_s3-1vs"
},
"execution_count": 11,
"outputs": []
},
{
"cell_type": "code",
"source": [
"def get_recommendations_with_score(title, cosine_sim=cosine_sim):\n",
" # Check if the title exists in the indices\n",
" if title not in indices:\n",
" print(f'\"{title}\" not found in the dataset.')\n",
" return None\n",
"\n",
" # Retrieve the index of the movie that matches the provided title\n",
" idx = indices[title]\n",
"\n",
" # Get the similarity scores for all movies compared to the selected movie\n",
" sim_scores = list(enumerate(cosine_sim[idx]))\n",
"\n",
" # Sort the similarity scores in descending order\n",
" sim_scores = sorted(sim_scores, key=lambda x: x[1], reverse=True)\n",
"\n",
" # Select the top 10 most similar movies, excluding the first (which is the movie itself)\n",
" sim_scores = sim_scores[1:11]\n",
"\n",
" # Extract the movie indices and similarity scores\n",
" movie_indices = [i[0] for i in sim_scores]\n",
" similarity_scores = [i[1] for i in sim_scores]\n",
"\n",
" # Get the movie titles\n",
" movie_titles = df_movies['title'].iloc[movie_indices].values\n",
"\n",
" # Create a DataFrame with titles and similarity scores\n",
" recommendations = pd.DataFrame({\n",
" 'title': movie_titles,\n",
" 'similarity_score': similarity_scores\n",
" })\n",
"\n",
" return recommendations"
],
"metadata": {
"id": "Vt2tM5o7DbtH"
},
"execution_count": 12,
"outputs": []
},
{
"cell_type": "code",
"source": [
"get_recommendations(\"Pirates of the Caribbean: At World's End\")"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 398
},
"id": "icxaN4h6_Unj",
"outputId": "041263c1-3e91-4671-c2b7-497c7a20ed42"
},
"execution_count": 13,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
"2542 What's Love Got to Do with It\n",
"3095 My Blueberry Nights\n",
"2102 The Descendants\n",
"1280 Disturbia\n",
"3632 90 Minutes in Heaven\n",
"792 Just Like Heaven\n",
"1709 Space Pirate Captain Harlock\n",
"1799 Original Sin\n",
"2652 Bathory: Countess of Blood\n",
"4423 Bang Bang Baby\n",
"Name: title, dtype: object"
],
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>title</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>2542</th>\n",
" <td>What's Love Got to Do with It</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3095</th>\n",
" <td>My Blueberry Nights</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2102</th>\n",
" <td>The Descendants</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1280</th>\n",
" <td>Disturbia</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3632</th>\n",
" <td>90 Minutes in Heaven</td>\n",
" </tr>\n",
" <tr>\n",
" <th>792</th>\n",
" <td>Just Like Heaven</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1709</th>\n",
" <td>Space Pirate Captain Harlock</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1799</th>\n",
" <td>Original Sin</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2652</th>\n",
" <td>Bathory: Countess of Blood</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4423</th>\n",
" <td>Bang Bang Baby</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div><br><label><b>dtype:</b> object</label>"
]
},
"metadata": {},
"execution_count": 13
}
]
},
{
"cell_type": "code",
"source": [
"get_recommendations('Snowpiercer')"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 398
},
"id": "EkT493qV_uGe",
"outputId": "87534388-9db4-4b99-c43e-5ac0104e487b"
},
"execution_count": 14,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
"4350 An Inconvenient Truth\n",
"1643 Howard the Duck\n",
"4710 Antarctic Edge: 70° South\n",
"4427 Charly\n",
"3840 Train\n",
"2410 Good Boy!\n",
"2768 21 & Over\n",
"16 The Avengers\n",
"1704 The Big Short\n",
"3330 The Wave\n",
"Name: title, dtype: object"
],
"text/html": [
"<div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>title</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>4350</th>\n",
" <td>An Inconvenient Truth</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1643</th>\n",
" <td>Howard the Duck</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4710</th>\n",
" <td>Antarctic Edge: 70° South</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4427</th>\n",
" <td>Charly</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3840</th>\n",
" <td>Train</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2410</th>\n",
" <td>Good Boy!</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2768</th>\n",
" <td>21 &amp; Over</td>\n",
" </tr>\n",
" <tr>\n",
" <th>16</th>\n",
" <td>The Avengers</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1704</th>\n",
" <td>The Big Short</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3330</th>\n",
" <td>The Wave</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div><br><label><b>dtype:</b> object</label>"
]
},
"metadata": {},
"execution_count": 14
}
]
},
{
"cell_type": "markdown",
"source": [
"Although our system effectively identifies movies with similar plot descriptions, the quality of the recommendations can be improved. For example, *\\\"Pirates of the Caribbean: At World's End\\\"* returns all similarly plotted movies, whereas individuals who enjoyed that movie may be more interested in other films directed by the same director. This nuance is not captured by the current system."
],
"metadata": {
"id": "EdD_9g9Y_-wU"
}
},
{
"cell_type": "markdown",
"source": [
"### Credits, Genre, and Keywords based recommendations\n",
"\n",
"It is evident that the quality of our recommendation system can be significantly enhanced by utilizing richer metadata. This is precisely what we will address in this section. We will build a recommendation system based on the following metadata: the top 3 actors, the director, related genres, and the movie plot keywords.\n",
"\n",
"From the cast, crew, and keyword features, we will extract the three most prominent actors, the director, and the relevant keywords for each movie. Currently, this data is stored as \"stringified\" lists, and we need to convert it into a structured and usable format."
],
"metadata": {
"id": "GAzkSOVbAd1i"
}
},
{
"cell_type": "code",
"source": [
"# Convert the stringified features into their respective Python objects.\n",
"from ast import literal_eval\n",
"\n",
"features = ['crew', 'cast', 'genres', 'keywords']\n",
"for feature in features:\n",
" df_movies[feature] = df_movies[feature].apply(literal_eval)"
],
"metadata": {
"id": "gw4R6RXRAZvu"
},
"execution_count": 15,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"Next up, we will define functions to extract the necessary information from each feature."
],
"metadata": {
"id": "PcW_i3sACUB-"
}
},
{
"cell_type": "code",
"source": [
"# Extract the director's name from the 'crew' feature. If the director is not listed, return NaN.\n",
"def get_director_from_crew(crew_list):\n",
" for member in crew_list:\n",
" if member['job'] == 'Director':\n",
" return member['name']\n",
" return np.nan\n"
],
"metadata": {
"id": "qovVTdCyBUpo"
},
"execution_count": 16,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Return the top 3 elements from the list or the entire list if fewer than 3 elements are present.\n",
"def get_list(feature_list):\n",
" if isinstance(feature_list, list):\n",
" names = [element['name'] for element in feature_list]\n",
"\n",
" # If the list contains more than 3 elements, return only the first three; otherwise, return the full list.\n",
" if len(names) > 3:\n",
" return names[:3]\n",
" return names\n",
"\n",
" # Return an empty list if the data is missing or malformed.\n",
" return []\n"
],
"metadata": {
"id": "p0s2-JuFCvBX"
},
"execution_count": 17,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Create new features for director, cast, genres, and keywords in a more usable format.\n",
"df_movies['director'] = df_movies['crew'].apply(get_director_from_crew)\n",
"\n",
"# Apply the 'get_list' function to the 'cast', 'genres', and 'keywords' features to extract relevant information.\n",
"features = ['cast', 'genres', 'keywords']\n",
"for feature in features:\n",
" df_movies[feature] = df_movies[feature].apply(get_list)\n"
],
"metadata": {
"id": "O8cG-J8JC902"
},
"execution_count": 18,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Let's see the new features\n",
"df_movies[['title', 'cast', 'director', 'genres', 'keywords']].head()"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 293
},
"id": "sB32qIuuDeYc",
"outputId": "619ec9e2-f173-4dd8-f3ec-12bb4514317e"
},
"execution_count": 19,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
" title \\\n",
"0 Avatar \n",
"1 Pirates of the Caribbean: At World's End \n",
"2 Spectre \n",
"3 The Dark Knight Rises \n",
"4 John Carter \n",
"\n",
" cast director \\\n",
"0 [Sam Worthington, Zoe Saldana, Sigourney Weaver] James Cameron \n",
"1 [Johnny Depp, Orlando Bloom, Keira Knightley] Gore Verbinski \n",
"2 [Daniel Craig, Christoph Waltz, Léa Seydoux] Sam Mendes \n",
"3 [Christian Bale, Michael Caine, Gary Oldman] Christopher Nolan \n",
"4 [Taylor Kitsch, Lynn Collins, Samantha Morton] Andrew Stanton \n",
"\n",
" genres keywords \n",
"0 [Action, Adventure, Fantasy] [culture clash, future, space war] \n",
"1 [Adventure, Fantasy, Action] [ocean, drug abuse, exotic island] \n",
"2 [Action, Adventure, Crime] [spy, based on novel, secret agent] \n",
"3 [Action, Crime, Drama] [dc comics, crime fighter, terrorist] \n",
"4 [Action, Adventure, Science Fiction] [based on novel, mars, medallion] "
],
"text/html": [
"\n",
" <div id=\"df-eeb80354-aa47-4b8a-8718-425dc5dd0b5e\" class=\"colab-df-container\">\n",
" <div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>title</th>\n",
" <th>cast</th>\n",
" <th>director</th>\n",
" <th>genres</th>\n",
" <th>keywords</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>Avatar</td>\n",
" <td>[Sam Worthington, Zoe Saldana, Sigourney Weaver]</td>\n",
" <td>James Cameron</td>\n",
" <td>[Action, Adventure, Fantasy]</td>\n",
" <td>[culture clash, future, space war]</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>Pirates of the Caribbean: At World's End</td>\n",
" <td>[Johnny Depp, Orlando Bloom, Keira Knightley]</td>\n",
" <td>Gore Verbinski</td>\n",
" <td>[Adventure, Fantasy, Action]</td>\n",
" <td>[ocean, drug abuse, exotic island]</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>Spectre</td>\n",
" <td>[Daniel Craig, Christoph Waltz, Léa Seydoux]</td>\n",
" <td>Sam Mendes</td>\n",
" <td>[Action, Adventure, Crime]</td>\n",
" <td>[spy, based on novel, secret agent]</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>The Dark Knight Rises</td>\n",
" <td>[Christian Bale, Michael Caine, Gary Oldman]</td>\n",
" <td>Christopher Nolan</td>\n",
" <td>[Action, Crime, Drama]</td>\n",
" <td>[dc comics, crime fighter, terrorist]</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>John Carter</td>\n",
" <td>[Taylor Kitsch, Lynn Collins, Samantha Morton]</td>\n",
" <td>Andrew Stanton</td>\n",
" <td>[Action, Adventure, Science Fiction]</td>\n",
" <td>[based on novel, mars, medallion]</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>\n",
" <div class=\"colab-df-buttons\">\n",
"\n",
" <div class=\"colab-df-container\">\n",
" <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-eeb80354-aa47-4b8a-8718-425dc5dd0b5e')\"\n",
" title=\"Convert this dataframe to an interactive table.\"\n",
" style=\"display:none;\">\n",
"\n",
" <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\">\n",
" <path d=\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\"/>\n",
" </svg>\n",
" </button>\n",
"\n",
" <style>\n",
" .colab-df-container {\n",
" display:flex;\n",
" gap: 12px;\n",
" }\n",
"\n",
" .colab-df-convert {\n",
" background-color: #E8F0FE;\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: #1967D2;\n",
" height: 32px;\n",
" padding: 0 0 0 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-convert:hover {\n",
" background-color: #E2EBFA;\n",
" box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: #174EA6;\n",
" }\n",
"\n",
" .colab-df-buttons div {\n",
" margin-bottom: 4px;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert {\n",
" background-color: #3B4455;\n",
" fill: #D2E3FC;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert:hover {\n",
" background-color: #434B5C;\n",
" box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
" filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
" fill: #FFFFFF;\n",
" }\n",
" </style>\n",
"\n",
" <script>\n",
" const buttonEl =\n",
" document.querySelector('#df-eeb80354-aa47-4b8a-8718-425dc5dd0b5e button.colab-df-convert');\n",
" buttonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
"\n",
" async function convertToInteractive(key) {\n",
" const element = document.querySelector('#df-eeb80354-aa47-4b8a-8718-425dc5dd0b5e');\n",
" const dataTable =\n",
" await google.colab.kernel.invokeFunction('convertToInteractive',\n",
" [key], {});\n",
" if (!dataTable) return;\n",
"\n",
" const docLinkHtml = 'Like what you see? Visit the ' +\n",
" '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
" + ' to learn more about interactive tables.';\n",
" element.innerHTML = '';\n",
" dataTable['output_type'] = 'display_data';\n",
" await google.colab.output.renderOutput(dataTable, element);\n",
" const docLink = document.createElement('div');\n",
" docLink.innerHTML = docLinkHtml;\n",
" element.appendChild(docLink);\n",
" }\n",
" </script>\n",
" </div>\n",
"\n",
"\n",
"<div id=\"df-fa1bfa7f-e090-4944-b2c7-ca26dce74112\">\n",
" <button class=\"colab-df-quickchart\" onclick=\"quickchart('df-fa1bfa7f-e090-4944-b2c7-ca26dce74112')\"\n",
" title=\"Suggest charts\"\n",
" style=\"display:none;\">\n",
"\n",
"<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
" width=\"24px\">\n",
" <g>\n",
" <path d=\"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z\"/>\n",
" </g>\n",
"</svg>\n",
" </button>\n",
"\n",
"<style>\n",
" .colab-df-quickchart {\n",
" --bg-color: #E8F0FE;\n",
" --fill-color: #1967D2;\n",
" --hover-bg-color: #E2EBFA;\n",
" --hover-fill-color: #174EA6;\n",
" --disabled-fill-color: #AAA;\n",
" --disabled-bg-color: #DDD;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-quickchart {\n",
" --bg-color: #3B4455;\n",
" --fill-color: #D2E3FC;\n",
" --hover-bg-color: #434B5C;\n",
" --hover-fill-color: #FFFFFF;\n",
" --disabled-bg-color: #3B4455;\n",
" --disabled-fill-color: #666;\n",
" }\n",
"\n",
" .colab-df-quickchart {\n",
" background-color: var(--bg-color);\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: var(--fill-color);\n",
" height: 32px;\n",
" padding: 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-quickchart:hover {\n",
" background-color: var(--hover-bg-color);\n",
" box-shadow: 0 1px 2px rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: var(--button-hover-fill-color);\n",
" }\n",
"\n",
" .colab-df-quickchart-complete:disabled,\n",
" .colab-df-quickchart-complete:disabled:hover {\n",
" background-color: var(--disabled-bg-color);\n",
" fill: var(--disabled-fill-color);\n",
" box-shadow: none;\n",
" }\n",
"\n",
" .colab-df-spinner {\n",
" border: 2px solid var(--fill-color);\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" animation:\n",
" spin 1s steps(1) infinite;\n",
" }\n",
"\n",
" @keyframes spin {\n",
" 0% {\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" border-left-color: var(--fill-color);\n",
" }\n",
" 20% {\n",
" border-color: transparent;\n",
" border-left-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" }\n",
" 30% {\n",
" border-color: transparent;\n",
" border-left-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" border-right-color: var(--fill-color);\n",
" }\n",
" 40% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" }\n",
" 60% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" }\n",
" 80% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" border-bottom-color: var(--fill-color);\n",
" }\n",
" 90% {\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" }\n",
" }\n",
"</style>\n",
"\n",
" <script>\n",
" async function quickchart(key) {\n",
" const quickchartButtonEl =\n",
" document.querySelector('#' + key + ' button');\n",
" quickchartButtonEl.disabled = true; // To prevent multiple clicks.\n",
" quickchartButtonEl.classList.add('colab-df-spinner');\n",
" try {\n",
" const charts = await google.colab.kernel.invokeFunction(\n",
" 'suggestCharts', [key], {});\n",
" } catch (error) {\n",
" console.error('Error during call to suggestCharts:', error);\n",
" }\n",
" quickchartButtonEl.classList.remove('colab-df-spinner');\n",
" quickchartButtonEl.classList.add('colab-df-quickchart-complete');\n",
" }\n",
" (() => {\n",
" let quickchartButtonEl =\n",
" document.querySelector('#df-fa1bfa7f-e090-4944-b2c7-ca26dce74112 button');\n",
" quickchartButtonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
" })();\n",
" </script>\n",
"</div>\n",
"\n",
" </div>\n",
" </div>\n"
],
"application/vnd.google.colaboratory.intrinsic+json": {
"type": "dataframe",
"summary": "{\n \"name\": \"df_movies[['title', 'cast', 'director', 'genres', 'keywords']]\",\n \"rows\": 5,\n \"fields\": [\n {\n \"column\": \"title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Pirates of the Caribbean: At World's End\",\n \"John Carter\",\n \"Spectre\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"cast\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"director\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 5,\n \"samples\": [\n \"Gore Verbinski\",\n \"Andrew Stanton\",\n \"Sam Mendes\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"genres\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"keywords\",\n \"properties\": {\n \"dtype\": \"object\",\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"
}
},
"metadata": {},
"execution_count": 19
}
]
},
{
"cell_type": "markdown",
"source": [
"Now we need to convert the names and keyword instances to lowercase and remove any spaces. This ensures that our vectorizer distinguishes between different entities, such as *\\\"Johnny Depp\\\"* and *\\\"Johnny Galecki,\\\"* and does not treat them as the same."
],
"metadata": {
"id": "7ES_5YLiEiGe"
}
},
{
"cell_type": "code",
"source": [
"# Function to convert all strings to lowercase and remove spaces from names\n",
"def clean_data(value):\n",
" if isinstance(value, list):\n",
" return [str.lower(item.replace(\" \", \"\")) for item in value]\n",
" else:\n",
" # If the value is a string (e.g., director's name), convert it to lowercase and remove spaces\n",
" if isinstance(value, str):\n",
" return str.lower(value.replace(\" \", \"\"))\n",
" # If the value is missing or malformed, return an empty string\n",
" return ''\n"
],
"metadata": {
"id": "twEho5bNEtNn"
},
"execution_count": 20,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Apply the clean_data function to the relevant features (cast, keywords, director, and genres).\n",
"features = ['cast', 'director', 'genres', 'keywords']\n",
"\n",
"for feature in features:\n",
" df_movies[feature] = df_movies[feature].apply(clean_data)\n"
],
"metadata": {
"id": "ADaQaekWFeWR"
},
"execution_count": 21,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"We are now ready to create our *metadata summary*, which is a consolidated string containing all the relevant metadata — such as actors, director, and keywords — that we will use as input for our vectorizer."
],
"metadata": {
"id": "oZ1R2jhMF09f"
}
},
{
"cell_type": "code",
"source": [
"# Function to create a metadata summary by combining cast, director, genres, and keywords into a single string\n",
"def create_metadata_summary(row):\n",
" return ' '.join(row['cast']) + ' ' + row['director'] + ' ' + ' '.join(row['genres']) + ' ' + ' '.join(row['keywords'])\n",
"\n",
"# Apply the create_metadata_summary function to generate the summary for each movie\n",
"df_movies['meta_summary'] = df_movies.apply(create_metadata_summary, axis=1)\n"
],
"metadata": {
"id": "A3m7nEJVGFpy"
},
"execution_count": 22,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"The next steps are similar to the movie's plot based recommendations. However, a key difference is that we will use `CountVectorizer()` instead of `TF-IDF`. This is because we do not want to down-weight an actor or director simply because they have been involved in a larger number of movies. In this context, retaining the full weight of their presence makes more intuitive sense."
],
"metadata": {
"id": "441AzSi2Hiaj"
}
},
{
"cell_type": "code",
"source": [
"# Import CountVectorizer and create a count matrix based on the metadata summary\n",
"from sklearn.feature_extraction.text import CountVectorizer\n",
"\n",
"# Initialize CountVectorizer and apply it to the 'meta_feature' feature, excluding common English stop words\n",
"count = CountVectorizer(stop_words='english')\n",
"count_matrix = count.fit_transform(df_movies['meta_summary'])\n"
],
"metadata": {
"id": "gXDbw9AzIT-L"
},
"execution_count": 23,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Compute the cosine similarity matrix based on the count matrix\n",
"from sklearn.metrics.pairwise import cosine_similarity\n",
"\n",
"# Calculate the cosine similarity between all movies using the count matrix\n",
"cosine_sim2 = cosine_similarity(count_matrix, count_matrix)\n"
],
"metadata": {
"id": "m__Da0ZUIizZ"
},
"execution_count": 24,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Reset the index of the main DataFrame and create a reverse mapping from movie titles to their indices\n",
"df_movies = df_movies.reset_index()\n",
"\n",
"# Create a reverse mapping where the index is the movie title and the value is the corresponding DataFrame index\n",
"indices = pd.Series(df_movies.index, index=df_movies['title'])\n"
],
"metadata": {
"id": "6zkcrsnKIsZ7"
},
"execution_count": 25,
"outputs": []
},
{
"cell_type": "markdown",
"source": [
"We can now reuse the `get_recommendations()` function by passing the newly created `cosine_sim2` matrix as the second argument."
],
"metadata": {
"id": "LFZcgj-yI3hX"
}
},
{
"cell_type": "code",
"source": [
"get_recommendations_with_score('Iron Man', cosine_sim2)"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 363
},
"id": "gNf42u8HI-PF",
"outputId": "d1df527d-2c53-4aad-927d-9a8ea522612d"
},
"execution_count": 26,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
" title similarity_score\n",
"0 Iron Man 2 0.600000\n",
"1 Avengers: Age of Ultron 0.400000\n",
"2 The Avengers 0.400000\n",
"3 Captain America: Civil War 0.400000\n",
"4 Iron Man 3 0.400000\n",
"5 TRON: Legacy 0.400000\n",
"6 The Helix... Loaded 0.365148\n",
"7 The Lovers 0.358569\n",
"8 After Earth 0.335410\n",
"9 Six-String Samurai 0.335410"
],
"text/html": [
"\n",
" <div id=\"df-34ed8997-e3d5-4ccd-ae57-253c1f5b6a5e\" class=\"colab-df-container\">\n",
" <div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>title</th>\n",
" <th>similarity_score</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>Iron Man 2</td>\n",
" <td>0.600000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>Avengers: Age of Ultron</td>\n",
" <td>0.400000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>The Avengers</td>\n",
" <td>0.400000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>Captain America: Civil War</td>\n",
" <td>0.400000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>Iron Man 3</td>\n",
" <td>0.400000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>TRON: Legacy</td>\n",
" <td>0.400000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>The Helix... Loaded</td>\n",
" <td>0.365148</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>The Lovers</td>\n",
" <td>0.358569</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>After Earth</td>\n",
" <td>0.335410</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>Six-String Samurai</td>\n",
" <td>0.335410</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>\n",
" <div class=\"colab-df-buttons\">\n",
"\n",
" <div class=\"colab-df-container\">\n",
" <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-34ed8997-e3d5-4ccd-ae57-253c1f5b6a5e')\"\n",
" title=\"Convert this dataframe to an interactive table.\"\n",
" style=\"display:none;\">\n",
"\n",
" <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\">\n",
" <path d=\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\"/>\n",
" </svg>\n",
" </button>\n",
"\n",
" <style>\n",
" .colab-df-container {\n",
" display:flex;\n",
" gap: 12px;\n",
" }\n",
"\n",
" .colab-df-convert {\n",
" background-color: #E8F0FE;\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: #1967D2;\n",
" height: 32px;\n",
" padding: 0 0 0 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-convert:hover {\n",
" background-color: #E2EBFA;\n",
" box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: #174EA6;\n",
" }\n",
"\n",
" .colab-df-buttons div {\n",
" margin-bottom: 4px;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert {\n",
" background-color: #3B4455;\n",
" fill: #D2E3FC;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert:hover {\n",
" background-color: #434B5C;\n",
" box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
" filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
" fill: #FFFFFF;\n",
" }\n",
" </style>\n",
"\n",
" <script>\n",
" const buttonEl =\n",
" document.querySelector('#df-34ed8997-e3d5-4ccd-ae57-253c1f5b6a5e button.colab-df-convert');\n",
" buttonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
"\n",
" async function convertToInteractive(key) {\n",
" const element = document.querySelector('#df-34ed8997-e3d5-4ccd-ae57-253c1f5b6a5e');\n",
" const dataTable =\n",
" await google.colab.kernel.invokeFunction('convertToInteractive',\n",
" [key], {});\n",
" if (!dataTable) return;\n",
"\n",
" const docLinkHtml = 'Like what you see? Visit the ' +\n",
" '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
" + ' to learn more about interactive tables.';\n",
" element.innerHTML = '';\n",
" dataTable['output_type'] = 'display_data';\n",
" await google.colab.output.renderOutput(dataTable, element);\n",
" const docLink = document.createElement('div');\n",
" docLink.innerHTML = docLinkHtml;\n",
" element.appendChild(docLink);\n",
" }\n",
" </script>\n",
" </div>\n",
"\n",
"\n",
"<div id=\"df-7ce19fe2-d47e-4486-8be8-916fb7b34517\">\n",
" <button class=\"colab-df-quickchart\" onclick=\"quickchart('df-7ce19fe2-d47e-4486-8be8-916fb7b34517')\"\n",
" title=\"Suggest charts\"\n",
" style=\"display:none;\">\n",
"\n",
"<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
" width=\"24px\">\n",
" <g>\n",
" <path d=\"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z\"/>\n",
" </g>\n",
"</svg>\n",
" </button>\n",
"\n",
"<style>\n",
" .colab-df-quickchart {\n",
" --bg-color: #E8F0FE;\n",
" --fill-color: #1967D2;\n",
" --hover-bg-color: #E2EBFA;\n",
" --hover-fill-color: #174EA6;\n",
" --disabled-fill-color: #AAA;\n",
" --disabled-bg-color: #DDD;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-quickchart {\n",
" --bg-color: #3B4455;\n",
" --fill-color: #D2E3FC;\n",
" --hover-bg-color: #434B5C;\n",
" --hover-fill-color: #FFFFFF;\n",
" --disabled-bg-color: #3B4455;\n",
" --disabled-fill-color: #666;\n",
" }\n",
"\n",
" .colab-df-quickchart {\n",
" background-color: var(--bg-color);\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: var(--fill-color);\n",
" height: 32px;\n",
" padding: 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-quickchart:hover {\n",
" background-color: var(--hover-bg-color);\n",
" box-shadow: 0 1px 2px rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: var(--button-hover-fill-color);\n",
" }\n",
"\n",
" .colab-df-quickchart-complete:disabled,\n",
" .colab-df-quickchart-complete:disabled:hover {\n",
" background-color: var(--disabled-bg-color);\n",
" fill: var(--disabled-fill-color);\n",
" box-shadow: none;\n",
" }\n",
"\n",
" .colab-df-spinner {\n",
" border: 2px solid var(--fill-color);\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" animation:\n",
" spin 1s steps(1) infinite;\n",
" }\n",
"\n",
" @keyframes spin {\n",
" 0% {\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" border-left-color: var(--fill-color);\n",
" }\n",
" 20% {\n",
" border-color: transparent;\n",
" border-left-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" }\n",
" 30% {\n",
" border-color: transparent;\n",
" border-left-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" border-right-color: var(--fill-color);\n",
" }\n",
" 40% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" }\n",
" 60% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" }\n",
" 80% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" border-bottom-color: var(--fill-color);\n",
" }\n",
" 90% {\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" }\n",
" }\n",
"</style>\n",
"\n",
" <script>\n",
" async function quickchart(key) {\n",
" const quickchartButtonEl =\n",
" document.querySelector('#' + key + ' button');\n",
" quickchartButtonEl.disabled = true; // To prevent multiple clicks.\n",
" quickchartButtonEl.classList.add('colab-df-spinner');\n",
" try {\n",
" const charts = await google.colab.kernel.invokeFunction(\n",
" 'suggestCharts', [key], {});\n",
" } catch (error) {\n",
" console.error('Error during call to suggestCharts:', error);\n",
" }\n",
" quickchartButtonEl.classList.remove('colab-df-spinner');\n",
" quickchartButtonEl.classList.add('colab-df-quickchart-complete');\n",
" }\n",
" (() => {\n",
" let quickchartButtonEl =\n",
" document.querySelector('#df-7ce19fe2-d47e-4486-8be8-916fb7b34517 button');\n",
" quickchartButtonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
" })();\n",
" </script>\n",
"</div>\n",
"\n",
" </div>\n",
" </div>\n"
],
"application/vnd.google.colaboratory.intrinsic+json": {
"type": "dataframe",
"summary": "{\n \"name\": \"get_recommendations_with_score('Iron Man', cosine_sim2)\",\n \"rows\": 10,\n \"fields\": [\n {\n \"column\": \"title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 10,\n \"samples\": [\n \"After Earth\",\n \"Avengers: Age of Ultron\",\n \"TRON: Legacy\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"similarity_score\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.07547766385676735,\n \"min\": 0.33541019662496846,\n \"max\": 0.6,\n \"num_unique_values\": 5,\n \"samples\": [\n 0.4,\n 0.33541019662496846,\n 0.3651483716701108\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"
}
},
"metadata": {},
"execution_count": 26
}
]
},
{
"cell_type": "code",
"source": [
"get_recommendations_with_score('Superman', cosine_sim2)"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 363
},
"id": "F58gWgSKJE0z",
"outputId": "a897fe91-127e-4bea-ca9d-5529b1cb821a"
},
"execution_count": 27,
"outputs": [
{
"output_type": "execute_result",
"data": {
"text/plain": [
" title similarity_score\n",
"0 Superman II 0.600000\n",
"1 Superman IV: The Quest for Peace 0.572078\n",
"2 Superman Returns 0.500000\n",
"3 Man of Steel 0.500000\n",
"4 Superman III 0.500000\n",
"5 Batman v Superman: Dawn of Justice 0.400000\n",
"6 The Mummy: Tomb of the Dragon Emperor 0.358569\n",
"7 The Monkey King 2 0.335410\n",
"8 Indiana Jones and the Kingdom of the Crystal S... 0.316228\n",
"9 The Sorcerer's Apprentice 0.316228"
],
"text/html": [
"\n",
" <div id=\"df-b725bc64-5335-414d-87ab-b997a4932eec\" class=\"colab-df-container\">\n",
" <div>\n",
"<style scoped>\n",
" .dataframe tbody tr th:only-of-type {\n",
" vertical-align: middle;\n",
" }\n",
"\n",
" .dataframe tbody tr th {\n",
" vertical-align: top;\n",
" }\n",
"\n",
" .dataframe thead th {\n",
" text-align: right;\n",
" }\n",
"</style>\n",
"<table border=\"1\" class=\"dataframe\">\n",
" <thead>\n",
" <tr style=\"text-align: right;\">\n",
" <th></th>\n",
" <th>title</th>\n",
" <th>similarity_score</th>\n",
" </tr>\n",
" </thead>\n",
" <tbody>\n",
" <tr>\n",
" <th>0</th>\n",
" <td>Superman II</td>\n",
" <td>0.600000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>1</th>\n",
" <td>Superman IV: The Quest for Peace</td>\n",
" <td>0.572078</td>\n",
" </tr>\n",
" <tr>\n",
" <th>2</th>\n",
" <td>Superman Returns</td>\n",
" <td>0.500000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>3</th>\n",
" <td>Man of Steel</td>\n",
" <td>0.500000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>4</th>\n",
" <td>Superman III</td>\n",
" <td>0.500000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>5</th>\n",
" <td>Batman v Superman: Dawn of Justice</td>\n",
" <td>0.400000</td>\n",
" </tr>\n",
" <tr>\n",
" <th>6</th>\n",
" <td>The Mummy: Tomb of the Dragon Emperor</td>\n",
" <td>0.358569</td>\n",
" </tr>\n",
" <tr>\n",
" <th>7</th>\n",
" <td>The Monkey King 2</td>\n",
" <td>0.335410</td>\n",
" </tr>\n",
" <tr>\n",
" <th>8</th>\n",
" <td>Indiana Jones and the Kingdom of the Crystal S...</td>\n",
" <td>0.316228</td>\n",
" </tr>\n",
" <tr>\n",
" <th>9</th>\n",
" <td>The Sorcerer's Apprentice</td>\n",
" <td>0.316228</td>\n",
" </tr>\n",
" </tbody>\n",
"</table>\n",
"</div>\n",
" <div class=\"colab-df-buttons\">\n",
"\n",
" <div class=\"colab-df-container\">\n",
" <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-b725bc64-5335-414d-87ab-b997a4932eec')\"\n",
" title=\"Convert this dataframe to an interactive table.\"\n",
" style=\"display:none;\">\n",
"\n",
" <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\">\n",
" <path d=\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\"/>\n",
" </svg>\n",
" </button>\n",
"\n",
" <style>\n",
" .colab-df-container {\n",
" display:flex;\n",
" gap: 12px;\n",
" }\n",
"\n",
" .colab-df-convert {\n",
" background-color: #E8F0FE;\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: #1967D2;\n",
" height: 32px;\n",
" padding: 0 0 0 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-convert:hover {\n",
" background-color: #E2EBFA;\n",
" box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: #174EA6;\n",
" }\n",
"\n",
" .colab-df-buttons div {\n",
" margin-bottom: 4px;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert {\n",
" background-color: #3B4455;\n",
" fill: #D2E3FC;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-convert:hover {\n",
" background-color: #434B5C;\n",
" box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
" filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
" fill: #FFFFFF;\n",
" }\n",
" </style>\n",
"\n",
" <script>\n",
" const buttonEl =\n",
" document.querySelector('#df-b725bc64-5335-414d-87ab-b997a4932eec button.colab-df-convert');\n",
" buttonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
"\n",
" async function convertToInteractive(key) {\n",
" const element = document.querySelector('#df-b725bc64-5335-414d-87ab-b997a4932eec');\n",
" const dataTable =\n",
" await google.colab.kernel.invokeFunction('convertToInteractive',\n",
" [key], {});\n",
" if (!dataTable) return;\n",
"\n",
" const docLinkHtml = 'Like what you see? Visit the ' +\n",
" '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
" + ' to learn more about interactive tables.';\n",
" element.innerHTML = '';\n",
" dataTable['output_type'] = 'display_data';\n",
" await google.colab.output.renderOutput(dataTable, element);\n",
" const docLink = document.createElement('div');\n",
" docLink.innerHTML = docLinkHtml;\n",
" element.appendChild(docLink);\n",
" }\n",
" </script>\n",
" </div>\n",
"\n",
"\n",
"<div id=\"df-e5831da0-50ce-4667-a837-747c6908e75c\">\n",
" <button class=\"colab-df-quickchart\" onclick=\"quickchart('df-e5831da0-50ce-4667-a837-747c6908e75c')\"\n",
" title=\"Suggest charts\"\n",
" style=\"display:none;\">\n",
"\n",
"<svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\"viewBox=\"0 0 24 24\"\n",
" width=\"24px\">\n",
" <g>\n",
" <path d=\"M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zM9 17H7v-7h2v7zm4 0h-2V7h2v10zm4 0h-2v-4h2v4z\"/>\n",
" </g>\n",
"</svg>\n",
" </button>\n",
"\n",
"<style>\n",
" .colab-df-quickchart {\n",
" --bg-color: #E8F0FE;\n",
" --fill-color: #1967D2;\n",
" --hover-bg-color: #E2EBFA;\n",
" --hover-fill-color: #174EA6;\n",
" --disabled-fill-color: #AAA;\n",
" --disabled-bg-color: #DDD;\n",
" }\n",
"\n",
" [theme=dark] .colab-df-quickchart {\n",
" --bg-color: #3B4455;\n",
" --fill-color: #D2E3FC;\n",
" --hover-bg-color: #434B5C;\n",
" --hover-fill-color: #FFFFFF;\n",
" --disabled-bg-color: #3B4455;\n",
" --disabled-fill-color: #666;\n",
" }\n",
"\n",
" .colab-df-quickchart {\n",
" background-color: var(--bg-color);\n",
" border: none;\n",
" border-radius: 50%;\n",
" cursor: pointer;\n",
" display: none;\n",
" fill: var(--fill-color);\n",
" height: 32px;\n",
" padding: 0;\n",
" width: 32px;\n",
" }\n",
"\n",
" .colab-df-quickchart:hover {\n",
" background-color: var(--hover-bg-color);\n",
" box-shadow: 0 1px 2px rgba(60, 64, 67, 0.3), 0 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
" fill: var(--button-hover-fill-color);\n",
" }\n",
"\n",
" .colab-df-quickchart-complete:disabled,\n",
" .colab-df-quickchart-complete:disabled:hover {\n",
" background-color: var(--disabled-bg-color);\n",
" fill: var(--disabled-fill-color);\n",
" box-shadow: none;\n",
" }\n",
"\n",
" .colab-df-spinner {\n",
" border: 2px solid var(--fill-color);\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" animation:\n",
" spin 1s steps(1) infinite;\n",
" }\n",
"\n",
" @keyframes spin {\n",
" 0% {\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" border-left-color: var(--fill-color);\n",
" }\n",
" 20% {\n",
" border-color: transparent;\n",
" border-left-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" }\n",
" 30% {\n",
" border-color: transparent;\n",
" border-left-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" border-right-color: var(--fill-color);\n",
" }\n",
" 40% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" border-top-color: var(--fill-color);\n",
" }\n",
" 60% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" }\n",
" 80% {\n",
" border-color: transparent;\n",
" border-right-color: var(--fill-color);\n",
" border-bottom-color: var(--fill-color);\n",
" }\n",
" 90% {\n",
" border-color: transparent;\n",
" border-bottom-color: var(--fill-color);\n",
" }\n",
" }\n",
"</style>\n",
"\n",
" <script>\n",
" async function quickchart(key) {\n",
" const quickchartButtonEl =\n",
" document.querySelector('#' + key + ' button');\n",
" quickchartButtonEl.disabled = true; // To prevent multiple clicks.\n",
" quickchartButtonEl.classList.add('colab-df-spinner');\n",
" try {\n",
" const charts = await google.colab.kernel.invokeFunction(\n",
" 'suggestCharts', [key], {});\n",
" } catch (error) {\n",
" console.error('Error during call to suggestCharts:', error);\n",
" }\n",
" quickchartButtonEl.classList.remove('colab-df-spinner');\n",
" quickchartButtonEl.classList.add('colab-df-quickchart-complete');\n",
" }\n",
" (() => {\n",
" let quickchartButtonEl =\n",
" document.querySelector('#df-e5831da0-50ce-4667-a837-747c6908e75c button');\n",
" quickchartButtonEl.style.display =\n",
" google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
" })();\n",
" </script>\n",
"</div>\n",
"\n",
" </div>\n",
" </div>\n"
],
"application/vnd.google.colaboratory.intrinsic+json": {
"type": "dataframe",
"summary": "{\n \"name\": \"get_recommendations_with_score('Superman', cosine_sim2)\",\n \"rows\": 10,\n \"fields\": [\n {\n \"column\": \"title\",\n \"properties\": {\n \"dtype\": \"string\",\n \"num_unique_values\": 10,\n \"samples\": [\n \"Indiana Jones and the Kingdom of the Crystal Skull\",\n \"Superman IV: The Quest for Peace\",\n \"Batman v Superman: Dawn of Justice\"\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n },\n {\n \"column\": \"similarity_score\",\n \"properties\": {\n \"dtype\": \"number\",\n \"std\": 0.10731754189425172,\n \"min\": 0.31622776601683794,\n \"max\": 0.6,\n \"num_unique_values\": 7,\n \"samples\": [\n 0.6,\n 0.5720775535473555,\n 0.33541019662496846\n ],\n \"semantic_type\": \"\",\n \"description\": \"\"\n }\n }\n ]\n}"
}
},
"metadata": {},
"execution_count": 27
}
]
},
{
"cell_type": "markdown",
"source": [
"Our recommender has successfully captured more information by incorporating additional metadata, resulting in *(arguably)* better recommendations. For instance, it is more likely that fans of Marvel or DC comics will prefer movies from the same production house. Therefore, we can enhance our features by including the `production_company`. Additionally, we can increase the influence of the director by adding the `director` feature multiple times in the `metadata summary`."
],
"metadata": {
"id": "vBCDDhflJ1R7"
}
},
{
"cell_type": "markdown",
"source": [
"## Collaborative Filtering\n",
"\n",
"A recommendation model based on user-movie interaction data. This method focuses on leveraging user preferences and interactions with various movies to make recommendations that align with user tastes."
],
"metadata": {
"id": "YkLGRsKBJ_5Z"
}
},
{
"cell_type": "markdown",
"source": [
"Since the dataset we used before did not have `userId` *(which is necessary for collaborative filtering)*, let's load the Movie Dataset. We'll be using the `Surprise` library to implement `SVD`."
],
"metadata": {
"id": "DQPopPiEOvas"
}
},
{
"cell_type": "code",
"source": [
"!pip install scikit-surprise"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "_Sfl5kGkZRAE",
"outputId": "1e669e3a-e7b6-4b8c-e56a-8e7247f792bc"
},
"execution_count": 28,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"Collecting scikit-surprise\n",
" Downloading scikit_surprise-1.1.4.tar.gz (154 kB)\n",
"\u001b[?25l \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m0.0/154.4 kB\u001b[0m \u001b[31m?\u001b[0m eta \u001b[36m-:--:--\u001b[0m\r\u001b[2K \u001b[91m━━━━━\u001b[0m\u001b[90m╺\u001b[0m\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m20.5/154.4 kB\u001b[0m \u001b[31m4.5 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\r\u001b[2K \u001b[91m━━━━━━━━━━━━━\u001b[0m\u001b[90m╺\u001b[0m\u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m51.2/154.4 kB\u001b[0m \u001b[31m993.2 kB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\r\u001b[2K \u001b[91m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m\u001b[91m╸\u001b[0m \u001b[32m153.6/154.4 kB\u001b[0m \u001b[31m1.6 MB/s\u001b[0m eta \u001b[36m0:00:01\u001b[0m\r\u001b[2K \u001b[90m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\u001b[0m \u001b[32m154.4/154.4 kB\u001b[0m \u001b[31m1.4 MB/s\u001b[0m eta \u001b[36m0:00:00\u001b[0m\n",
"\u001b[?25h Installing build dependencies ... \u001b[?25l\u001b[?25hdone\n",
" Getting requirements to build wheel ... \u001b[?25l\u001b[?25hdone\n",
" Preparing metadata (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n",
"Requirement already satisfied: joblib>=1.2.0 in /usr/local/lib/python3.10/dist-packages (from scikit-surprise) (1.4.2)\n",
"Requirement already satisfied: numpy>=1.19.5 in /usr/local/lib/python3.10/dist-packages (from scikit-surprise) (1.26.4)\n",
"Requirement already satisfied: scipy>=1.6.0 in /usr/local/lib/python3.10/dist-packages (from scikit-surprise) (1.13.1)\n",
"Building wheels for collected packages: scikit-surprise\n",
" Building wheel for scikit-surprise (pyproject.toml) ... \u001b[?25l\u001b[?25hdone\n",
" Created wheel for scikit-surprise: filename=scikit_surprise-1.1.4-cp310-cp310-linux_x86_64.whl size=2357286 sha256=d1b7065f3ca8aa9b20568fb4277f8402b961ad7860d371c4716b7bd4c712062d\n",
" Stored in directory: /root/.cache/pip/wheels/4b/3f/df/6acbf0a40397d9bf3ff97f582cc22fb9ce66adde75bc71fd54\n",
"Successfully built scikit-surprise\n",
"Installing collected packages: scikit-surprise\n",
"Successfully installed scikit-surprise-1.1.4\n"
]
}
]
},
{
"cell_type": "code",
"source": [
"from surprise import Reader, Dataset, SVD\n",
"from surprise.model_selection import train_test_split\n",
"from surprise import accuracy\n",
"from collections import defaultdict"
],
"metadata": {
"id": "ZZA7DkgJ40Os"
},
"execution_count": 29,
"outputs": []
},
{
"cell_type": "code",
"source": [
"ratings = pd.read_csv('/content/drive/My Drive/movie-dataset/ml-latest-small/ratings.csv')\n",
"ratings.info()"
],
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/"
},
"id": "cMJ36e5hOsKR",
"outputId": "783b9960-f92c-495b-94a9-04f79cb77a25"
},
"execution_count": 30,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"<class 'pandas.core.frame.DataFrame'>\n",
"RangeIndex: 100836 entries, 0 to 100835\n",
"Data columns (total 4 columns):\n",
" # Column Non-Null Count Dtype \n",
"--- ------ -------------- ----- \n",
" 0 userId 100836 non-null int64 \n",
" 1 movieId 100836 non-null int64 \n",
" 2 rating 100836 non-null float64\n",
" 3 timestamp 100836 non-null int64 \n",
"dtypes: float64(1), int64(3)\n",
"memory usage: 3.1 MB\n"
]
}
]
},
{
"cell_type": "code",
"source": [
"reader = Reader(rating_scale=(0, 5)) # Define the rating scale; 0 min and 5 max rating\n",
"ratings_data = Dataset.load_from_df(ratings[['userId', 'movieId', 'rating']], reader)"
],
"metadata": {
"id": "VVRQ4gD0bu1u"
},
"execution_count": 31,
"outputs": []
},
{
"cell_type": "code",
"source": [
"trainset, testset = train_test_split(ratings_data, test_size=0.30, shuffle=True)"
],
"metadata": {
"id": "88vyutdBdRnq"
},
"execution_count": 32,
"outputs": []
},
{
"cell_type": "code",
"source": [
"# Compute Precision and Recall at k for each user.\n",
"def precision_recall_at_k(predictions, k=5, threshold=3.5):\n",
" # Map the predictions to each user.\n",
" user_est_true = defaultdict(list)\n",
" for uid, iid, true_r, est, details in predictions:\n",
" user_est_true[uid].append((est, true_r))\n",
"\n",
" precisions = []\n",
" recalls = []\n",
"\n",
" for uid, user_ratings in user_est_true.items():\n",
" # Sort user ratings by estimated value\n",
" user_ratings.sort(key=lambda x: x[0], reverse=True)\n",
"\n",
" # Number of relevant items\n",
" n_rel = sum((true_r >= threshold) for (_, true_r) in user_ratings)\n",
"\n",
" # Number of recommended items in top k\n",
" n_rec_k = sum((est >= threshold) for (est, _) in user_ratings[:k])\n",
"\n",
" # Number of relevant and recommended items in top k\n",
" n_rel_and_rec_k = sum(((true_r >= threshold) and (est >= threshold))\n",
" for (est, true_r) in user_ratings[:k])\n",
"\n",
" # Precision@K\n",
" if n_rec_k != 0:\n",
" precisions.append(n_rel_and_rec_k / n_rec_k)\n",
" else:\n",
" precisions.append(0)\n",
"\n",
" # Recall@K\n",
" if n_rel != 0:\n",
" recalls.append(n_rel_and_rec_k / n_rel)\n",
" else:\n",
" recalls.append(0)\n",
"\n",
" # Average precision and recall over all users\n",
" avg_precision = sum(precisions) / len(precisions)\n",
" avg_recall = sum(recalls) / len(recalls)\n",
"\n",
" return avg_precision, avg_recall"
],
"metadata": {
"id": "4BwEvmD447Oh"
},
"execution_count": 33,
"outputs": []
},
{
"cell_type": "code",
"source": [
"n_epochs = 100\n",
"rmse_values = []\n",
"precision_values = []\n",
"recall_values = []\n",
"\n",
"for epoch in range(1, n_epochs + 1):\n",
" svd_model = SVD(n_epochs=epoch, reg_all=0.1)\n",
" svd_model.fit(trainset)\n",
"\n",
" # Evaluate on test set\n",
" predictions = svd_model.test(testset)\n",
" rmse = accuracy.rmse(predictions, verbose=False)\n",
" rmse_values.append(rmse)\n",
"\n",
" # Compute precision and recall on test set\n",
" precision, recall = precision_recall_at_k(predictions, k=5, threshold=3.5)\n",
" precision_values.append(precision)\n",
" recall_values.append(recall)\n",
"\n",
" print(f\"Epoch: {epoch} | RMSE: {rmse:.4f} | Precision: {precision:.4f} | Recall: {recall:.4f}\")\n",
"\n"
],
"metadata": {
"id": "6GNLOKkwdV71",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "f4f9c10c-ec4b-4426-8cbb-d89d1cc58bde"
},
"execution_count": 34,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"Epoch: 1 | RMSE: 0.9403 | Precision: 0.7572 | Recall: 0.3068\n",
"Epoch: 2 | RMSE: 0.9178 | Precision: 0.7772 | Recall: 0.3139\n",
"Epoch: 3 | RMSE: 0.9062 | Precision: 0.7802 | Recall: 0.3074\n",
"Epoch: 4 | RMSE: 0.8986 | Precision: 0.7833 | Recall: 0.3110\n",
"Epoch: 5 | RMSE: 0.8940 | Precision: 0.7805 | Recall: 0.3076\n",
"Epoch: 6 | RMSE: 0.8898 | Precision: 0.7865 | Recall: 0.3089\n",
"Epoch: 7 | RMSE: 0.8874 | Precision: 0.7816 | Recall: 0.3087\n",
"Epoch: 8 | RMSE: 0.8847 | Precision: 0.7823 | Recall: 0.3078\n",
"Epoch: 9 | RMSE: 0.8819 | Precision: 0.7844 | Recall: 0.3074\n",
"Epoch: 10 | RMSE: 0.8798 | Precision: 0.7805 | Recall: 0.3067\n",
"Epoch: 11 | RMSE: 0.8783 | Precision: 0.7839 | Recall: 0.3026\n",
"Epoch: 12 | RMSE: 0.8771 | Precision: 0.7826 | Recall: 0.3046\n",
"Epoch: 13 | RMSE: 0.8761 | Precision: 0.7903 | Recall: 0.3038\n",
"Epoch: 14 | RMSE: 0.8754 | Precision: 0.7908 | Recall: 0.3038\n",
"Epoch: 15 | RMSE: 0.8740 | Precision: 0.7829 | Recall: 0.3015\n",
"Epoch: 16 | RMSE: 0.8729 | Precision: 0.7823 | Recall: 0.3007\n",
"Epoch: 17 | RMSE: 0.8724 | Precision: 0.7892 | Recall: 0.3034\n",
"Epoch: 18 | RMSE: 0.8718 | Precision: 0.7823 | Recall: 0.3009\n",
"Epoch: 19 | RMSE: 0.8709 | Precision: 0.7815 | Recall: 0.2973\n",
"Epoch: 20 | RMSE: 0.8702 | Precision: 0.7848 | Recall: 0.3020\n",
"Epoch: 21 | RMSE: 0.8698 | Precision: 0.7814 | Recall: 0.2982\n",
"Epoch: 22 | RMSE: 0.8698 | Precision: 0.7815 | Recall: 0.2980\n",
"Epoch: 23 | RMSE: 0.8691 | Precision: 0.7792 | Recall: 0.3015\n",
"Epoch: 24 | RMSE: 0.8679 | Precision: 0.7810 | Recall: 0.3002\n",
"Epoch: 25 | RMSE: 0.8682 | Precision: 0.7721 | Recall: 0.2992\n",
"Epoch: 26 | RMSE: 0.8674 | Precision: 0.7783 | Recall: 0.2986\n",
"Epoch: 27 | RMSE: 0.8672 | Precision: 0.7769 | Recall: 0.3000\n",
"Epoch: 28 | RMSE: 0.8667 | Precision: 0.7831 | Recall: 0.2989\n",
"Epoch: 29 | RMSE: 0.8661 | Precision: 0.7809 | Recall: 0.3015\n",
"Epoch: 30 | RMSE: 0.8658 | Precision: 0.7839 | Recall: 0.2970\n",
"Epoch: 31 | RMSE: 0.8654 | Precision: 0.7815 | Recall: 0.2993\n",
"Epoch: 32 | RMSE: 0.8650 | Precision: 0.7770 | Recall: 0.2987\n",
"Epoch: 33 | RMSE: 0.8652 | Precision: 0.7801 | Recall: 0.2964\n",
"Epoch: 34 | RMSE: 0.8645 | Precision: 0.7795 | Recall: 0.3020\n",
"Epoch: 35 | RMSE: 0.8638 | Precision: 0.7804 | Recall: 0.2968\n",
"Epoch: 36 | RMSE: 0.8630 | Precision: 0.7846 | Recall: 0.3010\n",
"Epoch: 37 | RMSE: 0.8624 | Precision: 0.7810 | Recall: 0.3004\n",
"Epoch: 38 | RMSE: 0.8643 | Precision: 0.7749 | Recall: 0.2998\n",
"Epoch: 39 | RMSE: 0.8629 | Precision: 0.7813 | Recall: 0.2974\n",
"Epoch: 40 | RMSE: 0.8620 | Precision: 0.7856 | Recall: 0.3002\n",
"Epoch: 41 | RMSE: 0.8620 | Precision: 0.7714 | Recall: 0.2980\n",
"Epoch: 42 | RMSE: 0.8620 | Precision: 0.7885 | Recall: 0.3027\n",
"Epoch: 43 | RMSE: 0.8607 | Precision: 0.7868 | Recall: 0.3013\n",
"Epoch: 44 | RMSE: 0.8616 | Precision: 0.7829 | Recall: 0.2986\n",
"Epoch: 45 | RMSE: 0.8607 | Precision: 0.7921 | Recall: 0.3014\n",
"Epoch: 46 | RMSE: 0.8608 | Precision: 0.7798 | Recall: 0.2999\n",
"Epoch: 47 | RMSE: 0.8595 | Precision: 0.7853 | Recall: 0.3014\n",
"Epoch: 48 | RMSE: 0.8602 | Precision: 0.7769 | Recall: 0.2981\n",
"Epoch: 49 | RMSE: 0.8586 | Precision: 0.7890 | Recall: 0.3006\n",
"Epoch: 50 | RMSE: 0.8588 | Precision: 0.7904 | Recall: 0.3021\n",
"Epoch: 51 | RMSE: 0.8583 | Precision: 0.7837 | Recall: 0.3032\n",
"Epoch: 52 | RMSE: 0.8581 | Precision: 0.7914 | Recall: 0.3025\n",
"Epoch: 53 | RMSE: 0.8577 | Precision: 0.7903 | Recall: 0.3022\n",
"Epoch: 54 | RMSE: 0.8583 | Precision: 0.7831 | Recall: 0.3024\n",
"Epoch: 55 | RMSE: 0.8584 | Precision: 0.7874 | Recall: 0.3003\n",
"Epoch: 56 | RMSE: 0.8579 | Precision: 0.7925 | Recall: 0.3035\n",
"Epoch: 57 | RMSE: 0.8568 | Precision: 0.7933 | Recall: 0.3053\n",
"Epoch: 58 | RMSE: 0.8572 | Precision: 0.7942 | Recall: 0.3030\n",
"Epoch: 59 | RMSE: 0.8577 | Precision: 0.7851 | Recall: 0.3013\n",
"Epoch: 60 | RMSE: 0.8570 | Precision: 0.7850 | Recall: 0.2969\n",
"Epoch: 61 | RMSE: 0.8567 | Precision: 0.7856 | Recall: 0.2987\n",
"Epoch: 62 | RMSE: 0.8563 | Precision: 0.7892 | Recall: 0.3022\n",
"Epoch: 63 | RMSE: 0.8563 | Precision: 0.7886 | Recall: 0.3020\n",
"Epoch: 64 | RMSE: 0.8565 | Precision: 0.7889 | Recall: 0.3015\n",
"Epoch: 65 | RMSE: 0.8554 | Precision: 0.7960 | Recall: 0.3063\n",
"Epoch: 66 | RMSE: 0.8552 | Precision: 0.7883 | Recall: 0.3051\n",
"Epoch: 67 | RMSE: 0.8557 | Precision: 0.7936 | Recall: 0.3043\n",
"Epoch: 68 | RMSE: 0.8570 | Precision: 0.7942 | Recall: 0.3030\n",
"Epoch: 69 | RMSE: 0.8554 | Precision: 0.7973 | Recall: 0.3074\n",
"Epoch: 70 | RMSE: 0.8563 | Precision: 0.7821 | Recall: 0.2997\n",
"Epoch: 71 | RMSE: 0.8546 | Precision: 0.7974 | Recall: 0.3024\n",
"Epoch: 72 | RMSE: 0.8556 | Precision: 0.7955 | Recall: 0.3037\n",
"Epoch: 73 | RMSE: 0.8552 | Precision: 0.7948 | Recall: 0.3026\n",
"Epoch: 74 | RMSE: 0.8543 | Precision: 0.8004 | Recall: 0.3054\n",
"Epoch: 75 | RMSE: 0.8546 | Precision: 0.7935 | Recall: 0.3004\n",
"Epoch: 76 | RMSE: 0.8550 | Precision: 0.7894 | Recall: 0.3040\n",
"Epoch: 77 | RMSE: 0.8543 | Precision: 0.7864 | Recall: 0.3037\n",
"Epoch: 78 | RMSE: 0.8552 | Precision: 0.7924 | Recall: 0.3045\n",
"Epoch: 79 | RMSE: 0.8547 | Precision: 0.7880 | Recall: 0.3048\n",
"Epoch: 80 | RMSE: 0.8536 | Precision: 0.7957 | Recall: 0.3036\n",
"Epoch: 81 | RMSE: 0.8537 | Precision: 0.7928 | Recall: 0.3035\n",
"Epoch: 82 | RMSE: 0.8538 | Precision: 0.7956 | Recall: 0.3051\n",
"Epoch: 83 | RMSE: 0.8536 | Precision: 0.7919 | Recall: 0.3024\n",
"Epoch: 84 | RMSE: 0.8529 | Precision: 0.7930 | Recall: 0.3043\n",
"Epoch: 85 | RMSE: 0.8524 | Precision: 0.7969 | Recall: 0.3020\n",
"Epoch: 86 | RMSE: 0.8519 | Precision: 0.7971 | Recall: 0.3034\n",
"Epoch: 87 | RMSE: 0.8538 | Precision: 0.7935 | Recall: 0.3063\n",
"Epoch: 88 | RMSE: 0.8529 | Precision: 0.8011 | Recall: 0.3060\n",
"Epoch: 89 | RMSE: 0.8521 | Precision: 0.7981 | Recall: 0.3065\n",
"Epoch: 90 | RMSE: 0.8524 | Precision: 0.7859 | Recall: 0.3024\n",
"Epoch: 91 | RMSE: 0.8514 | Precision: 0.7959 | Recall: 0.3088\n",
"Epoch: 92 | RMSE: 0.8533 | Precision: 0.7976 | Recall: 0.3078\n",
"Epoch: 93 | RMSE: 0.8523 | Precision: 0.7967 | Recall: 0.3068\n",
"Epoch: 94 | RMSE: 0.8542 | Precision: 0.7899 | Recall: 0.3036\n",
"Epoch: 95 | RMSE: 0.8520 | Precision: 0.7898 | Recall: 0.3041\n",
"Epoch: 96 | RMSE: 0.8534 | Precision: 0.7961 | Recall: 0.3043\n",
"Epoch: 97 | RMSE: 0.8529 | Precision: 0.7974 | Recall: 0.3073\n",
"Epoch: 98 | RMSE: 0.8530 | Precision: 0.7926 | Recall: 0.3051\n",
"Epoch: 99 | RMSE: 0.8534 | Precision: 0.7981 | Recall: 0.3085\n",
"Epoch: 100 | RMSE: 0.8517 | Precision: 0.7978 | Recall: 0.3075\n"
]
}
]
},
{
"cell_type": "code",
"source": [
"import matplotlib.pyplot as plt\n",
"\n",
"epochs = range(1, n_epochs + 1)\n",
"\n",
"plt.figure(figsize=(10, 6))\n",
"plt.plot(epochs, rmse_values, label='RMSE')\n",
"plt.plot(epochs, precision_values, label='Precision')\n",
"plt.plot(epochs, recall_values, label='Recall')\n",
"plt.xlabel('Number of Epochs')\n",
"plt.ylabel('Values')\n",
"plt.legend()\n",
"plt.grid(True)\n",
"plt.show()"
],
"metadata": {
"id": "AFgbkgBWihoq",
"colab": {
"base_uri": "https://localhost:8080/",
"height": 542
},
"outputId": "1a0247ae-be50-483d-d777-e93574966ecc"
},
"execution_count": 35,
"outputs": [
{
"output_type": "display_data",
"data": {
"text/plain": [
"<Figure size 1000x600 with 1 Axes>"
],
"image/png": "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LmAIa/HTFEVBZmYmvv/+e/zhD39AUVERwsLC8K9//QsGgwEA8P7770OWZfzrX/+CJEkAgHfeeQdRUVFYs2YNJk2ahGeeeQZ/+tOfcN999/lfe+TIkQ1uMysrCz179sSFF14ISZLQpUsXjB07Fjabrd66H374IZxOJ/79738jLEy8vzfeeANXX301XnjhBSQmJgIAoqOj8cYbb0Cr1aJPnz648sorkZmZiTvvvLPFn0lLsMUpxEWHGfD+HaPRJdaC7NJqzPjXRpRUuYJdLCIiIiI6S3z11VewWq0wmUy44oorMH36dDzxxBMAgIEDB/pDEwDs3LkThw8fRnh4OKxWK6xWK2JiYuB0OnHkyBEUFhYiNzcXl19+ebO2fdttt2HHjh3o3bs37r33XqxYsaLRdfft24fBgwf7QxMAjBs3DrIs48CBA/77+vfvD61W67+dnJyMwsLC5n4crcYWp7NAQoQJH/x2NG5YtAFHiuy4dfFm/OeuCxBp1ge7aERERETnJ71FtPwEa9stcOmll+LNN9+EwWBASkoKdLq6CHBqSAGAqqoqDB8+HB988EG914mPj4dG07J2l2HDhuHYsWP49ttvsXLlStx44424/PLLsXjx4ha9zqn0+sB9YEmSIMtyq1+vudjidJboFG3BB78djTirAXvzbJj9zmbYXe03jSQRERERNUGSRHe5YCw1XeiaKywsDD169EDnzp0DQlNDhg0bhkOHDiEhIQE9evQIWCIjIxEeHo709HRkZmY2e/sRERGYPn063n77bSxbtgyff/45ysrK6q3Xt29f7Ny5E3a73X/fTz/9BI1G45+4IpgYnM4i3eKt+PftoxFh0mF7VjluensjjhZVqT+RiIiIiKgZZsyYgbi4OFx77bX44YcfcOzYMaxZswb33nsvcnJyAABPPPEEXn75Zbz22ms4dOgQtm/fjtdff73B13vllVfwn//8B/v378fBgwfxySefICkpCZGRkQ1u22QyYdasWdi9ezdWr16NP/zhD7j11lv945uCKejBaeHChUhPT4fJZMLo0aOxefPmRtf1eDx46qmn0L17d5hMJgwePBjfffddB5Y2+PqlRGDp7aMQadbjl5wKXPnaj/hg0wkoZzi7ChERERGRxWLBunXr0LlzZ1x33XXo27cv7rjjDjidTkRERAAAZs2ahQULFuAf//gH+vfvj6uu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IiIiokYwOAXTwe/F5bj7AWtCUItCRERERESN4wlwg+m6fwL9fwV0Gx/skhARERERURMYnIJJkoA+U4NdCiIiIiIiUsGuekRERERERCoYnIiIiIiIiFQwOBEREREREalgcCIiIiIiIlLB4ERERERERKSCwYmIiIiIiEgFgxMREREREZEKBiciIiIiIiIVDE5EREREREQqGJyIiIiIiIhUMDgRERERERGpYHAiIiIiIiJSweBERERERESkgsGJiIiIiIhIBYMTERERERGRCgYnIiIiIiIiFQxOREREREREKhiciIiIiIiIVDA4ERERERERqWBwIiIiIiIiUsHgREREREREpILBiYiIiIiISAWDExERERERkQoGJyIiIiIiIhUMTkRERERERCoYnIiIiIiIiFQwOBEREREREalgcCIiIiIiIlLB4ERERERERKSCwYmIiIiIiEhFSASnhQsXIj09HSaTCaNHj8bmzZsbXfeSSy6BJEn1liuvvLIDS0xEREREROeToAenZcuWYe7cuXj88cexfft2DB48GJMnT0ZhYWGD63/++efIy8vzL7t374ZWq8UNN9zQwSUnIiIiIqLzRdCD0yuvvII777wTs2fPRr9+/bBo0SJYLBYsWbKkwfVjYmKQlJTkXzIyMmCxWBiciIiIiIio3eiCuXG3241t27Zh3rx5/vs0Gg0mTJiADRs2NOs1Fi9ejN/85jcICwtr8HGXywWXy+W/bbPZAAAejwcej+cMSt88tdvoiG3RuYV1h1qD9YZag/WGWot1h1ojlOpNS8oQ1OBUXFwMn8+HxMTEgPsTExOxf/9+1edv3rwZu3fvxuLFixtdZ/78+XjyySfr3b9ixQpYLJaWF7qVMjIyOmxbdG5h3aHWYL2h1mC9odZi3aHWCIV643A4mr1uUIPTmVq8eDEGDhyIUaNGNbrOvHnzMHfuXP9tm82GtLQ0TJo0CREREe1eRo/Hg4yMDEycOBF6vb7dt0fnDtYdag3WG2oN1htqLdYdao1Qqje1vdGaI6jBKS4uDlqtFgUFBQH3FxQUICkpqcnn2u12fPTRR3jqqaeaXM9oNMJoNNa7X6/Xd+gX1dHbo3MH6w61BusNtQbrDbUW6w61RijUm5ZsP6iTQxgMBgwfPhyZmZn++2RZRmZmJsaMGdPkcz/55BO4XC7ccsst7V1MIiIiIiI6zwW9q97cuXMxa9YsjBgxAqNGjcKCBQtgt9sxe/ZsAMDMmTORmpqK+fPnBzxv8eLFmDZtGmJjY4NRbCIiIiIiOo8EPThNnz4dRUVFeOyxx5Cfn48hQ4bgu+++808YkZWVBY0msGHswIED+PHHH7FixYpgFJmIiIiIiM4zQQ9OADBnzhzMmTOnwcfWrFlT777evXtDUZR2LhUREREREZEQ9BPgEhERERERhToGJyIiIiIiIhUMTkRERERERCoYnIiIiIiIiFQwOBEREREREalgcCIiIiIiIlLB4ERERERERKSCwYmIiIiIiEgFgxMREREREZEKBiciIiIiIiIVDE5EREREREQqGJyIiIiIiIhUMDgRERERERGpYHAiIiIiIiJSweBERERERESkgsGJiIiIiIhIBYMTERERERGRCgYnIiIiIiIiFQxOREREREREKhiciIiIiIiIVDA4ERERERERqWBwIiIiIiIiUsHgREREREREpILBiYiIiIiISAWDExERERERkQoGJyIiIiIiIhUMTkRERERERCpaHJyys7ORk5Pjv71582bcf//9+Oc//9mmBSMiIiIiIgoVLQ5ON998M1avXg0AyM/Px8SJE7F582b85S9/wVNPPdXmBSQiIiIiIgq2Fgen3bt3Y9SoUQCAjz/+GAMGDMD69evxwQcfYOnSpW1dPiIiIiIioqBrcXDyeDwwGo0AgJUrV+Kaa64BAPTp0wd5eXltWzoiIiIiIqIQ0OLg1L9/fyxatAg//PADMjIyMGXKFABAbm4uYmNj27yAREREREREwdbi4PTCCy/grbfewiWXXIKbbroJgwcPBgB8+eWX/i58RERERERE5xJdS59wySWXoLi4GDabDdHR0f7777rrLlgsljYtHBERERERUSho1XmcFEXBtm3b8NZbb6GyshIAYDAYGJyIiIiIiOic1OIWpxMnTmDKlCnIysqCy+XCxIkTER4ejhdeeAEulwuLFi1qj3ISEREREREFTYtbnO677z6MGDECZWVlMJvN/vt/9atfITMzs00LR0REREREFApa3OL0ww8/YP369TAYDAH3p6en4+TJk21WMCIiIiIiolDR4hYnWZbh8/nq3Z+Tk4Pw8PA2KRQREREREVEoaXFwmjRpEhYsWOC/LUkSqqqq8Pjjj2Pq1KltWTYiIiIiIqKQ0OKuei+//DImT56Mfv36wel04uabb8ahQ4cQFxeH//znP+1RRiIiIiIioqBqcXDq1KkTdu7ciY8++gi//PILqqqqcMcdd2DGjBkBk0UQERERERGdK1ocnABAp9PhlltuaeuyEBERERERhaQWB6d///vfTT4+c+bMVheGiIiIiIgoFLU4ON13330Btz0eDxwOBwwGAywWC4MTERERERGdc1o8q15ZWVnAUlVVhQMHDuDCCy/k5BBERERERHROanFwakjPnj3x/PPP12uNIiIiIiIiOhe0SXACxIQRubm5bfVyREREREREIaPFY5y+/PLLgNuKoiAvLw9vvPEGxo0b12YFIyIiIiIiChUtDk7Tpk0LuC1JEuLj43HZZZfh5ZdfbqtyERERERERhYwWBydZltujHERERERERCGrzcY4ERERERERnaua1eI0d+7cZr/gK6+80urCEBERERERhaJmBaeff/65WS8mSdIZFYaIiIiIiCgUNSs4rV69ut0KsHDhQrz00kvIz8/H4MGD8frrr2PUqFGNrl9eXo6//OUv+Pzzz1FaWoouXbpgwYIFmDp1aruVkYiIiIiIzm8tnhyiLS1btgxz587FokWLMHr0aCxYsACTJ0/GgQMHkJCQUG99t9uNiRMnIiEhAZ9++ilSU1Nx4sQJREVFdXzhiYiIiIjovNGq4LR161Z8/PHHyMrKgtvtDnjs888/b/brvPLKK7jzzjsxe/ZsAMCiRYvw9ddfY8mSJXj44Yfrrb9kyRKUlpZi/fr10Ov1AID09PTWvAUiIiIiIqJma/Gseh999BHGjh2Lffv24YsvvoDH48GePXuwatUqREZGNvt13G43tm3bhgkTJtQVRqPBhAkTsGHDhgaf8+WXX2LMmDG45557kJiYiAEDBuC5556Dz+dr6dsgIiIiIiJqtha3OD333HN49dVXcc899yA8PBx///vf0bVrV/zud79DcnJys1+nuLgYPp8PiYmJAfcnJiZi//79DT7n6NGjWLVqFWbMmIFvvvkGhw8fxv/93//B4/Hg8ccfb/A5LpcLLpfLf9tmswEAPB4PPB5Ps8vbWrXb6Iht0bmFdYdag/WGWoP1hlqLdYdaI5TqTUvKICmKorTkxcPCwrBnzx6kp6cjNjYWa9aswcCBA7Fv3z5cdtllyMvLa9br5ObmIjU1FevXr8eYMWP89z/44INYu3YtNm3aVO85vXr1gtPpxLFjx6DVagGI7n4vvfRSo9t94okn8OSTT9a7/8MPP4TFYmlWWYmIiIiI6NzjcDhw8803o6KiAhEREU2u2+IWp+joaFRWVgIAUlNTsXv3bgwcOBDl5eVwOBzNfp24uDhotVoUFBQE3F9QUICkpKQGn5OcnAy9Xu8PTQDQt29f5Ofnw+12w2Aw1HvOvHnzAs5DZbPZkJaWhkmTJql+OG3B4/EgIyMDEydO9I/LImoO1h1qDdYbag3WG2ot1h1qjVCqN7W90Zqj2cFp9+7dGDBgAC6++GJkZGRg4MCBuOGGG3Dfffdh1apVyMjIwOWXX97sDRsMBgwfPhyZmZmYNm0aAECWZWRmZmLOnDkNPmfcuHH48MMPIcsyNBoxPOvgwYNITk5uMDQBgNFohNForHe/Xq/v0C+qo7dH5w7WHWoN1htqDdYbai3WHWqNUKg3Ldl+syeHGDRoEEaPHu0PTADwl7/8BXPnzkVBQQGuv/56LF68uEUFnTt3Lt5++228++672LdvH+6++27Y7Xb/LHszZ87EvHnz/OvffffdKC0txX333YeDBw/i66+/xnPPPYd77rmnRdslIiIiIiJqiWa3OK1duxbvvPMO5s+fj2effRbXX389fvvb3zY4bXhzTZ8+HUVFRXjssceQn5+PIUOG4LvvvvNPGJGVleVvWQKAtLQ0fP/99/jjH/+IQYMGITU1Fffddx8eeuihVpeBiIiIiIhITbOD00UXXYSLLroIr7/+Oj7++GMsXboU48ePR48ePXDHHXdg1qxZjY5NasqcOXMa7Zq3Zs2aeveNGTMGGzdubPF2iIiIiIiIWqvF53EKCwvD7NmzsXbtWhw8eBA33HADFi5ciM6dO+Oaa65pjzISEREREREFVYuD06l69OiBRx55BI8++ijCw8Px9ddft1W5iIiIiIiIQkaLpyOvtW7dOixZsgSfffYZNBoNbrzxRtxxxx1tWTYiIiIiIqKQ0KLglJubi6VLl2Lp0qU4fPgwxo4di9deew033ngjwsLC2quMREREREREQdXs4HTFFVdg5cqViIuLw8yZM3H77bejd+/e7Vk2IiIiIiKikNDs4KTX6/Hpp5/iqquuglarbc8yERERERERhZRmB6cvv/yyPctBREREREQUss5oVj0iIiIiIqLzAYMTERERERGRCgYnIiIiIiIiFQxOREREREREKhiciIiIiIiIVDA4ERERERERqWBwIiIiIiIiUsHgREREREREpILBiYiIiIiISAWDExERERERkQoGJyIiIiIiIhUMTkRERERERCoYnIiIiIiIiFQwOBEREREREalgcCIiIiIiIlLB4ERERERERKSCwYmIiIiIiEgFgxMREREREZEKBiciIiIiIiIVDE5EREREREQqGJyIiIiIiIhUMDgRERERERGpYHAiIiIiIiJSweBERERERESkgsGJiIiIiIhIBYMTERERERGRCgYnIiIiIiIiFQxOREREREREKhiciIiIiIiIVDA4ERERERERqWBwIiIiIiIiUsHgREREREREpILBiYiIiIiISAWDExERERERkQoGJyIiIiIiIhUMTkRERERERCoYnIiIiIiIiFQwOBEREREREalgcCIiIiIiIlLB4ERERERERKSCwYmIiIiIiEgFgxMREREREZEKBiciIiIiIiIVDE5EREREREQqGJyIiIiIiIhUMDgRERERERGpYHAiIiIiIiJSweBERERERESkIiSC08KFC5Geng6TyYTRo0dj8+bNja67dOlSSJIUsJhMpg4sLRERERERnW+CHpyWLVuGuXPn4vHHH8f27dsxePBgTJ48GYWFhY0+JyIiAnl5ef7lxIkTHVhiIiIiIiI63wQ9OL3yyiu48847MXv2bPTr1w+LFi2CxWLBkiVLGn2OJElISkryL4mJiR1YYiIiIiIiOt/ogrlxt9uNbdu2Yd68ef77NBoNJkyYgA0bNjT6vKqqKnTp0gWyLGPYsGF47rnn0L9//wbXdblccLlc/ts2mw0A4PF44PF42uidNK52Gx2xLTq3sO5Qa7DeUGuw3lBrse5Qa4RSvWlJGSRFUZR2LEuTcnNzkZqaivXr12PMmDH++x988EGsXbsWmzZtqvecDRs24NChQxg0aBAqKirwt7/9DevWrcOePXvQqVOneus/8cQTePLJJ+vd/+GHH8JisbTtGyIiIiIiorOGw+HAzTffjIqKCkRERDS5blBbnFpjzJgxASFr7Nix6Nu3L9566y08/fTT9dafN28e5s6d679ts9mQlpaGSZMmqX44bcHj8SAjIwMTJ06EXq9v9+3RuYN1h1qD9YZag/WGWot1h1ojlOpNbW+05ghqcIqLi4NWq0VBQUHA/QUFBUhKSmrWa+j1egwdOhSHDx9u8HGj0Qij0djg8zryi+ro7dG5g3WHWoP1hlqD9YZai3WHWiMU6k1Lth/UySEMBgOGDx+OzMxM/32yLCMzMzOgVakpPp8Pu3btQnJycnsVk4iIiIiIznNB76o3d+5czJo1CyNGjMCoUaOwYMEC2O12zJ49GwAwc+ZMpKamYv78+QCAp556ChdccAF69OiB8vJyvPTSSzhx4gR++9vfBvNtEBERERHROSzowWn69OkoKirCY489hvz8fAwZMgTfffedf4rxrKwsaDR1DWNlZWW48847kZ+fj+joaAwfPhzr169Hv379gvUWiIiIiIjoHBf04AQAc+bMwZw5cxp8bM2aNQG3X331Vbz66qsdUCoiIiIiIiIh6CfAJSIiIiIiCnUMTkRERERERCoYnIiIiIiIiFQwOBEREREREalgcCIiIiIiIlLB4ERERERERKSCwYmIiIiIiEgFgxMREREREZEKBiciIiIiIiIVDE5EREREREQqGJyIiIiIiIhUMDgRERERERGpYHAiIiIiIiJSweBERERERESkgsGJiIiIiIhIBYMTERERERGRCgYnIiIiIiIiFQxOREREREREKhiciIiIiIiIVDA4ERERERERqWBwIiIiIiIiUsHgREREREREpILBiYiIiIiISAWDExERERERkQoGJyIiIiIiIhUMTkRERERERCoYnIiIiIiIiFQwOBEREREREalgcCIiIiIiIlLB4ERERERERKSCwYmIiIiIiEgFgxMREREREZEKBiciIiIiIiIVDE5EREREREQqGJyIiIiIiIhUMDgRERERERGpYHAiIiIiIiJSweBERERERESkgsGJiIiIiIhIBYMTERERERGRCgYnIiIiIiIiFQxOREREREREKhiciIiIiIiIVDA4ERERERERqWBwIiIiIiIiUsHgREREREREpILBiYiIiIiISAWDExERERERkQoGJyIiIiIiIhUMTkRERERERCoYnIiIiIiIiFQwOBEREREREalgcCIiIiIiIlLB4ERERERERKQiJILTwoULkZ6eDpPJhNGjR2Pz5s3Net5HH30ESZIwbdq09i0gERERERGd14IenJYtW4a5c+fi8ccfx/bt2zF48GBMnjwZhYWFTT7v+PHjeOCBB3DRRRd1UEmJiIiIiOh8FfTg9Morr+DOO+/E7Nmz0a9fPyxatAgWiwVLlixp9Dk+nw8zZszAk08+iW7dunVgaYmIiIiI6HykC+bG3W43tm3bhnnz5vnv02g0mDBhAjZs2NDo85566ikkJCTgjjvuwA8//NDkNlwuF1wul/+2zWYDAHg8Hng8njN8B+pqt9ER26JzC+sOtQbrDbUG6w21FusOtUYo1ZuWlCGowam4uBg+nw+JiYkB9ycmJmL//v0NPufHH3/E4sWLsWPHjmZtY/78+XjyySfr3b9ixQpYLJYWl7m1MjIyOmxbdG5h3aHWYL2h1mC9odZi3aHWCIV643A4mr1uUINTS1VWVuLWW2/F22+/jbi4uGY9Z968eZg7d67/ts1mQ1paGiZNmoSIiIj2Kqqfx+NBRkYGJk6cCL1e3+7bo3MH6w61BusNtQbrDbUW6w61RijVm9reaM0R1OAUFxcHrVaLgoKCgPsLCgqQlJRUb/0jR47g+PHjuPrqq/33ybIMANDpdDhw4AC6d+8e8Byj0Qij0VjvtfR6fYd+UR29PTp3sO5Qa7DeUGuw3lBrse5Qa4RCvWnJ9oM6OYTBYMDw4cORmZnpv0+WZWRmZmLMmDH11u/Tpw927dqFHTt2+JdrrrkGl156KXbs2IG0tLSOLD4REREREZ0ngt5Vb+7cuZg1axZGjBiBUaNGYcGCBbDb7Zg9ezYAYObMmUhNTcX8+fNhMpkwYMCAgOdHRUUBQL37iYiIiIiI2krQg9P06dNRVFSExx57DPn5+RgyZAi+++47/4QRWVlZ0GiCPms6ERERERGdx4IenABgzpw5mDNnToOPrVmzpsnnLl26tO0LREREREREdAo25RAREREREalgcCIiIiIiIlLB4ERERERERKSCwYmIiIiIiEgFgxMREREREZEKBiciIiIiIiIVDE5EREREREQqGJyIiIiIiIhUMDgRERERERGpYHAiIiIiIiJSweBERERERESkgsGJiIiIiIhIBYMTERERERGRCgYnIiIiIiIiFQxOREREREREKhiciIiIiIiIVDA4ERERERERqWBwIiIiIiIiUsHgREREREREpILBiYiIiIiISAWDExERERERkQoGJyIiIiIiIhUMTkRERERERCoYnIiIiIiIiFQwOBEREREREalgcCIiIiIiIlLB4ERERERERKSCwYmIiIiIiEgFgxMREREREZEKBiciIiIiIiIVDE5EREREREQqGJyIiIiIiIhUMDgRERER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\n"
},
"metadata": {}
}
]
},
{
"cell_type": "code",
"source": [
"user_id = 16\n",
"movie_id = 501\n",
"predicted_rating = svd_model.predict(user_id, movie_id)\n",
"print(predicted_rating)"
],
"metadata": {
"id": "h9mvZj7ydhx0",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "b0c3d860-43ad-4c05-feee-bc3073125955"
},
"execution_count": 36,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": [
"user: 16 item: 501 r_ui = None est = 3.61 {'was_impossible': False}\n"
]
}
]
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment