It is past midnight and you have just finished watching RRR on a streaming app. You give it five stars, close the app, and go to sleep. The next morning, three of your friends open the same app, and each one sees a completely different row of recommended movies at the top of the screen: one gets war dramas, another gets Telugu action films, a third gets gentle family comedies. Nobody searched for anything. Nobody typed in a favourite genre. So how does the app know?
The honest answer is that, for a huge share of what gets recommended to you online, whether it is a movie on a streaming app, a song on JioSaavn, or a product on Flipkart, the system has almost no understanding of the thing itself. It has never "watched" RRR. It cannot tell an action scene from a love song. What it has instead is a giant spreadsheet of who rated what, and a simple, powerful idea: people who agreed with you before are likely to agree with you again. This idea has a name: collaborative filtering. It is one of the most widely used techniques in machine learning, and by the end of this chapter you will be able to compute a real recommendation by hand, and then in code.
Two Ways to Recommend: Content or Crowd
There are two fundamentally different strategies a recommendation system can use, and it helps to name both before going further.
The first strategy is content-based filtering. Here the system studies the item itself: its genre, its actors, its director, the words in its description, the audio features of a song. If you watched RRR, a content-based system notices that it is an action drama with historical themes starring N. T. Rama Rao Jr. and Ram Charan, and recommends other action dramas with similar tags. This works, but it has an obvious limit: the system can only recommend things that look like what you already liked. It would never suggest a quiet family comedy to someone who has only ever watched action films, even if that person would have loved it.
The second strategy is collaborative filtering, and it throws away the content entirely. It does not read a plot summary or check a genre tag. It only looks at the pattern of ratings: which users rated which items, and how highly. If your ratings look a lot like Aarav's ratings across a dozen movies, the system trusts that your taste in the thirteenth movie will also line up, even if nobody, human or machine, could explain in words what "your taste" actually is. That is the trick: collaborative filtering finds patterns in behaviour that would be very hard to write down as content rules, because two people can love the same films for completely different reasons.
Most large platforms do not pick one strategy and abandon the other. A hybrid recommender system runs both at once: content-based signals cover new items and new users who have no rating history yet, while collaborative filtering catches the patterns that a list of genre tags could never predict, and the two rankings get blended into a single row of suggestions. The rest of this chapter builds the collaborative half of that pair, in full detail.
User-Based and Item-Based Collaborative Filtering
Collaborative filtering itself splits into two approaches, depending on what gets compared.
User-based collaborative filtering compares people. To recommend a movie to you, it first finds other users whose past ratings look similar to yours, then looks at what those similar users liked that you have not seen yet. This is the approach the rest of this chapter works through by hand.
Item-based collaborative filtering compares items instead. Rather than asking which users are like this user, it asks which movies tend to get rated the same way by the same people as this movie. If almost everyone who rated Dangal highly also rated 3 Idiots highly, the two films get treated as similar, based purely on shared rating patterns, regardless of whether they share a director or a genre. Amazon engineers Greg Linden, Brent Smith, and Jeremy York described this approach in a widely cited 2003 paper published in IEEE Internet Computing, explaining how comparing items instead of comparing customers let Amazon generate recommendations across a catalogue of millions of products without recalculating similarity between every pair of customers each time someone visited the site. Item-based methods tend to scale better on very large platforms for exactly this reason: the list of items usually changes far more slowly than the list of users.
Measuring "Similar Taste": Cosine Similarity
To find users who are similar to you, "similar" has to become a number. The most common way to do this in collaborative filtering is cosine similarity.
Picture each user's ratings as a list of numbers, one number per movie both people have rated. Riya rated 3 Idiots, Dangal, and Zindagi Na Milegi Dobara as 5, 4, and 5. Written as a vector, that is (5, 4, 5). Cosine similarity treats a list like this as an arrow pointing out from the origin in a multi-dimensional space, one axis per movie, and measures the angle between two users' arrows. Two arrows pointing in almost exactly the same direction, meaning both users rate the same movies high and the same movies low, get a similarity close to 1. Two arrows pointing in very different directions get a similarity close to 0.
The formula looks like this:
cosine_similarity(A, B) = (A · B) / (||A|| × ||B||)
Here, A · B is the dot product: multiply each pair of matching ratings and add up the results. ||A|| is the magnitude, or length, of vector A: square every rating, add the squares, and take the square root. Dividing by both magnitudes scales the result so it always falls between 0 and 1 for positive ratings, no matter how generous or harsh a rater someone is on average. Only the pattern of high and low ratings matters, not their absolute size.
Worked Example: Will Riya Like RRR?
Riya has watched and rated three movies. Her three friends, Aarav, Kabir, and Diya, have watched those same three movies plus one more: RRR, which Riya has not seen yet. Here are everyone's ratings out of 5:
- Riya: 3 Idiots = 5, Dangal = 4, Zindagi Na Milegi Dobara = 5, RRR = not watched yet
- Aarav: 3 Idiots = 5, Dangal = 4, Zindagi Na Milegi Dobara = 5, RRR = 5
- Kabir: 3 Idiots = 4, Dangal = 5, Zindagi Na Milegi Dobara = 3, RRR = 3
- Diya: 3 Idiots = 1, Dangal = 2, Zindagi Na Milegi Dobara = 1, RRR = 1
The question: based only on these numbers, what rating would Riya most likely give RRR?
Step 1: Compute similarity using only commonly rated movies. Riya has rated three movies, 3 Idiots, Dangal, and Zindagi Na Milegi Dobara (ZNMD for short), and those three are the only ones she shares with every friend, so similarity is computed only over those three, ignoring RRR for now since Riya has not rated it.
Riya's vector is (5, 4, 5), so her magnitude is:
||Riya|| = sqrt(5² + 4² + 5²) = sqrt(25 + 16 + 25) = sqrt(66) ≈ 8.124
Riya and Aarav. Aarav's vector on the same three movies is (5, 4, 5), identical to Riya's.
dot product = (5×5) + (4×4) + (5×5) = 25 + 16 + 25 = 66
||Aarav|| = sqrt(66) ≈ 8.124
similarity = 66 / (8.124 × 8.124) = 66 / 66 = 1.000
A perfect match. Riya and Aarav rated all three shared movies exactly the same way, so their arrows point in exactly the same direction.
Riya and Kabir. Kabir's vector on the same three movies is (4, 5, 3).
dot product = (5×4) + (4×5) + (5×3) = 20 + 20 + 15 = 55
||Kabir|| = sqrt(4² + 5² + 3²) = sqrt(16 + 25 + 9) = sqrt(50) ≈ 7.071
similarity = 55 / (8.124 × 7.071) = 55 / 57.446 ≈ 0.957
Riya and Diya. Diya's vector on the same three movies is (1, 2, 1).
dot product = (5×1) + (4×2) + (5×1) = 5 + 8 + 5 = 18
||Diya|| = sqrt(1² + 2² + 1²) = sqrt(1 + 4 + 1) = sqrt(6) ≈ 2.449
similarity = 18 / (8.124 × 2.449) = 18 / 19.900 ≈ 0.905
Ranking the three: Aarav at 1.000, Kabir at 0.957, Diya at 0.905. All three come out fairly high, since every rating here is a positive number, which pushes cosine similarity toward the high end of its range regardless of how different two people's taste really is. What matters for the recommendation is still the order: Aarav is clearly the closest match, Diya the furthest.
Step 2: Pick the nearest neighbours. Real systems do not average in every other user's opinion, since most of them are near-strangers in taste. Instead they pick the k users with the highest similarity score, called the k-nearest neighbours, and listen only to them. With k = 2, Riya's neighbours are Aarav and Kabir. Diya, the least similar, is dropped.
Step 3: Predict the rating as a weighted average. Both neighbours rated RRR: Aarav gave it 5, Kabir gave it 3. Their ratings get combined, weighted by how similar each neighbour is to Riya:
predicted rating = (sim(Riya, Aarav) × 5 + sim(Riya, Kabir) × 3)
/ (sim(Riya, Aarav) + sim(Riya, Kabir))
= (1.000 × 5 + 0.957 × 3) / (1.000 + 0.957)
= (5.000 + 2.871) / 1.957
= 7.871 / 1.957
≈ 4.02
Riya's predicted rating for RRR is about 4.02 out of 5, closer to Aarav's 5 than to Kabir's 3, because Aarav's taste matches hers more closely. A plain, unweighted average of the same two friends' RRR ratings would give (5 + 3) / 2 = 4.00, close but not identical. Throw Diya's rating of 1 into an unweighted average of all three friends and the result drops to (5 + 3 + 1) / 3 = 3.00, a full point lower. The weighting is what lets the system trust the friend who has agreed with Riya before more than the friend who almost never has.
Tracing It in Code
The same three steps, similarity, neighbour selection, and weighted prediction, turn into a short Python program. This one reproduces the exact numbers just calculated by hand:
import math
# 1-5 star ratings. A movie missing from a user's dict means "not watched yet".
ratings = {
"Riya": {"3 Idiots": 5, "Dangal": 4, "ZNMD": 5},
"Aarav": {"3 Idiots": 5, "Dangal": 4, "ZNMD": 5, "RRR": 5},
"Kabir": {"3 Idiots": 4, "Dangal": 5, "ZNMD": 3, "RRR": 3},
"Diya": {"3 Idiots": 1, "Dangal": 2, "ZNMD": 1, "RRR": 1},
}
def cosine_similarity(user_a, user_b):
common = set(ratings[user_a]) & set(ratings[user_b])
if not common:
return 0.0
dot = sum(ratings[user_a][m] * ratings[user_b][m] for m in common)
mag_a = math.sqrt(sum(ratings[user_a][m] ** 2 for m in common))
mag_b = math.sqrt(sum(ratings[user_b][m] ** 2 for m in common))
return dot / (mag_a * mag_b)
def predict_rating(target_user, target_movie, k=2):
others = [u for u in ratings if u != target_user and target_movie in ratings[u]]
sims = {u: cosine_similarity(target_user, u) for u in others}
neighbours = sorted(sims, key=sims.get, reverse=True)[:k]
numerator = sum(sims[u] * ratings[u][target_movie] for u in neighbours)
denominator = sum(sims[u] for u in neighbours)
return numerator / denominator, sims
predicted, sims = predict_rating("Riya", "RRR", k=2)
for user, score in sims.items():
print(f"similarity(Riya, {user}) = {score:.3f}")
print(f"Predicted rating for RRR: {predicted:.2f} / 5")
Running this prints:
similarity(Riya, Aarav) = 1.000
similarity(Riya, Kabir) = 0.957
similarity(Riya, Diya) = 0.905
Predicted rating for RRR: 4.02 / 5
Two lines carry the whole idea. Inside cosine_similarity, the common set restricts both the dot product and the magnitude calculations to movies both users have actually rated, so someone who has watched fifty movies is not unfairly penalised for having a "longer" vector than someone who has watched three. Inside predict_rating, sorted(sims, key=sims.get, reverse=True)[:k] performs the neighbour selection from Step 2, picking the k users with the highest similarity score before a single rating gets averaged. Change k=2 to k=3 and Diya's opinion joins the average too, pulling the predicted rating down from 4.02 toward the 3.07 that including all three friends would give. The choice of k matters: it decides how many opinions the system trusts before making a call.
Why Real Systems Go Further
The hand-worked example above is the real core of collaborative filtering, but production systems, the ones actually running behind Flipkart, JioSaavn, or a streaming app's homepage, add a few refinements to handle problems that a four-user example is too small to show.
The first problem is rating bias. Some people rate almost everything a 4 or 5; others are stingy and rarely go above a 3, even for movies they loved. Plain cosine similarity, as computed above, can be misled by this: a generous rater and a harsh rater who actually agree on which movies are better than which other movies can still end up looking less similar than they really are. The fix is mean-centering: before comparing two users, subtract each user's own average rating from all of their ratings first, so the comparison uses how far above or below a person's own average each rating falls. Cosine similarity computed on these adjusted ratings is often called adjusted cosine similarity, and it is closely related to another classic measure, the Pearson correlation coefficient, which the original GroupLens system used in 1994 to find similar readers of Usenet newsgroups.
The second problem is the cold-start problem: what does a system recommend to a brand new user who has not rated anything yet, and how does a brand new movie ever get recommended when nobody has rated it? Collaborative filtering, on its own, has no answer, since it depends entirely on existing ratings. Real platforms typically patch this by falling back on content-based signals, genre, popularity, cast, for new users and new items until enough ratings accumulate, then letting collaborative filtering take over.
The third problem is sparsity: in a real ratings matrix, the overwhelming majority of user-item pairs have no rating at all, because no single user has watched more than a tiny slice of the catalogue. Riya, Aarav, Kabir, and Diya rated almost every movie in this small example, but a real platform might have tens of millions of users and hundreds of thousands of titles. Most pairs of users share almost no commonly rated items, which makes similarity scores noisy and expensive to compute for every pair. This is exactly the scaling problem that pushed Amazon toward item-based comparisons instead of user-based ones: the catalogue of items changes slowly enough that item-item similarity scores can be precomputed and reused, while a user-based system would have to reconsider its neighbour list constantly as millions of people rate new things every day.
A fourth, quieter problem is that most users never rate anything at all. The five-star ratings used throughout this chapter are explicit feedback, and explicit feedback is rare: most people watch, skip, or buy without ever leaving a score behind. Real systems lean heavily on implicit feedback instead, signals a user never intended as a rating but that reveal preference anyway: what got clicked, how long a video played before it was abandoned, what got added to a cart, what actually got bought. A movie watched to the end counts as a strong positive signal even without a single star attached to it, and a movie abandoned after two minutes counts as a negative one. The similarity math in this chapter does not change when the input changes from stars to clicks; only the numbers going into each vector do.
How much this field matters in practice is easy to underestimate. In October 2006, Netflix launched the Netflix Prize, offering $1,000,000 to any team that could beat its existing recommendation algorithm, Cinematch, by at least 10 percent, measured using root mean squared error on predicted ratings. It took nearly three years of open competition before a team called BellKor's Pragmatic Chaos crossed that bar and claimed the prize in 2009. The techniques that came out of that competition, many of them extensions of the same similarity-and-neighbour idea worked through by hand above, still shape how recommendation systems are built today.
Back to Riya's Screen
Riya never told the app that she likes Aamir Khan films, or that she prefers an emotional story over pure action. The system does not know what RRR is about, and it does not need to. It only needed to know that Aarav's taste has lined up with Riya's before, that Kabir's has lined up a little less closely, and that Diya's has barely lined up at all, and from those three numbers alone it produced a specific, personalised guess: about 4 out of 5. Multiply this same arithmetic across millions of users and hundreds of thousands of titles, add the refinements from the last section to handle new users and new items, and the result is the engine behind the "Recommended for you" row on nearly every app on your phone. The math has not changed since the worked example: vectors, a dot product, a square root, a weighted average. What changes at scale is only the size of the spreadsheet.
The same arrow-and-angle idea used here to compare two people's taste in movies reappears later, in a different costume, to compare the meaning of two sentences once each one has been turned into a vector of its own. A search engine deciding whether "cheap phone under 15000" and "budget smartphone below fifteen thousand rupees" mean roughly the same thing runs the same cosine similarity computed by hand in this chapter, just on vectors built from words instead of vectors built from star ratings. Riya's four-user movie table was small enough to trace on paper; the underlying arithmetic is exactly what scales up to run the rest of the internet.
Think About It
Think about this: How would you explain collaborative filtering: learn from others to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.