Skip to content
Machine Learning
Recommender Systems 10 min read

Collaborative Filtering

Learn taste from behavior alone — build the user-item ratings matrix, predict missing ratings with item-item similarity, and uncover latent factors with SVD.

Download notebook Open Google ColabIn Colab: File → Upload notebook → pick the downloaded file.

Content-based filtering needs good item descriptions. Collaborative filtering needs none — it learns purely from behavior: who rated what, and how. The core bet is that if you and I rated ten movies the same way, my opinion of an eleventh movie is useful evidence about yours. In this lesson you'll build the ratings matrix, predict missing entries with neighborhood methods, and then compress the whole thing into latent "taste" factors with SVD.

The user-item matrix — mostly holes

Everything in collaborative filtering starts from one object: a matrix with one row per user, one column per item, and ratings in the cells. Its defining property is sparsity — almost every cell is empty, because no one rates more than a sliver of the catalog. (Netflix-scale matrices are over 99% empty; recommending is filling in the blanks.)

Python — runs in your browser

Squint at the matrix and two taste groups jump out: Andi, Budi, and Eko love the action films; Citra, Dewi, and Fira love the romances. The NaNs are exactly the questions a recommender must answer — should Eko watch John Wick?

Neighborhood methods: user-based vs item-based

The classic ("memory-based") approach predicts a missing rating from similar rows or similar columns:

  • User-based CF — find users whose rating vectors resemble yours, and average their ratings of the target item. "People like you loved this."
  • Item-based CF — find items whose rating columns resemble the target item's, and average your ratings of those. "You loved similar movies."

Item-based usually wins in production, for a practical reason: item similarities are stable. A catalog has fewer items than users, items accumulate many ratings each, and their similarity profile barely changes day-to-day — so the item-item matrix can be precomputed offline. User tastes shift constantly and there are millions of users, making user-user similarity expensive and stale. (Amazon's original "customers who bought X also bought Y" was exactly precomputed item-based CF.)

Let's do item-based on our small matrix: compute cosine similarity between item columns, then predict Eko's missing ratings as similarity-weighted averages of the ratings he did give.

Python — runs in your browser

John Wick's nearest neighbors are the other action films — learned from ratings alone, with zero genre metadata. And the prediction for Eko says exactly what intuition does: recommend John Wick (he loved the similar action films), not The Notebook.

Matrix factorization: latent taste factors

Neighborhood methods compare raw rows and columns. Matrix factorization goes deeper: assume each user and each item can be described by a handful of hidden numbers — latent factors — such that a rating is roughly the dot product of the user's factor vector and the item's. With movies, factors often end up meaning things like "action vs romance" or "mainstream vs arthouse", even though nobody labeled them. Approximating our 6×6 matrix with just 2 factors:

Python — runs in your browser

Look at the item factor table: one factor loads on the action films, the other on the romances — SVD rediscovered genre from ratings alone. The reconstruction fills every blank with a consistent estimate, and it compresses 36 cells into 24 numbers; on real data, 100 factors can summarize millions of users. (Production systems use factorization variants that fit only the observed cells rather than treating blanks as zeros, plus regularization — the idea is the same.)

Explicit vs implicit feedback

Star ratings are explicit feedback: rare, but unambiguous. Most real signal is implicit: clicks, watch time, purchases, skips. Implicit data is abundant but one-sided — a click means interest, but no click doesn't mean dislike. Implicit-feedback models therefore predict confidence-weighted preference rather than a rating, and they power most modern recommenders.

Cold starts, hybrids, and how to evaluate

Collaborative filtering's Achilles heel is the cold start: a new user has an empty row (nothing to match on), and a new item has an empty column (nobody can "collaborate" it into recommendations — no matter how good it is). Note the symmetry with the previous lessons: popularity needs no user history, and content-based handles brand-new items. So production systems are hybrids that route between families:

recommend(user, movie):
    if not logged in or brand-new account:
        popularity / weighted-rating charts        # lesson 1
    else:
        candidates = top-30 most content-similar   # lesson 2
        drop what the user already watched
        score candidates with collaborative model  # this lesson
        return the top 10

How do you know any of it works? Offline, hold out ratings as a test set — hide 20% of each user's ratings, train on the rest, and check the held-out ones. Two styles of metric:

  • Rating accuracy — RMSE/MAE between predicted and true held-out ratings.
  • Ranking qualityprecision@k: of the top-k items you recommended, what fraction did the user actually like (e.g. rated 4+)? Ranking metrics match the product better: users see a top-10 list, not your rating estimate.

The final verdict, though, always comes from online A/B tests — does the new recommender actually increase watches, saves, or purchases?

Scaling up: MovieLens in Colab

Our 6×6 matrix fits on a slide; the classic benchmark is MovieLens 100k (100,000 ratings, 943 users, 1,682 movies). The same code scales straight up — run this in Colab:

import pandas as pd
from sklearn.decomposition import TruncatedSVD
 
# Download MovieLens 100k
!wget -q https://files.grouplens.org/datasets/movielens/ml-100k.zip
!unzip -q -o ml-100k.zip
 
ratings = pd.read_csv("ml-100k/u.data", sep="\t",
                      names=["user_id", "movie_id", "rating", "timestamp"])
movies = pd.read_csv("ml-100k/u.item", sep="|", encoding="latin-1",
                     usecols=[0, 1], names=["movie_id", "title"])
 
R = ratings.pivot_table(index="user_id", columns="movie_id", values="rating")
print(f"Matrix: {R.shape[0]} users x {R.shape[1]} movies, "
      f"sparsity = {R.isna().sum().sum() / R.size:.1%}")
 
# 50 latent factors
svd = TruncatedSVD(n_components=50, random_state=42)
user_factors = svd.fit_transform(R.fillna(0))
recon = pd.DataFrame(user_factors @ svd.components_,
                     index=R.index, columns=R.columns)
 
# Top-10 unseen movies for user 1
user = 1
unseen = R.columns[R.loc[user].isna()]
top10 = recon.loc[user, unseen].sort_values(ascending=False).head(10)
print(movies.set_index("movie_id").loc[top10.index, "title"].to_string())

For a proper project, add the evaluation loop: split ratings into train/test per user, fit on train, and report precision@10 against the held-out likes.

Check your understanding

5 questions · free
  1. Q1.What is the fundamental input difference between collaborative and content-based filtering?

  2. Q2.Why do production systems usually prefer item-based over user-based neighborhood CF?

  3. Q3.In matrix factorization, what does a 'latent factor' represent?

  4. Q4.A brand-new movie was just added to the catalog with zero ratings. Which system can recommend it immediately?

  5. Q5.You recommend 10 movies; the user ends up liking 4 of them. What is precision@10?

Exercise: Recommend for the romance crowd

Using the item-based predict() and recommend() functions from the lesson, generate recommendations for Citra and Fira. Are the predicted ratings for the action movies high or low — and why does that make sense given the item-item similarity table? Then check whether the SVD reconstruction agrees with the neighborhood method for the same missing cells.

That wraps the recommender-systems module — you now have all three classic families and the hybrid pattern that ties a real production system together.