Introduction: Why Similarity Matters in Recommendations

Welcome back! In the previous lesson, you learned how to represent both music tracks and user preferences as vectors, called embeddings. This lets us compare musical taste mathematically — the first step toward making personalized recommendations.

Now it’s time to use those vectors in action. In this lesson, you’ll learn how to:

  • Compute similarity between a user’s preferences and available tracks
  • Filter out already-listened songs
  • Rank and recommend the top matches

We'll also walk through a dedicated test file (test_recommend.py) that shows you exactly how the recommendation logic works.

Recap: Accessing User and Track Embeddings

Before we dive into similarity, let’s quickly remind ourselves how we get the vectors for users and tracks. You have already seen how to generate these embeddings in the previous lesson. Here’s a quick code snippet to show how you might access them:

from src.user_model import generate_user_profile_vector, get_track_embeddings

user_id = "user123"
user_profile_vector = generate_user_profile_vector(user_id)
all_track_ids, all_track_embeddings = get_track_embeddings()
  • generate_user_profile_vector(user_id) returns a vector representing the user's preferences.
  • get_track_embeddings() returns a list of all track IDs and their corresponding vectors.

These vectors are the building blocks for making recommendations.

Cosine Similarity Explained With Simple Example

Cosine similarity is a way to measure how similar two vectors are, regardless of their size. Imagine each vector as an arrow pointing in space. Cosine similarity looks at the angle between these arrows:

  • If the arrows point in the same direction, the similarity is 1 (very similar).
  • If they point in opposite directions, the similarity is -1 (very different).
  • If they are at 90 degrees, the similarity is 0 (not similar at all).

The cosine similarity between two vectors A and B is defined as:

cosine_similarity(A, B) = (A · B) / (||A|| * ||B||)

Where:

  • A⋅B is the dot product of the two vectors.
  • ||A|| and ||B|| are the L2 norms (lengths) of the vectors.

This formula computes the cosine of the angle between two vectors. The result ranges from -1 (opposite directions) to 1 (same direction).

In the context of music recommendations, if a user’s preference vector and a track’s vector point in the same direction, it means the user is likely to enjoy that track.

Here’s a simple example using NumPy:

import numpy as np
from sklearn.metrics.pairwise import cosine_similarity

# Example user and track vectors
user_vector = np.array([1, 0, 1])
track_vector = np.array([0.8, 0.1, 0.9])

# Reshape for sklearn's cosine_similarity
user_vector_2d = user_vector.reshape(1, -1)
track_vector_2d = track_vector.reshape(1, -1)

similarity = cosine_similarity(user_vector_2d, track_vector_2d)[0][0]
print(f"Cosine similarity: {similarity:.2f}")

Output:

Cosine similarity: 0.99

This high score means the user and track are very similar, so the track is a good recommendation.

Note: You might wonder: why not just use Euclidean distance or subtract one vector from another? The reason is that cosine similarity focuses on the angle, not the magnitude. Two vectors pointing in the same direction are considered similar—even if one is longer than the other. This works especially well for comparing preference patterns like musical taste, where it’s the direction (i.e. relative importance of features) that matters more than the absolute values.

Walking Through recommend_tracks_by_similarity

Let’s break down the main function in src/recommend.py that puts everything together to recommend tracks:

def recommend_tracks_by_similarity(user_id: str, top_n: int = 5):
    """
    Recommends tracks for a user based on cosine similarity.
    Excludes tracks already listened to by the user.
    """
    user_profile_vector = generate_user_profile_vector(user_id)

    if user_profile_vector is None:
        # No profile, could return popular tracks or handle as error
        return [] 

    all_track_ids, all_track_embeddings = get_track_embeddings()

    if not all_track_ids or all_track_embeddings.shape[0] == 0:
        return []  # No tracks to recommend from

    # Reshape user_profile_vector to be a 2D array for cosine_similarity
    user_profile_vector_2d = user_profile_vector.reshape(1, -1)

    # Compute cosine similarity: shape (1, num_all_tracks)
    similarity_scores = cosine_similarity(user_profile_vector_2d, all_track_embeddings)

    # Flatten to a 1D array of scores
    similarity_scores_1d = similarity_scores.flatten()

    # Create a DataFrame for easy sorting and filtering
    recommendations_df = pd.DataFrame({
        'track_id': all_track_ids,
        'similarity': similarity_scores_1d
    })

    # Get user's listening history to exclude played tracks
    user_history_df = get_user_listening_history(user_id)
    listened_track_ids = []
    if not user_history_df.empty:
        listened_track_ids = user_history_df['track_id'].unique().tolist()
        
    # Filter out listened tracks
    recommendations_df = recommendations_df[~recommendations_df['track_id'].isin(listened_track_ids)]

    # Sort by similarity in descending order
    recommendations_df = recommendations_df.sort_values(by='similarity', ascending=False)

    # Get top N track IDs
    top_n_recommendations = recommendations_df.head(top_n)['track_id'].tolist()

    return top_n_recommendations

Let’s go through each step:

  • Get the user’s profile vector:
    This represents the user’s music taste. If the user has never played any tracks, this function will return None, which we check for early to avoid running similarity on an empty profile.

  • Get all track embeddings:
    These are the vectors for every track in the system.

  • Compute cosine similarity:
    This calculates how similar the user is to each track.

  • Create a DataFrame:
    This makes it easy to sort and filter the results.

  • Exclude tracks the user has already listened to:
    We don’t want to recommend songs the user already knows.

  • Sort by similarity and select the top N:
    The most similar tracks are recommended first.

Example Output:

Suppose user "user123" has already listened to two tracks. When you run:

print(recommend_tracks_by_similarity('user123', top_n=2))

You might see:

['track_5', 'track_8']

This means the two most similar tracks (that the user hasn’t heard yet) are recommended.

To help you connect theory with practice, here’s a real output from test_recommend.py, which you'll see in the upcoming practices. In this test, the user "user123" listens to two tracks:

Logged listening sessions for tracks: ['track001', 'track002']

After embedding all 5 tracks and generating the user profile, the cosine similarity scores between the user's vector and each track look like this:

Similarity Scores:
Track track001: similarity = 0.6677
Track track002: similarity = 0.5796
Track track003: similarity = -0.1471
Track track004: similarity = 0.1196
Track track005: similarity = 0.3623

Although track001 and track002 have the highest scores, they are excluded because the user already listened to them.

The function then returns the top 3 most similar tracks the user hasn’t heard yet:

>>> recommend_tracks_by_similarity("user123", top_n=3)
['track005', 'track004', 'track003']

As you can see, even though track003 has a negative similarity, it still makes the top 3 because it's one of the few unseen tracks.

Summary And What’s Next

In this lesson, you learned how to use cosine similarity to compare user preferences with track features and recommend the best matches. You saw how the recommend_tracks_by_similarity function works step by step, from getting vectors to filtering and sorting recommendations.

Here’s a quick recap:

  • Cosine similarity measures how close two vectors are in direction.
  • We use it to find tracks that match a user’s taste.
  • The function filters out tracks the user already knows and sorts the rest by similarity.

Now, you are ready to practice these concepts yourself. In the next exercises, you’ll get hands-on experience using and modifying the recommendation function. This will help you build a deeper understanding of how embedding-based recommendations work in real applications. Good luck!

Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal