.. _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. You can download the Jupyter notebook of this study `here `_. .. 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 VerticaPy: .. ipython:: python import verticapy as vp vp.__version__ Start by importing VerticaPy and loading the SQL extension, which allows you to query the Vertica database with SQL. .. ipython:: python %load_ext verticapy.sql Connect to Vertica. This example uses an existing connection called ``VerticaDSN``. 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 vp.connect("VerticaDSN") Create a new schema, "spotify". .. ipython:: python vp.drop("spotify", method = "schema") vp.create_schema("spotify") Data Loading ------------- Load the datasets into the :py:mod:`~verticapy.vDataFrame` with :py:func:`~verticapy.read_csv` and then view them with :py:func:`~verticapy.vDataFrame.head`. .. code-block:: # load datasets as vDataFrame objects artists = vp.read_csv("artists.csv", schema = "spotify", parse_nrows = 100) tracks = vp.read_csv("tracks.csv" , schema = "spotify", parse_nrows = 100) # Display artists.head(100) .. ipython:: python :suppress: artists = vp.read_csv( "/project/data/VerticaPy/docs/source/_static/website/examples/data/spotify/artists.csv", schema = "spotify", parse_nrows = 100, ) res = artists.head(100) html_file = open("/project/data/VerticaPy/docs/figures/examples_spotify_artists_table.html", "w") html_file.write(res._repr_html_()) html_file.close() .. code-block:: tracks.head(100) .. raw:: html :file: /project/data/VerticaPy/docs/figures/examples_spotify_artists_table.html .. ipython:: python :suppress: tracks = vp.read_csv("/project/data/VerticaPy/docs/source/_static/website/examples/data/spotify/tracks.csv",schema = "spotify",parse_nrows = 100) res = tracks.head(100) html_file = open("/project/data/VerticaPy/docs/figures/examples_spotify_tracks_table.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /project/data/VerticaPy/docs/figures/examples_spotify_tracks_table.html .. warning:: This example uses a sample dataset. For the full analysis, you should consider using the complete dataset. Since we are only focusing on Polish artists in this subset of data, let us save it in the database with the proper name. .. code-block:: polish_artists = artists # save it to the database polish_artists.to_db('"spotify"."polish_artists"', relation_type = "table") .. ipython:: python :suppress: polish_artists = artists # save it to the database vp.drop("spotify.polish_artists") polish_artists.to_db('"spotify"."polish_artists"', relation_type = "table") Data Exploration ----------------- We can visualize the top ``60`` most-followed Polish artists with a bar chart. .. code-block:: python # make a highchart of the top 50 most-followed Polish artists polish_artists.bar( ["name"], method = "mean", of = "followers", max_cardinality = 50, width = 800, ) .. ipython:: python :suppress: vp.set_option("plotting_lib","plotly") fig = polish_artists.bar( ["name"], method = "mean", of = "followers", max_cardinality = 50, width = 800, ) fig.write_html("/project/data/VerticaPy/docs/figures/examples_spotify_polish_followers_bar.html") .. raw:: html :file: /project/data/VerticaPy/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("artists ilike '%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("artists ilike '%brodka%'") fig = brodka_tracks.bar( ["name"], method = "mean", of = "popularity", max_cardinality = 25, width = 800, ) fig.write_html("/project/data/VerticaPy/docs/figures/examples_spotify_brodka_popularity_bar.html") .. raw:: html :file: /project/data/VerticaPy/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("/project/data/VerticaPy/docs/figures/examples_spotify_boxplot.html") .. raw:: html :file: /project/data/VerticaPy/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(release_date::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(release_date::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("/project/data/VerticaPy/docs/figures/examples_spotify_brodka_release_plot.html") .. raw:: html :file: /project/data/VerticaPy/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(release_date::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(release_date::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("/project/data/VerticaPy/docs/figures/examples_spotify_brodka_release_plot.html") .. raw:: html :file: /project/data/VerticaPy/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("/project/data/VerticaPy/docs/figures/examples_spotify_tracks_corr.html") .. raw:: html :file: /project/data/VerticaPy/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 spotify.polish_tracks; CREATE TABLE spotify.polish_tracks AS SELECT * FROM spotify.tracks WHERE id_artists IN (SELECT t.id_artists FROM spotify.tracks t JOIN spotify.polish_artists p ON t.id_artists LIKE '%' || p.id || '%'); CREATE TABLE spotify.polish_tracks_clean AS SELECT x.*, x.duration_ms / 60000 AS duration_minute, x.release_date::date AS release_year, y.followers AS artists_followers, y.popularity AS artist_popularity FROM spotify.polish_tracks AS x LEFT JOIN spotify.artists AS y ON x.id_artists LIKE '%' || y.id || '%'; .. ipython:: python :okwarning: :suppress: from verticapy._utils._sql._sys import _executeSQL _executeSQL( """ DROP TABLE IF EXISTS spotify.polish_tracks; CREATE TABLE spotify.polish_tracks AS SELECT * FROM spotify.tracks WHERE id_artists IN (SELECT t.id_artists FROM spotify.tracks t JOIN spotify.polish_artists p ON t.id_artists LIKE '%' || p.id || '%'); CREATE TABLE spotify.polish_tracks_clean AS SELECT x.*, x.duration_ms / 60000 AS duration_minute, x.release_date::date AS release_year, y.followers AS artists_followers, y.popularity AS artist_popularity FROM spotify.polish_tracks AS x LEFT JOIN spotify.artists AS y ON x.id_artists LIKE '%' || y.id || '%'; """ ) .. code-block:: python polish_tracks = vp.vDataFrame("spotify.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 = vp.vDataFrame("spotify.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("/project/data/VerticaPy/docs/figures/examples_spotify_polish_tracks_clean_table.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /project/data/VerticaPy/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 vp.drop("spotify.polish_tracks_data_final") polish_tracks.to_db('"spotify"."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, ) vp.drop("spotify.polish_tracks_data_final") polish_tracks.to_db('"spotify"."polish_tracks_data_final"', relation_type = "table") Machine Learning ----------------- We can use :py:mod:`~verticapy.machine_learning.vertica.automl.AutoML` to easily get a well-performing model. .. ipython:: python # define a random seed so models tested by AutoML produce consistent results vp.set_option("random_state", 2) :py:mod:`~verticapy.machine_learning.vertica.automl.AutoML` automatically tests several machine learning models and picks the best performing one. .. ipython:: python :okwarning: from verticapy.machine_learning.vertica.automl import AutoML # define the model auto_model = AutoML( "spotify.automl_spotify_polish", estimator = "fast", preprocess_data = True, stepwise = False, cv = 2, ) Train the model. .. ipython:: python :okwarning: auto_model.fit( "spotify.polish_tracks_data_final", predictors, response ) .. code-block:: auto_model.plot() .. ipython:: python :suppress: :okwarning: fig = auto_model.plot() fig.write_html("/project/data/VerticaPy/docs/figures/examples_spotify_automl_plot.html") .. raw:: html :file: /project/data/VerticaPy/docs/figures/examples_spotify_automl_plot.html Extract the best model according to :py:mod:`~verticapy.machine_learning.vertica.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:`~verticapy.machine_learning.vertica.automl.AutoML`, we know best model type and its hyperparameters. Let's create a new model with this information in mind. .. code-block:: from verticapy.machine_learning.vertica import LinearRegression # define the model rf_model = LinearRegression("spotify.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 verticapy.machine_learning.vertica import LinearRegression if "C" in hyperparams: hyperparams.pop("C") if "l1_ratio" in hyperparams: hyperparams.pop("l1_ratio") # define the model rf_model = LinearRegression("spotify.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("/project/data/VerticaPy/docs/figures/examples_spotify_lr_prediction.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /project/data/VerticaPy/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("/project/data/VerticaPy/docs/figures/examples_spotify_lr_report.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /project/data/VerticaPy/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("/project/data/VerticaPy/docs/figures/examples_spotify_lr_featrures.html") .. raw:: html :file: /project/data/VerticaPy/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("/project/data/VerticaPy/docs/figures/examples_spotify_lr_brodaka_predict_plot.html") .. raw:: html :file: /project/data/VerticaPy/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("/project/data/VerticaPy/docs/figures/examples_spotify_lr_akcent_predict_plot.html") .. raw:: html :file: /project/data/VerticaPy/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('"spotify"."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" artists_features.to_db('"spotify"."artists_features"') res = artists_features html_file = open("/project/data/VerticaPy/docs/figures/examples_spotify_artists_features.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /project/data/VerticaPy/docs/figures/examples_spotify_artists_features.html Grouping means clustering, so we use an :py:func:`~verticapy.machine_learning.model_selection.elbow` curve to find a suitable number of clusters. .. ipython:: python :okwarning: from verticapy.machine_learning.model_selection import elbow # define numerical features preds = [ "danceability", "energy", "speechiness", "acousticness", "instrumentalness", "liveness", "valence", ] # elbow curve elbow_curve = elbow( '"spotify"."artists_features"', preds, n_cluster = (1, 20), show = True, ) .. code-block:: elbow_curve .. ipython:: python :suppress: :okwarning: fig = elbow_curve fig.write_html("/project/data/VerticaPy/docs/figures/examples_spotify_lr_elbow.html") .. raw:: html :file: /project/data/VerticaPy/docs/figures/examples_spotify_lr_elbow.html Let's define and use the Vertica :py:mod:`~verticapy.machine_learning.vertica.cluster.KMeans` algorithm to create a model that can group artists together. .. ipython:: python :okwarning: from verticapy.machine_learning.vertica.cluster import KMeans # define k-means model = KMeans( '"spotify"."KMeans_spotify"', n_cluster = 7, ) We can train our new model on the ``artists_features`` relation we saved earlier. .. ipython:: python # train the model model.fit( '"spotify"."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("/project/data/VerticaPy/docs/figures/examples_spotify_cluster_plot.html") .. raw:: html :file: /project/data/VerticaPy/docs/figures/examples_spotify_cluster_plot.html .. ipython:: python # predict the genres pred_genres = model.predict( '"spotify"."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("/project/data/VerticaPy/docs/figures/examples_spotify_pred_genres.html", "w") html_file.write(res._repr_html_()) html_file.close() .. raw:: html :file: /project/data/VerticaPy/docs/figures/examples_spotify_pred_genres.html Conclusion ----------- We were able to predict the popularity Polish songs with a :py:mod:`~verticapy.machine_learning.vertica.ensemble.RandomForestRegressor` model suggested by :py:mod:`~verticapy.machine_learning.vertica.automl.AutoML`. We then created a :py:mod:`~verticapy.machine_learning.vertica.cluster.KMeans` model to group artists into ``genres`` (clusters) based on the feature-commonalities in their tracks.