Proyecto: recomendación de canciones en Spotify

Motivación

La industria musical, desde sus inicios, se ha dedicado a la "transmisión" de la música creada por los artistas. Esta transmisión se ha realizado, a lo largo de la historia, de dos maneras principalmente: mediante la puesta en escena de los artistas y mediante la grabación y distribución de las piezas musicales.

Esta industria no se ha quedo al margen de los avances tecnológicos, sino que más bien, los ha utilizado para poder llegar a un público mayor. El eje principal sobre el cual ha evolucionado esta industria corresponde al medio en el que se almacena la música. Entre los principales medios se destacan los siguientes: Disco de vinilo, Cassete y Disco Compacto (CD).

La principal característica que poseían los medios mencionados es que la música creada por los artistas estaba organizada en Álbumes. Estos álbumes usualemente estaban relacionados a una tématica en particular planteada por el artista, por lo que las canciones estaban de alguna manera relaiconadas entre sí.

Uno de los grandes saltos tecnológicos que tuvo la industria fue la aparición de servicios de streaming de música, como por ejemplo Spotify, iMusic, Deezer, etc. Estas plataformas de streaming permiten que el usuario cree conjuntos de música para su futura reproducción. A estos nuevos conjuntos de canciones se les denominó Playlist. Las playlists le entregan al usuario una manera fácil de escuchar sus canciones favoritas, ya que están todas en un solo lugar.

Para aplicaciones que entregan servicio de música online a través de streaming, como Spotify, resulta importante lograr que la creación de playlists sea sencilla, ya que esto permite que los usuario se "queden" y utilicen la plataforma. Para que sea "sencillo" crear playlist, es importante contar con un sistema de recomendación de canciones para cada playlist, ya que así el usuario tendría fácil acceso a sus canciones favoritas.

Es por eso que Spotify liberó un dataset con playlists y sus respectivas canciones para que la comunidad propusiera modelos de recomendación. Esto es de gran interés ya que al poder recomendar automáticamente, los usuarios podrán conocer nuevas canciones que se ajustan a sus estilos favoritos.

Tras observar y realizar un EDA sobre los datos, se pretende responder las siguientes problemáticas:

Problemáticas Iniciales

  1. ¿Es posible caracterizar las playlist en función de la metadata de las canciones que la componen?
  2. Se armará un clasificador que permita determinar si una canción pertenece o no a una playlist.
  3. En base a las características de las playlists, se pretende recomendar canciones éstas.

Base de Datos

Para abordar las problemáticas mencionadas, se ocupó una base de datos proveída por Spotify, la cual contiene el nombre de la playlist y las canciones que incluye. Cabe destacar que la base de datos está destinada para una competencia, en donde se debe recomendar canciones a las playlists. Es por esto que no se tiene información sobre las canciones de todas las playlists, ya que hay ciertas playlists en que sólo se indica su nombre, pero no las canciones que contienen.

El dataset de las playlists se descargó desde https://recsys-challenge.spotify.com/details. La información sobre las canciones se descargó desde la API de Spotify.

Todos los datos necesarios para ejecutar el notebook se encuentran en: https://drive.google.com/open?id=1PBcUeeJOGTvUFm49WCfHwBpIa3V32Cfd

In [1]:
# Importar librerías
import json
import numpy as np
import os
import pandas as pd

challenge_path = os.path.join('./Datos/', "challenge_set.json")

# Cargar tabla de playlists
f = open(challenge_path, 'r')
js = json.load(f)
challenge_df = pd.DataFrame(js['playlists'])
del js
f.close()

# Mostrar las primeras filas de la tabla
challenge_df.head()
Out[1]:
name num_holdouts num_samples num_tracks pid tracks
0 spanish playlist 11 0 11 1000002 []
1 Groovin 48 0 48 1000003 []
2 uplift 40 0 40 1000004 []
3 WUBZ 27 0 27 1000006 []
4 new 41 0 41 1000007 []

