.. _examples.business.spotify: Predicting Popularity on Spotify ================================= This example uses the publicly-available Spotify from Kaggle to predict the popularity of Polish songs and artists on Spotify. We'll also use a model to group artists together based on how similar their songs are. .. note:: We are only using polish artists and a subset of the tracks dataset filtered by a handful of artists. The ``tracks`` dataset (``tracks.csv``) have the following features: Id represents the Id of the track generated by Spotify Numerical: - **acousticness** (range: ``[0,1]``) - **danceability** (range: ``[0,1]``) - **energy** (range: ``[0,1]``) - **duration_ms** (range: ``[200000,300000]``) - **instrumentalness** (range: ``[0,1]``) - **valence** (range: ``[0,1]``) - **popularity** (range: ``[0,100]``) - **tempo** (range: ``[50,150]``) - **liveness** (range: ``[0,1]``) - **loudness** (range: ``[-60,0]``) - **speechiness** (range: ``[0,1]``) Dummy: - **mode**: (``0 = Minor``, ``1 = Major``) - **explicit**: (0 = No explicit content and 1 = Explicit content) Categorical: - **key**: keys on an octave encoded as integers in range ``[0,11]`` (``C = 0``, ``C = 1``, etc.) - **timesignature**: predicted time signature. - **artists**: list of contributing artists. - **artists**: list of IDs of contributing artists. - **release_date**: date of release (yyyy-mm-dd). - **name**: track name. The ``artists`` dataset (``artists.csv``) has the following features: - **id**: ID of the artist. - **name**: artist name. - **followers**: how many followers the artist has. - **popularity**: popularity of the artists based on their tracks. - **genres**: list of genres covered by the artist's tracks. We will follow the data science cycle (Data Exploration - Data Preparation - Data Modeling - Model Evaluation - Model Deployment) to solve this problem. Initialization --------------- This example uses the following version of vastorbit: .. ipython:: python import vastorbit as vo vo.__version__ Start by importing vastorbit and loading the SQL extension, which allows you to query the VAST DataBase with SQL. .. ipython:: python %load_ext vastorbit.sql Connect to VAST. This example uses an existing connection called ``VASTDSN``. For details on how to create a connection, see the :ref:`connection` tutorial. You can skip the below cell if you already have an established connection. .. code-block:: python vo.connect("VASTDSN") Data Loading ------------- Load the datasets into the :py:mod:`~vastorbit.VastFrame` with :py:func:`~vastorbit.read_csv` and then view them with :py:func:`~vastorbit.VastFrame.head`. .. code-block:: # load datasets as VastFrame objects artists = vo.read_csv("artists.csv") tracks = vo.read_csv("tracks.csv") # Display artists .. ipython:: python :suppress: try: artists = vo.read_csv( "/Users/badr.ouali/Documents/VastOrbit-master/docs/source/_static/website/examples/data/spotify/artists.csv", ) except: artists = vo.VastFrame("artists") res = artists html_file = open("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_artists_table.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_artists_table.html .. code-block:: tracks .. ipython:: python :suppress: try: tracks = vo.read_csv("/Users/badr.ouali/Documents/VastOrbit-master/docs/source/_static/website/examples/data/spotify/tracks.csv") except: tracks = vo.VastFrame("tracks") res = tracks html_file = open("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_tracks_table.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_tracks_table.html .. warning:: This example uses a sample dataset. It only includes Polish artists. For the full analysis, you should consider using the complete dataset. Data Exploration ----------------- We can visualize the top ``60`` most-followed Polish artists with a bar chart. .. code-block:: python # make a chart of the top 50 most-followed Polish artists artists.bar( ["name"], method = "mean", of = "followers", max_cardinality = 50, width = 800, ) .. ipython:: python :suppress: vo.set_option("plotting_lib","plotly") fig = artists.bar( ["name"], method = "mean", of = "followers", max_cardinality = 50, width = 800, ) fig.write_html("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_polish_followers_bar.html") .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_polish_followers_bar.html We can do the same with the most popular tracks. For example, we can graph Monika Brodka's most popular tracks like so: .. code-block:: # find Monika Brodka's songs brodka_tracks = tracks.search("LOWER(artists) like '%brodka%'") # plot Brodka's tracks ordered by popularity brodka_tracks.bar( ["name"], method = "mean", of = "popularity", max_cardinality = 25, width = 800, ) .. ipython:: python :suppress: :okwarning: brodka_tracks = tracks.search("LOWER(artists) like '%brodka%'") fig = brodka_tracks.bar( ["name"], method = "mean", of = "popularity", max_cardinality = 25, width = 800, ) fig.write_html("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_brodka_popularity_bar.html") .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_brodka_popularity_bar.html To get an idea of what makes Monika Brodka's songs popular, let's create a boxplot of the numerical feature distribution of her tracks. .. code-block:: ## list of the relevant numerical features numerical_features = [ "danceability", "energy", "speechiness", "acousticness", "instrumentalness", "valence", "liveness", ] # create a boxplot of the above features brodka_tracks.boxplot(columns = numerical_features) .. ipython:: python :suppress: :okwarning: ## list of the relevant numerical features numerical_features = [ "danceability", "energy", "speechiness", "acousticness", "instrumentalness", "valence", "liveness", ] # create a boxplot of the above features fig = brodka_tracks.boxplot(columns = numerical_features) fig.write_html("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_boxplot.html") .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_boxplot.html Timing is a classic factor for success, so let's look at the popularity of Monika's songs over time with a smooth curve. .. code-block:: # extract year from the date brodka_tracks["release_year"] = "YEAR(CAST(release_date AS DATE))" # smooth the popularity using rolling mean brodka_tracks.rolling( func = "mean", columns = "popularity", window = (-3, 3), order_by = "release_year", name = "smoothed_popularity", ) # plot the smoothed curve for popularity of her songs brodka_tracks.plot(ts = "release_date", columns=["smoothed_popularity"]) .. ipython:: python :okwarning: :suppress: # extract year from the date brodka_tracks["release_year"] = "YEAR(CAST(release_date AS DATE))" # smooth the popularity using rolling mean brodka_tracks.rolling( func = "mean", columns = "popularity", window = (-3, 3), order_by = "release_year", name = "smoothed_popularity", ) # plot the smoothed curve for popularity of her songs fig = brodka_tracks.plot(ts = "release_date", columns = ["smoothed_popularity"]) fig.write_html("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_brodka_release_plot.html") .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_brodka_release_plot.html Numerical-feature Analysis --------------------------- Bringing it all together, let's try to get an idea of how these numerical features change and correlate with each other in Monika's most popular songs. .. code-block:: # extract year from date tracks["release_year"] = "YEAR(CAST(release_date AS DATE))" # get the average of numerical features during the year yearly_aggs = tracks.groupby( "release_year", [ "AVG(danceability) as danceability", "AVG(energy) as energy", "AVG(speechiness) AS speechiness", "AVG(acousticness) AS acousticness", "AVG(instrumentalness) AS instrumentalness", "AVG(valence) AS valence", "AVG(liveness) AS liveness", ] ) # plot the cures for numerical features along the different years yearly_aggs.plot( ts = "release_year", columns = numerical_features, ) .. ipython:: python :suppress: :okwarning: tracks['release_year'] = "YEAR(CAST(release_date AS DATE))" yearly_aggs = tracks.groupby( "release_year", [ "AVG(danceability) as danceability", "AVG(energy) as energy", "AVG(speechiness) AS speechiness", "AVG(acousticness) AS acousticness", "AVG(instrumentalness) AS instrumentalness", "AVG(valence) AS valence", "AVG(liveness) AS liveness", ] ) fig = yearly_aggs.plot( ts = "release_year", columns = numerical_features, ) fig.write_html("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_brodka_release_plot.html") .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_brodka_release_plot.html .. code-block:: # correlation of numerical features tracks[tracks[numerical_features]].corr() .. ipython:: python :suppress: :okwarning: fig = tracks[tracks[numerical_features]].corr() fig.write_html("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_tracks_corr.html") .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_tracks_corr.html Feature Engineering -------------------- To expand our analysis, let's take into account some descriptive features. Since our goal is to predict popularity, some useful features might be: - number of followers - popularity for the artist of the track - the number of artists per track Additionally, we manipulate our data a bit to make things easier later on: - converting the duration unit from ``ms`` to ``minute`` - extracting the year from the date. .. code-block:: python %%sql DROP TABLE IF EXISTS polish_tracks; CREATE TABLE polish_tracks AS SELECT * FROM tracks WHERE id_artists IN (SELECT t.id_artists FROM tracks t JOIN artists p ON t.id_artists LIKE '%' || p.id || '%'); CREATE TABLE polish_tracks_clean AS SELECT x.*, x.duration_ms / 60000 AS duration_minute, CAST(x.release_date AS date) AS release_year, y.followers AS artists_followers, y.popularity AS artist_popularity FROM polish_tracks AS x LEFT JOIN artists AS y ON x.id_artists LIKE '%' || y.id || '%'; .. ipython:: python :okwarning: :suppress: from vastorbit._utils._sql._sys import _executeSQL _executeSQL( """ DROP TABLE IF EXISTS polish_tracks """ ) _executeSQL( """ CREATE TABLE polish_tracks AS SELECT * FROM tracks WHERE id_artists IN (SELECT t.id_artists FROM tracks t JOIN artists p ON t.id_artists LIKE '%' || p.id || '%') """ ) _executeSQL( """ CREATE TABLE polish_tracks_clean AS SELECT x.*, x.duration_ms / 60000 AS duration_minute, CAST(x.release_date AS DATE) AS release_year, y.followers AS artists_followers, y.popularity AS artist_popularity FROM polish_tracks AS x LEFT JOIN artists AS y ON x.id_artists LIKE '%' || y.id || '%' """ ) .. code-block:: python polish_tracks = vo.VastFrame("polish_tracks_clean") # count the number of artists per track polish_tracks.regexp( column = "artists", pattern = ",", method = "count", name = "nb_singers", ) polish_tracks["nb_singers"].add(1) .. ipython:: python :suppress: :okwarning: polish_tracks = vo.VastFrame("polish_tracks_clean") # count the number of artists per track polish_tracks.regexp( column = "artists", pattern = ",", method = "count", name = "nb_singers", ) polish_tracks["nb_singers"].add(1) res = polish_tracks html_file = open("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_polish_tracks_clean_table.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_polish_tracks_clean_table.html Define a list of predictors and the response, and then save the normalized version of the final dataset to the database. .. code-block:: # define predictors and response predictors = [ "duration_minute", # "release_year", "danceability", "energy", "loudness", "speechiness", "acousticness", "instrumentalness", "liveness", "valence", "artists_followers", "artist_popularity", "nb_singers", ] response = "popularity" polish_tracks.normalize( method = "minmax", columns = predictors, ) # save the final dataset to the database vo.drop("polish_tracks_data_final") polish_tracks.to_db("polish_tracks_data_final", relation_type = "table") .. ipython:: python :suppress: :okwarning: predictors = [ "duration_minute", # "release_year", "danceability", "energy", "loudness", "speechiness", "acousticness", "instrumentalness", "liveness", "valence", "artists_followers", "artist_popularity", "nb_singers", ] response = "popularity" polish_tracks.normalize( method = "minmax", columns = predictors, ) vo.drop("polish_tracks_data_final") polish_tracks.to_db("polish_tracks_data_final", relation_type = "table") Machine Learning ----------------- We can use :py:mod:`~vastorbit.machine_learning.vast.automl.AutoML` to easily get a well-performing model. .. ipython:: python # define a random seed so models tested by AutoML produce consistent results vo.set_option("random_state", 2) :py:mod:`~vastorbit.machine_learning.vast.automl.AutoML` automatically tests several machine learning models and picks the best performing one. .. ipython:: python :okwarning: from vastorbit.machine_learning.vast.automl import AutoML # define the model auto_model = AutoML( "automl_spotify_polish", estimator = "fast", preprocess_data = True, stepwise = False, cv = 2, ) Train the model. .. ipython:: python :okwarning: auto_model.fit( "polish_tracks_data_final", predictors, response ) .. code-block:: auto_model.plot() .. ipython:: python :suppress: :okwarning: fig = auto_model.plot() fig.write_html("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_automl_plot.html") .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_automl_plot.html Extract the best model according to :py:mod:`~vastorbit.machine_learning.vast.automl.AutoML`. From here, we can look at the model type and its hyperparameters. .. ipython:: python # extract the model type and hyperparameters best_model = auto_model.best_model_ bm_type = best_model._model_type hyperparams = best_model.get_params() print(bm_type) print(hyperparams) Thanks to :py:mod:`~vastorbit.machine_learning.vast.automl.AutoML`, we know best model type and its hyperparameters. Let's create a new model with this information in mind. .. code-block:: from vastorbit.machine_learning.vast import LinearRegression # define the model rf_model = LinearRegression("linear_regression_spotify", **hyperparams) # train the model rf_model.fit(polish_tracks, predictors, response) # use the model to predict rf_model.predict( polish_tracks, name = "estimated_popularity", ) .. ipython:: python :suppress: :okwarning: from vastorbit.machine_learning.vast import LinearRegression # define the model rf_model = LinearRegression("linear_regression_spotify", **hyperparams) # train the model rf_model.fit(polish_tracks, predictors, response) # use the model to predict res = rf_model.predict( polish_tracks, name = "estimated_popularity", ) html_file = open("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_lr_prediction.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_lr_prediction.html View the regression report and the importance of each feature. .. code-block:: rf_model.regression_report() .. ipython:: python :suppress: :okwarning: res = rf_model.regression_report() html_file = open("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_lr_report.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_lr_report.html .. code-block:: rf_model.features_importance() .. ipython:: python :suppress: :okwarning: fig = rf_model.features_importance() fig.write_html("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_lr_featrures.html") .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_lr_featrures.html To see how our model performs, let's plot the popularity and estimated popularity of songs by other Polish artists like Brodka and Akcent. .. code-block:: # results for Brodka polish_tracks.search( "LOWER(artists) LIKE '%brodka%'", usecols = [ "popularity", "name", "estimated_popularity", ], ).plot( ts = "name", columns = ["popularity", "estimated_popularity"], ) .. ipython:: python :suppress: :okwarning: fig = polish_tracks.search( "LOWER(artists) LIKE '%brodka%'", usecols = [ "popularity", "name", "estimated_popularity", ], ).plot( ts = "name", columns = ["popularity", "estimated_popularity"], ) fig.write_html("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_lr_brodaka_predict_plot.html") .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_lr_brodaka_predict_plot.html .. code-block:: # results for Brodka polish_tracks.search( "LOWER(artists) LIKE '%akcent%'", usecols = [ "popularity", "name", "estimated_popularity", ], ).plot( ts = "name", columns = [ "popularity", "estimated_popularity", ], ) .. ipython:: python :suppress: :okwarning: fig = polish_tracks.search( "LOWER(artists) LIKE '%akcent%'", usecols = [ "popularity", "name", "estimated_popularity", ], ).plot( ts = "name", columns = [ "popularity", "estimated_popularity", ], ) fig.write_html("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_lr_akcent_predict_plot.html") .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_lr_akcent_predict_plot.html Group Artists using Track Features ------------------------------------ While our tracks don't have an explicit "genre" feature, we can approximate the effect by grouping artists based on their tracks' numerical features. Let's start by taking the averages of these numerical features for each artist. .. code-block:: # group by artist artists_features = polish_tracks.groupby( [ "id_artists", "artists", ], expr=[ "AVG(danceability) AS danceability", "AVG(energy) AS energy", "AVG(speechiness) AS speechiness", "AVG(acousticness) AS acousticness", "AVG(instrumentalness) AS instrumentalness", "AVG(valence) AS valence", "AVG(liveness) AS liveness", ] ) # save relation to the database as "artists_features" artists_features.to_db("artists_features") .. ipython:: python :suppress: :okwarning: artists_features = polish_tracks.groupby( [ "id_artists", "artists", ], expr=[ "AVG(danceability) AS danceability", "AVG(energy) AS energy", "AVG(speechiness) AS speechiness", "AVG(acousticness) AS acousticness", "AVG(instrumentalness) AS instrumentalness", "AVG(valence) AS valence", "AVG(liveness) AS liveness", ] ) # save relation to the database as "artists_features" try: artists_features.to_db("artists_features") except: artists_features = vo.VastFrame("artists_features") res = artists_features html_file = open("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_artists_features.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_artists_features.html Grouping means clustering, so we use an :py:func:`~vastorbit.machine_learning.model_selection.elbow` curve to find a suitable number of clusters. .. ipython:: python :okwarning: from vastorbit.machine_learning.model_selection import elbow # define numerical features preds = [ "danceability", "energy", "speechiness", "acousticness", "instrumentalness", "liveness", "valence", ] # elbow curve elbow_curve = elbow( "artists_features", preds, n_clusters = (1, 20), show = True, ) .. code-block:: elbow_curve .. ipython:: python :suppress: :okwarning: fig = elbow_curve fig.write_html("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_lr_elbow.html") .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_lr_elbow.html Let's define and use the VAST :py:mod:`~vastorbit.machine_learning.vast.cluster.KMeans` algorithm to create a model that can group artists together. .. ipython:: python :okwarning: from vastorbit.machine_learning.vast.cluster import KMeans # define k-means model = KMeans( "KMeans_spotify", n_clusters = 7, ) We can train our new model on the ``artists_features`` relation we saved earlier. .. ipython:: python # train the model model.fit( "artists_features", X = preds, ) Plot the result of the k-means algoritm: .. code-block:: model.plot() .. ipython:: python :suppress: :okwarning: fig = model.plot() fig.write_html("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_cluster_plot.html") .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_cluster_plot.html .. ipython:: python # predict the genres pred_genres = model.predict( "artists_features", X = [ "danceability", "energy", "speechiness", "acousticness", "instrumentalness", "liveness", "valence", ], name = "pred_genres", ) Let's see how our model groups these artists together: .. code-block:: # observe the results pred_genres["artists", "pred_genres"].sort({"pred_genres": "desc"}) .. ipython:: python :suppress: :okwarning: res = pred_genres["artists", "pred_genres"].sort({"pred_genres": "desc"}) html_file = open("/Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_pred_genres.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /Users/badr.ouali/Documents/VastOrbit-master/docs/figures/examples_spotify_pred_genres.html Conclusion ----------- We were able to predict the popularity Polish songs with a :py:mod:`~vastorbit.machine_learning.vast.ensemble.RandomForestRegressor` model suggested by :py:mod:`~vastorbit.machine_learning.vast.automl.AutoML`. We then created a :py:mod:`~vastorbit.machine_learning.vast.cluster.KMeans` model to group artists into ``genres`` (clusters) based on the feature-commonalities in their tracks. .. ipython:: python :suppress: from vastorbit._utils._sql._sys import purge_memory purge_memory()