Tutorial: Recomendador musical con Spotify - Parte 1: pistas más escuchadas y canciones candidatas

En esta miniserie veremos cómo implementar en su totalidad un sistema de recomendación musical para Spotify, desde la generación de pistas candidatas hasta su implementación en una aplicación Web.

Y en esta primera parte del tutorial veremos cómo usar la API de Spotify para generar las lista de preferencias del usuario y el listado de canciones candidatas.

Al final de este tutorial se encuentra el enlace para descargar el código fuente.

Así que ¡listo, comencemos!

Video

Como siempre, en el canal de YouTube se encuentra el video de este post:

Introducción

La idea central de este sistema de recomendación es generarar una lista de reproducción (o playlist) que contenga canciones similares a los gustos musicales de un usuario de Spotify.

En esta primera parte del tutorial usaremos la API de Spotify para acceder al listado de pistas más escuchadas por el usuario así como para generar un listado de pistas candidatas que posteriormente filtraremos para conformar la lista de reproducción deseada.

En la segunda parte usaremos el filtrado basado en contenido para crear como tal la lista de reproducción en nuestro usuario de Spotify.

Y en la tercera parte veremos cómo combinar todos estos elementos en un aplicativo web para este sistema de recomendación.

Comencemos entonces viendo cómo interactuar con esta API de Spotify.

Recolección de datos con la API de Spotify

Configuración de la API

Esta API nos proporciona una forma sencilla de acceder a través de la web a la información asociada a nuestra cuenta de Spotify, así como a la información de artistas, albums y pistas disponibles en el catálogo de esta aplicación.

Para acceder a la información asociada a nuestro usuario debemos configurar una aplicación en el dashboard de Spotify, según el Web API tutorial. Para ello seguimos estos pasos:

  1. Ir a developer.spotify.com
  2. Ir a la pestaña DASHBOARD
  3. Ingresar con el nombre de usuario y contraseña de Spotify
  4. Crear una nueva aplicación (CREATE AN APP), definiendo un nombre y agregando una descripción
  5. Vamos a EDIT SETTINGS -> Redirect URIs -> http://localhost:8080. Este elemento lo usaremos posteriormente para la autenticación desde Python
  6. Vamos a USERS AND ACCESS -> ADD NEW USER y allí especificamos el nombre y el correo electrónico de la cuenta de Spotify. Este será el usuario que tendrá autorización para acceder a la API desde Python.

Al completar estos pasos ya habremos creado y configurado nuestra aplicación en la API de Spotify. El siguiente paso es volver al DASHBOARD y copiar los campos Client ID (el identificador de la aplicación) y Client Secret (la clave que permite acceder a la aplicación) así como la Redirect URI y crear estas variables en nuestro entorno de Python:

cid = '3eec3a625de449218e7f4XXXXXXXXX'
secret = 'e259c2875e4a4ff19b75XXXXXXXXX'
redirect_uri = 'http://localhost:8080'

Además debemos crear la variable username que corresponde a nuestro nombre de usuario de Spotify.

spotipy

Usaremos la librería spotipy que nos permitirá interactuar fácilmente con la API de Spotify desde Python.

Para conectarnos con esta API primero debemos definir los privilegios que asignaremos al usuario. En este caso nos interesa inicialmente acceder al listado de las pistas más escuchadas (current_user_top_tracks) por dicho usuario desde Spotify, para lo cual crearemos la variable scope:

scope = 'user-top-read'

Autenticación

El siguiente paso es autenticarnos ante la API. Lo primero que haremos será generar un token usando las credenciales mencionadas anteriormente y luego realizaremos como tal la autenticación usando el método Spotify:

token = util.prompt_for_user_token(username,scope,cid,secret,redirect_uri)
sp = spotipy.Spotify(auth=token)

Listado de las 20 pistas más escuchadas

Como punto de partida se usarán las user’s top tracks, es decir el listado de pistas más escuchadas por el usuario.

Para ello usaremos el método current_user_top_tracks de spotipy y limitaremos esta cantidad a las 20 pistas (limit=20) más escuchadas recientemente (time_range='short_term'). Todo esto lo podemos hacer con esta línea de código:

top20 = sp.current_user_top_tracks(time_range='short_term', limit=20)

La variable top20 será un diccionario de Python, en donde el key items contiene la información de las pistas.

