Final Blog Post

Overview, Vision, Goals, and Hypotheses

Spotify has millions of playlists, constructed by algorithms, teams of experts, and users just like you and I. Have you ever wondered how Spotify generates their playlists? Have you ever wished there was a magical button you could press to do all the work of making your own playlists?

In this data science project, completed for CS1951A Data Science at Brown University, four students worked together on bringing this dream to life. At the start of the Spring 2019 semester, we stumbled upon an online data set of 4000 Spotify user-generated playlists, and set the vision of being able to build our own playlist recommendation program. Initially, we pursued this idea and attempted to implement the idea of using sentiment analysis to group songs according to the titles of the playlists they belonged to. However, along this road, we encountered a dead end, and felt completely lost.

Luckily, we were able to pivot directions. We found that through the Spotify API, we were able to access a set of thirteen audio features for every song on Spotify. With this newfound database of knowledge and the excitement of new possibilities, we launched two different efforts to realize our goal.

Firstly, we wrote a data collection program to obtain all the audio feature data for the songs in our set of 4000 user-generated playlists, generating a database (.db) of each song with information about it’s features. We made this design decision after iterating through other data structures (2D arrays, JSON), and found that it would allow for the most efficient manipulation through using sqlite3 and SQL. Choosing four audio features, tempo, danceability, energy, and valence, and essentially having a set of data points with four dimensions, we were able to run K-Means clustering to group the songs into 7 clusters. This effectively divided the songs into 7 unlabelled “topics”. However, we wanted to take it a step further. In order to investigate which of the audio features were the features best representative of the variation in the data (essentially, best measures of similarity), we chose to run K-Means instead on every combination of pairs of audio features (e.g. Tempo + Danceability, Energy + Valence, etc). We created clustering scatter plots to view the resulting clusters, and created a bar plot showing the clustering errors from each combination. We looked for the lowest clustering error, which would suggest to us that that combination of features was best representative of the data.

Secondly, we decided to use the skills we gained from our Deep Learning Homework 8 to implement a version of the word2vec algorithm that was used. Looking back at the 4000 user-generated playlists, we treated each playlist like a “sentence”, and each song in the playlist like a “word”. Implementing song vectorization, we essentially were measuring how often songs appear in the same playlist together. From creating the song vectorizations of how often songs appear in the same context together, we were then able to distances between song vectors using the cosine similarity metric. This allowed us to then run K-Means clustering on our set of song vectors, to again group the songs into 6 clusters of general “topics”, but then also increase the amount of clusters to 1,500, in order to generate much smaller playlists of 10-20 songs.

Ultimately, our hypotheses/goals were to:

  1. Determine which audio features were most important in defining songs. We estimated, from our personal experience and interaction with the music we listen to,  that the two most important audio features that define songs are tempo and valence.
  2. Be able to generate our own algorithmic playlists usings two means:
    1. K-Means clustering on audio features
    2. Song vectorization and K-means clustering on song vectors

We hypothesized that the playlists generated from the song vectorization of

previous user-generated playlists would create better, more fitting groupings of songs,

than clustering by audio features.

Our Data: Spotify user-generated playlists & Spotify API

For our project, we used two bodies of data. Firstly, we used a dataset of 4000 user-generated Spotify playlists which consisted of a JSON object that contains a dictionary of playlists. Secondly, we used the Spotify API to collect a set of audio features for every song in those 4000 playlists.

We initially thought that 4000 playlists might be a little too small for our purposes, but we discovered that these playlists make up 75,833 unique songs. This was, if anything, more than enough for both our ML models (song vectorization, and clustering by audio features). We actually were not able to, and eventually chose not to, run the models on the entire song corpus, and instead truncate our data set,  for two reasons:

  • When we tried to perform song vectorization on the entire dataset, all of our computers ran out of RAM memory when constructing the lookup table. Specifically, the method that constructs the lookup table in utils_playlists.py would construct the table but not be able to return it because it was a huge NumPy array. We were going to change our program up so that the construction of the lookup_table is not done in a function, but we chose not to because of the second reason below.
  • We realized that the frequency by which each of the unique songs appears within playlists decreases dramatically when we increase the sample size that we truncate our dataset by. We truncate the dataset by choosing the top most popular N songs where popularity is determined by how frequently do these songs appear in unique playlists of the dataset. We found that for n = 5000, the least popular songs appear with frequency 9; for n = 15,000, the least popular songs appear with frequency 3; and for n = 20,000, the least popular songs appear with frequency 2. We then faced the design decision of either:
    • Choosing a larger N, which would have produced larger but less dense (compact) K-Means clusters since we would have more songs but they will be less similar (because they appear less frequently). This covers a larger portion of our data at the expense of playlist accuracy.
    • Choosing a lower N, which would have produced smaller but more dense (compact) clusters since we would have less songs but they will be more similar (because they appear more frequently). This sacrifices a big portion of the data but give us much more accurate playlists.

