Advanced Content Recommendations

Introduction to More Complex Content-Based Recommendations

In previous lessons, you learned about content-based recommendation systems and how they rely on user and item profiles. We covered how to extract content features such as likes, clicks, and genres, and how to compute similarities using straightforward methods like the dot product. This lesson will build on those foundations to guide you through a more complex example, using more advanced similarity calculations to generate recommendations.

We'll explore how to simulate user preferences, calculate genre similarities using cosine similarity, and score tracks based on how well they match a user's tastes. This will give you a glimpse into the practical applications of these systems in real-world scenarios, such as music streaming services. Let's dive into this sophisticated example step by step.

Recap of Initial Setup

As a reminder from our previous lessons, let's quickly revisit how to load and merge datasets in Go. We use Go's encoding/json package to read JSON files and unmarshal their contents into slices of structs. Then, we merge the track and author data using maps and new structs.

Here's a code block demonstrating this process:

package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "os"
)

type Track struct {
    TrackID      string `json:"track_id"`
    Title        string `json:"title"`
    Likes        int    `json:"likes"`
    Clicks       int    `json:"clicks"`
    FullListens  int    `json:"full_listens"`
    AuthorID     string `json:"author_id"`
}

type Author struct {
    AuthorID        string `json:"author_id"`
    Name            string `json:"name"`
    AuthorListeners int    `json:"author_listeners"`
    Genre           string `json:"genre"`
}

type MergedTrack struct {
    TrackID         string
    Title           string
    Likes           int
    Clicks          int
    FullListens     int
    AuthorID        string
    AuthorName      string
    AuthorListeners int
    Genre           string
}

func main() {
    // Read tracks.json
    tracksFile, err := os.Open("tracks.json")
    if err != nil {
        log.Fatal(err)
    }
    defer tracksFile.Close()
    tracksBytes, err := io.ReadAll(tracksFile)
    if err != nil {
        log.Fatal(err)
    }

    var tracks []Track
    if err := json.Unmarshal(tracksBytes, &tracks); err != nil {
        log.Fatal(err)
    }

    // Read authors.json
    authorsFile, err := os.Open("authors.json")
    if err != nil {
        log.Fatal(err)
    }
    defer authorsFile.Close()
    authorsBytes, err := io.ReadAll(authorsFile)
    if err != nil {
        log.Fatal(err)
    }

    var authors []Author
    if err := json.Unmarshal(authorsBytes, &authors); err != nil {
        log.Fatal(err)
    }

    // Build a map from author_id to Author
    authorMap := make(map[string]Author)
    for _, author := range authors {
        authorMap[author.AuthorID] = author
    }

    // Merge tracks with their corresponding author information
    var mergedTracks []MergedTrack
    for _, track := range tracks {
        author, ok := authorMap[track.AuthorID]
        if ok {
            merged := MergedTrack{
                TrackID:         track.TrackID,
                Title:           track.Title,
                Likes:           track.Likes,
                Clicks:          track.Clicks,
                FullListens:     track.FullListens,
                AuthorID:        track.AuthorID,
                AuthorName:      author.Name,
                AuthorListeners: author.AuthorListeners,
                Genre:           author.Genre,
            }
            mergedTracks = append(mergedTracks, merged)
        }
    }

    // Print merged tracks
    for _, mt := range mergedTracks {
        fmt.Printf("%+v\n", mt)
    }
}

By executing this code, you create a unified view of your music tracks, integrating both track details and author information, which will serve as a foundation for your recommendation system.

Simulating User Preferences

To offer personalized recommendations, we need to simulate user preferences. In Go, we can represent a user's genre preferences and listening behavior using a struct or a map.

Here's an example using a struct:

type UserProfile struct {
    RockPreference int // On a scale of 1-5
    PopPreference  int // On a scale of 1-5
    JazzPreference int // On a scale of 1-5
    Listens        int // Total listens
    Likes          int // Total likes
}

func main() {
    // ... (previous code for loading and merging data)

    // Simulate user listening history or preferences
    user := UserProfile{
        RockPreference: 5,
        PopPreference:  4,
        JazzPreference: 2,
        Listens:        50,
        Likes:          30,
    }

    fmt.Printf("User profile: %+v\n", user)
}

