Recommendation systems drive billions of dollars in e-commerce, streaming, and social media revenue. Netflix’s recommendations save $1 billion per year in prevented churn. Spotify’s Discover Weekly keeps users engaged. Amazon’s “customers also bought” drives 35% of its revenue. This guide builds recommendation systems from scratch using Python, covering collaborative filtering, content-based filtering, and matrix factorisation.
Types of Recommendation Systems
Collaborative filtering recommends items based on the behaviour of similar users or items — “users like you also liked.” It requires no item metadata but struggles with new users and items (cold start). Content-based filtering recommends items similar to what a user has liked before, based on item features. It handles cold start for items but requires good metadata. Hybrid systems combine both, overcoming individual weaknesses.
Data Preparation
import pandas as pd
import numpy as np
from scipy.sparse import csr_matrix
from sklearn.metrics.pairwise import cosine_similarity
# Movie rating dataset
ratings = pd.DataFrame({
'user_id': [1,1,1,2,2,3,3,3,4,4,4,5,5],
'movie_id': [1,2,3,1,4,2,3,5,1,3,5,2,4],
'rating': [5,4,3,4,5,3,4,5,2,5,4,4,3]
})
movies = pd.DataFrame({
'movie_id': [1,2,3,4,5],
'title': ['Inception','Interstellar','The Dark Knight','Dunkirk','Tenet'],
'genre': ['Sci-Fi','Sci-Fi','Action','War','Sci-Fi']
})
print(f'Users: {ratings["user_id"].nunique()} | Movies: {ratings["movie_id"].nunique()}')
print(f'Ratings: {len(ratings)} | Sparsity: {1 - len(ratings)/(ratings["user_id"].nunique() * ratings["movie_id"].nunique()):.1%}')
User-Based Collaborative Filtering
# Create user-item matrix
user_item = ratings.pivot_table(index='user_id', columns='movie_id',
values='rating', fill_value=0)
print(user_item)
# User similarity matrix (cosine similarity)
user_sim = cosine_similarity(user_item)
user_sim_df = pd.DataFrame(user_sim,
index=user_item.index,
columns=user_item.index)
def get_user_recommendations(user_id, n=3):
# Find most similar users (exclude self)
similar_users = (user_sim_df[user_id]
.drop(user_id)
.sort_values(ascending=False))
top_users = similar_users.head(3).index.tolist()
# Items the target user has NOT rated
user_rated = set(ratings[ratings['user_id'] == user_id]['movie_id'])
candidate_movies = set(ratings[ratings['user_id'].isin(top_users)]['movie_id'])
unrated = candidate_movies - user_rated
# Score by weighted average of similar users' ratings
scores = {}
for movie in unrated:
weights, weighted_ratings = 0, 0
for sim_user in top_users:
sim_rating = ratings[
(ratings['user_id'] == sim_user) &
(ratings['movie_id'] == movie)
]['rating']
if not sim_rating.empty:
sim = similar_users[sim_user]
weighted_ratings += sim * sim_rating.values[0]
weights += sim
if weights > 0:
scores[movie] = weighted_ratings / weights
top_movies = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:n]
return [(movies[movies['movie_id']==mid]['title'].values[0], round(score, 2))
for mid, score in top_movies]
print(f'
Recommendations for User 1:')
for title, score in get_user_recommendations(1):
print(f' {title}: predicted rating {score}')
Item-Based Collaborative Filtering
# Item similarity (often more stable than user similarity)
item_sim = cosine_similarity(user_item.T)
item_sim_df = pd.DataFrame(item_sim,
index=user_item.columns,
columns=user_item.columns)
def get_item_recommendations(movie_id, n=3):
similar = item_sim_df[movie_id].sort_values(ascending=False)
similar = similar.drop(movie_id).head(n)
result = []
for mid, score in similar.items():
title = movies[movies['movie_id'] == mid]['title'].values[0]
result.append((title, round(score, 3)))
return result
print(f'
Movies similar to Inception:')
for title, score in get_item_recommendations(1):
print(f' {title}: similarity {score}')
Matrix Factorisation with SVD
from sklearn.decomposition import TruncatedSVD
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
# Full user-item matrix (with NaN for unrated)
user_item_full = ratings.pivot_table(index='user_id', columns='movie_id',
values='rating')
# Fill NaN with row mean (user's average rating)
user_means = user_item_full.mean(axis=1)
user_item_filled = user_item_full.sub(user_means, axis=0).fillna(0)
# SVD: decompose into U × Σ × V^T
svd = TruncatedSVD(n_components=2, random_state=42)
U = svd.fit_transform(user_item_filled)
sigma = np.diag(svd.singular_values_)
Vt = svd.components_
# Reconstruct predictions
predictions = pd.DataFrame(
np.dot(U, np.dot(sigma, Vt)) + user_means.values.reshape(-1, 1),
index=user_item_full.index,
columns=user_item_full.columns
).clip(1, 5)
print('
Predicted ratings matrix:')
print(predictions.round(1))
def svd_recommend(user_id, n=3):
user_rated = set(ratings[ratings['user_id'] == user_id]['movie_id'])
user_preds = predictions.loc[user_id].drop(index=list(user_rated), errors='ignore')
top = user_preds.sort_values(ascending=False).head(n)
return [(movies[movies['movie_id'] == mid]['title'].values[0], round(score, 2))
for mid, score in top.items()]
print(f'
SVD Recommendations for User 4:')
for title, score in svd_recommend(4):
print(f' {title}: predicted {score}')
Content-Based Filtering
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import linear_kernel
# Movie metadata
movies_meta = pd.DataFrame({
'movie_id': [1, 2, 3, 4, 5],
'title': ['Inception', 'Interstellar', 'The Dark Knight', 'Dunkirk', 'Tenet'],
'description': [
'A thief who steals corporate secrets through dream-sharing technology',
'A team of explorers travel through a wormhole in space to ensure humanity survival',
'Batman fights the menace known as the Joker in Gotham City',
'Allied soldiers from Belgium Britain and Canada are surrounded by German Army in Dunkirk',
'A CIA operative teams up to prevent a world war through time manipulation'
],
'genre': ['sci-fi thriller', 'sci-fi drama', 'action thriller', 'war drama', 'sci-fi thriller']
})
movies_meta['soup'] = movies_meta['description'] + ' ' + movies_meta['genre']
tfidf = TfidfVectorizer(stop_words='english')
tfidf_matrix = tfidf.fit_transform(movies_meta['soup'])
cosine_sim = linear_kernel(tfidf_matrix, tfidf_matrix)
title_to_idx = {row['title']: idx for idx, row in movies_meta.iterrows()}
def content_recommend(title, n=3):
idx = title_to_idx[title]
scores = list(enumerate(cosine_sim[idx]))
scores = sorted(scores, key=lambda x: x[1], reverse=True)[1:n+1]
return [(movies_meta.iloc[i]['title'], round(score, 3)) for i, score in scores]
print(f'
Content-based: similar to Inception:')
for title, score in content_recommend('Inception'):
print(f' {title}: {score}')
Production with Surprise Library
pip install scikit-surprise
from surprise import SVD, Dataset, Reader, accuracy
from surprise.model_selection import cross_validate, train_test_split
reader = Reader(rating_scale=(1, 5))
data = Dataset.load_from_df(ratings[['user_id', 'movie_id', 'rating']], reader)
trainset, testset = train_test_split(data, test_size=0.2, random_state=42)
algo = SVD(n_factors=50, n_epochs=20, lr_all=0.005, reg_all=0.02)
algo.fit(trainset)
preds = algo.test(testset)
print(f'RMSE: {accuracy.rmse(preds):.4f}')
print(f'MAE: {accuracy.mae(preds):.4f}')
# Cross-validate
cv_results = cross_validate(algo, data, measures=['RMSE', 'MAE'],
cv=5, verbose=True)
print(f'Mean RMSE: {cv_results["test_rmse"].mean():.4f}')
Conclusion
Start with item-based collaborative filtering — it is interpretable, stable, and effective even with moderate amounts of data. Move to matrix factorisation (SVD or ALS) when you need better accuracy and can tolerate a black box. Use content-based filtering to solve the cold-start problem for new items. In production, combine all three in a hybrid system and serve recommendations from pre-computed embeddings via approximate nearest neighbour search (Faiss, Annoy, HNSW) for millisecond response times at scale.



