Spotify Analysis
Análisis de canciones escuchadas con Spotify
La API brinda un montón de información por cada track escuchado, pero solo se tuvieron en cuenta para el análisis la duración, popularidad y ubicación de la canción en album (número de track).
Origen de los datos: API oficial (https://developer.spotify.com/web-api/) usando la librería spotipy (https://github.com/plamere/spotipy).
Cantidad: Aproximadamente los últimos 50 tracks escuchados.
Pasos previos para obtener la info vía la API y guardarla en un .csv
# Obtener la info vía la API
# sudo pip3 install spotipy
# oficial api doc: https://developer.spotify.com/web-api/
# python spotify library: https://github.com/plamere/spotipy
# Doc Reference: http://spotipy.readthedocs.io/en/latest/
# Sacar token en Spotify.com: https://developer.spotify.com/my-applications/
# Examples: https://github.com/plamere/spotipy/tree/master/examples
import spotipy
import spotipy.util as util
import json
import urllib3
#import os
#os.environ["SPOTIPY_CLIENT_ID"] = ""
#os.environ["SPOTIPY_CLIENT_SECRET"] = ""
#os.environ["SPOTIPY_REDIRECT_URI"] = "http://localhost:8888/callback"
#username = os.environ["SPOTIFY_USERNAME"]
scope = 'user-read-recently-played'
cantidad = 50
token = util.prompt_for_user_token(username, scope)
# Obtener los últimos 50 tracks escuchados
http = urllib3.PoolManager()
url = 'https://api.spotify.com/v1/me/player/recently-played?limit='+str(cantidad)
headers = {'Content-Type': 'application/json',
'Authorization': 'Bearer '+token}
r = http.request('GET', url, headers=headers)
# Grabar toda la información en archivos json por separado
#tracks = json.loads(r.data)
#for item in tracks['items']:
# track = item['track']
# track['played_at'] = item['played_at']
# with open(dirname+track['id']+'.json', 'w') as out:
# out.write(json.dumps(track))
# Grabar el .csv
#x = pd.DataFrame(data=[])
#id_list = list()
#duration_list = list()
#popularity_list = list()
#popularity_list = list()
#track_number_list = list()
#played_at_list = list()
#for subdir, dirs, files in os.walk(files_dir):
# for f in files:
# if os.path.splitext(f)[1]==".json":
# data = json.load( open(files_dir + "/" + f,'r') )
# id_list.append( os.path.splitext(f)[0] )
# duration_list.append( data['duration_ms'] )
# popularity_list.append( data['popularity'] )
# track_number_list.append( data['track_number'] )
# played_at_list.append( data['played_at'] )
#x['spotify_id'] = id_list
#x['duration_ms'] = duration_list
#x['popularity'] = popularity_list
#x['track_number'] = track_number_list
#x['played_at'] = played_at_list
#x.to_csv('canciones_escuchadas.csv') # grabar .csv
Se levanta dataset del .csv
import pandas as pd
x = pd.DataFrame.from_csv('dataset-spotify/canciones_escuchadas.csv')
x
spotify_id | duration_ms | popularity | track_number | played_at | |
---|---|---|---|---|---|
index | |||||
0 | 05j5TH07znU7BPbz21pYbk | 167151 | 0 | 3 | 2017-08-08T05:16:19.223Z |
1 | 0B5vHrBko4yCY0ExpUGSiq | 264426 | 16 | 6 | 2017-08-02T04:39:14.920Z |
2 | 0oMyfSwnXVaQAZ28ZcjWGA | 250640 | 44 | 1 | 2017-08-03T06:35:39.996Z |
3 | 0QwZfbw26QeUoIy82Z2jYp | 166266 | 71 | 1 | 2017-09-15T23:33:53.637Z |
4 | 10SLqGymC9D3pkS0HO8CRW | 370111 | 0 | 7 | 2017-08-15T06:40:05.175Z |
5 | 13x5DIPCUlYCIbU9n4gE7P | 248546 | 51 | 6 | 2017-08-30T01:05:40.315Z |
6 | 16k2l6Mr9CIGdZ9B41964P | 643155 | 0 | 6 | 2017-08-08T05:27:43.196Z |
7 | 17rf2oZYDVymJeYI9ftDXc | 309973 | 55 | 3 | 2017-09-10T23:45:03.023Z |
8 | 1b1BxkyPd5NMbmPdpVzdzu | 255866 | 51 | 1 | 2017-08-30T00:54:12.178Z |
9 | 1jzDzZWeSDBg5fhNc3tczV | 166693 | 74 | 2 | 2017-09-15T23:36:49.526Z |
10 | 1kmt371vlHtTxQESLo1ekK | 188933 | 24 | 7 | 2017-08-01T07:10:06.887Z |
11 | 1MD4tX2g5hx0D2WQ6JsC2m | 210666 | 58 | 2 | 2017-08-02T04:23:52.112Z |
12 | 1QUNv7oFOr9kAqer7xCbRt | 260879 | 37 | 2 | 2017-08-25T06:25:59.071Z |
13 | 24TX7lkw91Btkt7KJGvQgP | 156706 | 19 | 2 | 2017-08-02T04:25:12.586Z |
14 | 2Bc4llhjJBW77I552RgA3L | 464293 | 59 | 3 | 2017-08-01T07:19:03.436Z |
15 | 2ClyVqe2QMALHpswQvOsyU | 337733 | 43 | 9 | 2017-08-30T00:37:41.172Z |
16 | 2D0ZzJCT6M93QVWbrhS2ga | 470066 | 47 | 6 | 2017-09-12T00:28:26.116Z |
17 | 2EFGPGdkzfkuVKQj4WwQrG | 201506 | 15 | 5 | 2017-08-02T04:35:53.407Z |
18 | 2MZSXhq4XDJWu6coGoXX1V | 125520 | 71 | 9 | 2017-08-03T06:49:05.266Z |
19 | 2VyBwEKfSOSZk5tSsW605v | 270782 | 0 | 5 | 2017-08-08T05:23:12.736Z |
20 | 30b7etegIkCs41boVC9O4P | 312586 | 61 | 1 | 2017-09-12T00:36:17.012Z |
21 | 30Ce5Z3hp3EkyXeyGAeysC | 432546 | 52 | 2 | 2017-08-30T00:58:27.445Z |
22 | 3HzWxmvpQU3QHQ59zw1X4V | 417840 | 61 | 4 | 2017-09-15T23:37:50.752Z |
23 | 3PTSoQvdubtSsn10jrsHIF | 239093 | 15 | 4 | 2017-08-02T04:31:54.306Z |
24 | 3qXEqKdzI3MSGB1FlGMBz5 | 395706 | 50 | 7 | 2017-09-10T23:57:10.637Z |
25 | 3ZuVfQriS93y6ofwbIf7lp | 314720 | 63 | 4 | 2017-09-15T23:37:19.855Z |
26 | 47cdhtxTfp7WvUbDpDeYa2 | 171093 | 60 | 1 | 2017-08-30T00:34:48.977Z |
27 | 4dIgclJtPuphBciKtMmdFg | 414720 | 60 | 9 | 2017-09-10T23:31:29.425Z |
28 | 4EDj8GXOlI45vG4SOfswK3 | 304460 | 52 | 1 | 2017-08-01T07:30:49.933Z |
29 | 4HhaWfgPrkxHe5a6L4OdTV | 387040 | 59 | 4 | 2017-09-12T00:50:55.707Z |
30 | 4kAflSfOBf6Wv5ZD5abUvZ | 348107 | 58 | 6 | 2017-08-01T07:13:15.077Z |
31 | 4kBsYpQBUbSIQNLTDF7es2 | 417226 | 54 | 4 | 2017-09-10T23:50:12.920Z |
32 | 4MpIwDaZdFLafMDcAx4k4q | 282493 | 56 | 3 | 2017-09-15T23:55:21.051Z |
33 | 4oC9U4zrIq9B86Ez26B3Qt | 574546 | 46 | 3 | 2017-08-30T00:25:10.924Z |
34 | 4QelFzhVgLomeQhvKrwM1S | 168093 | 58 | 4 | 2017-08-25T06:23:10.522Z |
35 | 4rGJzeshnLuGbnzMZeFMD0 | 300390 | 0 | 2 | 2017-08-08T05:11:19.152Z |
36 | 5lD5sdARTKwSzMkSm0c9HA | 278674 | 1 | 1 | 2017-08-08T05:06:40.526Z |
37 | 5pKuBVhP1BmHMeddgGiKz8 | 235653 | 47 | 1 | 2017-08-25T06:30:20.752Z |
38 | 5T8EDUDqKcs6OSOwEsfqG7 | 209413 | 78 | 12 | 2017-09-15T23:34:32.667Z |
39 | 62oNfnQqObaqARM0DTibAL | 364493 | 60 | 4 | 2017-09-15T23:44:47.855Z |
40 | 64or0cWQwoPDdLRYXjvJbG | 283506 | 65 | 1 | 2017-08-23T16:12:20.013Z |
41 | 67JpKSJ3NM9hVQbkJXOxPi | 228800 | 43 | 5 | 2017-08-03T06:39:53.967Z |
42 | 6baN5nSUIVTsUyugSuAj7U | 222373 | 51 | 1 | 2017-08-03T06:51:11.198Z |
43 | 6ddszDHOGF5F9NhBGbrIOl | 328145 | 21 | 5 | 2017-08-02T04:18:24.476Z |
44 | 6H7zMAVHz056pivmKRPpzm | 129000 | 22 | 5 | 2017-08-01T07:07:55.250Z |
45 | 6kRz6Juetg9FgBm7PB1jDl | 515240 | 72 | 2 | 2017-09-12T00:41:30.726Z |
46 | 6NQfUZb5VummNR8rozb8Ic | 322813 | 62 | 1 | 2017-08-03T06:43:41.779Z |
47 | 6Q3hwOcmdVandZcTS76EQK | 246000 | 17 | 3 | 2017-08-02T04:27:48.286Z |
48 | 6yUCeySJMRaSAEsnfqDeZK | 396986 | 52 | 2 | 2017-09-10T23:38:24.116Z |
49 | 71m28JeiyMRwiAOfhZ0kvW | 308973 | 19 | 5 | 2017-08-25T06:34:16.802Z |
50 | 7CcPe8TICOsQxQLqAN6Ogs | 246134 | 0 | 4 | 2017-08-08T05:19:06.707Z |
51 | 7et0LScgInvkXMhRkNq9k8 | 419074 | 58 | 3 | 2017-08-30T00:43:04.693Z |
52 | 7ezxnrzFzaOoy9yYUhP9S2 | 359947 | 51 | 3 | 2017-08-25T06:11:52.249Z |
53 | 7KcYTtKsLs1JnNYYohHNv0 | 350644 | 60 | 4 | 2017-08-30T00:50:15.718Z |
54 | 7q7BSSkNiuZyLgekXgRX13 | 240933 | 51 | 5 | 2017-08-01T07:26:48.703Z |
55 | 7wqF3BU0ykeKch6BcNqGiT | 267800 | 58 | 3 | 2017-09-15T23:50:52.574Z |
Se filtra y solo se dejan las columnas de interés para agrupar
# Genero una tabla sin el id de spotify ni la fecha+hora de escuchado
xnew = x
del_columns = ['spotify_id', 'played_at']
xnew.drop(del_columns, inplace=True, axis=1)
xnew
duration_ms | popularity | track_number | |
---|---|---|---|
index | |||
0 | 167151 | 0 | 3 |
1 | 264426 | 16 | 6 |
2 | 250640 | 44 | 1 |
3 | 166266 | 71 | 1 |
4 | 370111 | 0 | 7 |
5 | 248546 | 51 | 6 |
6 | 643155 | 0 | 6 |
7 | 309973 | 55 | 3 |
8 | 255866 | 51 | 1 |
9 | 166693 | 74 | 2 |
10 | 188933 | 24 | 7 |
11 | 210666 | 58 | 2 |
12 | 260879 | 37 | 2 |
13 | 156706 | 19 | 2 |
14 | 464293 | 59 | 3 |
15 | 337733 | 43 | 9 |
16 | 470066 | 47 | 6 |
17 | 201506 | 15 | 5 |
18 | 125520 | 71 | 9 |
19 | 270782 | 0 | 5 |
20 | 312586 | 61 | 1 |
21 | 432546 | 52 | 2 |
22 | 417840 | 61 | 4 |
23 | 239093 | 15 | 4 |
24 | 395706 | 50 | 7 |
25 | 314720 | 63 | 4 |
26 | 171093 | 60 | 1 |
27 | 414720 | 60 | 9 |
28 | 304460 | 52 | 1 |
29 | 387040 | 59 | 4 |
30 | 348107 | 58 | 6 |
31 | 417226 | 54 | 4 |
32 | 282493 | 56 | 3 |
33 | 574546 | 46 | 3 |
34 | 168093 | 58 | 4 |
35 | 300390 | 0 | 2 |
36 | 278674 | 1 | 1 |
37 | 235653 | 47 | 1 |
38 | 209413 | 78 | 12 |
39 | 364493 | 60 | 4 |
40 | 283506 | 65 | 1 |
41 | 228800 | 43 | 5 |
42 | 222373 | 51 | 1 |
43 | 328145 | 21 | 5 |
44 | 129000 | 22 | 5 |
45 | 515240 | 72 | 2 |
46 | 322813 | 62 | 1 |
47 | 246000 | 17 | 3 |
48 | 396986 | 52 | 2 |
49 | 308973 | 19 | 5 |
50 | 246134 | 0 | 4 |
51 | 419074 | 58 | 3 |
52 | 359947 | 51 | 3 |
53 | 350644 | 60 | 4 |
54 | 240933 | 51 | 5 |
55 | 267800 | 58 | 3 |
Gráficos y agrupación por k-means
from sklearn import datasets
from sklearn.cluster import KMeans
import sklearn.metrics as sm
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.cm as cm
# Se averigua el rango de (número de) tracks posibles
max_track = max(x.track_number)
print("Rango de tracks: [%i, %i]"%(min(x.track_number), max_track) )
Rango de tracks: [1, 12]
# Plot de duración vs popularidad, anotando el número de track en cada punto/marker...
fig, ax = plt.subplots(figsize=(14,7))
ax.plot(x.duration_ms/1000./60.,x.popularity, ls="", marker="o")
for xi, yi, pidi in zip(x.duration_ms/1000./60.,x.popularity,x.track_number):
ax.annotate(str(pidi), xy=(xi,yi))
plt.title('Duración (minutos) vs Popularidad (%). Número de track anotado 1 a 12')
plt.show()
colormap_original = cm.viridis_r(np.linspace(0,1,max_track+1))
plt.figure(figsize=(14,7))
plt.scatter(x.duration_ms/1000./60., x.popularity, c=colormap_original[xnew.track_number], s=40)
plt.title('Duración (minutos) vs Popularidad (%). El color denota nro de track') # más oscuro es más alto
plt.show()
Se observa que predominan números de tracks bajos, es decir ubicados entre los primeros temas del album o cd, colores verdosos y amarillos, con duraciones menores a 7 minutos. La “popularidad” esta por encima del 40% o por debajo del 20%.
Agrupación en 3 grupos/clusters y comparación
model = KMeans(n_clusters=3)
model.fit(xnew)
KMeans(algorithm='auto', copy_x=True, init='k-means++', max_iter=300,
n_clusters=3, n_init=10, n_jobs=1, precompute_distances='auto',
random_state=None, tol=0.0001, verbose=0)
plt.figure(figsize=(14,7))
colormap_original = cm.viridis_r(np.linspace(0,1,max_track+1))
# Plot the Original Classifications
plt.subplot(2, 2, 1)
plt.scatter(x.duration_ms/1000./60., x.popularity, c=colormap_original[xnew.track_number.as_matrix()], s=40)
plt.title('Original: Color track. Dur vs Pop.')
# Plot the Models Classifications
plt.subplot(2, 2, 2)
plt.scatter(x.duration_ms/1000./60., x.popularity, c=colormap_original[model.labels_], s=40)
plt.title('K=3 Classification. Dur vs Pop')
# Plot the Models Classifications
plt.subplot(2, 2, 3)
plt.scatter(x.track_number, x.popularity, c=colormap_original[model.labels_], s=40)
plt.title('K=3 Classification. Track vs Pop')
# Plot the Models Classifications
plt.subplot(2, 2, 4)
plt.scatter(x.track_number, x.duration_ms/1000./60., c=colormap_original[model.labels_], s=40)
plt.title('K=3 Classification. Track vs Dur')
plt.show()
A partir de la clasficación con k-means en 3 clusters o grupos, se observa que las canciones de mayor duración son pocas, pero en general son muy populares o nada populares (ver extremos en figura de arriba a la derecha). Se ven los 3 grupos claramente diferencidos por duración.
En la figura de abajo a la izquierda, no se observa demasiada correlación en la popularidad y el número de track, hay todo tipo de casos.
Los tracks de duraciones más cortas, menores a 6 minutos, se ubican en el disco también en los primeros lugares hasta el 6 o 7. Luego son casos excepcionales (Figura abajo a la derecha). De nuevo se ven los 3 grupos claramente diferencidos por duración.
K-means con 5 clusters
# 5 clusters
model = KMeans(n_clusters=5)
model.fit(x)
plt.figure(figsize=(14,7))
colormap_original = cm.viridis_r(np.linspace(0,1,max_track+1))
# Plot the Original Classifications
plt.subplot(2, 2, 1)
plt.scatter(x.duration_ms/1000./60., x.popularity, c=colormap_original[xnew.track_number.as_matrix()], s=40)
plt.title('Original: Color track. Dur vs Pop.')
# Plot the Models Classifications
plt.subplot(2, 2, 2)
plt.scatter(x.duration_ms/1000./60., x.popularity, c=colormap_original[model.labels_], s=40)
plt.title('K=5 Classification. Dur vs Pop')
# Plot the Models Classifications
plt.subplot(2, 2, 3)
plt.scatter(x.track_number, x.popularity, c=colormap_original[model.labels_], s=40)
plt.title('K=5 Classification. Track vs Pop')
# Plot the Models Classifications
plt.subplot(2, 2, 4)
plt.scatter(x.track_number, x.duration_ms/1000./60., c=colormap_original[model.labels_], s=40)
plt.title('K=5 Classification. Track vs Dur')
plt.show()
Dividiendo en 5 clusters, se observan las mismas características que con 3.
A lo sumo, la mayor división permite ver más claramente que los temas de mayor duración, en promedio, tienen mayor popularidad.
Conclusiones
A priori, sin utilizar k-means se observó que entre las canciones del dataset predominan aquellas con ubicaciones bajas en el album (números bajos de tracks) y duraciones menores a 7 minutos. La “popularidad” se encuenttra o por encima del 40% o por debajo del 20%, pero no en esa franja intermedia.
Luego de agrupar con este algoritmo se pudieron sacar más conclusiones como:
- La tendencia pareciera indicar que no hay termino medio de popularidad, o es muy baja o es alta. Esto se hace más evidente a medida que aumenta la duración de los tracks.
- Se observan grupos con caracaterísticas similares diferenciados por duración.
- No se observa demasiada correlación entre la popularidad y el número de track, hay todo tipo de casos.
- La mayor cantidad de tracks analizados se caracterizan por ser de poca duración y encontrarse entre las primeras ubicaciones del album.
Se podría también incluir en el análisis el valor de “danceability” que forma parte de la información que el sitio brinda por cada track y es almacenada en los archivos .json antes de exportar al .csv común.
La muestra es pequeña, de apenas más que 50 canciones, por lo tanto a lo sumo se puede usar como indicativo de los gustos musicales de los últimos días u horas del usuario. Puede ser interesante analizar una cantidad mucho más grande y comparar si las tendencias identificadas se mantienen o no.