En la tabla anterior, se puede ver el número de canciones que tiene la playlist (num_tracks), el número de canciones no mostradas (num_holdouts) y el número de canciones mostradas (num_samples). Además, la columna tracks continen una lista con algunas de las canciones que tiene la base de datos.

In [2]:
print('La base de datos cuenta con información de %s playlists y %s atributos.' %(challenge_df.shape[0], challenge_df.shape[1]))
La base de datos cuenta con información de 10000 playlists y 6 atributos.

Luego, se procedió a obtener los metadatos de las canciones de las que sí se tiene información en la base de datos mencionada anteriormente. A continuación, vemos las primeras filas de la tabla correspondiente a las canciones:

In [5]:
# Cargar tabla de canciones
tracks_path = os.path.join('./Datos/', "tracks.csv")
canciones = pd.read_csv(tracks_path)

# Mostrar las primeras filas de la tabla
canciones.head()
Out[5]:
acousticness album_id album_name album_uri analysis_url artist_id artist_name artist_uri danceability duration_ms ... speechiness tempo time_signature track_href track_id track_name track_uri type uri valence
0 0.7070 4S5MLjwRSi0NJ5nikflYnZ Little Swing spotify:album:4S5MLjwRSi0NJ5nikflYnZ https://api.spotify.com/v1/audio-analysis/66U0... 5vCOdeiQt9LyzdI87kt5Sh AronChupa spotify:artist:5vCOdeiQt9LyzdI87kt5Sh 0.890 163810 ... 0.0621 126.036 4.0 https://api.spotify.com/v1/tracks/66U0ASk1VHZs... 66U0ASk1VHZsqIkpMjKX3B Little Swing spotify:track:66U0ASk1VHZsqIkpMjKX3B audio_features spotify:track:66U0ASk1VHZsqIkpMjKX3B 0.5980
1 0.6010 1qHVYbxQ6IS8YRviorKDJI I'm an Albatraoz spotify:album:1qHVYbxQ6IS8YRviorKDJI https://api.spotify.com/v1/audio-analysis/5Mhs... 5vCOdeiQt9LyzdI87kt5Sh AronChupa spotify:artist:5vCOdeiQt9LyzdI87kt5Sh 0.883 166849 ... 0.2350 128.078 4.0 https://api.spotify.com/v1/tracks/5MhsZlmKJG6X... 5MhsZlmKJG6X5kTHkdwC4B I'm an Albatraoz spotify:track:5MhsZlmKJG6X5kTHkdwC4B audio_features spotify:track:5MhsZlmKJG6X5kTHkdwC4B 0.5950
2 0.0874 4UEPxQx0cTcYNsE0n32MHV Yellow Flicker Beat spotify:album:4UEPxQx0cTcYNsE0n32MHV https://api.spotify.com/v1/audio-analysis/0GZo... 163tK9Wjr9P9DmM0AVK7lm Lorde spotify:artist:163tK9Wjr9P9DmM0AVK7lm 0.586 232507 ... 0.0356 94.965 4.0 https://api.spotify.com/v1/tracks/0GZoB8h0kqXn... 0GZoB8h0kqXn7XFm4Sj06k Yellow Flicker Beat - From The Hunger Games: M... spotify:track:0GZoB8h0kqXn7XFm4Sj06k audio_features spotify:track:0GZoB8h0kqXn7XFm4Sj06k 0.0519
3 0.6340 0rmhjUgoVa17LZuS8xWQ3v Pure Heroine spotify:album:0rmhjUgoVa17LZuS8xWQ3v https://api.spotify.com/v1/audio-analysis/35ka... 163tK9Wjr9P9DmM0AVK7lm Lorde spotify:artist:163tK9Wjr9P9DmM0AVK7lm 0.643 216600 ... 0.0361 114.038 4.0 https://api.spotify.com/v1/tracks/35kahykNu00F... 35kahykNu00FPysz3C2euR White Teeth Teens spotify:track:35kahykNu00FPysz3C2euR audio_features spotify:track:35kahykNu00FPysz3C2euR 0.0901
4 0.1510 0rmhjUgoVa17LZuS8xWQ3v Pure Heroine spotify:album:0rmhjUgoVa17LZuS8xWQ3v https://api.spotify.com/v1/audio-analysis/3G6h... 163tK9Wjr9P9DmM0AVK7lm Lorde spotify:artist:163tK9Wjr9P9DmM0AVK7lm 0.696 193059 ... 0.0904 99.984 4.0 https://api.spotify.com/v1/tracks/3G6hD9B2ZHOs... 3G6hD9B2ZHOsgf4WfNu7X1 Team spotify:track:3G6hD9B2ZHOsgf4WfNu7X1 audio_features spotify:track:3G6hD9B2ZHOsgf4WfNu7X1 0.4620

5 rows × 30 columns

Notar que la base de datos inicial no cuenta con canciones para todas las playlists, por lo que las canciones aquí mostradas solamente tienen relación con algunas de las playlist entregadas por Spotify. A continuación, se muestra un resumen estadístico de la tabla de canciones:

In [6]:
canciones.describe()
Out[6]:
acousticness danceability duration_ms energy instrumentalness key liveness loudness mode pid pos speechiness tempo time_signature valence
count 279992.000000 279992.000000 2.800000e+05 279992.000000 279992.000000 279992.000000 279992.000000 279992.000000 279992.000000 2.800000e+05 280000.000000 279992.000000 279992.000000 279992.000000 279992.000000
mean 0.194102 0.617101 2.326266e+05 0.668169 0.028486 5.174698 0.189731 -6.738014 0.667644 1.025044e+06 59.183311 0.099355 122.319097 3.957785 0.505600
std 0.241884 0.153917 6.314441e+04 0.191519 0.127068 3.620681 0.151825 3.076671 0.471059 1.404465e+04 51.480659 0.103211 28.632735 0.300487 0.232649
min 0.000000 0.000000 0.000000e+00 0.000000 0.000000 0.000000 0.000000 -60.000000 0.000000 1.000000e+06 0.000000 0.000000 0.000000 0.000000 0.000000
25% 0.018900 0.514000 2.000400e+05 0.545000 0.000000 2.000000 0.094600 -8.053000 0.000000 1.012747e+06 14.000000 0.036200 99.962000 4.000000 0.323000
50% 0.084700 0.622000 2.245970e+05 0.694000 0.000001 5.000000 0.128000 -6.109000 1.000000 1.025420e+06 49.000000 0.052400 121.316000 4.000000 0.502000
75% 0.279000 0.728000 2.560130e+05 0.817000 0.000187 8.000000 0.244000 -4.700000 1.000000 1.037142e+06 90.000000 0.115000 140.077000 4.000000 0.688000
max 0.996000 0.988000 9.158194e+06 1.000000 0.998000 11.000000 1.000000 0.878000 1.000000 1.049360e+06 249.000000 0.965000 236.799000 5.000000 0.990000

A partir de la descripción general de la base de datos, se puede apreciar cuales son los atributos más importantes que se considerarán en el análisis futuro. Los atributos seleccionados serán: acousticness, danceability, duration_ms, energy, instrumentalness, key, liveness, loudness, mode, pos, speechiness, tempo, time_signature y valence.

Solamente se seleccionó estas variables, ya que el resto de los atributos que componen la tabla hacen referencia al nombre de la canción y su identificador único en la plataforma de Spotify.

El atributo key no será tomado en consideración, ya que solamente indica el tono en que fue escrita la canción (es un atributo discreto, sobre el cual no tiene sentido calcular promedios). El atributo mode no será considerado por razones similares al atributo anterior. El atributo pos tampoco será considerado, ya que indica la posición relativa que tiene la canción dentro de la playlist. Como primera intuición, esta variable no debiese afectar.

La variable loudness indica qué tan probable es que la canción haya sido grabada en vivo. Se debe tener especial cuidado con este atributo.

La tabla resultante se muestra a continuación:

In [7]:
# Eliminar columnas y mostrar tabla
canciones = canciones.drop(columns=['key', 'mode', 'pos', 'time_signature'])
canciones.describe()
Out[7]:
acousticness danceability duration_ms energy instrumentalness liveness loudness pid speechiness tempo valence
count 279992.000000 279992.000000 2.800000e+05 279992.000000 279992.000000 279992.000000 279992.000000 2.800000e+05 279992.000000 279992.000000 279992.000000
mean 0.194102 0.617101 2.326266e+05 0.668169 0.028486 0.189731 -6.738014 1.025044e+06 0.099355 122.319097 0.505600
std 0.241884 0.153917 6.314441e+04 0.191519 0.127068 0.151825 3.076671 1.404465e+04 0.103211 28.632735 0.232649
min 0.000000 0.000000 0.000000e+00 0.000000 0.000000 0.000000 -60.000000 1.000000e+06 0.000000 0.000000 0.000000
25% 0.018900 0.514000 2.000400e+05 0.545000 0.000000 0.094600 -8.053000 1.012747e+06 0.036200 99.962000 0.323000
50% 0.084700 0.622000 2.245970e+05 0.694000 0.000001 0.128000 -6.109000 1.025420e+06 0.052400 121.316000 0.502000
75% 0.279000 0.728000 2.560130e+05 0.817000 0.000187 0.244000 -4.700000 1.037142e+06 0.115000 140.077000 0.688000
max 0.996000 0.988000 9.158194e+06 1.000000 0.998000 1.000000 0.878000 1.049360e+06 0.965000 236.799000 0.990000

Pre-procesamiento

Para el estudio, solamente se considerarán aquellas playlists para las que se tiene información de 100 canciones. La decisión de realizar esto es para obtener resultados relevantes y conclusiones que sean apoyadas con una buena cantidad de datos. A partir del análisis previo, se puede observar que el número máximo de canciones que puede tener una playlist en la base de datos entregada inicialmente es de 250 canciones, por lo que 100 es una muestra considerable de la playlist. A continuación, se elimina de la tabla de canciones todas aquellas que no pertenezcan a una playlist con 100 canciones:

In [8]:
# Obtenemos una lista con los id de las playlist que tienen 100 canciones.
songsnot100 = challenge_df[challenge_df['num_samples'] != 100]['pid']
exclude = list(songsnot100)
canciones = canciones[~np.in1d(canciones.pid, exclude)]

Exploratory data analysis

In [9]:
# Importar librerías para graficar
import matplotlib.pyplot as plt  
import seaborn as sb

A continuación, se muestra información sobre la tabla de canciones y sus atributos:

In [10]:
canciones.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 200000 entries, 80000 to 279999
Data columns (total 26 columns):
acousticness        199995 non-null float64
album_id            200000 non-null object
album_name          200000 non-null object
album_uri           200000 non-null object
analysis_url        199995 non-null object
artist_id           200000 non-null object
artist_name         200000 non-null object
artist_uri          200000 non-null object
danceability        199995 non-null float64
duration_ms         200000 non-null int64
energy              199995 non-null float64
id                  199995 non-null object
instrumentalness    199995 non-null float64
liveness            199995 non-null float64
loudness            199995 non-null float64
pid                 200000 non-null int64
pl_name             200000 non-null object
speechiness         199995 non-null float64
tempo               199995 non-null float64
track_href          199995 non-null object
track_id            200000 non-null object
track_name          200000 non-null object
track_uri           200000 non-null object
type                199995 non-null object
uri                 199995 non-null object
valence             199995 non-null float64
dtypes: float64(9), int64(2), object(15)
memory usage: 41.2+ MB

Se procederá a realizar histogramas de los atributos de interés para observar sus distribuciones, considerando todas las canciones:

In [11]:
canciones.hist(bins = 30, figsize = (12, 10));

Se observará con mayor detalle los atributos instrumentalness y duration_ms:

In [12]:
# Graficar atributos
plt.scatter(canciones['pid'], canciones['duration_ms'])
plt.title('Scatter plot de duration_ms')
plt.show()


plt.hist(canciones['instrumentalness'].dropna())
plt.title('Histograma de instrumentalness')
plt.show()

print('Hay %s canciones con instrumentalness mayor a 0.1.' %len(canciones[canciones['instrumentalness']>0.1]))
Hay 10604 canciones con instrumentalness mayor a 0.1.
  1. Se puede observar que la mayoría de las canciones en la base de datos tiene una duración menor a 2000 segundos.
  2. A partir del segundo gráfico, se puede apreciar que la mayoría de las canciones están vocalizadas, mientras que una menor cantidad es instrumental (solamente 10.604 de 200.000).

En base a lo anterior, se eliminará aquellos outliers para disminuir el ruido:

In [13]:
# Eliminar outliers en relación a duration_ms e instrumentalness
print("Previo a quitar los outliers hay %s canciones." %canciones.shape[0])
canciones = canciones[canciones['duration_ms'] <= 2000000]
canciones = canciones[canciones['instrumentalness'] <= 0.5]
print("Post a quitar los outliers hay %s canciones." %canciones.shape[0])
Previo a quitar los outliers hay 200000 canciones.
Post a quitar los outliers hay 195462 canciones.

Finalmente, para caracterizar las playlists a partir de los atributos de las canciones que contienen, se promediarán sus atributos:

In [27]:
# Agrupar por playlists tomando el promedio de todas sus canciones
playlist_df = canciones.groupby(('pid', 'pl_name')).mean()
print("La tabla de playlists cuenta con {} filas".format(len(playlist_df)))
playlist_df.head(10)
La tabla de playlists cuenta con 2000 filas
/usr/local/lib/python2.7/dist-packages/ipykernel_launcher.py:2: FutureWarning: Interpreting tuple 'by' as a list of keys, rather than a single key. Use 'by=[...]' instead of 'by=(...)'. In the future, a tuple will always mean a single key.
  
Out[27]:
acousticness danceability duration_ms energy instrumentalness liveness loudness speechiness tempo valence
pid pl_name
1010382 April 0.304294 0.545085 213171.353659 0.582679 0.022808 0.168213 -7.205220 0.103548 109.487512 0.427698
1014403 Other 0.081241 0.563717 221503.606061 0.798242 0.008328 0.211580 -5.369485 0.060220 121.691384 0.540153
1014426 Classic 0.287212 0.506532 257260.574468 0.565684 0.034029 0.171962 -10.833351 0.067238 115.191872 0.560317
1014427 2016 0.123897 0.578071 217912.510204 0.700857 0.004279 0.180082 -6.317163 0.093508 124.548122 0.403300
1014441 Party songs 0.162386 0.691770 229080.240000 0.639990 0.000621 0.185040 -6.192210 0.139403 121.452470 0.480540
1014466 Country Hits 0.158167 0.557970 224655.909091 0.756061 0.000426 0.181929 -5.320202 0.041066 136.018515 0.567813
1014471 jay 0.371685 0.540081 228130.535354 0.552330 0.013258 0.163309 -7.720263 0.066422 120.863727 0.369981
1014481 rap 0.123959 0.757860 228117.970000 0.621200 0.000248 0.225002 -6.513930 0.196081 130.110210 0.465212
1014486 Rap 0.182314 0.686360 236780.520000 0.609360 0.001008 0.187068 -7.124570 0.200208 128.002240 0.377465
1014498 party playlist 0.152325 0.719150 228230.780000 0.653530 0.000279 0.217102 -6.136210 0.180989 131.766460 0.512890

Para el análisis siguiente, se asumió que las playlists están bien descritas por el promedio de los atributos de sus canciones. A continuación, se muestra la scatter plot matrix de la tabla de playlists anterior:

In [28]:
sb.pairplot(playlist_df);

Se puede ver que existe un correlación alta entre acousticness y energy, entre acousticness y loudness y entre energy y loudness. El resto de los pares de atributos no parecen estar correlacionados.

Clustering

A partir de la tabla obtenida, se quiere estudiar si es posible agrupar las playlists en conjuntos con características similares. Para esto, en primer lugar se redujo la dimensionalidad a dos para poder visualizar la distribución de playlists, utilizando el método de Principal component analysis (PCA)

In [32]:
# Definir nuevo dataframe con los datos escalados
from sklearn import preprocessing
clusters_df = playlist_df
clusters_df_scaled = pd.DataFrame(preprocessing.scale(clusters_df)) 

# Guardat dataframe
clusters_df_scaled.to_csv("clusters_scaled.csv", index=False)

# Usar PCA para graficar en dos dimensiones
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
coords = pca.fit(clusters_df_scaled).transform(clusters_df_scaled)
x = coords[:, 0]
y = coords[:, 1]

fig, ax = plt.subplots(figsize=(10, 8))
ax.scatter(x, y);
In [34]:
clusters_df_scaled.shape
Out[34]:
(2000, 10)

Luego de probar con distintos atributos, se decidió eliminar tempo y duration_ms. El resultado se muestra a continuación:

In [30]:
# Eliminar columnas
clusters_df = playlist_df.drop(columns=['tempo', 'duration_ms'])
clusters_df_scaled = pd.DataFrame(preprocessing.scale(clusters_df))

# Usar PCA para graficar en dos dimensiones
from sklearn.decomposition import PCA
pca = PCA(n_components=2)
coords = pca.fit(clusters_df_scaled).transform(clusters_df_scaled)
x = coords[:, 0]
y = coords[:, 1]
1
fig, ax = plt.subplots(figsize=(10, 8))
ax.scatter(x, y);

clusters_df.head()
Out[30]:
acousticness danceability energy instrumentalness liveness loudness speechiness valence
pid pl_name
1010382 April 0.304294 0.545085 0.582679 0.022808 0.168213 -7.205220 0.103548 0.427698
1014403 Other 0.081241 0.563717 0.798242 0.008328 0.211580 -5.369485 0.060220 0.540153
1014426 Classic 0.287212 0.506532 0.565684 0.034029 0.171962 -10.833351 0.067238 0.560317
1014427 2016 0.123897 0.578071 0.700857 0.004279 0.180082 -6.317163 0.093508 0.403300
1014441 Party songs 0.162386 0.691770 0.639990 0.000621 0.185040 -6.192210 0.139403 0.480540

Para el proceso de clustering, se decidió usar k-means. Este algoritmo requiere fijar desde un inicio el total de clusters a encontrar. Para determinar el número óptimo de clusters, se usó el método del codo como se muestra a continuación:

In [20]:
# Usamos el método del codo para búsqueda de número de clusters óptimo con kmeans
from sklearn.cluster import KMeans
from sklearn import metrics
from scipy.spatial.distance import cdist

distortions = []
K = range(1,20)
for k in K:
    kmeanModel = KMeans(n_clusters=k).fit(clusters_df_scaled)
    kmeanModel.fit(clusters_df_scaled)
    distortions.append(sum(np.min(cdist(clusters_df_scaled, kmeanModel.cluster_centers_, 'euclidean'), axis=1)) / clusters_df_scaled.shape[0])

# Graficar
fig, ax = plt.subplots(figsize=(10, 8))
plt.plot(K, distortions, 'bx-')
plt.xlabel('k')
plt.ylabel('Distortion')
plt.title('The Elbow Method showing the optimal k whith k-means')
plt.show()

El número de clusters óptimo encontrado es k = 8. A continuación, se muestra el resultado del clustering, primero utilizando PCA para reducir los datos a dos dimensiones y luego utilizando TSNE.

