Recientemente Spotify, el servicio de streaming musical compartió el “Wrapped” de sus usuarios, un resumen anual de su uso de la plataforma. Esta ha sido una iniciativa muy exitosa para Spotify, que genera mucho involucramiento y difusión, al grado que se ha convertido en un evento muy esperado por los usuarios de esta plataforma.
Este año, 2024, los usuarios no han estado particularmente contentos con su Wrapped por distintas razones. Para ser franco, sí me pareció más simple que en años anteriores, pero también es algo que no me sorprende tanto, pues Spotify ha despedido a una proporción considerable de su personal durante el último año.
Pero, lo relevante para este artículo es que podemos replicar las estadísticas básicas que han aparecido en el Wrapped analizando datos obtenidos mediante consultas (requests) a la API (Application Programming Interface) de last.fm, un servicio que se especializa en compilar estadísticas de escucha musical y con ellas generar una base de datos pública.
Haremos este análisis con R, para poner en práctica las consultas a APIs usando el paquete httr2 así distintas tareas de extracción, transformación y presentación de datos con los paquetes del tidyverse. En realidad, son los mismo resúmenes que siempre están disponibles en last.fm, pero es una oportunidad de hacer un análisis de datos divertido.
Empezaremos instalando estos paquetes, con la mención que el análisis ha sido realizado con la versión 4.4.2 de R (“Pile of Leaves”).
Los paquetes necesarios son:
Por supuesto, instalamos estamos paquetes a nuestra librería de R con la función install.packages.
Cargamos los paquetes a nuestra sesión con la función library.
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.0 ✔ stringr 1.5.1
## ✔ ggplot2 3.5.1 ✔ tibble 3.2.1
## ✔ lubridate 1.9.3 ✔ tidyr 1.3.1
## ✔ purrr 1.0.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
##
## Adjuntando el paquete: 'jsonlite'
##
## The following object is masked from 'package:purrr':
##
## flatten
Para poder hacer peticiones a last.fm necesitamos una API key, que podemos obtener en la siguiente liga, el proceso es muy sencillo y no tienen ningún costo:
Una vez que tenemos nuestra API key, la asignamos a una variable. Es muy importante que no compartas tu key con nadie más, por ello en este artículo no la verás mostrada.
Para hacer peticiones a la API de last.fm partimos de una URL raíz, que se comparte para todas las peticiones. La asignamos a una variable lastfm_root.
Los URL para hacer peticiones a la API de last.fm tienen la siguiente estructura:
[URL raiz][método][entidad:usuario/artista/canción/disco/etc.][API key][parámetros opcionales][formato json (opcional)]
Por ejemplo:
"http://ws.audioscrobbler.com/2.0/?method=user.gettopartists&user=zegim&api_key=xxx&period=12month&limit=50&format=json"
Mientras nuestras URL tengan una estructura similar, vamos por buen camino.
Para este análisis usaré mi propia información de last.fm del último año, así que asignaré mi nombre de usuario a una variable, así como el periodo de tiempo sobre el que haremos el análisis. El periodo de tiempo, además de anual “12month”, puede ser “overall”, “7day”, “1month”, “3month” o “6month”.
También pondremos un límite al número de artistas y canciones que recuperaremos para este “Wrapped” y lo asignamos a un par de variables. He fijado estos límites en 50, pues creo que esta cantidad da una buena cantidad de información relevante para analizar, pero por supuesto que puedes cambiarlo.
Ya con esta preparación hecha, comencemos el análisis obteniendo los artistas más reproducidos del último año.
Para recuperar estos datos usaremos el método “user.gettopartists”, el cual asignamos a una variable.
Con esta variable y las que hemos definido en la preparación del análisis, generamos el URL de petición o request.
request_artistas <- paste0(
lastfm_root,
"?method=", metodo_artistas,
"&user=", usuario,
"&api_key=", api_key,
"&period=", periodo,
"&limit=", limite_artistas,
"&format=json"
)
El resultado debe verse parecido a este:
Con esta URL lista, usaremos tres funciones del paquete **httr2* para hacer la petición a la API last.fm y obtener una respuesta con contenido en formato json.
Ejecutamos estas tres funciones en secuencia, usando pipes (%>%) y asignamos el resultado a la variable contenido_artistas.
Si todo ha salido bien, obtenemos una lista con una estructura un tanto compleja, en la que se encuentra bastante información de los cincuenta artistas que hemos pedido.
En caso de que algo haya marchado mal, nos aparecerá un error, generalmente un error 400, que indica que no se ha encontrado el recurso solicitado. De ser así, debemos verificar que el URL de la petición tenga la estructura y parámetros correctos, que nuestra API key sea correcta, y que el usuario del que solicitamos datos sea uno válido.
Como nuestra petición ha tenido éxito, veamos su primer elemento relevante:
## {
## "streamable": [
## "0"
## ],
## "image": [
## {
## "size": [
## "small"
## ],
## "#text": [
## "https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png"
## ]
## },
## {
## "size": [
## "medium"
## ],
## "#text": [
## "https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png"
## ]
## },
## {
## "size": [
## "large"
## ],
## "#text": [
## "https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png"
## ]
## },
## {
## "size": [
## "extralarge"
## ],
## "#text": [
## "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png"
## ]
## },
## {
## "size": [
## "mega"
## ],
## "#text": [
## "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png"
## ]
## }
## ],
## "mbid": [
## "381086ea-f511-4aba-bdf9-71c753dc5077"
## ],
## "url": [
## "https://www.last.fm/music/Kendrick+Lamar"
## ],
## "playcount": [
## "369"
## ],
## "@attr": {
## "rank": [
## "1"
## ]
## },
## "name": [
## "Kendrick Lamar"
## ]
## }
##
Vamos a extraer dos elementos de cada artista, su nombre (name) y el número de reproducciones que ha tenido (playcount).
Haremos esto usando la función map_df del paquete purrr, parte del tidyverse, que está diseñado para trabajar con programación funcional lo cual es muy útil para trabajar con listas. Esta función es parte de la familia map, que aplica una función a todos lo elementos de una lista.
Extraemos los dos elementos arriba mencionados y lo asignamos a un data frame con la función tibble del paquete del mismo nombre, también parte del tidyverse.
df_artistas <-
map_df(contenido_artistas$topartists$artist, function(x_artist) {
tibble(
"artista" = x_artist[["name"]],
"reproducciones" = as.numeric(x_artist[["playcount"]]))
})
El resultado será un data frame como el siguiente:
## # A tibble: 50 × 2
## artista reproducciones
## <chr> <dbl>
## 1 Kendrick Lamar 369
## 2 Coheed and Cambria 327
## 3 Bicep 229
## 4 Sasha 201
## 5 Placebo 194
## 6 Nick Drake 181
## 7 Aphex Twin 179
## 8 Aqua 175
## 9 Faithless 174
## 10 Kylie Minogue 172
## # ℹ 40 more rows
Sí, definitivamente esto fue lo que más escuché durante el último año… aunque no esperaba que hubiera tanta diferencia entre los dos primeros artistas y los demás.
Con esto ya tenemos nuestra listado de artistas con más reproducciones en el último año, así que podemos continuar obteniendo las canciones más reproducidas.
El procedimiento para obtener las canciones más reproducidas es básicamente el mismo que para obtener los artistas más reproducidos. Sólo necesitamos cambiar el método de nuestra petición de “user.gettopartists” a “user.gettoptracks”.
Generamos el URL de petición.
request_canciones <- paste0(
lastfm_root,
"?method=", metodo_canciones,
"&user=", usuario,
"&api_key=", api_key,
"&period=", periodo,
"&limit=", limite_canciones,
"&format=json"
)
Hacemos la petición usando las funciones request, req_perform y resp_body_json en secuencia.
Y,si todo ha salido bien, obtendremos una lista con la información de cincuenta canciones.
Veamos la primera canción:
## {
## "streamable": {
## "fulltrack": [
## "0"
## ],
## "#text": [
## "0"
## ]
## },
## "mbid": [
## "05de7dd7-8200-4487-93c8-e0b1b61dfd0f"
## ],
## "name": [
## "When It Rains, It Pours"
## ],
## "image": [
## {
## "size": [
## "small"
## ],
## "#text": [
## "https://lastfm.freetls.fastly.net/i/u/34s/2a96cbd8b46e442fc41c2b86b821562f.png"
## ]
## },
## {
## "size": [
## "medium"
## ],
## "#text": [
## "https://lastfm.freetls.fastly.net/i/u/64s/2a96cbd8b46e442fc41c2b86b821562f.png"
## ]
## },
## {
## "size": [
## "large"
## ],
## "#text": [
## "https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png"
## ]
## },
## {
## "size": [
## "extralarge"
## ],
## "#text": [
## "https://lastfm.freetls.fastly.net/i/u/300x300/2a96cbd8b46e442fc41c2b86b821562f.png"
## ]
## }
## ],
## "artist": {
## "url": [
## "https://www.last.fm/music/Bad+Boy+Chiller+Crew"
## ],
## "name": [
## "Bad Boy Chiller Crew"
## ],
## "mbid": [
## "4db5a006-33ce-45b9-a624-cc57948c27ec"
## ]
## },
## "url": [
## "https://www.last.fm/music/Bad+Boy+Chiller+Crew/_/When+It+Rains,+It+Pours"
## ],
## "duration": [
## "197"
## ],
## "@attr": {
## "rank": [
## "1"
## ]
## },
## "playcount": [
## "36"
## ]
## }
##
En este caso, para cada canción vamos a extraer el nombre (name), artista(artist), número de reproducciones (playcount) y su duración en segundos (duration). Con estos datos generaremos un data frame usando de nuevo las funciones tibble y map_df.
En el proceso, aplicamos la función as.numeric a los datos de reproducciones y duración, para asegurar que tendremos datos numéricos válidos.
df_canciones <-
map_df(contenido_canciones$toptracks$track, function(x){
tibble(
"cancion" = x[["name"]],
"artista" = x[["artist"]][["name"]],
"reproducciones" = as.numeric(x[["playcount"]]),
"duracion" = as.numeric(x[["duration"]])
)
})
Veamos nuestro resultado:
## # A tibble: 50 × 4
## cancion artista reproducciones duracion
## <chr> <chr> <dbl> <dbl>
## 1 When It Rains, It Pours Bad Boy C… 36 197
## 2 Time Machine Eelke Kle… 31 232
## 3 König oder Feigling - Ed’s Theme Blumio 30 0
## 4 Glue Bicep 29 269
## 5 A Disappearing Act Coheed an… 29 210
## 6 Like That Future 29 267
## 7 A Favor House Atlantic Coheed an… 28 344
## 8 You're So High (10 Years On) (Sasha Remix) Eli & Fur 28 0
## 9 All the Stars (with SZA) Kendrick … 28 0
## 10 Is It Enough R Plus 28 0
## # ℹ 40 more rows
Con eso ya tenemos nuestra lista de canciones más escuchadas del último año.
Vamos a provechar que tenemos la duración de cada canción para hacer una estimación de qué artista tuvo la mayor cantidad de tiempo de reproducción durante el último año. Es posible tener artistas cuyas canciones se escuchan muchas veces, pero son muy cortas, por lo que en realidad no son tan escuchados como otros artistas con canciones más largas, así que vamos a explorarlo.
Como nos dimos cuenta al darle un vistazo al data frame df_canciones, tenemos canciones con duración 0, debido a que last.fm no tiene esa información entre sus metadatos. Para no perder por completo la información de esas canciones, vamos a hacer un reemplazo de este valor cero por la mediana de duración entre todas las canciones de nuestro data frame.
Hacemos este reemplazo con la función mutate del paquete dplyr del tidyverse, dentro de la cual usamos la función ifelse; primero para reemplazar los valores 0 por NA, y después ese NA, identificados por la función is.na, por la mediana de duraciones, calculada con la función median.
El paso intermedio de 0 a NA evita que la mediana se encuentre muy sesgada precisamente por la presencia de valores iguales a cero.
df_canciones <-
df_canciones %>%
mutate(
duracion = ifelse(duracion == 0, NA, duracion),
duracion = ifelse(is.na(duracion), median(duracion, na.rm = TRUE), duracion)
)
Nuestro resultado es el siguiente:
## # A tibble: 50 × 4
## cancion artista reproducciones duracion
## <chr> <chr> <dbl> <dbl>
## 1 When It Rains, It Pours Bad Boy C… 36 197
## 2 Time Machine Eelke Kle… 31 232
## 3 König oder Feigling - Ed’s Theme Blumio 30 216
## 4 Glue Bicep 29 269
## 5 A Disappearing Act Coheed an… 29 210
## 6 Like That Future 29 267
## 7 A Favor House Atlantic Coheed an… 28 344
## 8 You're So High (10 Years On) (Sasha Remix) Eli & Fur 28 216
## 9 All the Stars (with SZA) Kendrick … 28 216
## 10 Is It Enough R Plus 28 216
## # ℹ 40 more rows
Hemos obtenido una duración de 216 segundos, poco más de tres minutos y medio, lo cual suena razonable para una canción. Con esto, podemos obtener los artistas con más tiempo de reproducción
Vamos a obtener el tiempo de reproducción al multiplicar el valor de reproducciones por el valor de duración dentro de una llamada a mutate.
Después vamos a agrupar los datos por artista usando la función group_by de dplyr del tidyverse. Con los datos agrupados, hacemos la suma de tiempo del artista, usando la función sum.
Desagrupamos los datos con ungroup, los ordenamos de mayor a menor usando las funciones arrange y desc, todas del paquete dplyr.
Por último, dentro de otro mutate, creamos las columnas minutos y horas, dividiendo el tiempo, que representa segundos, entre 60 y 3600, respectivamente.
Asignamos el resultado a la variable df_tiempo.
df_tiempo <-
df_canciones %>%
mutate(tiempo = reproducciones * duracion) %>%
group_by(artista) %>%
summarise(tiempo = sum(tiempo)) %>%
ungroup() %>%
arrange(desc(tiempo)) %>%
mutate(
minutos = tiempo / 60,
horas = tiempo / 3600
)
Veamos nuestro resultado:
## # A tibble: 36 × 4
## artista tiempo minutos horas
## <chr> <dbl> <dbl> <dbl>
## 1 Bicep 22009 367. 6.11
## 2 Kendrick Lamar 20525 342. 5.70
## 3 Coheed and Cambria 15722 262. 4.37
## 4 Burial 14211 237. 3.95
## 5 R Plus 10694 178. 2.97
## 6 Aqua 10465 174. 2.91
## 7 Mastodon 9736 162. 2.70
## 8 Paramore 9021 150. 2.51
## 9 Chappell Roan 8674 145. 2.41
## 10 TEKKEN Project 8208 137. 2.28
## # ℹ 26 more rows
Comprobamos que, efectivamente, tenemos una lista de artistas diferente a la que se obtiene tomando como criterio el número de reproducciones. Bicep tiene más tiempo de escucha que Kendrick Lamar y Coheed and Cambria, mientras que artistas como Burial y R Plus se hacen presentes cuando antes no lo estaban. Por supuesto, es una medida un tanto imprecisa, pero a mi me suena razonable, conociendo mis hábitos de escucha.
Finalmente, vamos a obtener las principales tags de los artistas con más reproducciones del último año.
No nos detendremos mucho en los detalles para generar los gráficos, pero en todos los casos limitaremos el número de datos para visualizar a diez renglones de los data frames usando la función top_n de dplyr y ordenamos estos datos de menor a mayor usando la función reorder.
El gráfico para los artistas más reproducidos es el siguiente.
df_artistas %>%
top_n(10, wt = reproducciones) %>%
mutate(artista = reorder(artista, reproducciones, decreasing = FALSE)) %>%
ggplot() +
aes(artista, reproducciones) +
coord_flip() +
geom_col(fill = "#1a759f") +
geom_text(
aes(label = reproducciones),
hjust = 1.5,
color = "#ffffff",
) +
geom_text(
aes(label = artista, y = 5),
hjust = "left",
color = "#ffffff"
) +
scale_y_continuous(expand = c(0, 0)) +
labs(title = "Artistas más reproducidos", x = "", y = "") +
theme_minimal() +
theme(
panel.grid = element_blank(),
axis.text = element_blank()
)
Para el gráfico que muestra las canciones más escuchadas, uniremos el nombre de la canción con el nombre del artista usando la función paste0, de modo que tengamos ambos datos visibles.
df_canciones %>%
top_n(10, wt = reproducciones) %>%
mutate(cancion = paste0(cancion, " [", artista, "]")) %>%
mutate(cancion = reorder(cancion, reproducciones, decreasing = FALSE)) %>%
ggplot() +
aes(cancion, reproducciones) +
coord_flip() +
geom_col(fill = "#55a630") +
geom_text(
aes(label = reproducciones),
hjust = 1.5,
color = "#ffffff",
) +
geom_text(
aes(label = cancion, y = 0.5),
hjust = "left",
color = "#ffffff"
) +
scale_y_continuous(expand = c(0, 0)) +
labs(title = "Canciones más reproducidas", x = "", y = "") +
theme_minimal() +
theme(
panel.grid = element_blank(),
axis.text = element_blank()
)
Para el gráfico que muestra el tiempo escuchado por artista, redondearemos el número de minutos usando la función round para mejorar la presentación de los datos.
df_tiempo %>%
mutate(
artista = reorder(artista, tiempo, decreasing = FALSE),
minutos = round(minutos)
) %>%
top_n(10, wt = tiempo) %>%
ggplot() +
aes(artista, round(minutos)) +
coord_flip() +
geom_col(fill = "#b5838d") +
geom_text(
aes(label = minutos),
hjust = "right",
nudge_y = -3,
color = "#ffffff",
) +
geom_text(
aes(label = artista, y = 3),
hjust = "left",
color = "#ffffff"
) +
scale_y_continuous(expand = c(0, 0)) +
labs(title = "Artistas con más tiempo de reproducción\n(minutos)", x = "", y = "") +
theme_minimal() +
theme(
panel.grid = element_blank(),
axis.text = element_blank()
)
Finalmente, el gráfico con el conteo de los tags no requiere mayor procesamiento adicional.
df_conteo_tags %>%
top_n(10, wt = n) %>%
mutate(tag = reorder(tag, n, decreasing = FALSE)) %>%
ggplot() +
aes(tag, n) +
coord_flip() +
geom_col(fill = "#967aa1") +
geom_text(
aes(label = n),
hjust = 1.5,
color = "#ffffff",
) +
geom_text(
aes(label = tag, y = .2),
hjust = "left",
color = "#ffffff",
) +
scale_y_continuous(expand = c(0, 0)) +
labs(title = "Tags principales", x = "", y = "") +
theme_minimal() +
theme(
panel.grid = element_blank(),
axis.text = element_blank()
)
Eso fue divertido, con todo y que quedó pendiente hacer el conteo de los discos más escuchados durante el año.
Sin embargo en el proceso practicamos hacer consultas a APIs usando R, así como tareas comunes de procesamiento de datos. No nos metimos muy a fondo a revisar cómo trabajar con json en R o cómo generar gráficos bastante personalizados con ggplot2, pero eso puede ser motivo de otro artículo.
Lo que seguramente sí será motivo de otro artículo es llevar todo el análisis anterior a una app de shiny para así poder generar estos “Wrapped” para cualquier usuario de last.fm.
Pero eso, será después.
Mientras puedes encontrar todo el código de este análisis en Github, incluido el Markdown de este artículo: