🧠 Classical ML · Tier 2 🔴 Advanced MODULE 09

Recommendation Systems

⏱️ 3 hours
🎬 Collaborative · Content-Based · SVD
🧩 5 Quiz Questions
Classical ML — Module 9 of 100%
🎯 What you'll learn: User-based vs. item-based collaborative filtering using cosine similarity on a user-item rating matrix, content-based filtering using TF-IDF on item descriptions, Truncated SVD (matrix factorisation) to handle sparsity, the cold-start problem, and a complete movie recommender project using the MovieLens dataset.

No Single "Best" Method

Recommender systems can be built three ways — each with distinct strengths and failure modes. Production systems (Netflix, Spotify) combine all three.

Collaborative Filtering
Finds users with similar taste (user-based) or similar items (item-based). Requires many ratings. Fails for new users/items (cold-start).
Content-Based
Uses item features (genre, director, description). Works for new items. Cannot recommend outside the user's known taste.
Matrix Factorisation
SVD / NMF decomposes the rating matrix into latent factors. Handles sparsity best. Basis of Netflix Prize winner.

User-Item Matrix + Cosine Similarity

Build a users × movies rating matrix (rows = users, columns = movies, cells = ratings). To recommend items to a user, find their most similar users (by cosine similarity of rating vectors), then recommend highly-rated items from those neighbours that the target user hasn't seen.

User-based collaborative filtering — MovieLensPYTHON
import pandas as pd
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

# Build user-item rating matrix
ratings  = pd.read_csv('ratings.csv')    # userId, movieId, rating, timestamp
movies   = pd.read_csv('movies.csv')     # movieId, title, genres

matrix   = ratings.pivot(index='userId', columns='movieId', values='rating')
matrix_f = matrix.fillna(0)  # cosine_similarity needs no NaN

# User-user cosine similarity matrix
user_sim = cosine_similarity(matrix_f)
user_sim_df = pd.DataFrame(user_sim, index=matrix.index, columns=matrix.index)

def get_similar_users(user_id, top_n=10):
    return (user_sim_df[user_id]
              .drop(user_id)          # exclude the user themselves
              .nlargest(top_n).index.tolist())

def recommend(user_id, top_n_items=10):
    similar     = get_similar_users(user_id)
    # Movies already rated by this user
    seen        = matrix.loc[user_id].dropna().index
    # Mean rating by similar users, excluding already-seen
    neighbour_ratings = matrix.loc[similar, ~matrix.columns.isin(seen)]
    avg_ratings = neighbour_ratings.mean().nlargest(top_n_items)
    return (movies.set_index('movieId').loc[avg_ratings.index, 'title'])

print(recommend(user_id=1))

Recommend by Item Features

Content-based filtering builds a profile of a user's taste based on the features of items they rated highly, then recommends items with similar feature vectors. No other users' data needed — great for cold-start on items, but it cannot surprise the user with discoveries outside their existing taste (the "filter bubble" problem).

Content-based: TF-IDF on genres + descriptionPYTHON
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import linear_kernel

# Use genres as the item "text" (or combine with overview/tags)
movies['genres_clean'] = movies['genres'].str.replace('|', ' ', regex=False)

tfidf  = TfidfVectorizer(stop_words='english')
tfidf_m = tfidf.fit_transform(movies['genres_clean'])

# Item-item cosine similarity (linear_kernel faster for TF-IDF)
item_sim = linear_kernel(tfidf_m, tfidf_m)
idx_map  = pd.Series(movies.index, index=movies['title'])

def content_recommend(title, top_n=10):
    idx = idx_map[title]
    scores = sorted(
        enumerate(item_sim[idx]), key=lambda x: x[1], reverse=True
    )[1:top_n+1]      # skip index 0 (same movie)
    return movies.iloc[[i[0] for i in scores]]['title'].tolist()

print(content_recommend('Toy Story (1995)'))

Latent Factors for Sparse Ratings

Real rating matrices are extremely sparse — a user might rate 50 movies out of 50,000. Cosine similarity on sparse vectors is unreliable. Truncated SVD (or NMF) decomposes the matrix into latent factors: users in latent space × items in latent space. The dot product of a user's and item's latent vectors predicts the rating. This was the core of the Netflix Prize winning approach.

Truncated SVD matrix factorisationPYTHON
from sklearn.decomposition import TruncatedSVD
from scipy.sparse import csr_matrix

# Sparse matrix is memory-efficient for millions of users
sparse_matrix = csr_matrix(matrix_f.values)

# Decompose into 50 latent factors
svd = TruncatedSVD(n_components=50, random_state=42)
U   = svd.fit_transform(sparse_matrix)  # users × 50
Vt  = svd.components_                   # 50 × movies

# Predicted rating matrix (dense)
predicted = U.dot(Vt)                   # users × movies
predicted_df = pd.DataFrame(predicted,
    index=matrix_f.index, columns=matrix_f.columns)

def svd_recommend(user_id, top_n=10):
    seen      = matrix.loc[user_id].dropna().index
    predicted_scores = predicted_df.loc[user_id].drop(index=seen)
    top_ids   = predicted_scores.nlargest(top_n).index
    return movies.set_index('movieId').loc[top_ids, 'title'].tolist()

print(svd_recommend(user_id=1))
🎬
The Netflix Prize
In 2009, Netflix awarded $1M to the team that improved their recommendation algorithm's RMSE by 10%. The winning solution combined 107 different models including multiple SVD variants, nearest-neighbour methods, and restricted Boltzmann machines. Modern Netflix uses deep learning — but the classical SVD approach here is still standard in small-to-medium systems.
🎉
Module 9 Complete!
One module left! Bring everything together in the Classical ML Capstone.
Module 10: Capstone → Course Home
🧩 Module 9 Check
5 questions · instant feedback
1. What is the "cold-start problem" in recommender systems?
2. In collaborative filtering, cosine similarity of 1.0 between two user vectors means?
3. Why does TruncatedSVD handle sparse rating matrices better than direct cosine similarity?
4. A content-based recommender suggests only action movies to a user who has watched 20 action films. What problem is this?
5. In the collaborative filtering function, why do you exclude movies the user has already rated?