Introduction: Why Prepare Training Data?

Welcome to the course. In this lesson, we will focus on preparing training data from session logs — a crucial step in building a system that can predict what music a user might like next.

Training data is the foundation of any machine learning model. For our smart music player, we want to teach the model to recognize patterns in what users listen to so it can make smart recommendations. Session logs are records of what tracks users have listened to. By turning these logs into structured training data, we give our model the information it needs to learn user preferences.

By the end of this lesson, you will understand how to transform raw session logs into a format that a machine learning model can use.

Recap: Project Setup and Data Sources

Before we dive in, let’s briefly remind ourselves of the project setup and where our data comes from. Our project uses several data sources:

  • Session logs: These are stored in a file called sessions.csv and record which users listened to which tracks.
  • Track data: Information about all available tracks.
  • User profiles: Vectors that summarize a user's listening history.
  • Track embeddings: Vectors that represent the features of each track.

Here is a quick example of how we load the necessary modules and data in our project:

import pandas as pd
import numpy as np
import os
from src.database import get_all_tracks, SESSIONS_FILE_PATH, get_user_listening_history
from src.user_model import generate_user_profile_vector, get_track_embeddings

# Load all tracks
all_tracks_df = get_all_tracks()

# Load session logs
if os.path.exists(SESSIONS_FILE_PATH):
    sessions_df = pd.read_csv(SESSIONS_FILE_PATH)
else:
    sessions_df = pd.DataFrame()

This code sets up the basic data we need for preparing our training data. If you are using the CodeSignal environment, these modules and files should already be available.

Data Check: If the session log file path doesn’t exist, it’s good practice to print a warning so it’s clear that no training data will be loaded. You can also print the first few rows of the DataFrame with sessions_df.head() when the file exists — this helps remind you what data you’re working with before moving on.

Positive and Negative Samples Explained

To train a model to predict user preferences, we need to show it examples of both what a user likes and what they don’t like (or at least, what they haven’t listened to yet).

  • Positive samples: These are track-user pairs where the user has listened to the track. In our session logs, these are easy to find.
  • Negative samples: These are track-user pairs where the user has not listened to the track. We create these by pairing users with tracks they haven’t played.

Note that we don’t include all unlistened tracks as negative samples — this would create an overwhelming number of negatives compared to positives and hurt model training. Instead, we sample a limited number using the negative_sample_ratio parameter, which controls how many negatives are generated per positive. This helps maintain a healthy balance in the training dataset. Keep in mind that if a user has listened to nearly all tracks, the number of unlistened tracks might be very small or even zero. In such cases, the function gracefully avoids creating excess negative samples — meaning fewer (or no) negatives will be generated, even if the negative_sample_ratio is set high. This behavior prevents index errors or sampling failures but might result in class imbalance for some users.

A very high ratio can create class imbalance (far more 0s than 1s), which often inflates accuracy while hurting recall for positives. It can also slow training and bias the model toward predicting “not liked.” Practical tips: start with 1–3, cap by available unlistened tracks (already handled), and monitor label counts. If you must go higher, consider class weights or downsampling negatives during training.

Why do we need both? If we only show the model what users like, it won’t learn to tell the difference between liked and unliked tracks. By including both, we help the model learn what makes a track appealing to a user.

What Happens If We Only Include Positives? Let’s try preparing the training data with negative_sample_ratio=0, meaning no negative examples are added:

X, y = prepare_training_data(negative_sample_ratio=0)

Sample Output:

X shape: (2, 22), y shape: (2,)

--- Training Data Summary ---
Total samples: 2
Positive labels (1): 2
Negative labels (0): 0

As expected, only positive samples are included. This is not ideal for training a classifier — without negatives, the model cannot learn to distinguish good from bad recommendations.

Heads-up: The function prepare_training_data is implemented later in this lesson. The example below is for intuition only.

How Feature Vectors Are Built