In [58]:
# Clustering k-means
kmeans = KMeans(n_clusters=8, random_state=0).fit(clusters_df_scaled)

# Gráfico con PCA
fig, ax = plt.subplots(figsize=(10, 8))
plt.scatter(x, y, c=kmeans.labels_, cmap='viridis', alpha=0.5);
plt.colorbar()
plt.show()

print(kmeans.inertia_)
print(np.bincount(kmeans.labels_))
8595.362997914424
[354 117 371 202 521 399  31   5]
In [80]:
# Gráfico con TSNE
from sklearn.manifold import TSNE
embedded = TSNE(n_components=2).fit_transform(clusters_df_scaled)
TSNE_x= embedded[:, 0]
TSNE_y = embedded[:, 1]

fig, ax = plt.subplots(figsize=(10, 8))
plt.scatter(TSNE_x, TSNE_y, c=kmeans.labels_, cmap='viridis', alpha=0.5);
plt.colorbar()
plt.show()

También se realizo un clustering jerárquico de los datos utilizando la función hclust de R, cortando el árbol al nivel 8 para poder comparar con los resultados de k-means. El resultado del clustering se guardó en el archivo hc_df.csv. A continuación, se muestra el resultado:

In [90]:
# Clustering jerárquico
df_hc = pd.read_csv("hc_df.csv")
hc_mean = df_hc.groupby("X11").mean()
hc_mean = hc_mean.drop(df_hc.columns[0], axis=1)

# Gráfico con PCA
fig, ax = plt.subplots(figsize=(10, 8))
plt.scatter(x, y, c=df_hc.X11, cmap='viridis', alpha=0.5);
plt.colorbar()
plt.show()

print(df_hc.X11.value_counts())
hc_mean
2    1346
3     278
1     271
5      56
4      30
6      14
7       3
8       2
Name: X11, dtype: int64
Out[90]:
X1 X2 X3 X4 X5 X6 X7 X8 X9 X10
X11
1 0.882065 -0.458637 0.460205 -0.919941 0.534798 -0.550871 -0.761658 -0.441532 -0.708999 -0.637036
2 -0.254298 0.025362 -0.275916 0.300719 -0.175052 -0.080669 0.270677 -0.114283 0.350509 0.185241
3 -0.353424 0.941798 0.480298 0.052508 -0.308384 0.781579 0.178611 1.256484 -0.765779 0.177079
4 -0.461905 -1.119008 1.875501 0.717222 2.207156 0.903166 -0.577194 -0.513290 0.488256 0.262101
5 3.377368 -1.510663 -0.432468 -2.749832 0.906729 -0.847640 -2.379232 -0.769163 -1.372468 -1.404341
6 0.020687 -2.495569 5.034080 -0.733543 -0.282001 4.819749 -0.458072 -1.004549 0.713442 -2.581249
7 4.512122 -3.532701 -2.121010 -5.361591 12.069639 -1.649378 -10.370679 -1.053237 -2.546371 -3.961035
8 6.197840 -3.981570 8.491619 -5.616211 13.584067 -0.783882 -9.749225 -0.063666 -3.448898 -3.562509

El error intracluster para el caso de k-means fue de 8595, mientras que en el caso del cluster jerárquico fue de 11907, por lo tanto se escogió el primero para el análisis posterior.