Para imprimir la información relevante de este diccionario podemos usar estas líneas de código:

for i, item in enumerate(top20['items']):
    print(i+1, item['name'], '//', item['artists'][0]['name'])

obteniendo este resultado:

1 The Adults Are Talking // The Strokes
2 ZITTI E BUONI // Måneskin
3 Chosen // Måneskin
4 Beggin' // Måneskin
5 Dani California // Red Hot Chili Peppers
6 You Should Be Dancing // Dee Gees
7 You Can't Put Your Arms Round a Memory // Billie Joe Armstrong
8 Selfless // The Strokes
9 FOR YOUR LOVE // Måneskin
10 The Getaway // Red Hot Chili Peppers
11 Vaccine // Logic
12 I WANNA BE YOUR SLAVE // Måneskin
13 Bad Decisions // The Strokes
14 Basket Case // Green Day
15 VENT'ANNI // Måneskin
16 CORALINE // Måneskin
17 Heroes - 2017 Remaster // David Bowie
18 Paper Cages // Franz Ferdinand
19 Snow (Hey Oh) // Red Hot Chili Peppers
20 Dark Necessities // Red Hot Chili Peppers

Muy bien, ya tenemos nuestras 20 pistas más escuchadas recientemente. Con esta información podemos comenzar a construir el dataset de preferencias del usuario. Así que veamos en detalle cómo hacerlo.

Dataset de preferencias del usuario

Con estas 20 pistas podemos ahora crear un set de datos que contendrá, por cada pista, su identificador (una cadena de caracteres - string - que permite diferenciar una pista de otra en Spotify) así como sus características sonoras.

Estas características sonoras son simplemente cantidades numéricas que cuantifican elementos propios de cada pista, como por ejemplo danceability (que indica qué tan bailable es la canción) o acousticness (que indica el contenido acústico de la pista), entre otras.

Para crear este set de datos usaremos la librería Pandas:

import pandas as pd

En primer lugar extraeremos los identificadores, los nombres de las pistas y las audio features (características sonoras) de cada pista presente en la variable top20:

tracks = top20['items']
track_ids = []
track_names = []
features = []

for track in tracks:
    track_id = track['id']
    track_name = track['name']
    audio_features = sp.audio_features(track_id)
    
    track_ids.append(track_id)
    track_names.append(track_name)
    features.append(audio_features[0])

en donde hemos usado el método audio_features para obtener estas características. Luego, convertimos el resultado en un DataFrame de Pandas:

top20_df = pd.DataFrame(features,index = track_names)

Este DataFrame tendrá un total de 20 filas (una por cada pista) y 13 columnas correspondientes al identificador (columna id) y a las características sonoras (las 12 columnas restantes: acousticness, danceability, etc.)

Estas características acústicas nos permitirán más adelante diferenciar una canción de otra y a la vez nos permitirán encontrar similitudes con otras pistas que el usuario aún no haya escuchado y que podremos incluir en la lista de reproducción.

Listado de pistas candidatas

El siguiente paso es tomar como punto de partida este listado de 20 pistas más escuchadas para obtener un listado de pistas candidatas, es decir pistas relacionadas que podrían gustarle al usuario.

Para construir este listado de pistas candidatas tomaremos las 20 canciones más escuchadas y:

  1. Extraeremos los artistas correspondientes (sin repeticiones)
  2. Usaremos la API para ampliar este listado incluyendo artistas relacionados y artistas con nuevos lanzamientos
  3. Y con este listado ampliado extraeremos un album representativo y su lista de pistas correspondiente para finalmente por cada pista extraer sus características sonoras.

El set de datos resultante será también un DataFrame de Pandas (candidatos_df) que contendrá un identificador y 12 características sonoras para cada una de las pistas candidatas.

Veamos entonces cómo implementar cada una de las etapas que acabamos de mencionar.

Listado de artistas

Usaremos la API de Spotify para extraer el artista autor de cada una de las pistas en nuestro top-20. Esta información está almacenada en el key artist del diccionario que contiene la información de cada una de las pistas:

ids_artists = []
print('Artistas en mi top20:')
print('=====================')
for item in top20['items']:
    artist_id = item['artists'][0]['id']
    artist_name = item['artists'][0]['name']
    print(f'{artist_id}: {artist_name}')
    ids_artists.append(artist_id)

