Recommender systems
Shrunk averages, item-to-item collaborative filtering, matrix factorization, and cold start.
Create a free account Try part of it here
Why it matters
"You might also like" rows, playlists, the order of search results and the next video are all recommender systems, and they drive a large share of what people buy and watch. They're also where machine learning meets its most lopsided data: millions of people, thousands of items, and almost nobody has rated almost anything. This lesson builds a recommender for the course's outdoor gear shop from first principles: averages that don't lie, similarity between items, and the factorization idea behind industrial systems.
What you'll be able to do
- Rank items fairly when some have only a few ratings, with a shrunk (Bayesian) average
- Recommend with item-to-item collaborative filtering: similar items, then a weighted prediction
- Explain matrix factorization: users and items as short vectors whose dot product predicts a rating
- Evaluate recommendations, and handle cold start and popularity feedback loops
What it covers
- The ratings matrix
- Averages that know how much to trust themselves
- A shrunk average
- Collaborative filtering: items liked by the same people
- A flaw, and the fix
- Matrix factorization: tastes as vectors
- Find the bug: recommending what they own
- Testing a recommender, and what goes wrong in production
- What goes wrong in production
Show the answer
One rating is too little evidence to trust
An average of one rating swings all the way to 1 or 5 by chance. The ranking needs a way to trust averages less when they rest on fewer ratings.
The ratings matrix
Picture a table with one row per customer and one column per product, holding each customer's rating where there is one. That's the ratings matrix, and it's mostly empty: in a real shop, well over 99% of the cells. Here's the course shop's, small enough to read:
| blank | tent | stove | lamp | mug | fleece | poles | hammock |
|---|---|---|---|---|---|---|---|
| ana | 5 | 4 | 4 | 2 | |||
| bo | 4 | 5 | 4 | 5 | |||
| chi | 2 | 5 | 4 | ||||
| dev | 5 | 4 | 3 | 5 | |||
| eve | 4 | 5 | 1 | ||||
| fay | 4 | 5 | 2 |
These are explicit ratings, stars someone chose to give. Most real systems lean on implicit signals instead: views, clicks, purchases, time spent. Those are plentiful but noisy, since a click isn't a like. Either way, the job is the same: fill in the empty cells well enough to put the right few items in front of each person.
The simplest recommender ignores who's asking and shows the most popular items. It's a surprisingly strong baseline, and every fancier system should be compared against it.
The same ratings, one row per rating. Group them by item, then switch between the mean and the count.
Write a sentence or two, then compare it with a good answer in the lesson.
Averages that know how much to trust themselves
A shrunk average (often called a Bayesian average) pretends every item starts with m imaginary ratings at the overall mean μ, then adds its real ones:
In plain words: every item starts at the shop's average; its own ratings pull it away as they pile up.
With few ratings, the score stays near the overall mean; as real ratings pile up, they take over. m says how many ratings it takes to be believed. Here the overall mean of the 21 ratings is about 3.9:
mu, m = 3.9, 3 hammock = [5] tent = [5, 4, 5, 4] for name, r in [("hammock", hammock), ("tent", tent)]: plain = sum(r) / len(r) shrunk = (sum(r) + m * mu) / (len(r) + m) print(f"{name}: plain {plain:.2f}, shrunk {shrunk:.2f}")
The tent now ranks above the hammock, and the hammock will rise on its own merits if more people love it. The same trick ranks sellers, restaurants and answers on Q&A sites.
A shrunk average
Write shrunk_average(ratings, prior_mean, m) that returns (sum(ratings) + m * prior_mean) / (len(ratings) + m), and returns prior_mean when there are no ratings.
def shrunk_average(ratings, prior_mean, m): return sum(ratings) / len(ratings) print(round(shrunk_average([5], 3.9, 3), 3))
import math tent = [5, 4] # ana's and dev's ratings of the tent stove = [4, 5] # ... and of the stove dot = sum(a * b for a, b in zip(tent, stove)) print(round(dot / (math.hypot(*tent) * math.hypot(*stove)), 3))
Show the answer
0.976
The dot product is 40 and each vector's length is √41, so the cosine is 40/41. Two people rated both highly, in nearly the same proportions, so the items look very similar.
Collaborative filtering: items liked by the same people
Collaborative filtering recommends from behavior alone, with no knowledge of what a tent or a stove is. The item-to-item version, the one large shops made famous, works in two steps.
First, for each pair of items, compare the ratings of the people who rated both. If ana and dev rated the tent and the stove alike, those two items are similar. Requiring at least two shared raters keeps a single coincidence from making two items look identical.
Second, to predict how a customer would rate an item they haven't seen, take the items they have rated, keep the k most similar to the new one, and average their ratings, weighted by similarity:
In plain words: predict from the person's ratings of similar items, trusting the most similar most.
Item-to-item suits shops: item similarities change slowly, so they can be computed overnight, and recommending becomes a quick lookup.
sims = [0.98, 0.95] ratings = [4, 2] print(round(sum(s * r for s, r in zip(sims, ratings)) / sum(sims), 2))
Show the answer
3.02
The weighted average leans slightly toward the more similar stove: 5.82 / 1.93 = 3.02. With nearly equal weights it lands near the plain average of 3.
A flaw, and the fix
Ratings are all positive, so raw cosine similarity between two items is nearly always close to 1: 0.98 for the tent and stove, 0.96 for the tent and lamp. Everything looks similar to everything, which flattens the predictions.
The standard fix is to center the ratings first: subtract each person's own average, so a rating says "more or less than this person usually gives". Centered ratings are positive and negative, and the cosine between items now spreads from -1 to 1. Two items the same people rate above their usual are similar; one loved and one disliked by the same people get a negative score and stop dragging each other's predictions.
Show the answer
Their angle, ignoring their lengths
Cosine is the dot product divided by both lengths, which leaves only the direction. That's why one person rating everything a little higher barely changes it, and why centering matters.
Write a sentence or two, then compare it with a good answer in the lesson.
Matrix factorization: tastes as vectors
The approach that won the famous Netflix Prize, and that sits under many large systems, gives every user and every item a short vector of k numbers, then predicts a rating as their dot product. Nobody chooses what the numbers mean; training finds them, and they tend to line up with real tastes, such as "likes ultralight gear" or "car camper", because those are what explain the ratings.
Training is gradient descent on the known ratings only: predict, measure the error, nudge both vectors to shrink it, with a small penalty that keeps the numbers from growing to fit noise.
import numpy as np users = ["ana", "bo", "chi", "dev", "eve", "fay"] items = ["tent", "stove", "lamp", "mug", "fleece", "poles", "hammock"] known = [(0, 0, 5), (0, 1, 4), (0, 2, 4), (0, 3, 2), (1, 0, 4), (1, 1, 5), (1, 5, 4), (1, 6, 5), (2, 2, 2), (2, 3, 5), (2, 4, 4), (3, 0, 5), (3, 1, 4), (3, 2, 3), (3, 5, 5), (4, 3, 4), (4, 4, 5), (4, 5, 1), (5, 0, 4), (5, 2, 5), (5, 4, 2)] rng = np.random.default_rng(0) U = rng.normal(0, 0.5, (len(users), 2)) V = rng.normal(0, 0.5, (len(items), 2)) mu = np.mean([r for _, _, r in known]) def rmse(): return np.sqrt(np.mean([(mu + U[u] @ V[i] - r) ** 2 for u, i, r in known])) print("before:", round(rmse(), 2)) for epoch in range(300): for u, i, r in known: err = r - (mu + U[u] @ V[i]) U[u], V[i] = U[u] + 0.05 * (err * V[i] - 0.02 * U[u]), V[i] + 0.05 * (err * U[u] - 0.02 * V[i]) print("after:", round(rmse(), 2)) print("ana and the poles:", round(float(np.clip(mu + U[0] @ V[5], 1, 5)), 1))
The error on known ratings falls, and every empty cell now has a prediction. A dot product can land outside the 1-to-5 scale, so predictions are clipped to it before they're shown.
Show the answer
Little: it may have memorized them
Error on the training ratings only shows the fit. With 26 learned numbers for 21 ratings, the model can nearly memorize them; hide some ratings to see how it does on new ones.
Find the bug: recommending what they own
recommend(predicted, owned, n) should return the n items with the highest predicted rating that the customer doesn't already have, best first. Customers keep being shown gear they bought. Fix it.
def recommend(predicted, owned, n): ranked = sorted(predicted, key=predicted.get, reverse=True) return ranked[:n] predicted = {"tent": 4.8, "poles": 4.5, "fleece": 2.9, "stove": 4.6} print(recommend(predicted, {"tent", "stove"}, 2))
Testing a recommender, and what goes wrong in production
To measure prediction, hide some known ratings, train on the rest, and compare: RMSE on the hidden ratings. But people see a short list, not a number, so ranking metrics matter more: of the top 5 you'd have shown, how many did the person go on to buy (precision at 5)? Split by time when you can, training on the past and testing on what happened next, because that's how the system will be used.
Show the answer
It matches real use: predicting the future from the past
A random split lets the model peek at later behavior, such as a December trend, while predicting earlier ratings. A time split tests what the live system will actually face.
What goes wrong in production
Three problems show up as soon as a recommender is live. Cold start: a new product has no ratings and a new visitor no history, so fall back on content (the product's category, price and description) or on popularity, and set aside a few slots to try new items. Popularity bias and feedback loops: what's shown gets clicked, and what gets clicked gets shown, so the same few items take over unless you measure diversity and explore deliberately. Business rules: don't recommend out-of-stock gear, the item someone bought yesterday, or the same thing in a different color twelve times.
Finally, settle it online, with an A/B test on purchases.
Show the answer
Use its category and description, or explore with it
With no ratings it has no similarities, which is the cold-start problem. Content features and deliberate exploration give it a first audience; behavior takes over once ratings arrive.
Write a sentence or two, then compare it with a good answer in the lesson.
Practice what you just read
- 2 code examples you can edit and run
- a practice exercise that checks your code
- a 5-question quiz
- 6 review cards for spaced repetition
Try a quiz question
Why does a shrunk (Bayesian) average help rank items?