For each user-track pair, we need to create a feature vector that the model can use. This vector combines information about the user and the track.

  • User profile vector: Summarizes the user’s listening history.
  • Track embedding: Represents the features of the track.

We combine these two vectors into one by simply joining them together (concatenation). This concatenation ensures that the model sees both the user’s musical taste and the track’s characteristics side by side. Over time, it learns how different combinations of user preferences and track features affect listening behavior — essentially, it’s learning a function f(user, track) → like or not. Here’s a simplified example:

user_profile_vec = np.array([0.2, 0.5, 0.3])  # Example user profile
track_emb = np.array([0.1, 0.4, 0.6])         # Example track embedding

feature_vector = np.concatenate([user_profile_vec, track_emb])
print(feature_vector)

Output:

[0.2 0.5 0.3 0.1 0.4 0.6]

This combined vector is what we use as input for our model. Each row in our training data will be one of these feature vectors, and each will have a label: 1 for positive, 0 for negative.

To avoid confusion, it’s important to understand that both the user profile vector and track embedding must have a fixed and consistent length. For example, if the user profile vector is of length 10 and the track embedding is also of length 12, the resulting feature vector will have 22 dimensions — which is exactly what you’ll observe in the training data. If their shapes are mismatched or inconsistent between users or tracks, the model training will fail with shape-related errors. Always ensure that the user and track vectors are computed using the same embedding logic.

Walking Through the Data Preparation Function

Now, let’s look at how we put all these ideas together in the prepare_training_data function. This function creates the training data for our model by:

  1. Loading all tracks and their embeddings.
  2. Reading the session logs to find which users listened to which tracks.
  3. For each user:
    • Generating their profile vector.
    • Creating positive samples for tracks they listened to.
    • Creating negative samples for tracks they did not listen to.
    • Combining user and track vectors into feature vectors.
  4. Returning the features (X) and labels (y).

Here is the main part of the function:

def prepare_training_data(negative_sample_ratio: int = 1):
    all_tracks_df = get_all_tracks()
    if all_tracks_df.empty:
        return pd.DataFrame(), pd.Series(dtype='int')

    all_track_ids_ordered, all_track_embeddings_matrix = get_track_embeddings()
    track_id_to_embedding = {tid: emb for tid, emb in zip(all_track_ids_ordered, all_track_embeddings_matrix)}

    if not os.path.exists(SESSIONS_FILE_PATH) or os.path.getsize(SESSIONS_FILE_PATH) == 0:
        print("Warning: sessions.csv is empty or not found. No training data can be generated.")
        return pd.DataFrame(), pd.Series(dtype='int')

    sessions_df = pd.read_csv(SESSIONS_FILE_PATH)
    features_list = []
    labels_list = []

    for user_id in sessions_df['user_id'].unique():
        user_profile_vec = generate_user_profile_vector(user_id)
        if user_profile_vec is None:
            continue

        listened_tracks = sessions_df[sessions_df['user_id'] == user_id]['track_id'].unique()

        # Positive samples
        for track_id in listened_tracks:
            if track_id in track_id_to_embedding:
                track_emb = track_id_to_embedding[track_id]
                features_list.append(np.concatenate([user_profile_vec, track_emb]))
                labels_list.append(1)

        # Negative samples
        unlistened_tracks = [tid for tid in all_track_ids_ordered if tid not in listened_tracks and tid in track_id_to_embedding]
        num_positive = len(listened_tracks)
        num_negative_to_sample = min(len(unlistened_tracks), num_positive * negative_sample_ratio)

        if num_negative_to_sample > 0:
            sampled_unlistened_ids = np.random.choice(unlistened_tracks, size=num_negative_to_sample, replace=False)
            for track_id in sampled_unlistened_ids:
                track_emb = track_id_to_embedding[track_id]
                features_list.append(np.concatenate([user_profile_vec, track_emb]))
                labels_list.append(0)

    if not features_list:
        return pd.DataFrame(), pd.Series(dtype='int')

    X = pd.DataFrame(features_list)
    y = pd.Series(labels_list, dtype='int')
    return X, y