Eventually, we decided to go with the second option so we can have better visualizations and more accurate playlists. We talked to Elie and she told us that this design choice is very common among data scientists and that it is often better to sample the head of the distribution because we have more confidence in it.

We were able to find exactly what we wanted in the dataset. We only needed playlist titles, track titles, and track URIs from the main dataset. The URIs were later used to make API calls to the Spotify server to retrieve a predefined set of audio features for each of the songs in our dataset.

Collecting the data was by far the most time-consuming part of our project. We wrote a script (data_collector.py) to collect the data that we needed and to dump it in a .db file and a .txt file. The data collection process was as follows (code referred to here is from data_collector.py):

  • We loaded the JSON file file into a python list and initialized a Spotipy object which we used for API calls later. This was done in the “__init__” method.
  • We created a main data corpus that was later used to construct our two databases used for clustering and vectorization. This main corpus was a table in which each row represented a playlist and each column represented one of the 11 features of that playlist. Most notably, the 1st column was the playlist name and the 8th column was a list of the tracks that comprise said playlist. The 8th column was a special one in that it was its own list of track features. That is, the tracks feature of each playlist was another list in which each row represented a track in that playlist and each column represented one of the 8 features of a track. This was done in the “construct_playlist_to_tracks” method.
  • We constructed a mapping from playlist titles to track titles within that playlist. This was a list of lists in which the i-th inner list was representative of the i-th playlist, and the j-th element within that list was representative of the j-th track within that playlist. This was done in the “construct_playlist_to_tracks” method, and its output mapping was dumped to a .txt file using the “pickle” library.
  • We constructed a mapping from track titles to values of its audio features (tempo – valence – danceability – energy). This was a dictionary in which keys were unique track titles and values were lists of these tracks’ audio features. This part of the data collection took a large portion of our time because it needed to make 75,833 API calls to the Spotify server which was way (way) beyond the imposed limit. We tried to batch API calls by 50 track URIs at a time (which is the server’s limit on URIs to be considered as one API call), but we later noticed that there were discrepancies resulting from the server returning audio features in an order different to how we sent the track URIs. As a result, we decided to make API calls one-at-a-time, and then store that result in a database so that we do not have to make the calls again. This took quite some time but we were eventually able to collect all of the needed data and store it in a database. We realized at a later point in the project that we only saved the track titles but not the artist names in our database, but we did not want to go through the API calls problem once again. To fix this, we wrote a script that constructs another database binding track titles to artist names. We then used sqlite3 to query both databases and join tables on track titles in order to obtain our desired table in which we also have artist names. We then used a vocabulary containing the titles of the most popular 5,000 track to truncate the database to include only the most frequent tracks in our dataset. This was done mostly in the “construct_song_to_features” and “save_data” methods, but involved some extra local scripts and data manipulation using sqlite3 and SQL command.

The end results of our data collections were the following two files:

  • tracks2features_full.csv: contains a table that has the track title, artist name, valence, tempo, danceability, energy, and speechiness of the most popular 5,000 songs in our dataset.
  • playlist2tracks_full.txt: contains a pickle dump which, when loaded, produced a list of lists in which the i-th inner list is the i-th playlist in our dataset, and the j-th element within that inner list is the j-th (track title, artist name) tuple within that playlist. This contains all of the playlists inner dataset and is later truncated in sparse_playlists.py

