Logo
Machine Learning (2026-2027) - Content-Based Recommendations

In the print dialog, choose "Save as PDF" as the destination.

Machine Learning, Week 9

Content-Based Recommendations

Describe every movie by its words, measure how alike two movies are with cosine similarity, and recommend the closest ones.

Objectives

  • Explain how a content-based recommender picks movies: similar content, recommended first
  • Turn text into count vectors with CountVectorizer, and say what stop_words and min_df do
  • Compute cosine similarity by hand: dot product, lengths, division
  • Build the similarity matrix with cosine_similarity and read one row of it
  • Trace get_recommendations line by line and explain where it goes wrong
  • Prepare your team's project presentation for this week's discussion

Week 9 of the plan

Where This Sits in the Course

  • Notebook: Movie Recommendations, a database of 4,803 movies with genres, keywords, cast and director
  • Real-world scenario: movie recommendations, a model that takes a movie title and returns similar movies
  • Project milestone: Project Teams Presentations and Discussions

Plan for the Two Hours

PartWhat we doTime
1The idea, and a worked example by hand35 min
2The notebook: vectors and the similarity matrix25 min
3get_recommendations and where it goes wrong20 min
4Project presentations and discussion10 min
5Practice with answers, then takeaways30 min

Part 1

The Idea

Similar content, similar taste

What Is a Content-Based Recommender?

Content-based recommendation
Recommend products, such as books and movies, based on other products that you like, by measuring how similar the products themselves are.

The Recipe in Four Steps

  1. Describe each movie as one text: its title, genres, keywords, cast and director
  2. Vectorize: count how many times each vocabulary word appears in that text
  3. Compare: compute the cosine similarity of every pair of movies
  4. Recommend: for a liked movie, return the movies with the highest similarity

From Text to Counts: CountVectorizer

python
texts = ['Action Adventure Fantasy Science Fiction',
         'Adventure Fantasy Action']
cv = CountVectorizer(stop_words='english')
m = cv.fit_transform(texts)
print(cv.get_feature_names_out())
print(m.toarray())
text
['action' 'adventure' 'fantasy' 'fiction' 'science']
[[1 1 1 1 1]
 [1 1 1 0 0]]
Output
  • fit builds the vocabulary: every distinct word, lowercased, sorted
  • transform gives each text one row of counts, one column per word
  • stop_words='english' drops very common words such as the and of
  • Word order is lost: only the counts remain

Cosine Similarity

cos(A, B) = A · B‖A‖   ‖B‖
A · B = a1 b1 + a2 b2 + ... + an bn ‖A‖ = √(a12 + a22 + ... + an2)
  • A and B are the count vectors of two movies, n is the number of vocabulary words
  • a_i and b_i are how many times word i appears in each movie
  • A · B is the dot product, ‖A‖ is the length of A

Reading a Cosine

  • 1: the two vectors point the same way, the same mix of words
  • 0: the two movies share no word at all
  • Counts are never negative, so for us the cosine is always between 0 and 1
  • Only the direction matters: doubling every count of a movie leaves its cosine unchanged

By hand

Worked Example: Five Movies

The first five rows of movies.csv, using only the genres column. A viewer liked Avatar: which movie do we recommend?

Moviegenres
AvatarAction Adventure Fantasy Science Fiction
Pirates of the Caribbean: At World's EndAdventure Fantasy Action
SpectreAction Adventure Crime
The Dark Knight RisesAction Crime Drama Thriller
John CarterAction Adventure Science Fiction

Step 1: The Vocabulary

text
['action' 'adventure' 'crime' 'drama' 'fantasy' 'fiction' 'science' 'thriller']
cv.get_feature_names_out()

Columns in vocabulary order

Step 1: The Count Vectors

action, adventure, crime, drama, fantasy, fiction, science, thriller

MovieCount vectorWords
Avatar[1 1 0 0 1 1 1 0]5
Pirates of the Caribbean[1 1 0 0 1 0 0 0]3
Spectre[1 1 1 0 0 0 0 0]3
The Dark Knight Rises[1 0 1 1 0 0 0 1]4
John Carter[1 1 0 0 0 1 1 0]4

Step 2: The Lengths

MovieSum of squaresLength ‖v‖
Avatar5√5 = 2.2361
Pirates of the Caribbean3√3 = 1.7321
Spectre3√3 = 1.7321
The Dark Knight Rises4√4 = 2.0000
John Carter4√4 = 2.0000

Step 3: Dot Products with Avatar

MovieShared wordsA · B
Pirates of the Caribbeanaction, adventure, fantasy3
Spectreaction, adventure2
The Dark Knight Risesaction1
John Carteraction, adventure, fiction, science4

A = Avatar, B = Pirates of the Caribbean

Step 4: One Cosine

