Build a Recommender: Collaborative Filtering, Matrix Factorization and the Metrics That Disagree
Hands-on lab · IDE in your browser

Build a Recommender: Collaborative Filtering, Matrix Factorization and the Metrics That Disagree

Build a movie recommender end to end and learn why the model with the best rating error is not the one you ship: a bias baseline, item-item collaborative filtering, matrix factorization by SGD, top-N recommendation with recall@k and precision@k, and the offline-metric trap where a plain popularity baseline beats your best model on the ranking the product actually shows.

Time
60 min
Checked steps
5
Level
Intermediate
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

The job

Lumen, a streaming service, wants to predict how a viewer would rate a title and turn those predictions into a short ranked list. You build the recommenders in order - a bias baseline, item-item collaborative filtering, matrix factorization - then measure them the way the product is measured and find that the model with the best rating error is not the one that ranks best.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    A rating baseline

    Lumen wants to predict how a viewer would rate a title they have not seen, then use those predictions to choose what to put in front of them.

  2. 2

    Item-item collaborative filtering

    The baseline gives every viewer the same ranking of titles, shifted by their offset.

  3. 3

    Recommend, then measure the ranking

    Predicting ratings is not the product.

  4. 4

    Matrix factorization

    CF looks up neighbours at prediction time.

  5. 5

    When offline metrics disagree

    You now have the model with the best rating error.

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: A rating baseline

Lumen wants to predict how a viewer would rate a title they have not seen, then use those predictions to choose what to put in front of them. Before any clever model, find the number a trivial model scores, so you know what "good" has to beat.

The data is already split: train.csv is what you learn from, test.csv is held out. Each row is a (user, item, rating) on a 1-to-5 scale.

Write two functions in recsys.py:

  • rmse(pred, actual): root mean squared error between two equal-length arrays. This is how every model in the lab is scored.
  • fit_bias(train, nu, ni): the baseline. Some viewers rate high, some low; some titles are loved, some not. Capture that with three numbers per prediction: the global mean mu, a per-user offset bu, and a per-item offset bi. Compute them in that order: mu first, then bu[u] as the mean of rating - mu over each user's ratings, then bi[i] as the mean of rating - mu - bu[u] over each item's ratings. Return (mu, bu, bi) with bu length nu and bi length ni; ids never seen stay 0.

predict_bias(model, pairs) is given. Run to see the baseline beat "predict the global mean for everyone".

recsys.py, the file you edit87 lines
"""Lumen streaming: a rating baseline, collaborative filtering, matrix factorization,
and the offline metrics that decide which recommender you would actually ship."""
import csv

import numpy as np


def load(path):
    """Read a ratings CSV into a list of (user, item, rating) triples."""
    out = []
    with open(path) as f:
        for row in csv.DictReader(f):
            out.append((int(row["user"]), int(row["item"]), float(row["rating"])))
    return out


def dims(train):
    """(n_users, n_items) large enough to index every id in train."""
    return max(u for u, _, _ in train) + 1, max(i for _, i, _ in train) + 1


def predict_bias(model, pairs):
    """Bias-model rating for each (user, item): the global mean plus the two offsets."""
    mu, bu, bi = model
    return np.array([mu + bu[u] + bi[i] for u, i in pairs])


def rmse(pred, actual):
    """Root mean squared error between two equal-length arrays of ratings."""
    # TODO (Step 1): root mean squared error between two equal-length arrays.
    raise NotImplementedError("Step 1: write rmse()")


def fit_bias(train, nu, ni):
    """Global mean, then a per-user offset, then a per-item offset on what is left.
    Returns (mu, bu, bi) with bu length nu and bi length ni (unseen ids stay 0)."""
    # TODO (Step 1): mu, then per-user offset over (rating-mu), then per-item offset over (rating-mu-bu[u]); return (mu, bu, bi).
    raise NotImplementedError("Step 1: write fit_bias()")


def item_similarity(train, model, nu, ni):
    """Cosine similarity between items over the bias residuals users left on them.
    Returns an (ni, ni) matrix with a zero diagonal (an item is no neighbour of itself)."""
    raise NotImplementedError("item_similarity() arrives in Step 2")


def predict_cf(train, model, S, pairs, k=20):
    """Predict a rating as the bias estimate plus a similarity-weighted average of the
    residuals the user left on their k most similar rated items."""
    raise NotImplementedError("predict_cf() arrives in Step 2")


def recommend(scores, seen, n=10):
    """The n highest-scoring item ids the user has not already rated, best first."""
    raise NotImplementedError("recommend() arrives in Step 3")


def recall_at_k(recommended, relevant):
    """Share of the relevant items that appear in the recommended list."""
    raise NotImplementedError("recall_at_k() arrives in Step 3")


def precision_at_k(recommended, relevant):
    """Share of the recommended items that are relevant."""
    raise NotImplementedError("precision_at_k() arrives in Step 3")


def fit_mf(train, nu, ni, k=8, epochs=40, lr=0.02, reg=0.05, seed=0):
    """Biased matrix factorization by SGD: r ~= mu + bu[u] + bi[i] + P[u].Q[i].
    Deterministic given seed. Returns (mu, bu, bi, P, Q)."""
    raise NotImplementedError("fit_mf() arrives in Step 4")


def predict_mf(model, pairs):
    """Rating for each (user, item) from a fitted factorization model."""
    raise NotImplementedError("predict_mf() arrives in Step 4")


def popularity(train, ni):
    """How many users rated each item: the popularity score, ignoring the ratings."""
    raise NotImplementedError("popularity() arrives in Step 5")


def ranking_recall(score_of, test, seen_by_user, n=10, min_rating=4.0):
    """Mean recall@n over the held-out items a user actually liked (rating >= min_rating).
    score_of(u) returns a score for every item; already-rated items are excluded first."""
    raise NotImplementedError("ranking_recall() arrives in Step 5")
Provided for you:catalog.csvtest.csvtrain.csvtry_it.py

Frequently asked questions

What is the difference between RMSE and recall@k for a recommender?

RMSE measures how close predicted ratings are to actual ones. Recall@k measures how many of the items a user liked appear in the top-k recommended list. A model can have the best RMSE and still rank worse than a trivial popularity baseline, so recommender teams evaluate the ranking itself.

What is matrix factorization in recommender systems?

Matrix factorization learns a short latent vector for every user and every item so that their dot product, plus per-user and per-item biases, reproduces the observed ratings. It is fitted by stochastic gradient descent and was the core of the Netflix-prize models.

Why is a popularity baseline so hard to beat?

Popular items are rated by many users, so they dominate the held-out test set and are correct recommendations for most people. Offline metrics that sample test items by activity flatter popularity, which is why teams also run online A/B tests.

Collaborative filtering and matrix factorization from scratch

A recommender is judged on the short ranked list it shows, and that list can disagree with the rating error. In this lab you build a bias baseline, item-item collaborative filtering and a matrix-factorization model by stochastic gradient descent, then score them with RMSE and with recall@k and precision@k. The result is the lesson every recommender team learns: matrix factorization wins on RMSE while a plain popularity baseline wins on recall@10, because the titles held out for testing are the popular ones. The metric you optimise is a product decision.