Notes:

  • We chose the set (valence, tempo, danceability, energy, and speechiness) as our audio features by talking to some of our musician friends in the music department. They suggested that this set is the most representative of a song out of the 13 pre-defined audio features in the Spotify API.
  • This is the main link from which we got our main dataset. Originally it was sliced into 4 slices of 1,000 playlists each, but we merged it into one file containing all 4,000 playlists. We will not be handing-in the merged file since it would be too big to hand-in, but it will be uploaded onto our Google Drive folder. The link from which were obtained our data slices is: https://github.com/vaslnk/Spotify-Song-Recommendation-ML/tree/master/data?fbclid=IwAR30mCJqGeehmMyhwkK26WvK5grtOB_ciP-Rjlpwa-2avySKdSNtu4c1-qs

FUN FACT: the dot at the bottom left corner is the song “Super Mario Bros – Original” with track URI “spotify:track:4DG4um6R0wx4WrqhhACc5g”. We discovered, through our API calls, that this song has 0 valence, 0 danceability, 0 tempo, and 0.331 energy (you can check that using the track URI at: https://developer.spotify.com/console/get-audio-features-track/?id=spotify%3Atrack%3A4DG4um6R0wx4WrqhhACc5g). We decided to use this point anyway since it has an energy value, and we were interested in how it could affect our clustering models. We found that it did not drastically make a difference to include it.

Method 1: Song Vectorization and K-Means Clustering

Our goal was to analyse song similarity based on how often songs appeared together in the same playlist. Using the skills we learnt in the DL assignment, we treated each song as a “word” and each playlist as a “sentence” in order to implement a similar sparse_vecs structure. Looking into our dataset, since many songs only appeared in a playlist once, we inferred that running K-Means clustering on our entire dataset would result in many outlier points of rare songs that are far away from the main clusters of frequent songs, while the playlists we generated wouldn’t be as accurate based on user-sentiment. Because of this, along with consultation from Ellie, we decided to only analyse the 5000 most popular songs, where each song appeared at least in 9 playlists, in order to optimize the tradeoff between having a large enough subset to have reliable results and generating accurate clusters and playlists.

To model these 5000 songs, we created a 2D array size 5000 x 5000, with each row index being a song id, which maps to the song title in our vocab dictionary, and the value being a vector of length 5000 counting the number of times each song appears with the current indexed song in the same playlist, which we calculate in linear time by looping through each song in each playlist once. After constructing this lookup table, we calculate the cosine similarity between each song pair, storing the result in a 2D array of 5000 x 5000 where each row is now a vector showing the similarity between each song and the indexed song. Now that we have 5000 known “features” describing each song, we used TSNE to dimensionally reduce the 5000 features into 2, in order to easily cluster and visualize our results while maintaining the most important variance in our data-points. To determine the optimum number of clusters for K-Means, we used the following elbow-point graph:

As shown from above, having n=6 clusters seemed to be the most optimum point since it was the last inflexion point where the subsequent decrease in gradient was the smallest, so we determined this was our “elbow” point.  Using K Means with 6 clusters, we had the following visualization:

Most of our data seems to cluster very well, except for a few outliers in our red and green clusters, although it was difficult to label each cluster since each cluster had around 1000 songs spanning across many genres and time periods. However, when running K-Means on the entire dataset with 500 clusters, we were able to generate playlists with an average of 10 songs each, which we present in our conclusion with more analytical detail. We also tried generating playlists for all the songs in our dataset (75,833 unique), but was unable to do so due to limits in RAM.

Method 2: Audio Feature Clustering

We also wanted to use machine learning techniques to analyze song similarity using the audio features provided by the Spotify API. These audio features all have numerical values, making them a good fit for K-means. Spotify provides 13 audio features in total, but after interviewing and speaking to friends in the music department, we settled on five initial audio features to cluster on: valence, tempo, danceability, energy, and speechiness. However, after conducting the two approaches below, we realized that speechiness was not an optimal determinant of similarity.  The clustering graphs with speechiness included were sparse and not tightly grouped at all, and clustering error was strangely low. We took a look through the dataset, and after all of this evidence, we concluded that speechiness wasn’t a good measure of variation in the data because most songs had speechiness values that leaned heavily towards zero. We thus narrowed down the total number of audio features to just four: valence, tempo, danceability, and energy.

K-Means on all four audio features

Our first approach involving audio feature clustering was to cluster with all four features, which is done in final_kmeans.py. To maintain consistency with the the song vectorization method, we also used the top 5,000 songs as our data points. Before proceeding, we normalized the audio features, as some of the audio features had different scales from one another. Our method for doing this was to find the maximum value for each audio feature, and then to divide the value for each data point by that maximum value. This resulted in a scale of zero to one for each audio feature, allowing us to maintain even more consistency. We produced a numpy array containing the all of normalized feature values for each data point, and we fed those into sci-kit learn to the cluster IDs for each data point. We concatenated those cluster IDs to the initial numpy array so that each data point now had its associated cluster ID as well, making it ready for visualization. However, because there are four features and therefore, four axis dimensions, our clusters were impossible to visualize in their original form. To remedy this, we used the hypertools library to perform PCA dimensionality reduction to three dimensions before plotting the clusters.

Clustering based on all of four our features created clean clusters, as seen in the visualization above. The above visualization has seven clusters, because after looking at the elbow point plot, seven was where the last inflection point was, signifying the last major change in rate of error.  

Because of the dimensionality reduction and because of the unsupervised nature of K-means, it is unclear what the axes represent and what specific “topics” that the clusters themselves actually represent. However, the fact that the clusters are so clean is a positive sign, and the main takeaway from this approach is that these four audio features can effectively predict similarity for songs together.

Audio feature pairs

Our second, and more important, approach involving audio feature clustering was to cluster with different combinations of the two of the four features. We do this in final_audio_pairs.py. There were six such combinations in total. As with the “all four features” analysis, we normalized the audio feature values. We produced six numpy arrays containing the different combinations of features for each data point, and we fed those into sci-kit learn to generate the cluster centers and the cluster IDs for each data point. We then used matplotlib to produce visualizations of the clusters with each combination of audio features, where each axis represents a measure of the audio feature.

We also decided to create a visualization for the errors of the six different audio feature pairs to better determine which pair produced the lowest error with audio_errors.py. Similar to above, we produced six numpy arrays containing the different combinations of features and fed those into sci-kit learn’s fit_predict. We then retrieved scores for those combinations and used those to calculate the error associated with each audio feature pair. Finally, we used the bar chart in the matplotlib library to visualize the errors.

As seen above, the combination of tempo and danceability produced the lowest error, suggesting that those two features were the best features for determining the variation between songs. This contradicted our initial hypothesis that valence and tempo were the best predictors of similarity.

Generating Playlists

To test our second hypothesis of whether song vectorization or audio featuring clustering was better at predicting song similarity, we also generated playlists with audio feature clustering in audio_playlist_generation.py. To maintain consistency, we also clustered into 500 clusters over the top 5,000 songs, as with song vectorization.

Conclusions

We’ve come a long way since the beginning of our project, and we are proud to present the results that we’ve been able to obtain.

Firstly, we determined through K-Means clustering on various audio features of songs, that through looking at the clustering errors, the pair of audio features that results in the lowest error is Tempo & Danceability. This breaks our original hypothesis, but also follows along intuition. However, it is important to note that there are limitations to this conclusion. In particular, all the songs that were analyzed were a part of the 4000 user-generated playlists, which is only a very small sample of the many millions of playlists on Spotify. Furthermore, this set of songs from these 4000 playlists may or may not be representative of the general population of Spotify users, and even more so, the general population of Spotify users may not represent the musical tastes of all human music-enjoyers. Thus, it is important to keep in mind the source of our data in interpreting this outcome. This applies also to our secondary results.

Secondly, through the method of K-Means on audio features and K-Means on song vectorizations, we were able to use two separate methods of algorithmically generating playlists. We hypothesized that the playlists generated by the song vectorization method would group songs that were more “similar”, fitting our human intuition. In order to visualize the many playlists that were created (which can also be accessed through our submission), we created album covers.

Here are some album covers generated from playlists using the song vectorization method:

Because these playlists are unnamed, as they were algorithmically generated, we labelled them ourselves. Above, we named the playlist on the left “pop”, and the playlist on the right “rap”. Below, we named the playlist on the left “throwbacks”, and the playlist on the right “christmas”.

Here we can clearly see that a lot of these playlists work very very well, which was a very exciting result for us. All these groupings make a lot of sense, since playlists in this method were generated depending on how often songs showed up in the same user-generated playlists together. Of course songs from the early 2000s showed up more often than not in the same playlists together, likely titled “throwback” for college aged students like us.

Alternatively, here are some playlists generated by the clustering using audio features:

In these examples of playlists generated from audio feature clustering, it is unclear whether or not there is a clear correlation between the songs. There are some surprising groupings, such as on the bottom right, which groups together country music, disney pop songs, Christian worship songs, and alternative metal. Unfortunately, this was a pattern for most of the playlists generated by the audio features grouping – songs often times did not fit into the same “vibe” or genre, creating groupings of songs that were simply clashing. Perhaps, however, many of these songs that are grouped using this method actually share similar quantitative features such as the tempo of the song, or the mood (valence) of the song, but regardless, songs are defined by much more than a set of quantifying numbers.

All in all, we were very excited to be able to come so far in our project, especially since we hit what felt like such a major roadblock around midway through the project. We enjoyed the process, developed and strengthened many skills that we had learned in class, and gained a greater appreciation for the complexity of how Spotify generates their countless playlists and is able to constantly suggest new, fitting music for its users. We were also very proud to be able to create our project poster, and are excited to continue using our skills in data collection, machine learning, and data analysis in the future. Special thanks to Anna Nakai, our team TA mentor, and Professor Ellie Pavlick, who always pulled through and advised us in the thickest of moments, when our backs were against the wall. Thank you for taking the time to read through our blog, and we wish you the best!

Cheers,

Jason Chan, Nazem Aldroubi, Martin Chu, Kevin Xiang

Big Boi Blog Post #1

INTRO:

Our project “Moodify for Spotify” aims to survey a handful of user-generated playlists from Spotify and use these playlists’ titles to associate individual songs with sentiments or feelings. A preliminary goal is to categorize each of these songs within a specific “mood” category from a set of predefined moods or feelings. The way we are planning to construct this set is by either:

(1) hardcode a wide set of feelings that we think are extensive enough to cover our dataset, or

(2) use the playlists’ titles as a starting point to generate this set depending on the feelings depicted in the dataset.

We are then planning to associate each feeling with a set of related words that depict that feeling and use that to bind each song to its corresponding category/ies. Our ultimate goal, is to use these sets of songs to generate more accurate playlists (by reshuffling the songs in the original playlists) that contain songs reflecting the feelings associated with their titles.

Our data:

Our data was provided by Spotify as a part of their 2018 RecSys Challenge. The current data we have is a 4000 playlist subset of the ‘Million Playlist Dataset’ available during the Challenge obtained from Github. We emailed Spotify to ask for the actual dataset, which would allow us to generate more accurate insights.

The data is stored as a list of JSON objects: each playlist is a a JSON object, and in each playlist object, there is a “track” key whose value is a list of JSON objects, with each of those JSON objects representing a song.

The data was supposed to be already cleaned, but we checked over it to make sure that it was clean, and it is indeed clean.

Next Steps:

Data Visualization:

We want to have a rough draft of visualization. We are considering using the following libraries:

  • D3
  • Matplotlib
  • Seaborn

D3 is a JavaScript library, while Matplotlib and seaborn are Python libraries. As such, we are leaning towards Matplotlib and seaborn because we are more comfortable with Python as a language. We also want to figure out what kinds of charts and tables we want to use that would communicate our data best to the user.

ML Clustering:

We are planning to use some of the ML clustering techniques we learn in class to extract a set of feelings that spans our dataset from the playlist titles. We are then planning to use similar techniques, alongside sentiment-analysis and possibly NLP, to examine and map each song to the set(s) of feelings that it depicts. We are still not entirely sure how are we going to do that but we are waiting to see how deep we go into ML clustering to decide exactly on the methods that we will use. We are also planning to use the hypothesis testing procedure we learned in class to test different ML models and decide on the best-fit algorithm for our project.

Emotion Metric:

We also want to figure out a metric that we can give to songs for their association with a particular mood/activity.

There are two possible methods we’re thinking of categorizing the playlists. Firstly, we can determine the list of categories, then fit the playlist names into each category. Alternatively, we can look at the data itself and the existing playlist names and bucket them accordingly.

Going deeper into exploring these, we wanted to gain inspiration for lists of emotions, and found the following three.

  • Robert Plutchick’s
    • Fear → feeling of being afraid, frightened, scared.
    • Anger → feeling angry. A stronger word for anger is rage
    • Sadness → feeling sad. Other words are sorrow, grief (a stronger feeling, for example when someone has died)
    • Joy → feeling happy. Other words are happiness, gladness
    • Disgust → feeling something is wrong or nasty.
    • Surprise → being unprepared for something.
    • Trust → a positive emotion; admiration is stronger; acceptance is weaker.
    • Anticipation → in the sense of looking forward positively to something which is going to happen. Expectation is more neutral.
  • Book Two of Aristotle’s “Rhetoric”
    • Anger, opposite calmness (not feeling excited)
    • Friendship, is where people have a bond of joy and will come together and have fun
    • Fear, opposite courage (having courage in the face of fear)
    • Shame, opposite confidence (shame: how one feels about one’s past bad actions or thoughts; shamelessness: one does not feel shame, but others think one should)
    • Kindness (benevolence), opposite cruelty (kindness: when people are good to other people)
    • Pity (when people feel sorry for other people)
    • Indignation (feeling angry because something is not fair, such as undeserved bad fortune)
    • Envy, jealous (pain when people have something that one wishes for oneself)
    • Love, a strong emotion of attachment one feels for someone else. Ranges to family, pets, friends, significant others or fictional characters.
  • Darwin’s ideas (The Expressions of the Emotions in Man and Animals)
    • Suffering and weeping
    • Low spirits, anxiety, grief, dejection, despair
    • Joy, high spirits, love, tender feelings, devotion
    • Reflection, meditation, ill-temper, sulkiness, determination
    • Hatred and anger
    • Disdain, contempt, disgust, guilt, pride, helplessness, patience, affirmation and negation
    • Surprise, astonishment, fear, horror
    • Self-attention, shame, shyness, modesty, blushing.

Drawing from those 3 possible lists, the categories for playlists we can imagine looking for are:

  • Anger (rage, frustration)
  • Sadness (reflection, meditation, sulkiness, suffering, weeping, life sucks),
  • Joy (high spirits, happy, optimistic),
  • Love (romantic),
  • Surprise (excitement, anticipation),

Other than emotions, we can also group by categories of activities of playlists such as:

  • Gym,
  • driving,
  • studying,
  • sleep,
  • sex,
  • shower,
  • Parties,
  • outdoors

The other thing we can do is try to look at the existing playlist names, and see if we can bucket them/categorize them based on what we already see, but how do we do that? Possibly through Sentiment Analysis

Sentiment analysis:

Here is what we’ve learned about sentiment analysis, through the website: https://monkeylearn.com/sentiment-analysis/

  • What is sentiment analysis?
    • A field in Natural Language Processing (NLP) that builds systems to attempt to identify and extract opinions from text. Aside from extracting opinions, sentiment analysis also attempts to identify exact attributes from the text: 1) polarity (positive or negative opinion), 2) Subject, 3) Opinion holder.
    • Opinions are subjective expressions describing peoples sentiments, appraisals, feelings on a particular subject, whereas facts as objective. There are two types of opinions
      • Direct opinions
        • “The picture quality of computer screen A is poor”
      • Comparative opinions
        • “The picture quality of computer screen B is better than screen A”
    • Sentiment analysis is a classification problem with two subproblems
      • Subjectivity classification
      • Polarity classification
    • Scope of sentiment analysis can be by
      • Document
      • Sentence
      • Or sub sentence level
    • Types of sentiment analysis
      • Fine-grained (5 categories from very negative to very positive)
      • Emotion detection
        • Emotion detection resort to lexicons or complex ML algorithms
      • Aspect-based sentiment analysis
      • Intent analysis
    • Sentiment analysis approached
      • Rule based
        • Perform analysis based on set of manually crafted rules
      • Automatic
        • Rely on machine learning
      • Hybrid
    • Automatic analysis
      • Machine learning classifier can be implemented with these steps and components:

Besides parsing words for specific feelings, many playlist titles contain emojis and symbols. We hope to isolate these emojis and perhaps map them to keywords using Twitter’s emoji library to be used in our general sentiment analysis. We can do this through considering “emoji lexicons”, databases of emojis alongside what the emoji represents. Examples of these can be seen here: