Content-Based Filtering
Recommend "more like this" by turning movie descriptions into TF-IDF vectors and ranking them with cosine similarity — a full search-style recommender in the browser.
Popularity charts treat everyone the same. Content-based filtering takes the
first step toward personalization: if you just watched Toy Story, recommend
movies that are like Toy Story. To do that we need two ingredients — a way
to represent each item as numbers, and a way to measure how close two items
are. This lesson builds both, then wires them into a working
get_recommendations() function.
From "more like this" to vectors
Content-based filtering compares item attributes: genres, keywords, cast, director, plot synopsis. For movies, the most flexible trick is to mash all of that text into a single string per movie — often called a metadata soup:
"animation family comedy toys jealousy a cowboy doll is threatened
when a new spaceman figure becomes the favorite toy"Once every movie is a bag of words, the problem becomes a document search problem: encode each soup as a vector, then find the vectors nearest to the one the user just watched. It's the same machinery behind a search engine — except the "query" is a movie instead of typed keywords.
Counting words — and why raw counts mislead
The simplest encoding is a word count: one dimension per vocabulary word, and
each movie's vector holds how often each word appears (scikit-learn's
CountVectorizer). The flaw: common words dominate. If half your catalog's
synopses contain "world" or "life", those words contribute big counts to many
pairs of movies — inflating similarity without carrying any real signal.
TF-IDF (term frequency × inverse document frequency) fixes this by
down-weighting words that appear in many documents and up-weighting rare,
distinctive ones. A word like "dinosaur" that appears in only two synopses
becomes a strong link between exactly those two movies; a word like "story"
that appears everywhere gets weight near zero. That's TfidfVectorizer.
Cosine similarity: direction, not length
How do we compare two vectors? Euclidean distance is tempting, but it punishes length — a long synopsis would look far from a short one even if they use identical vocabulary. Cosine similarity measures only the angle between vectors: 1 means "pointing the same way", 0 means "nothing in common". See it in two dimensions first:
Cosine gets it right: the two sci-fi synopses are a perfect 1.0 match because they point in the same direction, while Euclidean distance claims the romance synopsis is closer to the short sci-fi one — purely because the long synopsis has bigger numbers. For text, always think angles.
Building the recommender
Now the real thing: 15 movies, each with a genre + keyword + overview soup.
Pipeline: TfidfVectorizer → similarity matrix → look up a title → return the
five nearest neighbors (skipping the movie itself, which is always its own
best match).
The clusters fall out beautifully: Toy Story pulls the other animated family films, The Dark Knight finds Batman Begins through shared words like "gotham", "batman", and "superhero", and Titanic lands in the romance corner. Nobody told the model about genres as a concept — the vocabulary overlap alone encodes it.
CountVectorizer or TfidfVectorizer?
For free-text like synopses, TF-IDF is almost always better — it silences
filler words automatically. But for curated metadata (genre tags, cast names,
director), plain CountVectorizer is often the right call: every token was
chosen deliberately, and you may not want "Steven Spielberg" down-weighted
just because he directed many films. A common design: count-vectorize the
structured tags, TF-IDF the synopsis, and combine. You can also repeat
important tokens in the soup to weight them manually.
Strengths and the filter bubble
What content-based filtering gets right:
- No other users needed. It works from day one with a catalog of one user — similarity comes from item attributes, not crowd behavior. New items are no problem either: as soon as a movie has a synopsis, it can be recommended.
- It explains itself. "Recommended because you watched Toy Story — both are animated family films" is a legible, trust-building reason.
Its central weakness is over-specialization. The system can only recommend things similar to what you've already consumed. Watch three space movies and your homepage becomes an airlock — it will never discover that you'd also love jazz documentaries, because no vocabulary connects them. Users get trapped in a filter bubble of their own history. Breaking out requires information the item descriptions don't contain: what other people with tastes like yours enjoyed. That's collaborative filtering, next lesson.
Check your understanding
Q1.Why does TF-IDF usually beat raw word counts for comparing synopses?
Q2.Two synopses use the same words in the same proportions, but one is three times longer. What does cosine similarity report?
Q3.In get_recommendations, why do we drop the queried title before taking the top k?
Q4.Which problem can content-based filtering NOT solve, even in principle?
Exercise: More like A Quiet Place
Extend the 15-movie corpus with two movies of your own — one horror film
described with words overlapping A Quiet Place (monsters, silence,
survival…) and one family film. Rebuild the TF-IDF matrix and check
get_recommendations("A Quiet Place"): does your horror film appear in the
top 5? Which shared words do you think made the match?
Next up: collaborative filtering — dropping item descriptions entirely and learning taste from the ratings matrix itself.