Movie Recommendation System (Basic)
Abstract
A recommendation system is a small but real machine-learning project. In ~300 lines you implement two foundational algorithms — collaborative filtering (“users like you also enjoyed…”) and content-based filtering (“because you liked Action, Sci-Fi…”), then blend them in a hybrid model. The data is a list of movies with genres and a user-ratings table. The math is Pearson correlation and weighted scoring. The result genuinely surprises you with non-obvious suggestions.
You will learn:
- The two foundational recommendation paradigms and their trade-offs.
- How Pearson correlation measures rating-pattern similarity.
- Why content-based filtering solves the cold-start problem.
- How to blend recommenders for robustness.
- The path from this toy to production-grade matrix factorization (SVD, ALS).
Prerequisites
- Python 3.7 or above (for
dataclassesdataclasses). - A text editor or IDE.
- Comfort with classes, dictionaries, and basic statistics.
- Familiarity with Personal Diary for the JSON-persistence pattern.
Getting Started
Create the project
- Create folder
movie-recommendermovie-recommender. - Inside, create
movierecommendationsystem.pymovierecommendationsystem.py.
Write the code
Movie Recommender
Source# Movie Recommendation System (Basic)
import json
import os
import math
from typing import List, Dict, Tuple, Optional
from collections import defaultdict
class Movie:
def __init__(self, movie_id: int, title: str, genres: List[str], year: int = None):
self.id = movie_id
self.title = title
self.genres = genres
self.year = year
self.ratings = []
self.average_rating = 0.0
self.rating_count = 0
def add_rating(self, rating: float):
"""Add a rating to the movie"""
if 1.0 <= rating <= 5.0:
self.ratings.append(rating)
self.rating_count = len(self.ratings)
self.average_rating = sum(self.ratings) / self.rating_count
def to_dict(self) -> Dict:
"""Convert movie to dictionary"""
return {
'id': self.id,
'title': self.title,
'genres': self.genres,
'year': self.year,
'ratings': self.ratings,
'average_rating': self.average_rating,
'rating_count': self.rating_count
}
@classmethod
def from_dict(cls, data: Dict) -> 'Movie':
"""Create movie from dictionary"""
movie = cls(data['id'], data['title'], data['genres'], data.get('year'))
movie.ratings = data.get('ratings', [])
movie.average_rating = data.get('average_rating', 0.0)
movie.rating_count = data.get('rating_count', 0)
return movie
def __str__(self):
year_str = f" ({self.year})" if self.year else ""
rating_str = f" - Rating: {self.average_rating:.1f}/5.0 ({self.rating_count} ratings)" if self.rating_count > 0 else ""
return f"{self.title}{year_str} [{', '.join(self.genres)}]{rating_str}"
class User:
def __init__(self, user_id: int, name: str):
self.id = user_id
self.name = name
self.ratings = {} # movie_id -> rating
self.favorite_genres = []
def rate_movie(self, movie_id: int, rating: float):
"""Rate a movie"""
if 1.0 <= rating <= 5.0:
self.ratings[movie_id] = rating
def get_rating(self, movie_id: int) -> Optional[float]:
"""Get user's rating for a movie"""
return self.ratings.get(movie_id)
def update_favorite_genres(self, movies: Dict[int, Movie]):
"""Update favorite genres based on highly rated movies"""
genre_scores = defaultdict(list)
for movie_id, rating in self.ratings.items():
if movie_id in movies and rating >= 4.0:
movie = movies[movie_id]
for genre in movie.genres:
genre_scores[genre].append(rating)
# Calculate average rating per genre
genre_averages = {}
for genre, ratings in genre_scores.items():
if len(ratings) >= 2: # At least 2 highly rated movies
genre_averages[genre] = sum(ratings) / len(ratings)
# Sort genres by average rating
self.favorite_genres = sorted(genre_averages.keys(),
key=lambda g: genre_averages[g],
reverse=True)[:5]
def to_dict(self) -> Dict:
"""Convert user to dictionary"""
return {
'id': self.id,
'name': self.name,
'ratings': self.ratings,
'favorite_genres': self.favorite_genres
}
@classmethod
def from_dict(cls, data: Dict) -> 'User':
"""Create user from dictionary"""
user = cls(data['id'], data['name'])
user.ratings = data.get('ratings', {})
# Convert string keys back to int
user.ratings = {int(k): v for k, v in user.ratings.items()}
user.favorite_genres = data.get('favorite_genres', [])
return user
class MovieRecommendationSystem:
def __init__(self, data_file: str = "movie_data.json"):
self.data_file = data_file
self.movies = {} # movie_id -> Movie
self.users = {} # user_id -> User
self.load_data()
# Sample data if no data exists
if not self.movies:
self._create_sample_data()
def load_data(self):
"""Load data from JSON file"""
if os.path.exists(self.data_file):
try:
with open(self.data_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# Load movies
for movie_data in data.get('movies', []):
movie = Movie.from_dict(movie_data)
self.movies[movie.id] = movie
# Load users
for user_data in data.get('users', []):
user = User.from_dict(user_data)
self.users[user.id] = user
except (json.JSONDecodeError, KeyError) as e:
print(f"Error loading data: {e}")
self.movies = {}
self.users = {}
def save_data(self):
"""Save data to JSON file"""
try:
data = {
'movies': [movie.to_dict() for movie in self.movies.values()],
'users': [user.to_dict() for user in self.users.values()]
}
with open(self.data_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Error saving data: {e}")
def _create_sample_data(self):
"""Create sample movies for demonstration"""
sample_movies = [
(1, "The Shawshank Redemption", ["Drama", "Crime"], 1994),
(2, "The Godfather", ["Drama", "Crime"], 1972),
(3, "The Dark Knight", ["Action", "Crime", "Drama"], 2008),
(4, "Pulp Fiction", ["Crime", "Drama"], 1994),
(5, "Forrest Gump", ["Drama", "Romance"], 1994),
(6, "Inception", ["Action", "Sci-Fi", "Thriller"], 2010),
(7, "The Matrix", ["Action", "Sci-Fi"], 1999),
(8, "Goodfellas", ["Biography", "Crime", "Drama"], 1990),
(9, "The Lord of the Rings: The Return of the King", ["Adventure", "Drama", "Fantasy"], 2003),
(10, "Fight Club", ["Drama"], 1999),
(11, "Star Wars: Episode IV - A New Hope", ["Adventure", "Fantasy", "Sci-Fi"], 1977),
(12, "The Lord of the Rings: The Fellowship of the Ring", ["Adventure", "Drama", "Fantasy"], 2001),
(13, "One Flew Over the Cuckoo's Nest", ["Drama"], 1975),
(14, "Interstellar", ["Adventure", "Drama", "Sci-Fi"], 2014),
(15, "Casablanca", ["Drama", "Romance", "War"], 1942),
(16, "Titanic", ["Drama", "Romance"], 1997),
(17, "The Avengers", ["Action", "Adventure", "Sci-Fi"], 2012),
(18, "Jurassic Park", ["Adventure", "Sci-Fi", "Thriller"], 1993),
(19, "The Lion King", ["Animation", "Adventure", "Drama"], 1994),
(20, "Toy Story", ["Animation", "Adventure", "Comedy"], 1995)
]
for movie_id, title, genres, year in sample_movies:
movie = Movie(movie_id, title, genres, year)
# Add some sample ratings
import random
for _ in range(random.randint(10, 50)):
rating = random.uniform(3.0, 5.0) if random.random() > 0.3 else random.uniform(1.0, 5.0)
movie.add_rating(rating)
self.movies[movie_id] = movie
self.save_data()
def add_movie(self, title: str, genres: List[str], year: int = None) -> Movie:
"""Add a new movie"""
movie_id = max(self.movies.keys()) + 1 if self.movies else 1
movie = Movie(movie_id, title, genres, year)
self.movies[movie_id] = movie
self.save_data()
return movie
def add_user(self, name: str) -> User:
"""Add a new user"""
user_id = max(self.users.keys()) + 1 if self.users else 1
user = User(user_id, name)
self.users[user_id] = user
self.save_data()
return user
def rate_movie(self, user_id: int, movie_id: int, rating: float):
"""User rates a movie"""
if user_id not in self.users or movie_id not in self.movies:
return False
user = self.users[user_id]
movie = self.movies[movie_id]
# Remove old rating if exists
old_rating = user.get_rating(movie_id)
if old_rating is not None:
movie.ratings.remove(old_rating)
# Add new rating
user.rate_movie(movie_id, rating)
movie.add_rating(rating)
# Update user's favorite genres
user.update_favorite_genres(self.movies)
self.save_data()
return True
def get_movies_by_genre(self, genre: str) -> List[Movie]:
"""Get all movies in a specific genre"""
return [movie for movie in self.movies.values()
if genre.lower() in [g.lower() for g in movie.genres]]
def search_movies(self, query: str) -> List[Movie]:
"""Search movies by title"""
query = query.lower()
return [movie for movie in self.movies.values()
if query in movie.title.lower()]
def get_top_rated_movies(self, limit: int = 10, min_ratings: int = 5) -> List[Movie]:
"""Get top rated movies"""
eligible_movies = [movie for movie in self.movies.values()
if movie.rating_count >= min_ratings]
return sorted(eligible_movies, key=lambda m: m.average_rating, reverse=True)[:limit]
def calculate_user_similarity(self, user1_id: int, user2_id: int) -> float:
"""Calculate similarity between two users based on their ratings"""
if user1_id not in self.users or user2_id not in self.users:
return 0.0
user1 = self.users[user1_id]
user2 = self.users[user2_id]
# Find common movies
common_movies = set(user1.ratings.keys()) & set(user2.ratings.keys())
if len(common_movies) < 2:
return 0.0
# Calculate Pearson correlation coefficient
sum1 = sum(user1.ratings[movie] for movie in common_movies)
sum2 = sum(user2.ratings[movie] for movie in common_movies)
sum1_sq = sum(user1.ratings[movie] ** 2 for movie in common_movies)
sum2_sq = sum(user2.ratings[movie] ** 2 for movie in common_movies)
sum_products = sum(user1.ratings[movie] * user2.ratings[movie] for movie in common_movies)
n = len(common_movies)
# Calculate Pearson correlation
numerator = sum_products - (sum1 * sum2 / n)
denominator = math.sqrt((sum1_sq - sum1**2/n) * (sum2_sq - sum2**2/n))
if denominator == 0:
return 0.0
return numerator / denominator
def get_user_based_recommendations(self, user_id: int, limit: int = 10) -> List[Tuple[Movie, float]]:
"""Get recommendations based on similar users (collaborative filtering)"""
if user_id not in self.users:
return []
target_user = self.users[user_id]
# Find similar users
similarities = []
for other_user_id, other_user in self.users.items():
if other_user_id != user_id:
similarity = self.calculate_user_similarity(user_id, other_user_id)
if similarity > 0:
similarities.append((other_user_id, similarity))
# Sort by similarity
similarities.sort(key=lambda x: x[1], reverse=True)
# Get recommendations from similar users
recommendations = defaultdict(list)
for similar_user_id, similarity in similarities[:10]: # Top 10 similar users
similar_user = self.users[similar_user_id]
for movie_id, rating in similar_user.ratings.items():
# Only recommend movies the target user hasn't rated
if movie_id not in target_user.ratings and rating >= 4.0:
recommendations[movie_id].append(rating * similarity)
# Calculate weighted average scores
movie_scores = []
for movie_id, scores in recommendations.items():
if movie_id in self.movies:
avg_score = sum(scores) / len(scores)
movie_scores.append((self.movies[movie_id], avg_score))
# Sort by score and return top recommendations
movie_scores.sort(key=lambda x: x[1], reverse=True)
return movie_scores[:limit]
def get_content_based_recommendations(self, user_id: int, limit: int = 10) -> List[Movie]:
"""Get recommendations based on user's favorite genres and ratings"""
if user_id not in self.users:
return []
user = self.users[user_id]
# Get unrated movies
unrated_movies = [movie for movie in self.movies.values()
if movie.id not in user.ratings]
# Score movies based on genre preferences
movie_scores = []
for movie in unrated_movies:
score = 0.0
# Genre-based scoring
for i, genre in enumerate(user.favorite_genres):
if genre in movie.genres:
score += (len(user.favorite_genres) - i) * 2 # Higher weight for more preferred genres
# Add movie's average rating as a factor
if movie.rating_count > 0:
score += movie.average_rating
movie_scores.append((movie, score))
# Sort by score and return top recommendations
movie_scores.sort(key=lambda x: x[1], reverse=True)
return [movie for movie, score in movie_scores[:limit]]
def get_hybrid_recommendations(self, user_id: int, limit: int = 10) -> List[Movie]:
"""Get hybrid recommendations combining collaborative and content-based filtering"""
if user_id not in self.users:
return []
# Get both types of recommendations
user_based = self.get_user_based_recommendations(user_id, limit * 2)
content_based = self.get_content_based_recommendations(user_id, limit * 2)
# Combine recommendations
all_recommendations = {}
# Add user-based recommendations with weight
for movie, score in user_based:
all_recommendations[movie.id] = all_recommendations.get(movie.id, 0) + score * 0.7
# Add content-based recommendations with weight
for movie in content_based:
all_recommendations[movie.id] = all_recommendations.get(movie.id, 0) + movie.average_rating * 0.3
# Sort and return top recommendations
sorted_recommendations = sorted(all_recommendations.items(),
key=lambda x: x[1], reverse=True)
return [self.movies[movie_id] for movie_id, score in sorted_recommendations[:limit]]
def get_movie_statistics(self) -> Dict:
"""Get movie database statistics"""
if not self.movies:
return {}
total_movies = len(self.movies)
total_ratings = sum(movie.rating_count for movie in self.movies.values())
# Genre distribution
genre_count = defaultdict(int)
for movie in self.movies.values():
for genre in movie.genres:
genre_count[genre] += 1
# Year distribution
year_count = defaultdict(int)
for movie in self.movies.values():
if movie.year:
decade = (movie.year // 10) * 10
year_count[f"{decade}s"] += 1
return {
'total_movies': total_movies,
'total_users': len(self.users),
'total_ratings': total_ratings,
'average_ratings_per_movie': total_ratings / total_movies if total_movies > 0 else 0,
'most_common_genres': sorted(genre_count.items(), key=lambda x: x[1], reverse=True)[:10],
'movies_by_decade': dict(year_count)
}
def display_movie_list(movies: List[Movie], title: str = "Movies"):
"""Display a list of movies"""
print(f"\n=== {title} ===")
if not movies:
print("No movies found.")
return
for i, movie in enumerate(movies, 1):
print(f"{i}. {movie}")
def display_recommendations(recommendations: List[Tuple[Movie, float]], title: str = "Recommendations"):
"""Display recommendations with scores"""
print(f"\n=== {title} ===")
if not recommendations:
print("No recommendations available.")
return
for i, (movie, score) in enumerate(recommendations, 1):
print(f"{i}. {movie} (Score: {score:.2f})")
def main():
"""Main function to run the movie recommendation system"""
system = MovieRecommendationSystem()
current_user_id = None
while True:
print("\n=== Movie Recommendation System ===")
print("1. Register/Login User")
print("2. Browse Movies")
print("3. Search Movies")
print("4. Rate Movie")
print("5. Get Recommendations")
print("6. View Top Rated Movies")
print("7. Add New Movie")
print("8. View Statistics")
print("9. User Profile")
print("0. Exit")
try:
choice = input("\nEnter your choice: ").strip()
if choice == '1':
print("\n1. Register new user")
print("2. Login existing user")
user_choice = input("Enter choice: ").strip()
if user_choice == '1':
name = input("Enter your name: ").strip()
if name:
user = system.add_user(name)
current_user_id = user.id
print(f"User registered! Your ID is: {user.id}")
elif user_choice == '2':
try:
user_id = int(input("Enter your user ID: ").strip())
if user_id in system.users:
current_user_id = user_id
print(f"Logged in as: {system.users[user_id].name}")
else:
print("User not found!")
except ValueError:
print("Invalid user ID!")
elif choice == '2':
print("\n1. All movies")
print("2. Movies by genre")
browse_choice = input("Enter choice: ").strip()
if browse_choice == '1':
movies = list(system.movies.values())
display_movie_list(movies, "All Movies")
elif browse_choice == '2':
genre = input("Enter genre: ").strip()
movies = system.get_movies_by_genre(genre)
display_movie_list(movies, f"Movies in {genre}")
elif choice == '3':
query = input("Enter movie title to search: ").strip()
if query:
movies = system.search_movies(query)
display_movie_list(movies, f"Search Results for '{query}'")
elif choice == '4':
if current_user_id is None:
print("Please login first!")
continue
try:
movie_id = int(input("Enter movie ID to rate: ").strip())
if movie_id not in system.movies:
print("Movie not found!")
continue
rating = float(input("Enter rating (1.0-5.0): ").strip())
if system.rate_movie(current_user_id, movie_id, rating):
print("Rating added successfully!")
else:
print("Error adding rating!")
except ValueError:
print("Invalid input!")
elif choice == '5':
if current_user_id is None:
print("Please login first!")
continue
print("\n1. User-based recommendations")
print("2. Content-based recommendations")
print("3. Hybrid recommendations")
rec_choice = input("Enter choice: ").strip()
if rec_choice == '1':
recommendations = system.get_user_based_recommendations(current_user_id)
display_recommendations(recommendations, "User-based Recommendations")
elif rec_choice == '2':
recommendations = system.get_content_based_recommendations(current_user_id)
display_movie_list(recommendations, "Content-based Recommendations")
elif rec_choice == '3':
recommendations = system.get_hybrid_recommendations(current_user_id)
display_movie_list(recommendations, "Hybrid Recommendations")
elif choice == '6':
try:
limit = int(input("Enter number of movies to show (default 10): ").strip() or "10")
top_movies = system.get_top_rated_movies(limit)
display_movie_list(top_movies, f"Top {limit} Rated Movies")
except ValueError:
print("Invalid number!")
elif choice == '7':
title = input("Enter movie title: ").strip()
if not title:
print("Title cannot be empty!")
continue
genres_input = input("Enter genres (comma-separated): ").strip()
genres = [g.strip() for g in genres_input.split(',') if g.strip()]
year_input = input("Enter year (optional): ").strip()
year = int(year_input) if year_input else None
movie = system.add_movie(title, genres, year)
print(f"Movie added with ID: {movie.id}")
elif choice == '8':
stats = system.get_movie_statistics()
print("\n=== Movie Database Statistics ===")
print(f"Total movies: {stats['total_movies']}")
print(f"Total users: {stats['total_users']}")
print(f"Total ratings: {stats['total_ratings']}")
print(f"Average ratings per movie: {stats['average_ratings_per_movie']:.1f}")
if stats['most_common_genres']:
print("\nMost common genres:")
for genre, count in stats['most_common_genres'][:5]:
print(f" {genre}: {count}")
if stats['movies_by_decade']:
print("\nMovies by decade:")
for decade, count in sorted(stats['movies_by_decade'].items()):
print(f" {decade}: {count}")
elif choice == '9':
if current_user_id is None:
print("Please login first!")
continue
user = system.users[current_user_id]
print(f"\n=== Profile: {user.name} ===")
print(f"User ID: {user.id}")
print(f"Total ratings: {len(user.ratings)}")
if user.favorite_genres:
print(f"Favorite genres: {', '.join(user.favorite_genres)}")
if user.ratings:
print("\nRecent ratings:")
recent_ratings = list(user.ratings.items())[-5:]
for movie_id, rating in recent_ratings:
if movie_id in system.movies:
movie = system.movies[movie_id]
print(f" {movie.title}: {rating}/5.0")
elif choice == '0':
print("Thank you for using the Movie Recommendation System!")
break
else:
print("Invalid choice. Please try again.")
except KeyboardInterrupt:
print("\n\nGoodbye!")
break
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
main()
# Movie Recommendation System (Basic)
import json
import os
import math
from typing import List, Dict, Tuple, Optional
from collections import defaultdict
class Movie:
def __init__(self, movie_id: int, title: str, genres: List[str], year: int = None):
self.id = movie_id
self.title = title
self.genres = genres
self.year = year
self.ratings = []
self.average_rating = 0.0
self.rating_count = 0
def add_rating(self, rating: float):
"""Add a rating to the movie"""
if 1.0 <= rating <= 5.0:
self.ratings.append(rating)
self.rating_count = len(self.ratings)
self.average_rating = sum(self.ratings) / self.rating_count
def to_dict(self) -> Dict:
"""Convert movie to dictionary"""
return {
'id': self.id,
'title': self.title,
'genres': self.genres,
'year': self.year,
'ratings': self.ratings,
'average_rating': self.average_rating,
'rating_count': self.rating_count
}
@classmethod
def from_dict(cls, data: Dict) -> 'Movie':
"""Create movie from dictionary"""
movie = cls(data['id'], data['title'], data['genres'], data.get('year'))
movie.ratings = data.get('ratings', [])
movie.average_rating = data.get('average_rating', 0.0)
movie.rating_count = data.get('rating_count', 0)
return movie
def __str__(self):
year_str = f" ({self.year})" if self.year else ""
rating_str = f" - Rating: {self.average_rating:.1f}/5.0 ({self.rating_count} ratings)" if self.rating_count > 0 else ""
return f"{self.title}{year_str} [{', '.join(self.genres)}]{rating_str}"
class User:
def __init__(self, user_id: int, name: str):
self.id = user_id
self.name = name
self.ratings = {} # movie_id -> rating
self.favorite_genres = []
def rate_movie(self, movie_id: int, rating: float):
"""Rate a movie"""
if 1.0 <= rating <= 5.0:
self.ratings[movie_id] = rating
def get_rating(self, movie_id: int) -> Optional[float]:
"""Get user's rating for a movie"""
return self.ratings.get(movie_id)
def update_favorite_genres(self, movies: Dict[int, Movie]):
"""Update favorite genres based on highly rated movies"""
genre_scores = defaultdict(list)
for movie_id, rating in self.ratings.items():
if movie_id in movies and rating >= 4.0:
movie = movies[movie_id]
for genre in movie.genres:
genre_scores[genre].append(rating)
# Calculate average rating per genre
genre_averages = {}
for genre, ratings in genre_scores.items():
if len(ratings) >= 2: # At least 2 highly rated movies
genre_averages[genre] = sum(ratings) / len(ratings)
# Sort genres by average rating
self.favorite_genres = sorted(genre_averages.keys(),
key=lambda g: genre_averages[g],
reverse=True)[:5]
def to_dict(self) -> Dict:
"""Convert user to dictionary"""
return {
'id': self.id,
'name': self.name,
'ratings': self.ratings,
'favorite_genres': self.favorite_genres
}
@classmethod
def from_dict(cls, data: Dict) -> 'User':
"""Create user from dictionary"""
user = cls(data['id'], data['name'])
user.ratings = data.get('ratings', {})
# Convert string keys back to int
user.ratings = {int(k): v for k, v in user.ratings.items()}
user.favorite_genres = data.get('favorite_genres', [])
return user
class MovieRecommendationSystem:
def __init__(self, data_file: str = "movie_data.json"):
self.data_file = data_file
self.movies = {} # movie_id -> Movie
self.users = {} # user_id -> User
self.load_data()
# Sample data if no data exists
if not self.movies:
self._create_sample_data()
def load_data(self):
"""Load data from JSON file"""
if os.path.exists(self.data_file):
try:
with open(self.data_file, 'r', encoding='utf-8') as f:
data = json.load(f)
# Load movies
for movie_data in data.get('movies', []):
movie = Movie.from_dict(movie_data)
self.movies[movie.id] = movie
# Load users
for user_data in data.get('users', []):
user = User.from_dict(user_data)
self.users[user.id] = user
except (json.JSONDecodeError, KeyError) as e:
print(f"Error loading data: {e}")
self.movies = {}
self.users = {}
def save_data(self):
"""Save data to JSON file"""
try:
data = {
'movies': [movie.to_dict() for movie in self.movies.values()],
'users': [user.to_dict() for user in self.users.values()]
}
with open(self.data_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2)
except Exception as e:
print(f"Error saving data: {e}")
def _create_sample_data(self):
"""Create sample movies for demonstration"""
sample_movies = [
(1, "The Shawshank Redemption", ["Drama", "Crime"], 1994),
(2, "The Godfather", ["Drama", "Crime"], 1972),
(3, "The Dark Knight", ["Action", "Crime", "Drama"], 2008),
(4, "Pulp Fiction", ["Crime", "Drama"], 1994),
(5, "Forrest Gump", ["Drama", "Romance"], 1994),
(6, "Inception", ["Action", "Sci-Fi", "Thriller"], 2010),
(7, "The Matrix", ["Action", "Sci-Fi"], 1999),
(8, "Goodfellas", ["Biography", "Crime", "Drama"], 1990),
(9, "The Lord of the Rings: The Return of the King", ["Adventure", "Drama", "Fantasy"], 2003),
(10, "Fight Club", ["Drama"], 1999),
(11, "Star Wars: Episode IV - A New Hope", ["Adventure", "Fantasy", "Sci-Fi"], 1977),
(12, "The Lord of the Rings: The Fellowship of the Ring", ["Adventure", "Drama", "Fantasy"], 2001),
(13, "One Flew Over the Cuckoo's Nest", ["Drama"], 1975),
(14, "Interstellar", ["Adventure", "Drama", "Sci-Fi"], 2014),
(15, "Casablanca", ["Drama", "Romance", "War"], 1942),
(16, "Titanic", ["Drama", "Romance"], 1997),
(17, "The Avengers", ["Action", "Adventure", "Sci-Fi"], 2012),
(18, "Jurassic Park", ["Adventure", "Sci-Fi", "Thriller"], 1993),
(19, "The Lion King", ["Animation", "Adventure", "Drama"], 1994),
(20, "Toy Story", ["Animation", "Adventure", "Comedy"], 1995)
]
for movie_id, title, genres, year in sample_movies:
movie = Movie(movie_id, title, genres, year)
# Add some sample ratings
import random
for _ in range(random.randint(10, 50)):
rating = random.uniform(3.0, 5.0) if random.random() > 0.3 else random.uniform(1.0, 5.0)
movie.add_rating(rating)
self.movies[movie_id] = movie
self.save_data()
def add_movie(self, title: str, genres: List[str], year: int = None) -> Movie:
"""Add a new movie"""
movie_id = max(self.movies.keys()) + 1 if self.movies else 1
movie = Movie(movie_id, title, genres, year)
self.movies[movie_id] = movie
self.save_data()
return movie
def add_user(self, name: str) -> User:
"""Add a new user"""
user_id = max(self.users.keys()) + 1 if self.users else 1
user = User(user_id, name)
self.users[user_id] = user
self.save_data()
return user
def rate_movie(self, user_id: int, movie_id: int, rating: float):
"""User rates a movie"""
if user_id not in self.users or movie_id not in self.movies:
return False
user = self.users[user_id]
movie = self.movies[movie_id]
# Remove old rating if exists
old_rating = user.get_rating(movie_id)
if old_rating is not None:
movie.ratings.remove(old_rating)
# Add new rating
user.rate_movie(movie_id, rating)
movie.add_rating(rating)
# Update user's favorite genres
user.update_favorite_genres(self.movies)
self.save_data()
return True
def get_movies_by_genre(self, genre: str) -> List[Movie]:
"""Get all movies in a specific genre"""
return [movie for movie in self.movies.values()
if genre.lower() in [g.lower() for g in movie.genres]]
def search_movies(self, query: str) -> List[Movie]:
"""Search movies by title"""
query = query.lower()
return [movie for movie in self.movies.values()
if query in movie.title.lower()]
def get_top_rated_movies(self, limit: int = 10, min_ratings: int = 5) -> List[Movie]:
"""Get top rated movies"""
eligible_movies = [movie for movie in self.movies.values()
if movie.rating_count >= min_ratings]
return sorted(eligible_movies, key=lambda m: m.average_rating, reverse=True)[:limit]
def calculate_user_similarity(self, user1_id: int, user2_id: int) -> float:
"""Calculate similarity between two users based on their ratings"""
if user1_id not in self.users or user2_id not in self.users:
return 0.0
user1 = self.users[user1_id]
user2 = self.users[user2_id]
# Find common movies
common_movies = set(user1.ratings.keys()) & set(user2.ratings.keys())
if len(common_movies) < 2:
return 0.0
# Calculate Pearson correlation coefficient
sum1 = sum(user1.ratings[movie] for movie in common_movies)
sum2 = sum(user2.ratings[movie] for movie in common_movies)
sum1_sq = sum(user1.ratings[movie] ** 2 for movie in common_movies)
sum2_sq = sum(user2.ratings[movie] ** 2 for movie in common_movies)
sum_products = sum(user1.ratings[movie] * user2.ratings[movie] for movie in common_movies)
n = len(common_movies)
# Calculate Pearson correlation
numerator = sum_products - (sum1 * sum2 / n)
denominator = math.sqrt((sum1_sq - sum1**2/n) * (sum2_sq - sum2**2/n))
if denominator == 0:
return 0.0
return numerator / denominator
def get_user_based_recommendations(self, user_id: int, limit: int = 10) -> List[Tuple[Movie, float]]:
"""Get recommendations based on similar users (collaborative filtering)"""
if user_id not in self.users:
return []
target_user = self.users[user_id]
# Find similar users
similarities = []
for other_user_id, other_user in self.users.items():
if other_user_id != user_id:
similarity = self.calculate_user_similarity(user_id, other_user_id)
if similarity > 0:
similarities.append((other_user_id, similarity))
# Sort by similarity
similarities.sort(key=lambda x: x[1], reverse=True)
# Get recommendations from similar users
recommendations = defaultdict(list)
for similar_user_id, similarity in similarities[:10]: # Top 10 similar users
similar_user = self.users[similar_user_id]
for movie_id, rating in similar_user.ratings.items():
# Only recommend movies the target user hasn't rated
if movie_id not in target_user.ratings and rating >= 4.0:
recommendations[movie_id].append(rating * similarity)
# Calculate weighted average scores
movie_scores = []
for movie_id, scores in recommendations.items():
if movie_id in self.movies:
avg_score = sum(scores) / len(scores)
movie_scores.append((self.movies[movie_id], avg_score))
# Sort by score and return top recommendations
movie_scores.sort(key=lambda x: x[1], reverse=True)
return movie_scores[:limit]
def get_content_based_recommendations(self, user_id: int, limit: int = 10) -> List[Movie]:
"""Get recommendations based on user's favorite genres and ratings"""
if user_id not in self.users:
return []
user = self.users[user_id]
# Get unrated movies
unrated_movies = [movie for movie in self.movies.values()
if movie.id not in user.ratings]
# Score movies based on genre preferences
movie_scores = []
for movie in unrated_movies:
score = 0.0
# Genre-based scoring
for i, genre in enumerate(user.favorite_genres):
if genre in movie.genres:
score += (len(user.favorite_genres) - i) * 2 # Higher weight for more preferred genres
# Add movie's average rating as a factor
if movie.rating_count > 0:
score += movie.average_rating
movie_scores.append((movie, score))
# Sort by score and return top recommendations
movie_scores.sort(key=lambda x: x[1], reverse=True)
return [movie for movie, score in movie_scores[:limit]]
def get_hybrid_recommendations(self, user_id: int, limit: int = 10) -> List[Movie]:
"""Get hybrid recommendations combining collaborative and content-based filtering"""
if user_id not in self.users:
return []
# Get both types of recommendations
user_based = self.get_user_based_recommendations(user_id, limit * 2)
content_based = self.get_content_based_recommendations(user_id, limit * 2)
# Combine recommendations
all_recommendations = {}
# Add user-based recommendations with weight
for movie, score in user_based:
all_recommendations[movie.id] = all_recommendations.get(movie.id, 0) + score * 0.7
# Add content-based recommendations with weight
for movie in content_based:
all_recommendations[movie.id] = all_recommendations.get(movie.id, 0) + movie.average_rating * 0.3
# Sort and return top recommendations
sorted_recommendations = sorted(all_recommendations.items(),
key=lambda x: x[1], reverse=True)
return [self.movies[movie_id] for movie_id, score in sorted_recommendations[:limit]]
def get_movie_statistics(self) -> Dict:
"""Get movie database statistics"""
if not self.movies:
return {}
total_movies = len(self.movies)
total_ratings = sum(movie.rating_count for movie in self.movies.values())
# Genre distribution
genre_count = defaultdict(int)
for movie in self.movies.values():
for genre in movie.genres:
genre_count[genre] += 1
# Year distribution
year_count = defaultdict(int)
for movie in self.movies.values():
if movie.year:
decade = (movie.year // 10) * 10
year_count[f"{decade}s"] += 1
return {
'total_movies': total_movies,
'total_users': len(self.users),
'total_ratings': total_ratings,
'average_ratings_per_movie': total_ratings / total_movies if total_movies > 0 else 0,
'most_common_genres': sorted(genre_count.items(), key=lambda x: x[1], reverse=True)[:10],
'movies_by_decade': dict(year_count)
}
def display_movie_list(movies: List[Movie], title: str = "Movies"):
"""Display a list of movies"""
print(f"\n=== {title} ===")
if not movies:
print("No movies found.")
return
for i, movie in enumerate(movies, 1):
print(f"{i}. {movie}")
def display_recommendations(recommendations: List[Tuple[Movie, float]], title: str = "Recommendations"):
"""Display recommendations with scores"""
print(f"\n=== {title} ===")
if not recommendations:
print("No recommendations available.")
return
for i, (movie, score) in enumerate(recommendations, 1):
print(f"{i}. {movie} (Score: {score:.2f})")
def main():
"""Main function to run the movie recommendation system"""
system = MovieRecommendationSystem()
current_user_id = None
while True:
print("\n=== Movie Recommendation System ===")
print("1. Register/Login User")
print("2. Browse Movies")
print("3. Search Movies")
print("4. Rate Movie")
print("5. Get Recommendations")
print("6. View Top Rated Movies")
print("7. Add New Movie")
print("8. View Statistics")
print("9. User Profile")
print("0. Exit")
try:
choice = input("\nEnter your choice: ").strip()
if choice == '1':
print("\n1. Register new user")
print("2. Login existing user")
user_choice = input("Enter choice: ").strip()
if user_choice == '1':
name = input("Enter your name: ").strip()
if name:
user = system.add_user(name)
current_user_id = user.id
print(f"User registered! Your ID is: {user.id}")
elif user_choice == '2':
try:
user_id = int(input("Enter your user ID: ").strip())
if user_id in system.users:
current_user_id = user_id
print(f"Logged in as: {system.users[user_id].name}")
else:
print("User not found!")
except ValueError:
print("Invalid user ID!")
elif choice == '2':
print("\n1. All movies")
print("2. Movies by genre")
browse_choice = input("Enter choice: ").strip()
if browse_choice == '1':
movies = list(system.movies.values())
display_movie_list(movies, "All Movies")
elif browse_choice == '2':
genre = input("Enter genre: ").strip()
movies = system.get_movies_by_genre(genre)
display_movie_list(movies, f"Movies in {genre}")
elif choice == '3':
query = input("Enter movie title to search: ").strip()
if query:
movies = system.search_movies(query)
display_movie_list(movies, f"Search Results for '{query}'")
elif choice == '4':
if current_user_id is None:
print("Please login first!")
continue
try:
movie_id = int(input("Enter movie ID to rate: ").strip())
if movie_id not in system.movies:
print("Movie not found!")
continue
rating = float(input("Enter rating (1.0-5.0): ").strip())
if system.rate_movie(current_user_id, movie_id, rating):
print("Rating added successfully!")
else:
print("Error adding rating!")
except ValueError:
print("Invalid input!")
elif choice == '5':
if current_user_id is None:
print("Please login first!")
continue
print("\n1. User-based recommendations")
print("2. Content-based recommendations")
print("3. Hybrid recommendations")
rec_choice = input("Enter choice: ").strip()
if rec_choice == '1':
recommendations = system.get_user_based_recommendations(current_user_id)
display_recommendations(recommendations, "User-based Recommendations")
elif rec_choice == '2':
recommendations = system.get_content_based_recommendations(current_user_id)
display_movie_list(recommendations, "Content-based Recommendations")
elif rec_choice == '3':
recommendations = system.get_hybrid_recommendations(current_user_id)
display_movie_list(recommendations, "Hybrid Recommendations")
elif choice == '6':
try:
limit = int(input("Enter number of movies to show (default 10): ").strip() or "10")
top_movies = system.get_top_rated_movies(limit)
display_movie_list(top_movies, f"Top {limit} Rated Movies")
except ValueError:
print("Invalid number!")
elif choice == '7':
title = input("Enter movie title: ").strip()
if not title:
print("Title cannot be empty!")
continue
genres_input = input("Enter genres (comma-separated): ").strip()
genres = [g.strip() for g in genres_input.split(',') if g.strip()]
year_input = input("Enter year (optional): ").strip()
year = int(year_input) if year_input else None
movie = system.add_movie(title, genres, year)
print(f"Movie added with ID: {movie.id}")
elif choice == '8':
stats = system.get_movie_statistics()
print("\n=== Movie Database Statistics ===")
print(f"Total movies: {stats['total_movies']}")
print(f"Total users: {stats['total_users']}")
print(f"Total ratings: {stats['total_ratings']}")
print(f"Average ratings per movie: {stats['average_ratings_per_movie']:.1f}")
if stats['most_common_genres']:
print("\nMost common genres:")
for genre, count in stats['most_common_genres'][:5]:
print(f" {genre}: {count}")
if stats['movies_by_decade']:
print("\nMovies by decade:")
for decade, count in sorted(stats['movies_by_decade'].items()):
print(f" {decade}: {count}")
elif choice == '9':
if current_user_id is None:
print("Please login first!")
continue
user = system.users[current_user_id]
print(f"\n=== Profile: {user.name} ===")
print(f"User ID: {user.id}")
print(f"Total ratings: {len(user.ratings)}")
if user.favorite_genres:
print(f"Favorite genres: {', '.join(user.favorite_genres)}")
if user.ratings:
print("\nRecent ratings:")
recent_ratings = list(user.ratings.items())[-5:]
for movie_id, rating in recent_ratings:
if movie_id in system.movies:
movie = system.movies[movie_id]
print(f" {movie.title}: {rating}/5.0")
elif choice == '0':
print("Thank you for using the Movie Recommendation System!")
break
else:
print("Invalid choice. Please try again.")
except KeyboardInterrupt:
print("\n\nGoodbye!")
break
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == "__main__":
main()
Run it
python movierecommendationsystem.pypython movierecommendationsystem.py1. Register user
2. Rate movie
3. Get recommendations
4. Browse / search
5. Statistics
6. Quit
> 3
User ID: 1
Method: (1) collaborative (2) content (3) hybrid: 3
1. The Dark Knight (4.8 expected)
2. Pulp Fiction (4.6 expected)
3. The Matrix (4.5 expected)1. Register user
2. Rate movie
3. Get recommendations
4. Browse / search
5. Statistics
6. Quit
> 3
User ID: 1
Method: (1) collaborative (2) content (3) hybrid: 3
1. The Dark Knight (4.8 expected)
2. Pulp Fiction (4.6 expected)
3. The Matrix (4.5 expected)Step-by-Step Explanation
1. Data models
@dataclass
class Movie:
id: int
title: str
genres: list[str]
year: int | None = None
ratings: list[float] = field(default_factory=list)
@property
def avg(self) -> float:
return sum(self.ratings) / len(self.ratings) if self.ratings else 0.0
@dataclass
class User:
id: int
name: str
ratings: dict[int, float] = field(default_factory=dict) # {movie_id: rating}
@property
def favorite_genres(self) -> list[str]:
scores = {}
for mid, r in self.ratings.items():
if r >= 4:
for g in movie_by_id[mid].genres:
scores[g] = scores.get(g, 0) + 1
return sorted(scores, key=scores.get, reverse=True)@dataclass
class Movie:
id: int
title: str
genres: list[str]
year: int | None = None
ratings: list[float] = field(default_factory=list)
@property
def avg(self) -> float:
return sum(self.ratings) / len(self.ratings) if self.ratings else 0.0
@dataclass
class User:
id: int
name: str
ratings: dict[int, float] = field(default_factory=dict) # {movie_id: rating}
@property
def favorite_genres(self) -> list[str]:
scores = {}
for mid, r in self.ratings.items():
if r >= 4:
for g in movie_by_id[mid].genres:
scores[g] = scores.get(g, 0) + 1
return sorted(scores, key=scores.get, reverse=True)@property@property lets user.favorite_genresuser.favorite_genres look like an attribute but compute fresh from current ratings every time.
2. Pearson similarity
import math
def pearson(u: User, v: User) -> float:
common = set(u.ratings) & set(v.ratings)
if len(common) < 2: return 0.0 # too little overlap
n = len(common)
sum_u = sum(u.ratings[m] for m in common)
sum_v = sum(v.ratings[m] for m in common)
sq_u = sum(u.ratings[m] ** 2 for m in common)
sq_v = sum(v.ratings[m] ** 2 for m in common)
cross = sum(u.ratings[m] * v.ratings[m] for m in common)
num = cross - (sum_u * sum_v / n)
den = math.sqrt((sq_u - sum_u**2/n) * (sq_v - sum_v**2/n))
return num / den if den else 0.0import math
def pearson(u: User, v: User) -> float:
common = set(u.ratings) & set(v.ratings)
if len(common) < 2: return 0.0 # too little overlap
n = len(common)
sum_u = sum(u.ratings[m] for m in common)
sum_v = sum(v.ratings[m] for m in common)
sq_u = sum(u.ratings[m] ** 2 for m in common)
sq_v = sum(v.ratings[m] ** 2 for m in common)
cross = sum(u.ratings[m] * v.ratings[m] for m in common)
num = cross - (sum_u * sum_v / n)
den = math.sqrt((sq_u - sum_u**2/n) * (sq_v - sum_v**2/n))
return num / den if den else 0.0Pearson correlation is the standard similarity metric for collaborative filtering. It is bias-aware — two users with the same shape of opinions but different “harshness” still correlate strongly.
3. Collaborative filtering
def collaborative(user: User, movies: dict, users: dict, k: int = 10):
scores = {}
for other_id, other in users.items():
if other_id == user.id: continue
sim = pearson(user, other)
if sim <= 0: continue
for mid, rating in other.ratings.items():
if mid in user.ratings: continue # skip movies user already rated
scores.setdefault(mid, [0.0, 0.0])
scores[mid][0] += sim * rating # weighted sum
scores[mid][1] += sim # normalization factor
ranked = sorted(
((mid, num/den) for mid, (num, den) in scores.items() if den > 0),
key=lambda x: x[1], reverse=True)
return [movies[mid] for mid, _ in ranked[:k]]def collaborative(user: User, movies: dict, users: dict, k: int = 10):
scores = {}
for other_id, other in users.items():
if other_id == user.id: continue
sim = pearson(user, other)
if sim <= 0: continue
for mid, rating in other.ratings.items():
if mid in user.ratings: continue # skip movies user already rated
scores.setdefault(mid, [0.0, 0.0])
scores[mid][0] += sim * rating # weighted sum
scores[mid][1] += sim # normalization factor
ranked = sorted(
((mid, num/den) for mid, (num, den) in scores.items() if den > 0),
key=lambda x: x[1], reverse=True)
return [movies[mid] for mid, _ in ranked[:k]]Score = Σ(similarity × their_rating) / Σ similarity. The division produces a number on the rating scale, not just a ranking.
4. Content-based filtering
def content_based(user: User, movies: dict, k: int = 10):
if not user.favorite_genres:
return top_rated(movies, k) # cold-start fallback
scores = {}
fav = set(user.favorite_genres[:3])
for mid, m in movies.items():
if mid in user.ratings: continue
overlap = len(set(m.genres) & fav)
if overlap == 0: continue
scores[mid] = overlap * (1 + m.avg) # boost well-rated matches
return [movies[mid] for mid, _ in
sorted(scores.items(), key=lambda x: x[1], reverse=True)[:k]]def content_based(user: User, movies: dict, k: int = 10):
if not user.favorite_genres:
return top_rated(movies, k) # cold-start fallback
scores = {}
fav = set(user.favorite_genres[:3])
for mid, m in movies.items():
if mid in user.ratings: continue
overlap = len(set(m.genres) & fav)
if overlap == 0: continue
scores[mid] = overlap * (1 + m.avg) # boost well-rated matches
return [movies[mid] for mid, _ in
sorted(scores.items(), key=lambda x: x[1], reverse=True)[:k]]Score = (genre overlap) × (1 + average rating). New users with no ratings get the top-rated list as a fallback — the cold-start solution.
5. Hybrid
def hybrid(user, movies, users, k=10):
collab = collaborative(user, movies, users, k * 2)
content = content_based(user, movies, k * 2)
score = {}
for i, m in enumerate(collab): score[m.id] = score.get(m.id, 0) + 0.7 * (len(collab) - i)
for i, m in enumerate(content): score[m.id] = score.get(m.id, 0) + 0.3 * (len(content) - i)
return [movies[mid] for mid, _ in
sorted(score.items(), key=lambda x: x[1], reverse=True)[:k]]def hybrid(user, movies, users, k=10):
collab = collaborative(user, movies, users, k * 2)
content = content_based(user, movies, k * 2)
score = {}
for i, m in enumerate(collab): score[m.id] = score.get(m.id, 0) + 0.7 * (len(collab) - i)
for i, m in enumerate(content): score[m.id] = score.get(m.id, 0) + 0.3 * (len(content) - i)
return [movies[mid] for mid, _ in
sorted(score.items(), key=lambda x: x[1], reverse=True)[:k]]70 % weight on collaborative, 30 % on content — a typical starting point. Tune empirically.
The Cold-Start Problem
When a new user joins, collaborative filtering has nothing to work with. Three standard fixes:
- Content-based fallback for the first few ratings.
- Onboarding survey — ask “pick three favorite genres.”
- Popularity baseline — show top-rated movies first.
When a new movie is added, the same problem appears from the other side. Content-based handles this fine because the algorithm only needs the new movie’s genres, not its ratings.
Common Mistakes
| Problem | Cause | Fix |
|---|---|---|
| All recommendations are the same | Tiny rating dataset | Add minimum-overlap threshold, fall back to popularity |
| Pearson returns NaN | Identical ratings (variance zero) | Guard with if den == 0if den == 0 |
| Recommended movies user already rated | Forgot to filter | Skip mid in user.ratingsmid in user.ratings |
| Bias toward heavy raters | No normalization | Mean-center each user’s ratings before similarity |
| Score scale meaningless | Used Σ instead of weighted average | Divide by Σ similarity |
| Hybrid no better than collaborative | Wrong weights | Cross-validate on held-out ratings |
Variations to Try
1. Mean-centered Pearson
Subtract each user’s average rating before computing similarity — removes “harsh rater” bias entirely.
2. Cosine similarity
Cheaper than Pearson:
def cosine(u, v):
common = set(u.ratings) & set(v.ratings)
if not common: return 0
dot = sum(u.ratings[m] * v.ratings[m] for m in common)
nu = math.sqrt(sum(u.ratings[m]**2 for m in common))
nv = math.sqrt(sum(v.ratings[m]**2 for m in common))
return dot / (nu * nv) if nu * nv else 0def cosine(u, v):
common = set(u.ratings) & set(v.ratings)
if not common: return 0
dot = sum(u.ratings[m] * v.ratings[m] for m in common)
nu = math.sqrt(sum(u.ratings[m]**2 for m in common))
nv = math.sqrt(sum(v.ratings[m]**2 for m in common))
return dot / (nu * nv) if nu * nv else 03. Item-based collaborative filtering
Find movies that are similar to each other based on rating patterns. Recommend movies similar to the user’s highly-rated ones. Often outperforms user-based at scale.
4. TF-IDF on genres + descriptions
Pull movie plot summaries from TMDB or OMDb. Run TF-IDF on the text and use cosine similarity over the resulting vectors. Far richer content-based recommendations.
5. Matrix factorization (SVD)
Decompose the user-item rating matrix into low-rank factors. Use scipy.sparse.linalg.svdsscipy.sparse.linalg.svds or the SurpriseSurprise library:
pip install scikit-surprisepip install scikit-surprisefrom surprise import Dataset, Reader, SVD
algo = SVD()
algo.fit(Dataset.load_from_df(ratings_df, Reader(rating_scale=(1, 5))).build_full_trainset())
predicted = algo.predict(user_id, movie_id).estfrom surprise import Dataset, Reader, SVD
algo = SVD()
algo.fit(Dataset.load_from_df(ratings_df, Reader(rating_scale=(1, 5))).build_full_trainset())
predicted = algo.predict(user_id, movie_id).estThis is what Netflix used in their famous prize-winning recommender.
6. Implicit feedback
Switch from explicit ratings (1-5 stars) to implicit (watched / not watched). Different math (ALS instead of SVD) but same shape.
7. Diversity & serendipity
After ranking, post-process to ensure variety — penalize candidates whose genres overlap heavily with already-shown items.
8. TMDB API integration
Replace the hard-coded 20 movies with a fresh feed of currently-popular titles from themoviedb.org/documentation/api.
9. Save & evaluate
Hold out 20 % of ratings; measure RMSE on predicted vs. actual. The standard offline-evaluation pattern.
10. Web frontend
Flask app that renders the user’s recommendations with poster art from TMDB. See Basic Web Server.
11. Tag-based filtering
User-added tags (“psychological thriller”, “based on a true story”) often outperform official genres. Combine.
12. Real-time updates
Re-run recommendations every time a new rating arrives, not just on demand.
Evaluation Metrics
For any non-trivial recommender, measure:
- RMSE — square root of mean squared prediction error.
- MAE — mean absolute error.
- Precision@K — fraction of top-K recommendations that the user actually liked.
- Recall@K — fraction of liked items that appeared in top-K.
- Coverage — fraction of catalog that ever gets recommended.
- Serendipity — non-obvious good recommendations.
Real-World Recommender Systems
- Netflix — matrix factorization + deep learning, billions of dollars of ROI.
- YouTube — two-stage: candidate generation → ranking, both neural.
- Spotify — collaborative filtering + audio embeddings.
- Amazon — item-based collaborative filtering (“customers who bought this…”).
- TikTok — embedding-based with continuous learning from watch time.
- Goodreads / Letterboxd — explicit ratings, classic CF.
Educational Value
- Collaborative vs. content-based — two paradigms, different strengths.
- Pearson correlation — a textbook statistic with real predictive power.
- Cold-start strategies — universal recommender system problem.
- Hybrid models — when combining two mediocre models beats either alone.
- Evaluation methodology — train/test splits, held-out ratings.
Next Steps
- Implement item-based collaborative filtering and compare to user-based.
- Add TMDB integration for live, plot-aware data.
- Switch to SVD via
SurpriseSurprisefor a real model. - Evaluate RMSE on held-out ratings to pick the best algorithm.
- Wrap with a Flask UI that renders posters.
- Cross-link with Personal Diary for the JSON-persistence pattern.
Conclusion
You implemented two of the foundational recommendation algorithms and combined them — the same architecture used by the first decade of commercial recommenders. The state of the art has moved on to deep learning, but every modern system still rests on the same two ideas: “users like this one” and “items like this one.” Full source on GitHub. Find more ML projects on Python Central Hub.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
