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()

png

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()

png

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()

png

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()

png

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.


Written on September 17, 2017