Al ejecutar estas líneas de código obtenemos el siguiente resultado:

Artistas en mi top20:
=====================
0epOFNiUfyON9EYx7Tpr6V: The Strokes
0lAWpj5szCSwM4rUMHYmrr: Måneskin
0lAWpj5szCSwM4rUMHYmrr: Måneskin
0lAWpj5szCSwM4rUMHYmrr: Måneskin
0L8ExT028jH3ddEcZwqJJ5: Red Hot Chili Peppers
0mCTPQ5oa1lbPvbw4kc0eX: Dee Gees
1MrEurzLcL8ugfP1PrUPWG: Billie Joe Armstrong
0epOFNiUfyON9EYx7Tpr6V: The Strokes
0lAWpj5szCSwM4rUMHYmrr: Måneskin
0L8ExT028jH3ddEcZwqJJ5: Red Hot Chili Peppers
4xRYI6VqpkE3UwrDrAZL8L: Logic
0lAWpj5szCSwM4rUMHYmrr: Måneskin
0epOFNiUfyON9EYx7Tpr6V: The Strokes
7oPftvlwr6VrsViSDV7fJY: Green Day
0lAWpj5szCSwM4rUMHYmrr: Måneskin
0lAWpj5szCSwM4rUMHYmrr: Måneskin
0oSGxfWSnnOXhD2fKuz2Gy: David Bowie
0XNa1vTidXlvJ2gHSsRi4A: Franz Ferdinand
0L8ExT028jH3ddEcZwqJJ5: Red Hot Chili Peppers
0L8ExT028jH3ddEcZwqJJ5: Red Hot Chili Peppers

donde el primer elemento es el identificador (id) y el segundo es como tal el nombre del artista. Vemos que es probable que algunos artistas se repitan en este listado, por lo cual usaremos la función set de la Librería Estándar de Python para obtener un listado equivalente pero sin repeticiones:

ids_artists = list(set(ids_artists))
print(f'Número de artistas (sin repeticiones): {len(ids_artists)}')

pasando de 20 artistas en el listado original a un total de 9 sin incluir las repeticiones:

Número de artistas (sin repeticiones): 9

Muy bien. El siguiente paso en la construcción del listado de pistas candidatas es ampliar nuestro listado de artistas. Veamos esta fase.

Artistas relacionados

Como el objetivo de este sistema de recomendación es encontrar música afín a los gustos del usuario, lo que haremos inicialmente será ampliar la lista de artistas obtenida anteriormente para encontrar artistas similares y que probablemente el usuario no haya escuchado.

Para construir este listado usaremos nuevamente la API de Spotify y en particular el método artist_related_artists de spotipy.

En este método lo que haremos será introducir el identificador del artista (obtenido en el paso anterior) y como resultado obtendremos un listado de artistas similares, en donde este grado de similitud está basado en el análisis del historial de reproducción de la comunidad de Spotify. Estas son las líneas de código requeridas:

print('Artistas similares:')
print('=====================')
ids_similar_artists = []
for artist_id in ids_artists:
    artists = sp.artist_related_artists(artist_id)['artists']
    for item in artists:
        artist_id = item['id']
        artist_name = item['name']
        print(f'{artist_id}: {artist_name}')
        ids_similar_artists.append(artist_id)

Al igual que en la etapa anterior, en este caso el listado de artistas similares (ids_similar_artists) contendrá únicamente el identificador (artist_id) y el nombre (artist_name) del artista (aunque este último nos servirá únicamente como referencia, pues no resulta necesario para la construcción del listado).

El resultado de ejecutar las anteriores líneas de código es el siguiente:

Artistas similares:
=====================
5iot8OPcosJN9nCl7I5SdK: Irama
3hN3iJMbbBmqBSAMx5veDa: Ultimo
2nftqfbLohpDYzY8VUlvbm: Benji & Fede
76UCIJTB0jcJvBaL0CdIqx: Takagi & Ketra
1CF7hrTuWgErEa6HBFJ8d3: Michele Bravi
2iK8weavvfS2xJCmzNzNE5: J-AX
3o7fC2O4nraaicpID6bBZW: Elettra Lamborghini
1xqolkIzTFMmqgCuD48WNt: Shade
0tTS475qIqv3KXYZMXjsYy: Lorenzo Fragola
...

