Spaces:
Sleeping
Sleeping
movie recommedation
Browse files- Home.py +70 -0
- README.md +2 -2
- data/movies.csv +0 -0
- data/ratings.csv +0 -0
- pages/1_Dataset_Analysis.py +59 -0
- requirements.txt +59 -0
- utils.py +250 -0
Home.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from utils import Collaborative_filtering, Content_based_filtering
|
| 3 |
+
|
| 4 |
+
st.title("MovieLens Recommender System")
|
| 5 |
+
st.caption(
|
| 6 |
+
"MovieLens is a recommender system that was developed by GroupLens, a computer science research lab at the University of Minnesota. It recommends movies to its users based on their movie ratings. It is also a dataset that is widely used in research and teaching contexts. [link](https://grouplens.org/datasets/movielens/)")
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
rec_1 = Collaborative_filtering()
|
| 10 |
+
rec_2 = Content_based_filtering()
|
| 11 |
+
|
| 12 |
+
movie_title = st.selectbox("Select a movie you like 💖",
|
| 13 |
+
options=rec_1.movies.title.to_list())
|
| 14 |
+
|
| 15 |
+
tab_1, tab_2, tab_3, tab_4 = st.tabs(
|
| 16 |
+
["Collaborative Filtering", "Content-based Filtering", "Matrix Factorization", "Content-based Filtering from Feedback"])
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
with tab_1:
|
| 20 |
+
st.caption("""I'm going to use a technique called colaborative filtering to generate recommendations for you. This technique is based on the premise that similar people like similar things. The beauty of collaborative filtering is that it doesn't require any information about the users or the movies to generate recommendations.""")
|
| 21 |
+
|
| 22 |
+
st.caption(
|
| 23 |
+
f"🎬 You selected `{movie_title}` so I would recommed the following")
|
| 24 |
+
|
| 25 |
+
for movie in rec_1.find_similar_movies(movie_title):
|
| 26 |
+
st.code("🍿 "+movie)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
with tab_2:
|
| 30 |
+
st.caption("""The **cold start problem** is when there are new users and movies in our matrix that do not have any ratings. How do we handle the cold-start problem.
|
| 31 |
+
|
| 32 |
+
Collaborative filtering relies solely on user-item interactions within the utility matrix. The issue with this approach is that brand new users or items with no iteractions get excluded from the recommendation system. This is called the **cold start problem**. Content-based filtering is a way to handle this problem by generating recommendations based on user and item features.""")
|
| 33 |
+
|
| 34 |
+
st.caption(
|
| 35 |
+
f"🎬 You selected `{movie_title}` so I would recommed the following")
|
| 36 |
+
|
| 37 |
+
for movie in rec_2.find_similar_movies(movie_title):
|
| 38 |
+
st.code("🍿 "+movie)
|
| 39 |
+
|
| 40 |
+
with tab_3:
|
| 41 |
+
st.caption("""Matrix factorization (MF) is a linear algebra technique that can help us discover latent features underlying the interactions between users and movies. These latent features give a more compact representation of user tastes and item descriptions. MF is particularly useful for very sparse data and can enhance the quality of recommendations. The algorithm works by factorizing the original user-item matrix into two factor matrices:
|
| 42 |
+
|
| 43 |
+
- user-factor matrix (n_users, k)
|
| 44 |
+
- item-factor matrix (k, n_items)""")
|
| 45 |
+
st.caption(
|
| 46 |
+
f"🎬 You selected `{movie_title}` so I would recommed the following")
|
| 47 |
+
for movie in rec_1.find_similar_movies(movie_title, use_matrix_factorization=True):
|
| 48 |
+
st.code("🍿 "+movie)
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
with tab_4:
|
| 52 |
+
st.caption("The **cold start problem** is when there are new users and movies in our matrix that do not have any ratings. Content-based filtering uses user-item features to recommend other items similar to what the user likes, based on their previous actions or explicit feedback.")
|
| 53 |
+
col_1, col_2 = st.columns(spec=[25, 75])
|
| 54 |
+
|
| 55 |
+
with col_1:
|
| 56 |
+
movie_id = rec_2.movie_id_title_invmap[movie_title]
|
| 57 |
+
movie_index = rec_2.movie_id_index_map[movie_id]
|
| 58 |
+
row = rec_2.user_feature_matrix.iloc[movie_index]
|
| 59 |
+
options = []
|
| 60 |
+
st.caption("**Feedback**")
|
| 61 |
+
for k, v in row.items():
|
| 62 |
+
options.append(st.checkbox(label=k, value=bool(v)))
|
| 63 |
+
|
| 64 |
+
with col_2:
|
| 65 |
+
vector = []
|
| 66 |
+
for item in options:
|
| 67 |
+
vector.append(item)
|
| 68 |
+
st.caption("**Movie Recommendation based on feedback**")
|
| 69 |
+
for movie in rec_2.find_similar_movies_based_on_feedback(vector):
|
| 70 |
+
st.code("🍿 "+movie)
|
README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
| 1 |
---
|
| 2 |
title: Movie Recommender System
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: purple
|
| 6 |
sdk: streamlit
|
| 7 |
sdk_version: 1.25.0
|
| 8 |
-
app_file:
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
|
|
|
| 1 |
---
|
| 2 |
title: Movie Recommender System
|
| 3 |
+
emoji: 🍿🎬
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: purple
|
| 6 |
sdk: streamlit
|
| 7 |
sdk_version: 1.25.0
|
| 8 |
+
app_file: Home.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
data/movies.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
data/ratings.csv
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
pages/1_Dataset_Analysis.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
from utils import DataAnalysis
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
analysis = DataAnalysis()
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
st.title('MovieLens Dataset Analysis')
|
| 9 |
+
|
| 10 |
+
st.caption(
|
| 11 |
+
f'👉 Number of unique movies in the dataset # :orange[{analysis.ratings.movieId.nunique()}] | 👉 Number of unique users in the dataset # :orange[{analysis.ratings.userId.nunique()}]')
|
| 12 |
+
# st.caption(
|
| 13 |
+
# f'Number of unique users in the dataset # :orange[{analysis.ratings.userId.nunique()}]')
|
| 14 |
+
st.caption(
|
| 15 |
+
f'👉 Average Number of ratings per user # :orange[{analysis.ratings.shape[0]/ analysis.ratings.userId.nunique() :0.2f}] | 👉 Average Number of ratings per movie # :orange[{analysis.ratings.shape[0]/ analysis.ratings.movieId.nunique() :0.2f}]')
|
| 16 |
+
# st.caption(
|
| 17 |
+
# f'Average Number of ratings per movie # :orange[{analysis.ratings.shape[0]/ analysis.ratings.movieId.nunique() :0.2f}]')
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
st.header('Distribution of Ratings in the dataset')
|
| 21 |
+
rating_count, rating_kde, rating_ecdf, rating_scatter = st.tabs(
|
| 22 |
+
['Ratings countplot', 'Ratings kdeplot', 'Ratings ecdfplot', 'Rating scatterplot'])
|
| 23 |
+
with rating_count:
|
| 24 |
+
st.pyplot(fig=analysis.ratings_countplot(), use_container_width=True)
|
| 25 |
+
with rating_kde:
|
| 26 |
+
st.pyplot(fig=analysis.ratings_kdeplot(), use_container_width=True)
|
| 27 |
+
with rating_ecdf:
|
| 28 |
+
st.pyplot(fig=analysis.ratings_ecdfplot(), use_container_width=True)
|
| 29 |
+
with rating_scatter:
|
| 30 |
+
st.pyplot(fig=analysis.rating_scatterplot(), use_container_width=True)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
st.header("Which movies are most frequently rated?")
|
| 34 |
+
top_k_rated_movies = st.number_input(
|
| 35 |
+
label="Top k rated movies", min_value=10, max_value=50, value=15, step=5)
|
| 36 |
+
st.bar_chart(data=analysis.most_rated_movie(top_k=top_k_rated_movies),
|
| 37 |
+
x='Number of Ratings', y='Movie Title')
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
st.header("Which movie has the lowest and highest average rating?")
|
| 41 |
+
st.caption('Click the header for sorting')
|
| 42 |
+
rating_config, rating_data = st.columns([25, 75])
|
| 43 |
+
st.write(analysis.rating_stats())
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
st.markdown('''
|
| 47 |
+
`Gypsy` is one the movie with the lowest average rating, but only one person rated it.
|
| 48 |
+
|
| 49 |
+
Similarly `Lamerica` may be one of tthe "highest" rated movie, but it only has 2 ratings.
|
| 50 |
+
|
| 51 |
+
A better approach for evaluating movie popularity is to do look at the [Bayesian average](https://en.wikipedia.org/wiki/Bayesian_average).
|
| 52 |
+
''')
|
| 53 |
+
|
| 54 |
+
st.write(analysis.ratings_bayesian_avg())
|
| 55 |
+
st.markdown("Using the Bayesian average, we see that `Shawshank Redemption`, `The Godfather`, and `The Usual Suspects` are the most highly rated movies. This result makes much more sense since these movies are critically acclaimed films.")
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
st.header("How many movie genres are there?")
|
| 59 |
+
st.pyplot(fig=analysis.genres_count())
|
requirements.txt
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
altair==5.0.1
|
| 2 |
+
attrs==23.1.0
|
| 3 |
+
blinker==1.6.2
|
| 4 |
+
cachetools==5.3.1
|
| 5 |
+
certifi==2023.7.22
|
| 6 |
+
charset-normalizer==3.2.0
|
| 7 |
+
click==8.1.7
|
| 8 |
+
colorama==0.4.6
|
| 9 |
+
contourpy==1.1.0
|
| 10 |
+
cycler==0.11.0
|
| 11 |
+
fonttools==4.42.1
|
| 12 |
+
gitdb==4.0.10
|
| 13 |
+
GitPython==3.1.32
|
| 14 |
+
idna==3.4
|
| 15 |
+
importlib-metadata==6.8.0
|
| 16 |
+
Jinja2==3.1.2
|
| 17 |
+
joblib==1.3.2
|
| 18 |
+
jsonschema==4.19.0
|
| 19 |
+
jsonschema-specifications==2023.7.1
|
| 20 |
+
kiwisolver==1.4.4
|
| 21 |
+
markdown-it-py==3.0.0
|
| 22 |
+
MarkupSafe==2.1.3
|
| 23 |
+
matplotlib==3.7.2
|
| 24 |
+
mdurl==0.1.2
|
| 25 |
+
numpy==1.25.2
|
| 26 |
+
packaging==23.1
|
| 27 |
+
pandas==2.0.3
|
| 28 |
+
Pillow==9.5.0
|
| 29 |
+
protobuf==4.24.1
|
| 30 |
+
pyarrow==12.0.1
|
| 31 |
+
pydeck==0.8.0
|
| 32 |
+
Pygments==2.16.1
|
| 33 |
+
Pympler==1.0.1
|
| 34 |
+
pyparsing==3.0.9
|
| 35 |
+
python-dateutil==2.8.2
|
| 36 |
+
pytz==2023.3
|
| 37 |
+
pytz-deprecation-shim==0.1.0.post0
|
| 38 |
+
referencing==0.30.2
|
| 39 |
+
requests==2.31.0
|
| 40 |
+
rich==13.5.2
|
| 41 |
+
rpds-py==0.9.2
|
| 42 |
+
scikit-learn==1.3.0
|
| 43 |
+
scipy==1.11.2
|
| 44 |
+
seaborn==0.12.2
|
| 45 |
+
six==1.16.0
|
| 46 |
+
smmap==5.0.0
|
| 47 |
+
streamlit==1.25.0
|
| 48 |
+
tenacity==8.2.3
|
| 49 |
+
threadpoolctl==3.2.0
|
| 50 |
+
toml==0.10.2
|
| 51 |
+
toolz==0.12.0
|
| 52 |
+
tornado==6.3.3
|
| 53 |
+
typing_extensions==4.7.1
|
| 54 |
+
tzdata==2023.3
|
| 55 |
+
tzlocal==4.3.1
|
| 56 |
+
urllib3==2.0.4
|
| 57 |
+
validators==0.21.2
|
| 58 |
+
watchdog==3.0.0
|
| 59 |
+
zipp==3.16.2
|
utils.py
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import pandas as pd
|
| 2 |
+
import numpy as np
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import seaborn as sns
|
| 5 |
+
|
| 6 |
+
from sklearn.decomposition import TruncatedSVD
|
| 7 |
+
from sklearn.neighbors import NearestNeighbors
|
| 8 |
+
from sklearn.metrics.pairwise import cosine_similarity
|
| 9 |
+
from scipy.sparse import csr_matrix
|
| 10 |
+
|
| 11 |
+
from collections import Counter
|
| 12 |
+
from functools import cached_property
|
| 13 |
+
|
| 14 |
+
plt.style.use("fivethirtyeight")
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class DataAnalysis:
|
| 18 |
+
def __init__(self) -> None:
|
| 19 |
+
self.movies: pd.DataFrame = pd.read_csv(
|
| 20 |
+
'./data/movies.csv')
|
| 21 |
+
self.ratings: pd.DataFrame = pd.read_csv(
|
| 22 |
+
'./data/ratings.csv')
|
| 23 |
+
|
| 24 |
+
def ratings_countplot(self,):
|
| 25 |
+
fig, ax = plt.subplots(nrows=1, ncols=1)
|
| 26 |
+
sns.countplot(data=self.ratings, x='rating', ax=ax)
|
| 27 |
+
return fig
|
| 28 |
+
|
| 29 |
+
def ratings_kdeplot(self,):
|
| 30 |
+
fig, ax = plt.subplots(nrows=1, ncols=1)
|
| 31 |
+
sns.kdeplot(data=self.ratings, x='rating', ax=ax)
|
| 32 |
+
return fig
|
| 33 |
+
|
| 34 |
+
def ratings_ecdfplot(self,):
|
| 35 |
+
fig, ax = plt.subplots(nrows=1, ncols=1)
|
| 36 |
+
sns.ecdfplot(data=self.ratings, x='rating', ax=ax)
|
| 37 |
+
return fig
|
| 38 |
+
|
| 39 |
+
def rating_scatterplot(self,):
|
| 40 |
+
fig, ax = plt.subplots(nrows=1, ncols=1)
|
| 41 |
+
self.ratings[['userId', 'rating']].groupby(
|
| 42 |
+
'userId').mean().plot(ls='', marker='.', ax=ax)
|
| 43 |
+
ax.axhline(
|
| 44 |
+
y=self.ratings[['userId', 'rating']].groupby(
|
| 45 |
+
'userId').mean().mean().values.item(),
|
| 46 |
+
color='red', alpha=0.5
|
| 47 |
+
)
|
| 48 |
+
ax.legend(['Mean user rating', 'Mean rating across users'])
|
| 49 |
+
return fig
|
| 50 |
+
|
| 51 |
+
def most_rated_movie(self, top_k=10):
|
| 52 |
+
data = (self.ratings.movieId.value_counts()
|
| 53 |
+
.reset_index()
|
| 54 |
+
.merge(right=self.movies[['movieId', 'title']], on='movieId')[['title', 'count']]
|
| 55 |
+
.rename({'count': 'Number of Ratings', 'title': 'Movie Title'}, axis=1))
|
| 56 |
+
return data.head(top_k)
|
| 57 |
+
|
| 58 |
+
def rating_stats(self,):
|
| 59 |
+
avg_movie_rating = (self.ratings[['movieId', 'rating']]
|
| 60 |
+
.groupby('movieId').agg(['mean', 'count'])
|
| 61 |
+
.droplevel(axis=1, level=0)
|
| 62 |
+
.reset_index(level=0))
|
| 63 |
+
avg_movie_rating = avg_movie_rating.merge(
|
| 64 |
+
self.movies[['movieId', 'title']], on='movieId')
|
| 65 |
+
avg_movie_rating = (avg_movie_rating
|
| 66 |
+
.rename(axis=1,
|
| 67 |
+
mapper={'mean': 'Average Rating',
|
| 68 |
+
'count': "Number of Rating",
|
| 69 |
+
'title': 'Movie Title',
|
| 70 |
+
'genres': 'Genres'}
|
| 71 |
+
))
|
| 72 |
+
avg_movie_rating = avg_movie_rating.drop(columns='movieId')
|
| 73 |
+
return avg_movie_rating
|
| 74 |
+
|
| 75 |
+
def bayesian_avg(self, C: float, m: float):
|
| 76 |
+
return lambda rating: (C*m + rating.sum()) / (C + rating.count())
|
| 77 |
+
|
| 78 |
+
def ratings_bayesian_avg(self,):
|
| 79 |
+
rating_agg = (self.ratings[['rating', 'movieId']]
|
| 80 |
+
.groupby('movieId').agg(['mean', 'count'])
|
| 81 |
+
.droplevel(axis=1, level=0)
|
| 82 |
+
.reset_index()
|
| 83 |
+
)
|
| 84 |
+
C = rating_agg['count'].mean()
|
| 85 |
+
m = rating_agg['mean'].mean()
|
| 86 |
+
bay_avg_fn = self.bayesian_avg(C=C, m=m)
|
| 87 |
+
rating_bay_avg = (self.ratings[['rating', 'movieId']]
|
| 88 |
+
.groupby('movieId').agg([bay_avg_fn, 'count'])
|
| 89 |
+
).droplevel(level=0, axis=1).reset_index(level=0)
|
| 90 |
+
rating_bay_avg = rating_bay_avg.merge(
|
| 91 |
+
self.movies[['title', 'movieId']], on='movieId')
|
| 92 |
+
|
| 93 |
+
rating_bay_avg = rating_bay_avg.rename({'<lambda_0>': 'Bayesian Average',
|
| 94 |
+
'count': 'Number of ratings', 'title': 'Movie Title'}, axis=1)
|
| 95 |
+
return rating_bay_avg.drop(columns=['movieId'])
|
| 96 |
+
|
| 97 |
+
def genres_count(self,):
|
| 98 |
+
movie_genres = self.movies.copy()
|
| 99 |
+
movie_genres.genres = self.movies.genres.str.split(pat='|')
|
| 100 |
+
genre_counter = Counter(
|
| 101 |
+
[genre for genres in movie_genres.genres for genre in genres])
|
| 102 |
+
genre_counter_df = pd.DataFrame(
|
| 103 |
+
data=dict(genre_counter.most_common()), index=['Count'])
|
| 104 |
+
genre_counter_df.columns.name = "Genres"
|
| 105 |
+
genre_counter_df = genre_counter_df.T.reset_index()
|
| 106 |
+
|
| 107 |
+
fig, ax = plt.subplots(nrows=1, ncols=1)
|
| 108 |
+
sns.barplot(data=genre_counter_df, x='Count', y='Genres', ax=ax)
|
| 109 |
+
return fig
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
class Recommender:
|
| 113 |
+
def __init__(self) -> None:
|
| 114 |
+
self.ratings: pd.DataFrame = pd.read_csv(
|
| 115 |
+
'./data/ratings.csv')
|
| 116 |
+
self.movies: pd.DataFrame = pd.read_csv(
|
| 117 |
+
'./data/movies.csv')
|
| 118 |
+
|
| 119 |
+
self.M: int = self.ratings.userId.nunique()
|
| 120 |
+
self.N: int = self.ratings.movieId.nunique()
|
| 121 |
+
|
| 122 |
+
self.ratings_userid_index_map = dict(
|
| 123 |
+
zip(self.ratings.userId.unique(), range(self.M)))
|
| 124 |
+
self.ratings_movieid_index_map = dict(
|
| 125 |
+
zip(self.ratings.movieId.unique(), range(self.N)))
|
| 126 |
+
self.ratings_userid_index_invmap = dict(
|
| 127 |
+
zip(range(self.M), self.ratings.userId.unique()))
|
| 128 |
+
self.ratings_movieid_index_invmap = dict(
|
| 129 |
+
zip(range(self.N), self.ratings.movieId.unique()))
|
| 130 |
+
|
| 131 |
+
self.movie_id_title_map = dict(
|
| 132 |
+
zip(self.movies.movieId, self.movies.title))
|
| 133 |
+
self.movie_id_title_invmap = dict(
|
| 134 |
+
zip(self.movies.title, self.movies.movieId))
|
| 135 |
+
self.movie_id_index_map = dict(
|
| 136 |
+
zip(self.movies.movieId, self.movies.index))
|
| 137 |
+
self.movie_id_index_invmap = dict(
|
| 138 |
+
zip(self.movies.index, self.movies.movieId))
|
| 139 |
+
|
| 140 |
+
def nearest_neighbors(self, matrix: np.ndarray | csr_matrix):
|
| 141 |
+
knn = NearestNeighbors(
|
| 142 |
+
n_neighbors=10, algorithm="brute", metric="cosine")
|
| 143 |
+
knn.fit(matrix)
|
| 144 |
+
return knn
|
| 145 |
+
|
| 146 |
+
def output_recommendation(self, search_id: int, similar_movies: np.ndarray, mapper_index_id: dict):
|
| 147 |
+
response = []
|
| 148 |
+
for i in similar_movies:
|
| 149 |
+
movie_id = mapper_index_id[i]
|
| 150 |
+
if movie_id != search_id:
|
| 151 |
+
response.append(self.movie_id_title_map[movie_id])
|
| 152 |
+
return response
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
class Collaborative_filtering(Recommender):
|
| 156 |
+
def __init__(self) -> None:
|
| 157 |
+
super(Collaborative_filtering, self).__init__()
|
| 158 |
+
pass
|
| 159 |
+
|
| 160 |
+
@cached_property
|
| 161 |
+
def user_item_matrix(self,) -> csr_matrix:
|
| 162 |
+
# build user-item matrix
|
| 163 |
+
user_index = [self.ratings_userid_index_map[id]
|
| 164 |
+
for id in self.ratings.userId]
|
| 165 |
+
movie_index = [self.ratings_movieid_index_map[id]
|
| 166 |
+
for id in self.ratings.movieId]
|
| 167 |
+
|
| 168 |
+
user_item_matrix = csr_matrix(
|
| 169 |
+
(self.ratings.rating, (user_index, movie_index)), shape=(self.M, self.N))
|
| 170 |
+
return user_item_matrix
|
| 171 |
+
|
| 172 |
+
@cached_property
|
| 173 |
+
def matrix_factorization(self,) -> np.ndarray:
|
| 174 |
+
svd = TruncatedSVD(n_components=20, n_iter=10, random_state=42)
|
| 175 |
+
Q = svd.fit_transform(self.user_item_matrix.T)
|
| 176 |
+
return Q
|
| 177 |
+
|
| 178 |
+
def find_similar_movies(self, title: str, k: int = 11, use_matrix_factorization=False) -> np.ndarray:
|
| 179 |
+
search_id: int = self.movie_id_title_invmap[title]
|
| 180 |
+
movie_index: int = self.ratings_movieid_index_map[search_id]
|
| 181 |
+
|
| 182 |
+
if use_matrix_factorization:
|
| 183 |
+
matrix: np.ndarray = self.matrix_factorization
|
| 184 |
+
else:
|
| 185 |
+
matrix: csr_matrix = self.user_item_matrix.T
|
| 186 |
+
|
| 187 |
+
movie_vector: np.ndarray = matrix[movie_index]
|
| 188 |
+
|
| 189 |
+
if isinstance(movie_vector, np.ndarray):
|
| 190 |
+
movie_vector = movie_vector.reshape((1, -1))
|
| 191 |
+
|
| 192 |
+
knn = self.nearest_neighbors(matrix=matrix)
|
| 193 |
+
neighbors: np.ndarray = knn.kneighbors(
|
| 194 |
+
movie_vector, n_neighbors=k, return_distance=False)
|
| 195 |
+
|
| 196 |
+
response = self.output_recommendation(
|
| 197 |
+
search_id=search_id,
|
| 198 |
+
similar_movies=neighbors[0],
|
| 199 |
+
mapper_index_id=self.ratings_movieid_index_invmap)
|
| 200 |
+
return response
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class Content_based_filtering(Recommender):
|
| 204 |
+
def __init__(self) -> None:
|
| 205 |
+
super(Content_based_filtering, self).__init__()
|
| 206 |
+
|
| 207 |
+
@cached_property
|
| 208 |
+
def user_feature_matrix(self,):
|
| 209 |
+
movie_genres = self.movies.copy()
|
| 210 |
+
movie_genres.genres = self.movies.genres.str.split(pat='|')
|
| 211 |
+
genres = set(
|
| 212 |
+
[genre_ for genres_ in movie_genres.genres for genre_ in genres_])
|
| 213 |
+
for genre in genres:
|
| 214 |
+
movie_genres[genre] = movie_genres.genres.transform(
|
| 215 |
+
lambda x: int(genre in x))
|
| 216 |
+
user_feature_matrix = movie_genres.drop(
|
| 217 |
+
columns=['movieId', 'title', 'genres'])
|
| 218 |
+
return user_feature_matrix
|
| 219 |
+
|
| 220 |
+
@cached_property
|
| 221 |
+
def cosine_similarity(self):
|
| 222 |
+
user_feature_matrix = self.user_feature_matrix
|
| 223 |
+
similarity_matirx = cosine_similarity(
|
| 224 |
+
user_feature_matrix, user_feature_matrix)
|
| 225 |
+
return similarity_matirx
|
| 226 |
+
|
| 227 |
+
def find_similar_movies(self, title: str, k: int = 11):
|
| 228 |
+
search_id: int = self.movie_id_title_invmap[title]
|
| 229 |
+
search_index: int = self.movie_id_index_map[search_id]
|
| 230 |
+
|
| 231 |
+
scores: np.ndarray = self.cosine_similarity[search_index]
|
| 232 |
+
scores: list[tuple[int, float]] = list(zip(self.movies.index, scores))
|
| 233 |
+
scores = sorted(scores, key=lambda x: x[1], reverse=True)
|
| 234 |
+
neighbors: list[int] = [item[0] for item in scores[:k]]
|
| 235 |
+
response = self.output_recommendation(
|
| 236 |
+
search_id=search_id,
|
| 237 |
+
similar_movies=neighbors,
|
| 238 |
+
mapper_index_id=self.movie_id_index_invmap)
|
| 239 |
+
return response
|
| 240 |
+
|
| 241 |
+
def find_similar_movies_based_on_feedback(self, vector: list[bool], k: int = 11):
|
| 242 |
+
feedback_vector = np.array(vector, dtype=int).reshape((1, -1))
|
| 243 |
+
knn = self.nearest_neighbors(matrix=self.user_feature_matrix)
|
| 244 |
+
neighbors: np.ndarray = knn.kneighbors(
|
| 245 |
+
feedback_vector, n_neighbors=k, return_distance=False)
|
| 246 |
+
response = self.output_recommendation(
|
| 247 |
+
search_id=-1,
|
| 248 |
+
similar_movies=neighbors[0],
|
| 249 |
+
mapper_index_id=self.movie_id_index_invmap)
|
| 250 |
+
return response
|