Collaborative Filtering

Click any cell of a ten-by-twelve ratings matrix and watch four methods predict it, with the neighbour arithmetic printed and every method scored on data none of them fitted.

Four methods, one matrix, the same held-out ratings

This explorer needs JavaScript: every shape, parameter count and curve on it is computed in the page rather than downloaded as an image.

Worth knowing

The only input is who rated what. No genres, no descriptions, no demographics — and the methods still find the genres.
Deviations are averaged, not ratings. Someone who rates everything 4 and someone who rates everything 2 can agree perfectly about order.
Matrix factorisation's two bias terms carry most of the signal in a real dataset before any latent factor is consulted.
Every method here is scored on the same held-out cells, so the comparison is real. The matrix is printed in full so you can check it.

Overview

Click any cell of a ten-by-twelve ratings matrix and watch four methods predict it, with the neighbour arithmetic printed and every method scored on data none of them fitted.

In this guide
  1. The only input is the matrix
  2. User-based neighbours
  3. Item-based neighbours
  4. Matrix factorisation
  5. Reading the comparison
  6. What this cannot do
  7. Questions people ask
  8. Recap in one screen

Collaborative Filtering

Predict what someone will think of a thing they've never seen, using nothing but who rated what.

The only input is the matrix

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.

Check yourself

0 of 4

Answer without scrolling back up.

  1. Why do neighbourhood methods average deviations from each person's mean rather than raw ratings?

  2. In matrix factorisation, what do mu, b_u and b_i capture before any latent factor is used?

  3. With 8 factors and 138 training ratings, low regularisation gives a much worse test RMSE. Why?

  4. Why is item-based kNN the industry default over user-based?

Cheat sheet

Collaborative Filtering

Click any cell of a ratings matrix and watch user-kNN, item-kNN and matrix factorisation predict it, all scored on the same held-out ratings.

DEEP LEARNING · vizlearn.in/deep_learning/collaborative_filtering.html

Further reading

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.