import pandas as pd
from sklearn.metrics.pairwise import cosine_similarity
import pandas as pd
import numpy as np
import jsonLibraries for this project
Introduction
Have you ever had a friend ask you to recommend a movie or song based on something they just watched or listened to? It’s pretty common, right? You think about what they might like, considering their taste, and suggest something similar. That’s basically what a recommender system does, but with a lot more data and a bit more sophistication.
Recommender systems are like your personal assistant, suggesting things you might like based on your preferences and behavior. They’re everywhere: Netflix suggests movies, Spotify recommends songs, and Amazon points you to products you might want to buy.
In general, there are three types od recommender systems:
Content-Based. Imagine you just watched a movie with your favorite genre. A content-based system would suggest other movies with similar genres and plot.
Collaborative Filtering. Think of Netflix or Spotify, where you get recommendations based on what other users with similar tastes liked.
Hybrid. Like the name suggests, this recommender system combine both content-based and colaborative filtering to give a more accurate suggestions.
In this project, we’re building a content-based recommender system using the TMDB 5000 dataset. We’ll create a system that can suggest movies based on their content.
Dataset
For this project, we’re using the TMDB 5000 dataset, which is a collection of movie information gathered from The Movie Database (TMDB). This dataset provides a wealth of details about movies, including metadata like titles, genres, plot summaries, cast and crew, release dates, and user ratings.
Structure of the Dataset
The TMDB 5000 dataset consists of two main files:
- movies_metadata.csv: Contains metadata for each movie, including title, genres, overview, release date, and more.
- credits.csv: Contains information about the cast and crew for each movie.
Movies Metadata
movies_metadata_df = pd.read_csv('data/tmdb_5000_movies.csv')
movies_metadata_df.shape(4803, 20)
movies_metadata_df.head(3)| budget | genres | homepage | id | keywords | original_language | original_title | overview | popularity | production_companies | production_countries | release_date | revenue | runtime | spoken_languages | status | tagline | title | vote_average | vote_count | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 237000000 | [{"id": 28, "name": "Action"}, {"id": 12, "nam... | http://www.avatarmovie.com/ | 19995 | [{"id": 1463, "name": "culture clash"}, {"id":... | en | Avatar | In the 22nd century, a paraplegic Marine is di... | 150.437577 | [{"name": "Ingenious Film Partners", "id": 289... | [{"iso_3166_1": "US", "name": "United States o... | 2009-12-10 | 2787965087 | 162.0 | [{"iso_639_1": "en", "name": "English"}, {"iso... | Released | Enter the World of Pandora. | Avatar | 7.2 | 11800 |
| 1 | 300000000 | [{"id": 12, "name": "Adventure"}, {"id": 14, "... | http://disney.go.com/disneypictures/pirates/ | 285 | [{"id": 270, "name": "ocean"}, {"id": 726, "na... | en | Pirates of the Caribbean: At World's End | Captain Barbossa, long believed to be dead, ha... | 139.082615 | [{"name": "Walt Disney Pictures", "id": 2}, {"... | [{"iso_3166_1": "US", "name": "United States o... | 2007-05-19 | 961000000 | 169.0 | [{"iso_639_1": "en", "name": "English"}] | Released | At the end of the world, the adventure begins. | Pirates of the Caribbean: At World's End | 6.9 | 4500 |
| 2 | 245000000 | [{"id": 28, "name": "Action"}, {"id": 12, "nam... | http://www.sonypictures.com/movies/spectre/ | 206647 | [{"id": 470, "name": "spy"}, {"id": 818, "name... | en | Spectre | A cryptic message from Bond’s past sends him o... | 107.376788 | [{"name": "Columbia Pictures", "id": 5}, {"nam... | [{"iso_3166_1": "GB", "name": "United Kingdom"... | 2015-10-26 | 880674609 | 148.0 | [{"iso_639_1": "fr", "name": "Fran\u00e7ais"},... | Released | A Plan No One Escapes | Spectre | 6.3 | 4466 |
Movies Credit
movies_credit_df = pd.read_csv('data/tmdb_5000_credits.csv')
movies_credit_df.shape(4803, 4)
movies_credit_df.head(3)| movie_id | title | cast | crew | |
|---|---|---|---|---|
| 0 | 19995 | Avatar | [{"cast_id": 242, "character": "Jake Sully", "... | [{"credit_id": "52fe48009251416c750aca23", "de... |
| 1 | 285 | Pirates of the Caribbean: At World's End | [{"cast_id": 4, "character": "Captain Jack Spa... | [{"credit_id": "52fe4232c3a36847f800b579", "de... |
| 2 | 206647 | Spectre | [{"cast_id": 1, "character": "James Bond", "cr... | [{"credit_id": "54805967c3a36829b5002c41", "de... |
Data Preprocessing
movies_metadata_df.info()<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4803 entries, 0 to 4802
Data columns (total 20 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 budget 4803 non-null int64
1 genres 4803 non-null object
2 homepage 1712 non-null object
3 id 4803 non-null int64
4 keywords 4803 non-null object
5 original_language 4803 non-null object
6 original_title 4803 non-null object
7 overview 4800 non-null object
8 popularity 4803 non-null float64
9 production_companies 4803 non-null object
10 production_countries 4803 non-null object
11 release_date 4802 non-null object
12 revenue 4803 non-null int64
13 runtime 4801 non-null float64
14 spoken_languages 4803 non-null object
15 status 4803 non-null object
16 tagline 3959 non-null object
17 title 4803 non-null object
18 vote_average 4803 non-null float64
19 vote_count 4803 non-null int64
dtypes: float64(3), int64(4), object(13)
memory usage: 750.6+ KB
movies_credit_df.info()<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4803 entries, 0 to 4802
Data columns (total 4 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 movie_id 4803 non-null int64
1 title 4803 non-null object
2 cast 4803 non-null object
3 crew 4803 non-null object
dtypes: int64(1), object(3)
memory usage: 150.2+ KB
Combining both tables
movies_df = movies_metadata_df.merge(movies_credit_df.drop('title',axis=1),left_on='id',right_on='movie_id', how='left').drop('movie_id', axis=1)Handling missing value
As can be seen in the previous chunk of code, there are some columns with missing values. Generally, we need to handle these missing values. In this case, we are lucky because most of the missing values are not directly related to the content of the movie. For example, the homepage is unlikely to be a significant factor when recommending a movie.
On the other hand, the overview column contains a summary of the movie’s plot, which is crucial for our recommendation system. Therefore, we need to handle missing values in this column carefully.
For now, we will ignore missing values in columns that do not directly impact the movie content and drop rows with missing values in crucial columns like overview.
Although tagline can contains information crucial for the content, we choose not to drop rows with missing values in the tagline column. The tagline is a more condensed version of the information in the overview, meaning its information is already covered by the overview. Therefore, the absence of a tagline does not significantly impact our ability to recommend movies based on their content. Additionally not all movie have a tagline.
movies_df = movies_df[~movies_df['overview'].isna()].reset_index(drop=True)movies_df.info()<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4800 entries, 0 to 4799
Data columns (total 22 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 budget 4800 non-null int64
1 genres 4800 non-null object
2 homepage 1712 non-null object
3 id 4800 non-null int64
4 keywords 4800 non-null object
5 original_language 4800 non-null object
6 original_title 4800 non-null object
7 overview 4800 non-null object
8 popularity 4800 non-null float64
9 production_companies 4800 non-null object
10 production_countries 4800 non-null object
11 release_date 4799 non-null object
12 revenue 4800 non-null int64
13 runtime 4800 non-null float64
14 spoken_languages 4800 non-null object
15 status 4800 non-null object
16 tagline 3959 non-null object
17 title 4800 non-null object
18 vote_average 4800 non-null float64
19 vote_count 4800 non-null int64
20 cast 4800 non-null object
21 crew 4800 non-null object
dtypes: float64(3), int64(4), object(15)
memory usage: 825.1+ KB
Exploratory Data Analysis
Since we have a content-based recommender system in mind, we will explore the data that contains information about the content of the movie. Imagine that your friend comes to you and says, “I’ve just watched The Dark Knight and I love it. Do you know any other movies like that?” There are several factors you can consider to give your friend recommendations:
The Genre. Maybe your friend likes action-packed movies or superhero movies. Considering this, you can recommend other action or superhero movies.
Keywords of the Movie. This movie might have some keywords attached to it, like DC Comics, superhero, Batman. Considering this, you can recommend not only random superhero movies but something like Superman or Wonder Woman that share the same keywords.
The Plot. Maybe your friend is interested in movies with similar plots. A movie where the hero has to save a city from a terrorist attack, for example.
The Director. Each movie director has their own style. Maybe your friend is more interested in the style of Christopher Nolan.
Considering all these factors and many others is the key to making a reliable recommender system.
Exploring Genre
How many Genre a movie have?
movies_df['genres'].apply(lambda x: len(json.loads(x))).describe()count 4800.000000
mean 2.532708
std 1.120651
min 0.000000
25% 2.000000
50% 2.000000
75% 3.000000
max 7.000000
Name: genres, dtype: float64
movies_df['genres'].apply(lambda x: len(json.loads(x))).value_counts().sort_index().plot.bar()
As can be seen in the description and the plot, a movie can have a range of 0-7 genres. A movie with 0 genres can be considered as a movie for which we don’t know the genre. From the visualization, we can see that most movies have 2 to 3 genres associated with them.
Genre Distribution
def create_genre_list(x):
genre_movie = []
jdata = json.loads(x)
for d in jdata:
genre_movie.append(d['name'])
return genre_movie
genre_series = []
for i, r in movies_df.iterrows():
genre_series += create_genre_list(r['genres'])
genre_series = pd.Series(genre_series)genre_series.value_counts().plot.bar()
len(genre_series.value_counts())20
We can see there are 20 genres recorded in total. From these 20 genres, the most common one is drama. However, keep in mind that what makes a movie unique is not just a single genre but combinations of genres. For example, an Action-Comedy movie will have a very different tone from an Action-Mystery movie. Therefore, it is important to maintain this connection later when we create our recommender system.
Exploring Keywords
Numbers of Keywords in a movie
movies_df['keywords'].apply(lambda x: len(json.loads(x))).describe()count 4800.000000
mean 7.538750
std 6.280662
min 0.000000
25% 3.000000
50% 6.000000
75% 11.000000
max 97.000000
Name: keywords, dtype: float64
movies_df['keywords'].apply(lambda x: len(json.loads(x))).value_counts().sort_index().plot.bar()
Unlike genres, a movie can have a lot more keywords associated with it. In our database, it can range up to 97 keywords, with a median of 6. This means half of the movies have at least 6 keywords attached to them.
How many keywords there are
keywords = []
for i,r in movies_df.iterrows():
jdata = json.loads(r['keywords'])
for d in jdata:
keywords.append(d['name'])
keywords = pd.Series(keywords)len(keywords.unique())9808
keywords.value_counts().describe()count 9808.000000
mean 3.689437
std 9.669485
min 1.000000
25% 1.000000
50% 1.000000
75% 3.000000
max 324.000000
Name: count, dtype: float64
The total number of keywords available is 9808, however, only a quarter of them appear in at least three different movies. We can keep this in mind, since a keyword that appears only in one or two movies will not provide much information for the recommender system.
Exploring Overview/The plot description
How many words contained in an overview?
movies_df['overview'].apply(lambda x: len(x.split())).describe()count 4800.000000
mean 52.157292
std 27.867016
min 0.000000
25% 30.000000
50% 48.000000
75% 67.000000
max 175.000000
Name: overview, dtype: float64
movies_df['overview'].apply(lambda x: len(x.split())).hist()
movies_df[movies_df['overview'].apply(lambda x: len(x.split())) == 0]| budget | genres | homepage | id | keywords | original_language | original_title | overview | popularity | production_companies | ... | revenue | runtime | spoken_languages | status | tagline | title | vote_average | vote_count | cast | crew | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 4399 | 0 | [{"id": 28, "name": "Action"}, {"id": 35, "nam... | NaN | 43630 | [] | en | The Helix... Loaded | 0.0206 | [] | ... | 0 | 97.0 | [{"iso_639_1": "en", "name": "English"}] | Rumored | NaN | The Helix... Loaded | 4.8 | 2 | [] | [] |
1 rows × 22 columns
As can be seen that the range of overview length is between 0 to 175. Remember that we’ve drop a movie with no overview. It seems like there are still a movie that don’t have an overview. Since it’s only one movie i think it is fair to also drop this movie
movies_df = movies_df[movies_df['overview'].apply(lambda x: len(x.split())) > 0].reset_index(drop=True)Exploring Director
Extracting Director from the Crew
def return_director(x):
jdata = json.loads(x)
for d in jdata:
if d['job'] == 'Director':
return d['name']
return ''movies_df['crew'].apply(return_director).value_counts().describe()count 2347.000000
mean 2.044738
std 2.173457
min 1.000000
25% 1.000000
50% 1.000000
75% 2.000000
max 29.000000
Name: count, dtype: float64
movies_df['crew'].apply(return_director).value_counts()[:20].plot.bar()
movies_df['director'] = movies_df['crew'].apply(return_director)There are 2346 director recorded in the data, however the majority of them only have one movie directed by them. With the same logic as keywords, we need to keep in mind that the director with only few movies will not enrich our recommender system
Feature Engineering
Considering Ranking and Scoring
When you recommend something to someone, you usually rank options based on how well they match what the person likes. In recommender systems, we do the same by calculating a score for each item. This score tells us how relevant each item is to the user’s interests, and we use these scores to rank the items.
In a recommender system, feature engineering is all about transforming raw data into something meaningful that can be used to make recommendations. Considering scoring in recommender systems, we need to transform all the content we have into something that can be quantified. In other words, we have to extract the features into vectors that we can calculate the similarities.
Genres
For our genres, we want to make a vector representation that considers that a movie can have more than one genre. Since there are only 20 genres available, it is reasonable for us to use a binary vector.
For example, if there are three genres only: Action, Drama, and Comedy: * A movie with Action-Drama genres will be represented with 1 1 0. * A movie with Drama-Comedy genres will be represented with 0 1 1.
from sklearn.preprocessing import FunctionTransformer
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
def extract_genre(X):
genre_matrix = []
for d in X:
row_genres = []
jdata = json.loads(d)
for j in jdata:
row_genres.append(j['name'])
genre_matrix.append(row_genres)
return genre_matrix
genre_extractor = FunctionTransformer(extract_genre)genre_vectorizer = CountVectorizer(analyzer= lambda x: x, binary=True)
genre_matrix = genre_vectorizer.fit_transform(genre_extractor.fit_transform(movies_df['genres'])).toarray()genre_matrixarray([[1, 1, 0, ..., 0, 0, 0],
[1, 1, 0, ..., 0, 0, 0],
[1, 1, 0, ..., 0, 0, 0],
...,
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0]])
Keywords
We’re using TF-IDF (Term Frequency-Inverse Document Frequency) to convert keywords into vectors. TF-IDF helps capture the importance of each keyword by balancing how often a keyword appears in a movie against how common it is across all movies. This way, unique keywords for specific movies get more weight, while common ones get less.
Since we know that most keywords appear only once or twice, we won’t need to map those keywords to the vector. This can reduce our dimensionality significantly without losing much information. For now, we will map only keywords that appear at least 5 times.
def extract_keywords(X):
keywords_matrix = []
for d in X:
row_keywords = []
jdata = json.loads(d)
for j in jdata:
row_keywords.append(j['name'])
keywords_matrix.append(row_keywords)
return keywords_matrix
keywords_extractor = FunctionTransformer(extract_keywords)keywords_vectorizer = TfidfVectorizer(analyzer= lambda x: x, min_df=5, binary=True)
keywords_matrix = keywords_vectorizer.fit_transform(keywords_extractor.fit_transform(movies_df['keywords'])).toarray()keywords_matrixarray([[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]])
Overview
For plot overviews, we will use Sentence-BERT (SBERT) to create embeddings. Sentence Transformers, based on the SBERT architecture, allow us to convert text into high-dimensional vectors that capture the semantic meaning of the text.
Unlike simple TF-IDF, SBERT captures the deeper semantic meaning of the text, understanding the context and relationships between words. SBERT is particularly effective for longer texts like plot overviews, providing a more nuanced representation than TF-IDF. Using SBERT embeddings for plot overviews helps us create rich, meaningful representations of the movie plots, enhancing the quality of our recommendations.
from sklearn.base import BaseEstimator, TransformerMixin
from sentence_transformers import SentenceTransformer
class SbertTransformer(BaseEstimator, TransformerMixin):
def __init__(self, model_name="all-MiniLM-L6-v2") -> None:
self.model_name =model_name
self.model = SentenceTransformer(model_name)
def fit(self, X, y=None):
return self
def transform(self, X):
return self.model.encode(X.str.lower())/opt/homebrew/Caskroom/miniforge/base/envs/forecasting/lib/python3.10/site-packages/sentence_transformers/cross_encoder/CrossEncoder.py:11: TqdmExperimentalWarning:
Using `tqdm.autonotebook.tqdm` in notebook mode. Use `tqdm.tqdm` instead to force console mode (e.g. in jupyter console)
overview_embedder = SbertTransformer() # Using small model
overview_embeddings = overview_embedder.fit_transform(movies_df['overview'])overview_embeddings.shape(4799, 384)
Director
We can use similar logic as keywords and genres to vectorize director information. Since director with a few movie wont help us much in term of recommendation, we will use only director with more than 4 movie to vectorize. Due to the fact a movie can only have one director we will be usinng binary representation instead of tfidf
movies_df['director']0 James Cameron
1 Gore Verbinski
2 Sam Mendes
3 Christopher Nolan
4 Andrew Stanton
...
4794 Robert Rodriguez
4795 Edward Burns
4796 Scott Smith
4797 Daniel Hsia
4798 Brian Herzlinger
Name: director, Length: 4799, dtype: object
director_vectorizer = CountVectorizer(analyzer= lambda x: [x] if x != '' else [], binary=True, min_df=5)
director_matrix = director_vectorizer.fit_transform(movies_df['director']).toarray()director_matrix.shape(4799, 211)
Putting it Together
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformerCreating Pipeline for Feature Extraction
# Pipeline for Genre Feature Extraction
genre_pipeline = Pipeline([
('genre_extractor', FunctionTransformer(extract_genre)),
('genre_vectorizer', CountVectorizer(analyzer= lambda x: x, binary=True))
])
# Pipeline for Keywords Feature Extraction
keywords_pipeline = Pipeline([
('keywords_extractor', FunctionTransformer(extract_keywords)),
('keywords_vectorizer', TfidfVectorizer(analyzer= lambda x: x, min_df=5, binary=True))
])Joining all feature
feature_extractor = ColumnTransformer([
('genres_pipeline', genre_pipeline, "genres"),
('keywords_pipeline', keywords_pipeline, "keywords"),
('overview_pipeline', SbertTransformer(), "overview"),
('director_pipeline', CountVectorizer(analyzer= lambda x: [x] if x != '' else [], binary=True, min_df=5), "director")
])feature_matrix = feature_extractor.fit_transform(movies_df).toarray()feature_matrixarray([[1., 1., 0., ..., 0., 0., 0.],
[1., 1., 0., ..., 0., 0., 0.],
[1., 1., 0., ..., 0., 0., 0.],
...,
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.],
[0., 0., 0., ..., 0., 0., 0.]])
Build a Recommender System
Remember that I mentioned about scoring and ranking. Now in order to actually recommend a movie, we need to score their similirarity to each other. There are several ways to do this, but the most common one for data involving text is Cosine Similarity.
Cosine similarity measures how similar two items are by looking at the angle between their vectors. Think of it as comparing the direction of two arrows, regardless of their length. If the arrows point in the same direction, they’re similar. In addition to that, It’s quick and can handle lots of data without much fuss.
movies_list = movies_df[['id','original_title']].copy()similarity_matrix = cosine_similarity(feature_matrix, feature_matrix)def find_similar_movie_using_similarity(movie_id: int):
indx = movies_list[movies_list['id'] == movie_id].index.tolist()[0]
top_similar = sorted(list(enumerate(similarity_matrix[indx])), key=lambda x: x[1], reverse=True)[1:11]
recommended_movies = movies_list.iloc[[t[0] for t in top_similar]].copy()
recommended_movies['similarity_score'] = [t[1] for t in top_similar]
return recommended_moviesRunning the Recommender Function
Now let’s use this recommender system to recommend movies to our friend who just watched Batman Begins.
movies_list[movies_list['original_title'].str.contains('Batman')]| id | original_title | |
|---|---|---|
| 9 | 209112 | Batman v Superman: Dawn of Justice |
| 119 | 272 | Batman Begins |
| 210 | 415 | Batman & Robin |
| 299 | 414 | Batman Forever |
| 428 | 364 | Batman Returns |
| 1359 | 268 | Batman |
| 3853 | 142061 | Batman: The Dark Knight Returns, Part 2 |
| 4265 | 2661 | Batman |
batman_begins_id = 272find_similar_movie_using_similarity(batman_begins_id)| id | original_title | similarity_score | |
|---|---|---|---|
| 65 | 155 | The Dark Knight | 0.792444 |
| 3 | 49026 | The Dark Knight Rises | 0.770914 |
| 3325 | 312113 | Black November | 0.663642 |
| 1742 | 254473 | Brick Mansions | 0.659899 |
| 4634 | 378237 | Amidst the Devil's Wings | 0.630629 |
| 3358 | 22314 | In Too Deep | 0.625375 |
| 1486 | 7220 | The Punisher | 0.624695 |
| 3818 | 34769 | Defendor | 0.622456 |
| 3827 | 9516 | Menace II Society | 0.612793 |
| 1661 | 9989 | Antitrust | 0.607391 |
As can be seen from our algorithm, when asked to recommend movies like “Batman Begins,” it recommends “The Dark Knight” and “The Dark Knight Rises,” which are sequels to the movie. On top of that, we also see movies with some detective and superhero vibes. This indicates that our content-based recommender system is effectively identifying and recommending movies that share significant characteristics with the user’s input, thus providing relevant and meaningful suggestions.
Improving Recomender System
Can we improve this model? Yes. In order to improve a model, we need a strategy to quantify the performance of the model itself. In terms of recommender systems, one common evaluation metric is Precision at K (P@K). Precision at K measures the relevance of the top K recommendations made by the system. It is defined as the proportion of recommended items in the top K set that are relevant.
Unfortunately, our dataset is not ideal for evaluating the recommender system since we don’t have the actual user consumption data. Without knowing what movies users have actually watched and liked, we can’t accurately measure how well our recommendations match user preferences.
That being said, there are several strategies to improve the model: 1. Give Different Weight to Each Feature: Currently, we are treating every feature as equally important. Depending on the case, this might not be the most optimal. By adjusting the weights of different features (e.g., giving more importance to plot similarity over genre similarity), we can potentially improve the relevance of our recommendations.
Add More Relevant Features: We can incorporate additional features that are relevant to the content of the movie, such as the cast, the director, or even more granular details like the sound engineer. These additional features can provide a richer context for making recommendations.
Combine Content-Based Approach with Collaborative Filtering: Creating a hybrid recommender system by combining content-based filtering with collaborative filtering can leverage the strengths of both approaches. While content-based filtering focuses on item attributes, collaborative filtering leverages user behavior and preferences. This combination can result in more accurate and personalized recommendations.
Conclusion
Building a content-based recommender system using the TMDB 5000 dataset has allowed us to explore various aspects of feature engineering and similarity scoring. By converting genres into binary vectors, using TF-IDF for keywords, and employing SBERT for plot overviews, we created a comprehensive model capable of providing relevant movie recommendations.
Our model, when asked to recommend movies like “Batman Begins,” successfully suggested sequels like “The Dark Knight” and “The Dark Knight Rises,” as well as other movies with similar detective and superhero vibes. This demonstrates the effectiveness of our content-based approach in capturing the nuances of movie content.
However, there is always room for improvement. Precision at K (P@K) is a common metric for evaluating recommender systems, but our dataset lacks actual user consumption data, which limits our ability to accurately measure performance.