cos(A, B) = 3√5 × √3 = 3√15 = 33.8730 = 0.7746

A = Avatar in every row

Step 4: The Other Three

Movie BComputationcos(A, B)
Spectre2 / (2.2361 × 1.7321) = 2 / 3.87300.5164
The Dark Knight Rises1 / (2.2361 × 2) = 1 / 4.47210.2236
John Carter4 / (2.2361 × 2) = 4 / 4.47210.8944

Step 5: Rank and Recommend

RankMoviecos(A, B)
1John Carter0.8944
2Pirates of the Caribbean0.7746
3Spectre0.5164
4The Dark Knight Rises0.2236

Check with scikit-learn

python
from sklearn.metrics.pairwise import cosine_similarity

cv = CountVectorizer(stop_words='english')
m = cv.fit_transform(df['genres'].head(5))
sim = cosine_similarity(m)
sim[0]
text
array([1.        , 0.77459667, 0.51639778,
       0.2236068 , 0.89442719])
Row 0 = Avatar, our hand results
Heatmap of the 5 by 5 cosine similarity matrix of the five genre vectors: 1 on the diagonal, 0.8944 for Avatar and John Carter, 0.2236 for Avatar and The Dark Knight Rises

Try It: Cosine Step by Step

Part 2

The Notebook on 4,803 Movies

Vectors and the similarity matrix

Open the Notebook in Colab

text
https://colab.research.google.com/github/
jeffprosise/Machine-Learning/blob/master/
Movie%20Recommendations.ipynb
Join the three lines into one address, or click "Open in Colab" on the lesson page.

Load the Data

python
import pandas as pd

url = ('https://raw.githubusercontent.com/'
       'jeffprosise/Machine-Learning/'
       'master/Data/movies.csv')
df = pd.read_csv(url)
df.shape
  • (4803, 24): 4,803 movies, 24 columns
  • Among them: title, genres, keywords, cast, director
  • Also budget, revenue, runtime, votes: not used by this model
  • The file is about 23 MB, so the cell takes a few seconds

Keep Five Columns, Fill the Gaps

python
df = df[['title', 'genres', 'keywords', 'cast', 'director']]
df = df.fillna('')  # Fill missing values with empty strings

One Text per Movie: the features Column

python
df['features'] = (df['title'] + ' ' + df['genres'] + ' ' +
                  df['keywords'] + ' ' + df['cast'] + ' ' +
                  df['director'])
  • Five columns glued into one string, with a space between them
  • Names become ordinary words: a director's first and last name are two words
  • Example: the title The Dark Knight Rises gives dark, knight, rises (the is a stop word)

Vectorize All 4,803 Movies

python
vectorizer = CountVectorizer(stop_words='english', min_df=20)
word_matrix = vectorizer.fit_transform(df['features'])
word_matrix.shape

Same features column, stop_words='english'

How min_df Shapes the Vocabulary

min_dfWords kept
117,286
53,852
20 (notebook)918
50271

The Similarity Matrix

python
from sklearn.metrics.pairwise import cosine_similarity

sim = cosine_similarity(word_matrix)
sim.shape
  • (4803, 4803): sim[i, j] is the cosine of movies i and j
  • Symmetric: sim[i, j] equals sim[j, i]
  • The diagonal is 1.0 for 4,794 movies; the other 9 have no word left, so their row is all 0

Real vectors from word_matrix

Worked Example: Skyfall and Spectre

Every count is 1 in both vectors

Words fromSkyfallSpectreShared
genres332: action, adventure
keywords563: spy, secret, agent
cast and director names766
Total151511

A = Skyfall, B = Spectre

Skyfall and Spectre: the Cosine

cos(A, B) = 11√15 × √15 = 1115 = 0.7333

Part 3

Generating Recommendations

get_recommendations, line by line

Find the Movie's Row

python
def get_recommendations(title, df, sim, count=10):
    # Get the row index of the specified title in the DataFrame
    index = df.index[df['title'].str.lower() == title.lower()]

    # Return an empty list if there is no entry for the specified title
    if (len(index) == 0):
        return []

    # Get the corresponding row in the similarity matrix
    similarities = list(enumerate(sim[index[0]]))
  • The title match ignores case: 'skyfall' works too
  • An unknown title returns [] instead of an error
  • enumerate pairs each score with its movie number; for Skyfall: (0, 0.1732), (1, 0.1333), (2, 0.7333), ...

Sort, Skip, Return Titles

python
    recommendations = sorted(similarities, key=lambda x: x[1], reverse=True)
    top_recs = recommendations[1:count + 1]
    titles = []
    for i in range(len(top_recs)):
        title = df.iloc[top_recs[i][0]]['title']
        titles.append(title)
    return titles

get_recommendations('Skyfall', df, sim)

Skyfall: the Ten Recommendations

