Content-based recommendation shows the items — genre, director, keywords — and matches them to a profile. Collaborative filtering has none of that. It has one thing: a mat with users down the side, items across the top, and a rating in the cells that are filled.
The mat in the explorer is printed in full: sixteen people, fifteen titles, 138 training ratings, and 37 more held back so that each method is scored on data none of them fitted. Click any cell and four methods will predict it.
Two properties of that mat drive all.
It's mostly empty. Here about a quarter of the cells are missing. On a real catalogue it's 99.9% or worse, because no one has watched more than a vanishing fraction of a million titles. each method below is a way of guessing a missing entry from the present ones.
Nothing in it says what an item is. The words "sci-fi" and "romance" appear in the explorer for your gain. No method reads them. That the methods recover the genre groups anyway is the point of the field.
User-based neighbours
Find people who agreed with you in the past, and mean what they thought of the thing you haven't seen.
Select user-based kNN, click an empty cell, and the neighbour table appears with the math. Read the columns carefully, because one of them is where the method's real content lives.
Likeness is Pearson link over the items both people rated. It's a link, not an share count. So It asks whether two people's ratings move together rather than whether they're by number close.
Then the guess:
prediction = your average
+ sum over neighbours of sim * (their rating - their average)
divided by sum of |sim|
It means deviations, not ratings. That matters more than it looks. Someone who rates all 4 and someone who rates all 2 can agree just right about which titles are better. Centring each on their own mean is what lets the method notice that, and it's what stops a generous rater from dragging each guess upward.
Move the k slider. At k = 1 the guess is one person's view and the error is high. Raising k means more people and smooths, until eventually you are including neighbours who barely correlate and the extra noise costs more than the extra evidence buys.
Click a cell where no neighbour has rated the item and the method falls back to the mean, with a note. That isn't a bug in the setup — it's the sparsity problem, and on a real catalogue it's the common case rather than the exception.
Item-based neighbours
Switch to item-based kNN. Instead of "people like you also liked", it's "people who liked this also liked", and the guess mixes your own ratings of similar items.
Amazon's 2003 paper made this the industry default, for reasons that are hands-on rather than statistical. Item-item likenesses are far more stable than user-user ones — a film's audience changes slowly, a person's taste and rating history change constantly. So the likeness mat can be found offline overnight and served from a cache. And it shows itself: "because you watched X" is a sentence you can put in the interface.
On this mat the two methods score likewise. On a real system with many more users than items, item-based wins on operational grounds before fit is discussed.
Matrix factorisation
Both zone methods are local: they look at a handful of rows or columns. mat factorisation is global. It assumes the whole mat is about low rank:
rating(u, i) ~ mu + b_u + b_i + p_u . q_i
each user gets a vector p_u, each item a vector q_i, and the guess is their dot product plus three offsets. The vectors are learned by grad descent on the seen entries only. The missing ones aren't treated as zero, they're simply not in the sum.
Select it and look at the math line for your chosen cell. The three terms before the dot product are doing an huge amount of work: mu is the global mean, b_u is "this person rates generously", b_i is "this title is well liked". On real ratings data those three explain most of the variance before any latent factor is consulted. This Is why a bias-only model is a truly strong baseline and why leaving the biases out is a common way to build a disappointing recommender.
Then look at the item factor table. Sort it by any factor column and the three genres separate. No one supplied a genre. A factor is what direction best shows who rated what, and on this data that turns out to be genre — on a real catalogue the factors match to nothing nameable, which is the trade: fit for the ability to say why.
Now move the smoothing slider with 5 or more factors selected. At 0.02 the test RMSE is clearly worse than at 0.2. With 138 training ratings and eight factors for each of 31 users and items, the model carries nearly twice as many latent params as it has readings, and will fit the noise just unless a thing stops it. This is the clearest possible demo of why the Netflix Prize answers were as much about smoothing as about factorisation.
Reading the comparison
The bars at the bottom score each method on the same 37 held-out cells. Lower is better and the bars are drawn to scale. A longer bar is a worse method.
The sorting is the notable part. The global mean, at about 1.34, is the floor. The user's own mean is *worse* than that. This Is a real effect of a small sample: an mean over eight or nine ratings is a noisy guess, and a noisy personalised guess can be worse than an accurate impersonal one. Both zone methods beat both baselines clearly, landing near 0.9. mat factorisation, smoothed sensibly, goes lower again, to about 0.78.
Thirty-seven test cells is still a small sample and the caption says so — treat small differences as noise. The gap to the baselines isn't small.
One setup detail that is easy to omit and worth about a tenth of an RMSE point: clip the guess to the rating scale. A zone method extrapolates past the ends routinely. A user mean of 4.2 plus a plus deviation lands at 5.4 — and 5.4 isn't a rating. each guess on this page is clipped to [1, 5] before it's shown or scored.
import numpy as np
# ratings: [U, I]; mask: [U, I] with 1 where a rating is observed.
def sgd_factorise(ratings, mask, factors=8, steps=400, lr=0.02, reg=0.1, seed=0):
rng = np.random.default_rng(seed)
U, I = ratings.shape
P = rng.normal(0, 0.1, (U, factors))
Q = rng.normal(0, 0.1, (I, factors))
bu, bi = np.zeros(U), np.zeros(I)
mu = ratings[mask == 1].mean()
observed = list(zip(*np.nonzero(mask)))
for _ in range(steps):
for u, i in observed: # observed entries ONLY
pred = mu + bu[u] + bi[i] + P[u] @ Q[i]
err = ratings[u, i] - pred
bu[u] += lr * (err - reg * bu[u])
bi[i] += lr * (err - reg * bi[i])
pu = P[u].copy()
P[u] += lr * (err * Q[i] - reg * pu)
Q[i] += lr * (err * pu - reg * Q[i])
return P, Q, bu, bi, mu
The line to look at is pu = P[u].copy(). Update P[u] first and the Q[i] update uses the already-changed value. This Isn't the grad you derived. It often still trains, slightly worse. This Is what makes it hard to notice.
What this cannot do
Cold start. A new user has no ratings. So There are no neighbours and no learned vector. A new item has no one who has rated it. Collaborative filtering is silent on both, and each production system pairs it with a content-based fallback for just this reason.
Popularity bias. common items have more ratings. So They appear in more zones and get recommended more. This Earns them more ratings. The feedback loop is real, measurable, and the reason "range" and "serendipity" are metrics people really track.
The missing entries aren't missing at random. People rate what they chose to consume, and they chose it because they expected to like it. The seen ratings are a biased sample of all possible ratings, and each method on this page quietly assumes they aren't.
The next page takes the same mat and replaces the dot product with a neural network, which handles none of these three. But does open the door to the side info that fixes the first.
Questions people ask
Pearson or cosine likeness? Pearson centres each person on their own mean. It asks whether two people's ratings move together rather than whether they're by number close. Which is what you want when one rater is generous and another is harsh. On silent plus-only data there's no mean to centre against. Cosine is the standard choice instead.
User-based or item-based? Item-based, often, and on operational grounds before fit is discussed. Item-item likenesses are far more stable. A title's audience changes slowly where a person's history changes constantly. So the mat can be found offline and served from a cache, and "because you watched X" is a sentence that fits in the interface.
Why is the user's own mean worse than the global mean? Small-sample noise. An mean over eight or nine ratings is a poor guess of any of it. A noisy personalised guess truly can lose to an accurate impersonal one. It's worth remembering as the cheapest available proof that personalisation isn't automatically an improvement.
How many latent factors should I use? Fewer than you'd guess on small data. The smoothing matters more than the count. With 138 training ratings and eight factors across 31 users and items the model carries nearly twice as many latent params as it has readings. It will fit the noise just unless a thing stops it. Which is why the Netflix Prize answers were as much about smoothing as about factorisation.
Should I fill the missing entries with zeros? No. A missing rating isn't a rating of zero. Treating it as one tells the model that all unseen is terrible. The loss is summed over seen entries only. This Is what makes this the factorisation of a in part-seen mat rather than of a mostly-zero one.
Why clip the guesses? Because zone methods extrapolate past the ends of the scale routinely. A user mean of 4.2 plus a plus deviation lands at 5.4. 5.4 isn't a rating anyone can give. Clipping to the scale is one line and worth roughly a tenth of an RMSE point.
Recap in one screen
- The only input is who rated what. Nothing tells any method what an item *is*. That the factors recover the genre groups anyway is the whole point of the field.
- Zone methods mean deviations from each person's own mean. This Is what lets a generous rater and a harsh one be recognised as agreeing.
- Item-based is the industry default because item-item likeness is stable enough to precompute and explainable enough to show the user.
- Factorisation is global and low-rank. Its three offsets. Global, user, item. Explain most of the variance before a single latent factor is consulted.
- When latent params outnumber readings, smoothing is the difference between a model and a memoriser.
- Cold start, popularity bias, and ratings that aren't missing at random are the three problems none of this handles.