Explanation:

  • Load all tracks using get_all_tracks(). If the returned DataFrame is empty, we immediately return empty training data — this is an early-exit safeguard in case the music catalog isn't loaded properly.
  • Get track embeddings via get_track_embeddings(). This returns two things:
    • A list of track IDs.
    • A NumPy matrix of embeddings (one row per track). These are zipped into a dictionary (track_id_to_embedding) for fast lookup by track ID when creating training samples.
  • Check the session logs: If sessions.csv doesn’t exist or is empty, return early with no data and a warning. This prevents runtime errors due to missing logs and informs the developer of the issue.
  • Read the session logs into a DataFrame so we can iterate over user behavior and build training samples from real user listening history.
  • Initialize lists for features and labels. These will collect the final training data points for the model: features_list stores the concatenated vectors, and labels_list stores the binary labels (1 for listened/positive, 0 for unlistened/negative).
  • Iterate through each unique user in the session logs:
    • Generate the user’s profile vector with generate_user_profile_vector(user_id). If the user has no valid listening history (e.g., new or malformed data), this returns None and we skip the user.
    • Extract all tracks listened to by this user to build the positive samples.
  • Create positive samples by:
    • For each listened track, checking if the embedding exists in the dictionary.
    • Concatenating the user profile vector with the track embedding.
    • Appending the result to the feature list and adding a label of 1 to the labels list.
  • Create negative samples by:
    • Finding all tracks the user did not listen to.
    • Calculating how many negatives to sample, based on the number of positives and the negative_sample_ratio parameter.
    • Sampling this many unlistened tracks at random (without replacement).
    • For each sampled unlistened track, concatenate its embedding with the user profile vector and assign a label of 0.
  • Avoid empty results: If no valid samples were generated at all (e.g., no valid users, all embeddings missing), the function returns empty DataFrame/Series to avoid breaking downstream training.
  • Finally, return the dataset:
    • X: a DataFrame where each row is a feature vector (user + track).
    • y: a Series of corresponding labels (1 for listened, 0 for unlistened).

Let’s run the function with the default setting, where we generate one negative sample for every positive:

X, y = prepare_training_data()
print(f"X shape: {X.shape}, y shape: {y.shape}")

Sample Output:

X shape: (4, 22), y shape: (4,)

--- Training Data Summary ---
Total samples: 4
Positive labels (1): 2
Negative labels (0): 2
Feature vector length: 22

This confirms that our training set includes 2 positive (listened) and 2 negative (unlistened) samples. Each feature vector has 22 dimensions — a result of combining the user profile and track embedding vectors.

We can also test with a higher ratio — 10 negatives per positive — to observe how the function handles it:

X, y = prepare_training_data(negative_sample_ratio=10)

Sample Output:

X shape: (5, 22), y shape: (5,)

--- Training Data Summary ---
Total samples: 5
Positive labels (1): 2
Negative labels (0): 3

Even though we asked for 10 negatives per positive, only 3 were included. That’s because there were only 3 unlistened tracks available. The function smartly avoids over-sampling, keeping the dataset valid.

Summary And What’s Next

In this lesson, you learned how to turn raw session logs into structured training data for a machine learning model. We covered:

  • The importance of both positive and negative samples
  • How to build feature vectors by combining user and track information
  • How the prepare_training_data function works step by step and also includes checks for edge cases such as missing session files, empty track lists, or users with no listen history. These are verified in the test suite you’ll use shortly, ensuring robustness even when the data is sparse or incomplete.

This foundation is essential for building models that can predict user preferences. In the next practice exercises, you will get hands-on experience running and testing this data preparation process yourself. Be sure to pay attention to how the data is structured and how the function handles different scenarios, as this will help you build more robust recommendation systems in the future.

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