In [62]:
# Incluimos los cluster como variable en el dataframe
playlist_df["Cluster"] = kmeans.labels_
playlist_df.head()
Out[62]:
acousticness danceability duration_ms energy instrumentalness liveness loudness speechiness tempo valence Cluster
pid pl_name
1010382 April 0.304294 0.545085 213171.353659 0.582679 0.022808 0.168213 -7.205220 0.103548 109.487512 0.427698 0
1014403 Other 0.081241 0.563717 221503.606061 0.798242 0.008328 0.211580 -5.369485 0.060220 121.691384 0.540153 4
1014426 Classic 0.287212 0.506532 257260.574468 0.565684 0.034029 0.171962 -10.833351 0.067238 115.191872 0.560317 3
1014427 2016 0.123897 0.578071 217912.510204 0.700857 0.004279 0.180082 -6.317163 0.093508 124.548122 0.403300 4
1014441 Party songs 0.162386 0.691770 229080.240000 0.639990 0.000621 0.185040 -6.192210 0.139403 121.452470 0.480540 2
In [63]:
# Agregamos los atributos según clusters
mean = pd.DataFrame(playlist_df.groupby('Cluster').aggregate('mean'))
variance = pd.DataFrame(playlist_df.groupby('Cluster').aggregate('var'))
mean.head()
Out[63]:
acousticness danceability duration_ms energy instrumentalness liveness loudness speechiness tempo valence
Cluster
0 0.240796 0.603686 231414.576462 0.618621 0.011516 0.171700 -6.995790 0.086286 119.067579 0.445974
1 0.509338 0.524256 223400.968834 0.456931 0.019519 0.169673 -9.722965 0.061298 116.923677 0.402941
2 0.153298 0.709847 234275.208191 0.642125 0.004317 0.205110 -6.572587 0.192755 123.262168 0.454457
3 0.215349 0.577573 248507.843000 0.670135 0.024399 0.190517 -8.311283 0.057712 122.535481 0.603782
4 0.133952 0.566155 223277.581236 0.757462 0.007756 0.188499 -5.551763 0.059873 127.581143 0.543129

A partir de los resultados de la tabla anterior, se podría diferenciar y nombrar las características de cada cluster, según sus atributos promedio. En general, la mayoría de los atributos presentan diferencia entre los clusters, porque las variables incluidas en el modelo permiten generar categorías a partir de los atributos, es decir, las playlist son caracterizables.

A continuación, se muestra un word cloud en base a los nombres de playlist de dos clusters en particular.

In [4]:
from IPython.display import Image
Image("./Datos/cluster1.png")
Out[4]:
In [3]:
Image("./Datos/cluster2.png")
Out[3]:

Podemos ver claramente que playlists de un mismo estilo musical quedaron en un mismo cluster, lo que confirma que las playlists quedaron caracterizadas por los atributos de las canciones que la componen.

Clasificadores

Finalmente, se implementó un clasificador. Dadas 6 playlists elegidas al azar, se consideró cada una de ellas como una clase y se intentó determinar a qué playlist pertenecía una canción dada. Para esto, se usó los algoritmos de K-nearest neighbors con k=19 (valor con el cual se obtuvo los mejores resultados), y Support vector machine. Los resultados se muestran a continuación:

In [5]:
Image("./Datos/knn_19.png")
Out[5]:
In [7]:
Image("./Datos/svm.png")
Out[7]:

Vemos que los mejores resultados se obtuvieron con K-nearest neighbors. Sin embargo, los resultados no son para nada satisfactorios.

Conclusiones

El resultado del clustering aplicado sobre las playlists, agrupó de forma efectiva aquellas de estilos musicales similares. Esto confirma la suposición de que las playlists quedaban bien descritas por los atributos de sus canciones. Sin embargo, es importante notar que al promediar los atributos de todas las canciones pertenecientes a una playlist, se pierde mucha información sobre la dispersión de los datos que podría ser importante para un análisis futuro.

Los resultados de los clasificadores implementados no fueron satisfactorios. Esto podría deberse a que no se contaba con tantas canciones para cada playlist, ni tantos atributos para cada canción. Esto también confirma que el problema de recomendación es complejo y requiere de otros algoritmos para ser llevado a cabo.

Para un análisis futuro, podría ser interesante utilizar reglas de asociación, donde cada playlist puede ser vista como una transacción y los items corresponden a los nombres de las canciones. De esta forma, se podría determinar si la ocurrencia de dos o más canciones en una misma playlist están correlacionadas con la aparición de otra canción dentro de la misma.