Recommendation Systems
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.
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.
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).
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.
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))