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:
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
# 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()
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.
print('La base de datos cuenta con información de %s playlists y %s atributos.' %(challenge_df.shape[0], challenge_df.shape[1]))
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:
# 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()
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:
canciones.describe()
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:
# Eliminar columnas y mostrar tabla
canciones = canciones.drop(columns=['key', 'mode', 'pos', 'time_signature'])
canciones.describe()
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:
# 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)]
# 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:
canciones.info()
Se procederá a realizar histogramas de los atributos de interés para observar sus distribuciones, considerando todas las canciones:
canciones.hist(bins = 30, figsize = (12, 10));
Se observará con mayor detalle los atributos instrumentalness y duration_ms:
# 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]))
En base a lo anterior, se eliminará aquellos outliers para disminuir el ruido:
# 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])
Finalmente, para caracterizar las playlists a partir de los atributos de las canciones que contienen, se promediarán sus atributos:
# 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)
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:
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.
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)
# 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);
clusters_df_scaled.shape
Luego de probar con distintos atributos, se decidió eliminar tempo y duration_ms. El resultado se muestra a continuación:
# 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()
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:
# 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.
# 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_))
# 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:
# 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
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.
# Incluimos los cluster como variable en el dataframe
playlist_df["Cluster"] = kmeans.labels_
playlist_df.head()
# 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()
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.
from IPython.display import Image
Image("./Datos/cluster1.png")
Image("./Datos/cluster2.png")
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.
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:
Image("./Datos/knn_19.png")
Image("./Datos/svm.png")
Vemos que los mejores resultados se obtuvieron con K-nearest neighbors. Sin embargo, los resultados no son para nada satisfactorios.
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.