Horizontal bars of the ten highest cosine similarities to Skyfall: Spectre 0.7333, Quantum of Solace 0.5729, Johnny English Reborn 0.4140, down to Sanctum and Blackthorn at 0.3651

A = Mulan (10 words), B = Shrek (15 words)

Why Does Mulan Get Shrek?

cos(A, B) = 3 + 2√10 × √15 = 512.2474 = 0.4082

Explore: Any Movie, Any Recommendation

Where It Goes Wrong

  • Empty vectors: 9 movies keep no word after min_df=20, so every score in their row is 0
  • Duplicate titles: two movies are called Batman; the function always uses the first one
  • Ties with itself: in 14 rows the movie is not entry 0, so [1:] can skip the wrong movie
  • Content only: the model never sees ratings, so it cannot tell a good movie from a bad one

One of the 9 empty vectors

Worked Example: Sharkskin

text
["Pirates of the Caribbean: At World's End", 'Spectre', 'The Dark Knight Rises']
Output

Part 4

Project Teams Presentations and Discussions

This week's project milestone

Your team project

Your Team Presentation

  1. The idea, or the research paper, and why it matters
  2. The data: source, exploration and cleaning
  3. The features and the target variable you chose
  4. The model you implemented and how you evaluated it
  5. What you will improve next

Before you practise

Common Mistakes

  • Reading Data/movies.csv in Colab, where the folder does not exist
  • Forgetting fillna(''), so missing text breaks CountVectorizer
  • Returning entry 0: the liked movie recommends itself
  • Adding shared words and forgetting to divide by the lengths
  • Trusting a list without its scores (empty vectors, ties, duplicates)
  • Expecting a rare title word to count when min_df has dropped it

Part 5

Practice: Your Turn

About 30 minutes, answers follow each task

About 10 minutes

Practice 1: A Sixth Movie

  • Row 11 of the file, Quantum of Solace, has genres Adventure Action Thriller Crime
  • Write its count vector over the same 8 words and compute its length
  • Compute its cosine with each of the five movies of the worked example
  • Which two movies would you recommend to a viewer who liked it?

Answers

Practice 1: Answer

  • A = Quantum of Solace = [1 1 1 0 0 0 0 1], ‖A‖ = √4 = 2
Movie BA · Bcos(A, B)
Spectre33 / (2 × 1.7321) = 0.8660
The Dark Knight Rises33 / (2 × 2) = 0.7500
Pirates of the Caribbean22 / (2 × 1.7321) = 0.5774
John Carter22 / (2 × 2) = 0.5000
Avatar22 / (2 × 2.2361) = 0.4472

About 5 minutes

Practice 2: Counts and Length

  • Three short texts: X = space war space alien, Y = space alien robot, and Z = X written twice
  • Use the vocabulary alien, robot, space, war and write X, Y and Z as count vectors
  • Compute cos(X, Y), cos(Z, Y) and cos(X, Z)
  • What does repeating a text do to its cosine?

Answers

Practice 2: Answer

  • X = [1, 0, 2, 1], Y = [1, 1, 1, 0], Z = [2, 0, 4, 2]
cos(X, Y) = 3√6 × √3 = 34.2426 = 0.7071
cos(Z, Y) = 6√24 × √3 = 68.4853 = 0.7071

About 10 minutes

Practice 3: Scores in Colab

  1. Run the notebook up to sim = cosine_similarity(word_matrix)
  2. Copy get_recommendations and make it return (title, score) pairs, with the score rounded to 4 places
  3. Print the top 5 for 'The Dark Knight Rises'
  4. Check the first score by hand from word_matrix: the dot product and the two sums of squares

Answers

Practice 3: Answer

RankMovieScore
1The Dark Knight0.7924
2Batman Begins0.7348
3Harsh Times0.4667
4The Killer Inside Me0.4648
5Amidst the Devil's Wings0.4619
cos(A, B) = 19√25 × √23 = 195 × 4.7958 = 0.7924

About 5 minutes

Practice 4: Change min_df

  • Shape (4803, 3852): many more words survive
  • Spectre is still first, but drops from 0.7333 to 0.5500: each vector now holds 20 words, still 11 shared
  • Among the newcomers: Casino Royale (0.3500) and Goldfinger (0.2635)
cos(A, B) = 11√20 × √20 = 1120 = 0.55

Key Takeaways

  1. A content-based recommender suggests items whose content is closest to one you liked
  2. CountVectorizer turns text into count vectors; stop_words and min_df decide which words count
  3. Cosine similarity is the dot product divided by the two lengths: between 0 and 1 for counts
  4. cosine_similarity gives the full matrix; one sorted row is a recommendation list
  5. Always look at the scores: empty vectors, ties and duplicate titles give misleading lists

Open this lesson

Mahmoud AbasContent-Based Recommendations