en donde hemos mostrado en pantalla sólo una parte del listado obtenido.

Antes de ir al siguiente paso debemos hacer dos tareas. Primero debemos añadir el listado recién obtenido al listado inicial de artistas proveniente de nuestro top-20. Esto lo logramos usando el método extend de las listas en Python:

ids_artists.extend(ids_similar_artists)

Y al igual que en la fase anterior, en este caso también es probable que haya repeticiones de artistas, por lo que usaremos nuevamente la función set para eliminar estas posibles repeticiones:

ids_artists = list(set(ids_artists))
print(f'Número de artistas (sin repeticiones): {len(ids_artists)}')

Así, este listado depurado tendrá un total de 164 artistas.

El siguiente paso es ampliar aún más este listado incluyendo artistas con nuevos lanzamientos.

Artistas con nuevos lanzamientos

Este elemento nos permitirá agregarle el elemento novedoso a la lista de reproducción de nuestro sistema de recomendación.

De nuevo acudiremos a la API y en particular al método get_new_releases que nos entregará un listado de álbumes lanzados recientemente e incluidos en la base de datos de Spotify:

new_releases = sp.new_releases(limit=20)['albums']

Ahora tenemos que procesar este listado (new_releases) para extraer los artistas correspondientes e incluirlos en el listado que hemos venido actualizando (ids_artists).

Así que, de manera muy similar a como lo hicimos en los pasos anteriores, por cada ítem en new_releases tomaremos únicamente el identificador (el key id) y como información adicional (con el único propósito de visualizar los resultados) incluiremos el nombre del album, el nombre del artista y la fecha de lanzamiento. Al final de este procedimiento añadiremos el identificador de este artista (artist_id) al listado previamente creado (ids_artists):

print('')
print('Artistas con nuevos lanzamientos:')
print('=====================')
for item in new_releases['items']:
    artist_id = item['artists'][0]['id']   #[0] porque puede haber varios artistas, se tomará el primero
    artist_name = item['artists'][0]['name']
    album_name = item['name']   # Nombre del album, puramente informativo
    release_date = item['release_date'] # Fecha de lanzamiento, puramente informativo
    print(f'{artist_id}: {artist_name} - // {album_name}, {release_date}')
    ids_artists.append(artist_id)

Acá un ejemplo del resultado obtenido al ejecutar el código anterior (se muestra sólo una parte del extenso listado obtenido):

Artistas con nuevos lanzamientos:
=====================
6qqNVTkY8uBg9cP3Jd7DAH: Billie Eilish - // Happier Than Ever, 2021-07-30
0du5cEVh5yTK9QJze8zA0C: Bruno Mars - // Skate, 2021-07-30
2gqMBdyddvN82dzZt4ZF14: Yola - // Stand For Myself, 2021-07-30
4xRYI6VqpkE3UwrDrAZL8L: Logic - // Bobby Tarantino III, 2021-07-30
2eam0iDomRHGBypaDQLwWI: Bleachers - // Take the Sadness Out of Saturday Night, 2021-07-30
2tIP7SsRs7vjIcLrU85W8J: The Kid LAROI - // F*CK LOVE 3+: OVER YOU, 2021-07-27
6EPlBSH2RSiettczlz7ihV: Sleepy Hallow - // 2055 (feat. Coi Leray), 2021-07-28
5KNNVgR6LBIABRIomyCwKJ: Dermot Kennedy - // Better Days, 2021-07-28
1SupJlEpv7RS2tPNRaHViT: Nicky Jam - // Miami, 2021-07-29
4TshyQDihSYXSWqvclXl3I: Parmalee - // For You, 2021-07-30
0EFisYRi20PTADoJrifHrz: Jhay Cortez - // En Mi Cuarto, 2021-07-30
7oPxPZSk7y5q0fhzpmX5Gi: SEB - // IT’S OKAY, WE’RE DREAMING, 2021-07-30
...

Finalmente debemos depurar el listado obtenido para eliminar las repeticiones (tal como lo hicimos en los pasos anteriores):

ids_artists = list(set(ids_artists))
print(f'Número de artistas (sin repeticiones): {len(ids_artists)}')

obteniendo un total de ¡183 artistas!

