Index Ai Academy
Phase 3 · Machine learning · Models

Recommender systems

Shrunk averages, item-to-item collaborative filtering, matrix factorization, and cold start.

About 35 minutes · A free account opens the whole lesson

Create a free account Try part of it here

Already have an account? Sign in to open this lesson

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

  1. The ratings matrix
  2. Averages that know how much to trust themselves
  3. A shrunk average
  4. Collaborative filtering: items liked by the same people
  5. A flaw, and the fix
  6. Matrix factorization: tastes as vectors
  7. Find the bug: recommending what they own
  8. Testing a recommender, and what goes wrong in production
  9. What goes wrong in production
Quick check
The shop sorts gear by average rating. A hammock with a single 5-star rating now sits above a tent rated 5, 4, 5 and 4. What's the real problem?
  1. The hammock should be ranked by its price instead
  2. The tent's ratings must contain a fake review
  3. One rating is too little evidence to trust
  4. Averages should never be used for ratings
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:

blanktentstovelampmugfleecepoleshammock
ana5442
bo4545
chi254
dev5435
eve451
fay452

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.

Try it now

The same ratings, one row per rating. Group them by item, then switch between the mean and the count.

In your own words
Why can't a shop rank items by average rating alone?

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:

score=sum of ratings+m μn+m\text{score} = \frac{\text{sum of ratings} + m\,\mu}{n + m}

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}")
It runs right here in your browser, with nothing to install.

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.

Drill

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))
In the lesson, your code is checked as you write it.
Quick check
Item-to-item similarity starts with cosine similarity between two items' rating vectors. What does this print?
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))
  1. 0.5
  2. 40
  3. 1.0
  4. 0.976
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:

r^u,i=∑jsim(i,j) ru,j∑jsim(i,j)\hat r_{u,i} = \frac{\sum_j \text{sim}(i,j)\, r_{u,j}}{\sum_j \text{sim}(i,j)}

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.

Quick check
A customer rated the stove 4 and the mug 2. A new item has similarity 0.98 to the stove and 0.95 to the mug. What predicted rating does this print?
sims = [0.98, 0.95]
ratings = [4, 2]
print(round(sum(s * r for s, r in zip(sims, ratings)) / sum(sims), 2))
  1. 3.02
  2. 3.0
  3. 5.82
  4. 2.97
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.

Quick check
A reminder from the linear algebra phase. Cosine similarity compares two vectors by what?
  1. The straight-line distance between their tips
  2. Their angle, ignoring their lengths
  3. How many elements they share
  4. The sum of their elements
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.

In your own words
How does item-to-item collaborative filtering recommend?

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.

Quick check
After training, the error on the 21 known ratings is down to about 0.1 stars. What does that say about the predictions for the empty cells?
  1. Little: it may have memorized them
  2. They're all wrong, since the model overfit
  3. They'll match the shrunk averages exactly
  4. They'll be accurate to about 0.1 stars too
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.

Drill

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))
In the lesson, your code is checked as you write it.

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.

Quick check
Why test a shop's recommender by training on January to October and checking November, rather than on a random 10% of all ratings?
  1. It matches real use: predicting the future from the past
  2. November has more ratings than any other month
  3. Random splits take much longer to compute on big data
  4. A random split would leave the model too little data
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.

Quick check
The shop adds a new ultralight tent. Nobody has rated or bought it yet. What can an item-to-item recommender do?
  1. Use its category and description, or explore with it
  2. Give it the highest similarity to every other tent
  3. Copy the old tent's ratings onto it
  4. Wait: collaborative filtering handles new items itself
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.

In your own words
What is matrix factorization, and how do you test a recommender honestly?

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

Create a free account

Try a quiz question

Why does a shrunk (Bayesian) average help rank items?

  1. Thinly rated items stay near the overall mean
  2. It removes the highest and lowest ratings
  3. It gives the newest ratings more weight
  4. It rounds every average to a whole star

Answer it at the end of the lesson to see if you're right, and why.

Before this lesson