This user profile indicates that our hypothetical user enjoys rock the most, followed by pop, and has a moderate affinity for jazz. This profile will be used to tailor recommendations to their tastes.

Calculating Genre Similarities: Part 1

Next, let's map music genres into numerical vectors and compute genre similarities. In Go, we can use slices or arrays to represent one-hot encoded genre vectors.

A one-hot encoding means each genre is represented by a vector with a 1 in the position that corresponds to the specific genre and 0s elsewhere. For example, if we have three genres (Rock, Pop, Jazz), we can represent them as:

var genreMap = map[string][]float64{
    "Rock": {1, 0, 0},
    "Pop":  {0, 1, 0},
    "Jazz": {0, 0, 1},
}

Each genre has a distinct and orthogonal representation, which is useful for calculating similarities.

To represent the user's genre preferences as a vector, we can use a slice as well:

userGenrePreferences := []float64{
    float64(user.RockPreference),
    float64(user.PopPreference),
    float64(user.JazzPreference),
}

This setup allows us to compare the user's preferences with each track's genre using vector math.

Calculating Genre Similarities: Part 2

Now, let's build the code to calculate cosine similarity between the user's genre preferences and each track's genre. We'll also attach the similarity score to each track.

Cosine similarity measures the cosine of the angle between two vectors. A value of 1 means the vectors are identical, while 0 means they are orthogonal (no similarity).

Here's how you can implement cosine similarity and use it in Go:

import (
    "math"
)

// CosineSimilarity computes the cosine similarity between two vectors
func CosineSimilarity(a, b []float64) float64 {
    var dotProduct, normA, normB float64
    for i := 0; i < len(a); i++ {
        dotProduct += a[i] * b[i]
        normA += a[i] * a[i]
        normB += b[i] * b[i]
    }
    if normA == 0 || normB == 0 {
        return 0
    }
    return dotProduct / (math.Sqrt(normA) * math.Sqrt(normB))
}

type ScoredTrack struct {
    MergedTrack
    Similarity float64
}

func main() {
    // ... (previous code for loading, merging, and user profile)

    // Prepare user genre preferences vector
    userGenrePreferences := []float64{
        float64(user.RockPreference),
        float64(user.PopPreference),
        float64(user.JazzPreference),
    }

    // Map genres to one-hot vectors
    genreMap := map[string][]float64{
        "Rock": {1, 0, 0},
        "Pop":  {0, 1, 0},
        "Jazz": {0, 0, 1},
    }

    // Calculate similarity for each track and attach to track
    var scoredTracks []ScoredTrack
    for _, mt := range mergedTracks {
        genreVec, ok := genreMap[mt.Genre]
        if !ok {
            genreVec = []float64{0, 0, 0} // Unknown genre
        }
        similarity := CosineSimilarity(genreVec, userGenrePreferences)
        scoredTracks = append(scoredTracks, ScoredTrack{
            MergedTrack: mt,
            Similarity:  similarity,
        })
    }

    // Print tracks with similarity scores
    for _, st := range scoredTracks {
        fmt.Printf("Track: %s, Genre: %s, Similarity: %.3f\n", st.Title, st.Genre, st.Similarity)
    }
}

In this code:

  • We define a CosineSimilarity function to compute the similarity between two vectors.
  • For each track, we get its genre's one-hot vector and compute the cosine similarity with the user's genre preferences.
  • We store the similarity score alongside the track information in a new struct, ScoredTrack.

Higher similarity scores indicate a closer match to the user's tastes.

Summary and Preparation for Practice

In this lesson, you've successfully integrated more advanced content-based recommendation concepts, from simulating user preferences to calculating track similarities using cosine similarity. You've combined data merging, feature extraction, and similarity calculations to create a concrete recommendation system.

As you move on to practice exercises, use this lesson as a framework for applying similar techniques to your unique datasets and user scenarios. This practical experience will consolidate your understanding and proficiency, enabling you to build sophisticated content-based recommendation systems independently.

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