Recommendation Engine
Build collaborative filtering from scratch — the algorithm behind 'customers also bought' and your Netflix row — using cosine similarity to find your taste-twins and recommend what they loved that you haven't seen.
How it works in the real world
Collaborative filtering makes a bet: people who agreed in the past will agree again. No genres, no tags, just ratings.
- Represent — every user is a sparse vector of item ratings.
- Compare — cosine similarity measures the angle between two users, so it captures taste rather than how generously someone rates.
- Neighbour — rank everyone by similarity to the target user.
- Recommend — score unseen items by each neighbour's rating weighted by similarity.
This is the algorithm that won early recommender competitions, and it still underpins production systems. You'll implement it with nothing but math.
Explore the ratings matrix
Print how many users and distinct items exist, then one line per user (alphabetical) with how many items they rated and their average to two decimals.
items |= set(seen) unions the keys; sum(seen.values()) / len(seen) is the average.
ratings = {
"ana": {"dune": 5, "arrival": 4, "matrix": 5, "her": 2},
"bo": {"dune": 4, "arrival": 5, "matrix": 4},
"cleo": {"her": 5, "amelie": 5, "arrival": 3},
"dev": {"dune": 5, "matrix": 4, "amelie": 1},
}
items = set()
for user, seen in ratings.items():
items |= set(seen)
print(f"users: {len(ratings)} items: {len(items)}")
for user in sorted(ratings):
seen = ratings[user]
avg = sum(seen.values()) / len(seen)
print(f"{user:<5} rated {len(seen)} avg {avg:.2f}")
Run your code to check this step…
Cosine similarity
Write cosine(a, b): dot product over shared items, divided by the product of both vectors' magnitudes. Print ana's similarity to each other user, 3 decimals.
shared = set(a) & set(b); dot = sum(a[i] * b[i] for i in shared); divide by sqrt of each vector's sum of squares.
ratings = {
"ana": {"dune": 5, "arrival": 4, "matrix": 5, "her": 2},
"bo": {"dune": 4, "arrival": 5, "matrix": 4},
"cleo": {"her": 5, "amelie": 5, "arrival": 3},
"dev": {"dune": 5, "matrix": 4, "amelie": 1},
}
import math
def cosine(a, b):
shared = set(a) & set(b)
if not shared:
return 0.0
dot = sum(a[i] * b[i] for i in shared)
na = math.sqrt(sum(v * v for v in a.values()))
nb = math.sqrt(sum(v * v for v in b.values()))
return dot / (na * nb)
for other in ("bo", "cleo", "dev"):
print(f"ana vs {other:<5} {cosine(ratings['ana'], ratings[other]):.3f}")
Run your code to check this step…
Rank the neighbours
Sort every other user by similarity to ana, most similar first, breaking ties by name. Print the ranked list.
Build (score, user) pairs then sort with key=lambda pair: (-pair[0], pair[1]).
ratings = {
"ana": {"dune": 5, "arrival": 4, "matrix": 5, "her": 2},
"bo": {"dune": 4, "arrival": 5, "matrix": 4},
"cleo": {"her": 5, "amelie": 5, "arrival": 3},
"dev": {"dune": 5, "matrix": 4, "amelie": 1},
}
import math
def cosine(a, b):
shared = set(a) & set(b)
if not shared:
return 0.0
dot = sum(a[i] * b[i] for i in shared)
na = math.sqrt(sum(v * v for v in a.values()))
nb = math.sqrt(sum(v * v for v in b.values()))
return dot / (na * nb)
target = "ana"
neighbours = [(cosine(ratings[target], ratings[u]), u)
for u in ratings if u != target]
neighbours.sort(key=lambda pair: (-pair[0], pair[1]))
print(f"nearest to {target}:")
for score, user in neighbours:
print(f" {user:<5} {score:.3f}")
Run your code to check this step…
Recommend what to watch
For every item ana hasn't rated, sum similarity * neighbour_rating across all positively-similar users. Print the recommendations, best first, 3 decimals.
Skip items already in `seen`; scores[item] = scores.get(item, 0.0) + sim * rating.
ratings = {
"ana": {"dune": 5, "arrival": 4, "matrix": 5, "her": 2},
"bo": {"dune": 4, "arrival": 5, "matrix": 4},
"cleo": {"her": 5, "amelie": 5, "arrival": 3},
"dev": {"dune": 5, "matrix": 4, "amelie": 1},
}
import math
def cosine(a, b):
shared = set(a) & set(b)
if not shared:
return 0.0
dot = sum(a[i] * b[i] for i in shared)
na = math.sqrt(sum(v * v for v in a.values()))
nb = math.sqrt(sum(v * v for v in b.values()))
return dot / (na * nb)
target = "ana"
seen = ratings[target]
scores = {}
for user, their in ratings.items():
if user == target:
continue
sim = cosine(seen, their)
if sim <= 0:
continue
for item, rating in their.items():
if item in seen:
continue
scores[item] = scores.get(item, 0.0) + sim * rating
print(f"recommendations for {target}:")
for item, score in sorted(scores.items(), key=lambda kv: (-kv[1], kv[0])):
print(f" {item:<8} {score:.3f}")
Run your code to check this step…
Project shipped!
You just built a complete, working recommendation engine — the same architecture running in production software. Pick your next build →