Muy bien. Hemos pasado de un listado de 9 artistas (obtenidos a partir de las top-20 pistas originales) a uno con 183 artistas sugeridos.

El siguiente paso es tomar la información de los álbumes de estos artistas para poco a poco ir construyendo nuestro listado de pistas candidatas.

Listado de álbumes candidatos

Como lo que nos interesa es obtener un listado de canciones, pero en el paso anterior obtuvimos un listado de artistas, debemos llevar a cabo un paso intermedio que nos permita extraer los álbumes por cada artista en dicho listado. Posteriormente veremos cómo extraer las pistas asociadas a cada uno de estos álbumes.

Para extraer este listado de álbumes usaremos el método artist_albums de la API de Spotify. Sin embargo, para reducir el número de solicitudes que haremos esta API limitaremos la consulta a tan sólo 1 album por cada artista (limit=1). Esto evitará además tener una lista excesivamente grande y nos permitirá reducir la cantidad de tiempo requerida para construir nuestro listado de pistas candidatas.

id_albums = []
nartists = len(ids_artists)
for i, id_artist in enumerate(ids_artists):
    print(f'Procesando artista {i+1} de {nartists}...')
    albums = sp.artist_albums(id_artist, limit=1) # para evitar tener una lista gigantesca
    for album in albums['items']:
        id_albums.append(album['id'])
print('¡Listo!')

¡Perfecto! Con este listado de álbumes candidatos lo único que nos resta es construir el listado de pistas candidatas. Veamos cómo hacerlo.

Finalmente: ¡el listado de pistas candidatas!

Para obtener este listado debemos simplemente visitar uno a uno los álbumes obtenidos anteriormente (id_albums) y extraer las pistas asociadas (usando el método album_tracks de spotipy).

Sin embargo, para evitar tener un listado de pistas gigantesco limitaremos esta búsqueda a tan sólo 3 pistas por album (limit=3):

id_tracks = []
nalbums = len(id_albums)
for i, id_album in enumerate(id_albums):
    print(f'Procesando album {i+1} de {nalbums}...')
    album_tracks = sp.album_tracks(id_album, limit=3)
    for track in album_tracks['items']:
        id_tracks.append(track['id'])
print(f'¡Listo! Número total de tracks pre-candidatos: {len(id_tracks)}')

Al ejecutar estas líneas de código obtendremos cerca de 550 pistas candidatas.

El paso restante es tomar este listado y construir un DataFrame de Pandas similar al obtenido para las pistas del top-20: por cada pista tendremos un registro con 13 elementos, donde el primero será el identificador de la pista y los 12 restantes corresponderán a las características sonoras:

track_names = []
features = []
ntracks = len(id_tracks)
for i, track_id in enumerate(id_tracks):
    print(f'Procesando track {i+1} de {ntracks}...')
    track_name = sp.track(track_id)['name']
    audio_features = sp.audio_features(track_id)
    
    #No incluir pistas sin "features"
    if audio_features[0] != None:
        track_names.append(track_name)
        features.append(audio_features[0])
print('¡Listo!')

candidatos_df = pd.DataFrame(features,index = track_names)

¡Y listo, ya hemos logrado construir nuestro set de pistas candidatas!

Conclusión

Muy bien, acabamos de ver cómo usar la API de Spotify y la librería spotipy para acceder al historial de reproducciones del usuario y a partir de allí generar un listado de más de 500 canciones “candidatas” (que probablemente le gustarán a este usuario).

Cada una de estas pistas candidatas, al igual que las pistas de referencia del top-20, contiene un identificador y un total de 12 características sonoras.

El siguiente paso es “comparar” estas pistas candidatas con las de referencia para determinar cuáles de estas canciones candidatas se asemejan más a los gustos del usuario.

Y una de las formas de lograr esto es usando lo que se conoce como el filtrado basado en contenido, que será precisamente el tema de la segunda parte de este tutorial. Así que te invito a mirarlo en detalle.

Si tienes alguna duda de este artículo o tienes alguna sugerencia no dudes en contactarme diligenciando el siguiente formulario:

Debes introducir tu nombre.
Debes introducir tu apellido.
Debes introducir un email válido.
Debes introducir un mensaje.
Debes aceptar la política de privacidad.

Código fuente

En este enlace de Github está disponible el código fuente de este tutorial.