Recommender Systems
Reviewed & published by Brayan K
Learn how Netflix, Spotify, and Amazon decide what to show you — using cosine similarity, collaborative and content-based filtering, and the maths behind the Netflix Prize — and build a recommender by hand in plain Python.
Part of the free AI & Machine Learning course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- How content-based filtering recommends from item features
- User-user and item-item collaborative filtering
- Cosine similarity — and how to compute it by hand
- The intuition behind matrix factorization
- What the cold-start problem is and how to dodge it
- How to evaluate a recommender with precision@k
🌍 Real-World Analogy: A Friend Who Knows Your Taste
Picture the one friend who always nails movie night. Over the years they have learned what you love, so when they say "trust me, watch this," they are usually right. How do they do it? Two ways, really — and those two ways are the two big families of recommender systems.
Sometimes your friend reasons from the movie itself: "you loved the last gritty sci-fi thriller, this one is the same vibe." That is content-based filtering — matching an item's features to what you already liked. Other times they reason from people: "my colleague has the exact same taste as you, and she couldn't stop talking about this one." That is collaborative filtering — borrowing the verdict of users similar to you. The best friend (and the best system, like Netflix or Spotify) blends both.
1 Two Families (and the Hybrid)
A recommender system predicts which items you will like and shows you the best ones, usually as a ranked list. There are two core strategies, plus a hybrid that mixes them.
| Approach | How it works | Example | Weakness |
|---|---|---|---|
| Content-based | Match item features to your past likes | "Because you liked sci-fi…" | Filter bubble |
| Collaborative | Borrow ratings from similar users/items | "Users like you watched…" | Cold start |
| Hybrid | Combine both signals | Netflix, YouTube, Spotify | Complexity |
2 Cosine Similarity — Measuring Taste
Represent a user as a rating vector — just a list of their scores: [5, 2, 5, 1, 4]. To compare two users, you compare their vectors. The favourite tool is cosine similarity: the angle between the two vectors.
The formula is the dot product (multiply matching positions and add them up) divided by the product of the two vectors' lengths. The result lands between 0 (nothing in common) and 1 (identical direction of taste).
📐 Why cosine, not plain distance?
Cosine measures direction, not magnitude. A generous rater who hands out 5s and a stingy rater who hands out 3s can still be a perfect match if they rank things the same way. Cosine sees that; raw distance would wrongly call them far apart.
The worked example below computes it from scratch — no NumPy, just math.sqrt and a loop. Read every comment, then run it.
# === Cosine similarity between two rating vectors, in plain Python ===
# No libraries. A user's taste is just a list of 1-5 star ratings.
import math
# Each list holds one user's ratings for the SAME 5 movies, in order.
# [Inception, Titanic, Matrix, Notebook, Interstellar]
alice = [5, 2, 5, 1, 4]
bob = [4, 1, 4, 1, 5] # very like Alice
carol = [1, 5, 1, 5, 1] # opposite taste
def cosine_similarity(a, b):
# Dot product: multiply matching positions, then add them up.
dot = sum(a[i] * b[i] for i in range(len(a))) # how aligned they are
# Magnitude (length) of each vector = sqrt of sum of squares.
mag_a = math.sqrt(sum(x * x for x in a))
mag_b = math.sqrt(sum(x * x for x in b))
if mag_a == 0 or mag_b == 0: # avoid divide-by-zero
return 0.0
return dot / (mag_a * mag_b) # always between -1 and 1
print("Cosine similarity ranges from 0 (unrelated) to 1 (identical taste).")
print()
print(f"Alice vs Bob: {cosine_similarity(alice, bob):.3f}") # ~0.98 high
print(f"Alice vs Carol: {cosine_similarity(alice, carol):.3f}") # ~0.55 low
print()
print("Bob scores higher, so Bob is Alice's nearest neighbour.")
# Why cosine and not raw distance? It compares the DIRECTION of taste,
# ignoring whether someone rates generously high or stingily low.3 Collaborative Filtering — User-User and Item-Item
Collaborative filtering ignores what an item is and looks only at the pattern of ratings. It comes in two flavours:
Find the people most similar to you, then recommend what they liked that you have not seen. Netflix's original approach. Intuitive, but users come and go and there can be millions of them, so it is hard to scale.
Find items rated alike and recommend ones similar to what you already enjoyed — "because you watched Inception." Amazon's approach. Items are more stable than users and their similarities can be precomputed, so it scales far better.
To predict a missing rating, take a similarity-weighted average of your neighbours' ratings: the more similar a neighbour, the more their opinion counts. The worked example fills in the gaps in a small rating matrix and recommends the single best unseen movie for one user.
# === User-user collaborative filtering: recommend the top movie for Eve ===
# Find the users most similar to Eve, then borrow their ratings.
import math
movies = ["Inception", "Titanic", "Matrix", "Notebook", "Interstellar"]
# 0 means "not rated yet". Eve has only seen two movies.
ratings = {
"Alice": [5, 2, 5, 1, 4],
"Bob": [4, 1, 4, 1, 5],
"Carol": [1, 5, 1, 5, 1],
"Eve": [5, 0, 4, 0, 0], # target user — gaps are what we predict
}
def cosine_similarity(a, b):
# Only compare positions where BOTH users actually rated something.
pairs = [(a[i], b[i]) for i in range(len(a)) if a[i] > 0 and b[i] > 0]
if not pairs:
return 0.0
dot = sum(x * y for x, y in pairs)
mag_a = math.sqrt(sum(x * x for x, _ in pairs))
mag_b = math.sqrt(sum(y * y for _, y in pairs))
if mag_a == 0 or mag_b == 0:
return 0.0
return dot / (mag_a * mag_b)
eve = ratings["Eve"]
# Step 1 — how similar is every other user to Eve?
print("Similarity to Eve:")
neighbours = []
for name, vec in ratings.items():
if name == "Eve":
continue
sim = cosine_similarity(eve, vec)
neighbours.append((name, sim))
print(f" {name:<6} {sim:.3f}")
print()
# Step 2 — predict Eve's score for each UNSEEN movie as a
# similarity-weighted average of what her neighbours rated it.
print("Predicted ratings for movies Eve hasn't seen:")
predictions = []
for j in range(len(movies)):
if eve[j] > 0:
continue # already seen — skip
num, den = 0.0, 0.0
for name, sim in neighbours:
r = ratings[name][j]
if r > 0:
num += sim * r # weight each rating by similarity
den += sim
pred = num / den if den > 0 else 0.0
predictions.append((movies[j], pred))
print(f" {movies[j]:<13} {pred:.2f}/5")
print()
# Step 3 — recommend the single highest-scoring unseen movie.
predictions.sort(key=lambda pair: pair[1], reverse=True)
top_movie, top_score = predictions[0]
print(f"Top recommendation for Eve: {top_movie} ({top_score:.2f}/5)")4 Matrix Factorization — The Intuition
The user-item rating matrix is huge and mostly empty — millions of users, thousands of items, and almost every cell blank. Comparing shared ratings directly falls apart when two users have barely any items in common. Matrix factorization is the fix that won the Netflix Prize.
The idea: every user and every item secretly lives in a small space of hidden factors — think invisible dials like "how much sci-fi", "how much romance", "how light vs dark". Factorization learns, for each user, how much they want each dial, and for each item, how much of each dial it has. A predicted rating is just the dot product of a user's dials with an item's dials.
Decompose the big rating matrix R into two skinny matrices — one row of factors per user, one row of factors per item — so that multiplying them back together reconstructs the known ratings and fills in the blanks. Those filled-in blanks are your predictions.
You do not implement the training loop here (it is gradient descent over those factors), but the mental model is the payoff: users and items share one hidden taste space, and a dot product scores any pair. That is the same dot product you already coded for cosine similarity.
5 Evaluating a Recommender — precision@k
A recommender hands the user a ranked list, and users only ever look at the top of it. So the right question is not "how accurate are all my predictions" but "how many of my top few were any good?"
precision@k answers exactly that: of the top k items you recommended, what fraction were genuinely relevant to the user? A precision@5 of 0.6 means 3 of your top 5 hit the mark. It is simple, it matches what the user experiences, and it is far more honest than overall rating error.
The worked example builds an item-item recommendation list ("because you watched Inception…") and then scores that list with precision@k for k = 1, 2, 3.
# === Item-item similarity and precision@k (how good is the list?) ===
import math
# Columns are users; each row is one MOVIE's ratings across users.
# Item-item CF asks: "which movies get rated alike?" — Amazon's approach.
items = {
"Inception": [5, 4, 1, 5], # ratings from [u1, u2, u3, u4]
"Matrix": [5, 4, 1, 4], # almost identical to Inception
"Interstellar": [4, 5, 1, 5], # also a sci-fi neighbour
"Notebook": [1, 1, 5, 1], # the odd one out (romance)
}
def cosine_similarity(a, b):
dot = sum(a[i] * b[i] for i in range(len(a)))
mag_a = math.sqrt(sum(x * x for x in a))
mag_b = math.sqrt(sum(x * x for x in b))
if mag_a == 0 or mag_b == 0:
return 0.0
return dot / (mag_a * mag_b)
# "You watched Inception" -> find the most similar OTHER movies.
seed = "Inception"
print(f"Because you watched {seed}, you might like:")
neighbours = []
for name, vec in items.items():
if name == seed:
continue
neighbours.append((name, cosine_similarity(items[seed], vec)))
neighbours.sort(key=lambda pair: pair[1], reverse=True)
for name, sim in neighbours:
print(f" {name:<13} similarity {sim:.3f}")
print()
# --- precision@k: of the top-k we recommended, how many were good? ---
# We label which movies the user ACTUALLY liked (the "relevant" set).
recommended = [name for name, _ in neighbours] # ranked list
relevant = {"Matrix", "Interstellar"} # ground truth
def precision_at_k(recommended, relevant, k):
top_k = recommended[:k] # only judge the first k
hits = sum(1 for item in top_k if item in relevant)
return hits / k # fraction that were relevant
for k in (1, 2, 3):
print(f"precision@{k} = {precision_at_k(recommended, relevant, k):.2f}")
# precision@2 = 1.00 means BOTH of our top-2 picks were ones the user liked.❄️ The Cold-Start Problem
Collaborative filtering needs history. A brand-new user has rated nothing, and a brand-new item has been rated by nobody — so there is nothing to compare. This is the cold-start problem, and it is the single biggest reason a recommender feels useless on day one.
- Fall back to content-based. Item features exist even with zero ratings, so you can still match a new item to a user's taste, and a new user to items.
- Recommend popular items. Until you know someone, the safest bet is what most people enjoy — then personalise as data arrives.
- Ask a few onboarding questions. "Pick three movies you love" seeds a profile instantly, which is why so many apps do it on sign-up.
Now you try. Fill in the blanks marked ___. Cosine similarity is the engine of every example above — finish it and you understand the whole lesson.
# 🎯 YOUR TURN — finish the cosine similarity function.
# Cosine similarity tells you how aligned two taste vectors are (0 to 1).
import math
you = [5, 1, 4, 1, 5] # your star ratings for 5 movies
friend = [4, 2, 5, 1, 4] # a friend's ratings for the same 5 movies
def cosine_similarity(a, b):
dot = ___ # 👉 sum of a[i]*b[i] for every position i
mag_a = math.sqrt(sum(x * x for x in a))
mag_b = ___ # 👉 sqrt of the sum of squares of b, like mag_a
if mag_a == 0 or mag_b == 0:
return 0.0
return dot / (mag_a * mag_b)
score = cosine_similarity(you, friend)
print(f"Taste match: {score:.3f}")
# ✅ Expected output:
# Taste match: 0.974Your turn again. Score each unwatched movie against a taste profile and recommend the single best one.
# 🎯 YOUR TURN — recommend the single best unseen item by similarity.
# Your taste profile is given. Score each unseen movie, then pick the best.
import math
# Your average taste as a feature vector: [action, romance, sci_fi, drama]
profile = [0.8, 0.1, 0.9, 0.4]
# Movies you have NOT watched yet, each with the same 4 features.
candidates = {
"Blade Runner": [0.7, 0.1, 0.9, 0.7],
"La La Land": [0.0, 0.9, 0.0, 0.6],
"Arrival": [0.4, 0.1, 0.9, 0.8],
}
def cosine_similarity(a, b):
dot = sum(a[i] * b[i] for i in range(len(a)))
mag_a = math.sqrt(sum(x * x for x in a))
mag_b = math.sqrt(sum(x * x for x in b))
if mag_a == 0 or mag_b == 0:
return 0.0
return dot / (mag_a * mag_b)
best_movie = None
best_score = -1.0
for name, vec in candidates.items():
sim = ___ # 👉 cosine_similarity between profile and vec
print(f" {name:<13} {sim:.3f}")
if sim > best_score:
best_score = sim
best_movie = ___ # 👉 remember the NAME of this movie
print(f"Top pick: {best_movie}")
# ✅ Expected output:
# Blade Runner 0.969
# La La Land 0.305
# Arrival 0.951
# Top pick: Blade Runner6 Common Errors (And How to Fix Them)
These four mistakes trip up almost everyone building their first recommender:
A brand-new user has rated nothing, so collaborative filtering returns an empty or random list and the product feels broken on first use.
recs = collaborative_filter(new_user) # ❌ no history → nothing to return✅ Fix: fall back when there is no history.
if user_has_ratings(new_user):
recs = collaborative_filter(new_user)
else:
recs = content_based(new_user) or most_popular() # ✅ graceful fallbackBlockbusters get rated by everyone, so they look "similar" to everyone and end up recommended to everyone. Niche items the user would love never surface.
✅ Fix: down-weight popular items (penalise by how often they are rated) and add a diversity term so the list is not just the global top 10.
❌ Treating sparse data as real zeros
A blank cell means "not rated", not "rated zero". Counting missing entries as 0 makes two users who simply have not overlapped look like they disagree.
sim = cosine(a, b) # ❌ if a and b are padded with 0s, similarity is wrong✅ Fix: compare only positions both users actually rated.
pairs = [(a[i], b[i]) for i in range(len(a)) if a[i] > 0 and b[i] > 0]
# ✅ build the vectors from pairs, then take cosine of those❌ Data leakage in evaluation
Scoring your recommender on ratings it was trained on (or on items the user already interacted with) makes it look brilliant — then it flops on genuinely unseen items.
evaluate(model, all_ratings) # ❌ test items were seen during training✅ Fix: hold out a test set of future or unseen interactions and measure precision@k only on those.
train, test = split_by_time(ratings) # ✅ never train on the test rows
model.fit(train)
score = precision_at_k(model.recommend(user), test_relevant, k=5)📋 Quick Reference
| Concept | What it is | Use when |
|---|---|---|
| Content-based | Match item features to past likes | New users, rich item metadata |
| User-user CF | Recommend what similar users liked | Many ratings, manageable user count |
| Item-item CF | Recommend items rated like your likes | More items than users (e-commerce) |
| Cosine similarity | Angle between two vectors (0–1) | Comparing taste regardless of scale |
| Matrix factorization | Hidden user/item factors, dot product | Large, sparse rating matrices |
| precision@k | Fraction of top-k that were relevant | Judging a ranked recommendation list |
🎯 Mini-Challenge: A Tiny Content-Based Recommender
Time to fly with less scaffolding. The starter gives you a taste profile and three unwatched movies plus a comment outline — write the logic yourself. Reuse the cosine similarity you have built twice already.
# 🎯 MINI-CHALLENGE: a tiny content-based recommender
#
# You have one user profile and three unwatched movies (4 features each).
# 1. Write (or reuse) a cosine_similarity(a, b) function.
# 2. Score every candidate against the profile.
# 3. Print each movie with its score, sorted from best to worst.
# 4. Print the single top recommendation.
#
# ✅ Expected: "Dune" should win — it is the closest match to a sci-fi taste.
import math
profile = [0.9, 0.0, 1.0, 0.3] # [action, romance, sci_fi, drama]
candidates = {
"Dune": [0.8, 0.1, 1.0, 0.4],
"Pride & Joy": [0.0, 0.9, 0.0, 0.7],
"Gladiator": [0.9, 0.1, 0.2, 0.6],
}
# your code hereLesson complete — you can now build a recommender from scratch!
You can compute cosine similarity by hand, run user-user and item-item collaborative filtering, recommend from item features with content-based filtering, explain the intuition behind matrix factorization, and grade a ranked list with precision@k. You also know the four traps: cold start, popularity bias, treating sparse data as zeros, and leakage in evaluation.
Practice quiz
What does a recommender system do?
- Trains language models
- Compresses images
- Predicts which items a user will like and surfaces the best ones, usually as a ranked list
- Encrypts user data
Answer: Predicts which items a user will like and surfaces the best ones, usually as a ranked list. Netflix rows, Amazon's 'customers also bought', and Spotify's Discover Weekly are recommender systems.
How does content-based filtering make recommendations?
- By matching an item's own features to items you already liked
- By copying what similar users liked
- By picking random items
- By only recommending the most popular item
Answer: By matching an item's own features to items you already liked. Content-based uses item features (genre, tags), so it needs no other users and avoids cold start for new users.
How does collaborative filtering differ from content-based filtering?
- It uses item features only
- It never uses ratings
- They are identical
- It ignores item features and finds patterns across users' ratings
Answer: It ignores item features and finds patterns across users' ratings. Collaborative filtering borrows the verdict of similar users or items, not the item's own features.
What is user-user collaborative filtering?
- Recommend items rated alike
- Find people whose taste matches yours and recommend what those neighbours liked
- Recommend by item genre only
- Recommend the newest items
Answer: Find people whose taste matches yours and recommend what those neighbours liked. User-user CF finds similar users and borrows their ratings to predict yours.
Why is item-item CF often preferred in production?
- Items are more stable than users and their similarities can be precomputed, so it scales better
- Items change more than users
- It needs no ratings
- It ignores similarity
Answer: Items are more stable than users and their similarities can be precomputed, so it scales better. Amazon's 'because you watched X' uses item-item CF; precomputed item similarities scale well.
What does cosine similarity measure between two rating vectors?
- Their total length
- The number of items rated
- The angle (direction) between them, from 0 (unrelated) to 1 (identical direction)
- The average rating
Answer: The angle (direction) between them, from 0 (unrelated) to 1 (identical direction). Cosine = dot product / (product of lengths); it compares direction of taste, ignoring rating scale.
Why is cosine similarity preferred over raw distance for ratings?
- It is faster to compute only
- It compares the direction of taste, so a generous and a stingy rater can still match if they rank things alike
- It always returns 1
- It ignores the dot product
Answer: It compares the direction of taste, so a generous and a stingy rater can still match if they rank things alike. Cosine focuses on direction, not magnitude, so differing rating generosity does not break the match.
What does matrix factorization do?
- Deletes the rating matrix
- Counts shared ratings only
- Sorts users alphabetically
- Decomposes the user-item matrix into smaller latent-factor matrices whose dot product predicts missing ratings
Answer: Decomposes the user-item matrix into smaller latent-factor matrices whose dot product predicts missing ratings. It learns hidden user and item factors; multiplying them fills in the sparse matrix (won the Netflix Prize).
What is the cold-start problem?
- A model that runs too slowly
- The difficulty of recommending for a brand-new user or item with no interaction history
- Too many ratings to process
- A divide-by-zero error
Answer: The difficulty of recommending for a brand-new user or item with no interaction history. With no history, collaborative filtering has nothing to compare; fixes include content-based fallback or popular items.
What does precision@k measure for a recommender?
- The training time
- The total number of users
- Of the top k recommended items, the fraction the user actually found relevant
- The average rating error across all items
Answer: Of the top k recommended items, the fraction the user actually found relevant. precision@5 of 0.6 means 3 of the top 5 were good; ranking metrics matter more than overall RMSE.
Continue this course
- Previous: MLOps Fundamentals: Pipelines, CI/CD, Versioning
- Next: Graph Neural Networks (GNNs) for Social & Knowledge Graphs — Apply GNNs to social networks, knowledge graphs, and molecular data
- Quick reference: AI & Machine Learning cheat sheet
Frequently asked questions
What is a recommender system?
It is software that predicts which items a user will like and surfaces the best ones, usually as a ranked list. The Netflix home row, Amazon's 'customers also bought', and Spotify's Discover Weekly are all recommender systems. They learn from past behaviour — ratings, clicks, watches — to personalise what each person sees.
What is the difference between content-based and collaborative filtering?
Content-based filtering recommends items similar to ones you already liked, using the items' own features (genre, tags, text), so it never needs other users. Collaborative filtering ignores item features and instead finds patterns across users: 'people who rated things the way you do also liked X'. Content-based avoids the cold-start problem for new users but tends to recommend more of the same; collaborative filtering discovers surprising picks but struggles when data is sparse.
What is the difference between user-user and item-item collaborative filtering?
User-user CF finds people whose taste matches yours and recommends what those neighbours liked. Item-item CF instead measures which items are rated alike and recommends items similar to ones you already enjoyed — 'because you watched Inception'. Item-item is usually preferred in production because items are more stable than users and their similarities can be precomputed, which scales better.
What is cosine similarity and why use it for recommendations?
Cosine similarity measures the angle between two rating vectors: it is the dot product divided by the product of their lengths, giving a number from 0 (unrelated) to 1 (identical direction). It is popular for recommenders because it compares the direction of taste rather than its magnitude, so a generous rater who gives lots of 5s and a stingy rater who gives 3s can still count as similar if they rank things the same way.
What is matrix factorization in recommender systems?
Matrix factorization decomposes the big, mostly empty user-item rating matrix into two smaller matrices of hidden 'latent factors' — one describing each user, one describing each item. Multiplying a user's factor vector by an item's factor vector predicts the missing rating, which fills in the gaps. It powered the winning Netflix Prize entries and handles sparse data far better than counting shared ratings directly.
What is the cold-start problem?
Cold start is the difficulty of recommending for a brand-new user or a brand-new item that has no interaction history yet — collaborative filtering has nothing to compare. The usual fixes are to fall back on content-based filtering (which only needs item features), to recommend popular items at first, or to ask the user a few onboarding questions to seed a profile.
What is precision@k and how do you measure a recommender's quality?
precision@k looks only at the top k items you recommended and asks what fraction of them the user actually found relevant — precision@5 of 0.6 means 3 of your top 5 were good. Because users only ever see the top of the list, ranking metrics like precision@k, recall@k, MAP, and NDCG matter far more than overall rating error (RMSE), which judges predictions the user never sees.