Inspirado por análisis realizados por otras personas, decidí que es un buen momento de conocer mejor el contenido de la música de Coheed and Cambria, aplicando técnicas de minería de texto implementadas con R.

Coheed and Cambria es una de mis bandas favoritas. Tiene la distinción de ser una de las pocas bandas que he escuchado siendo adulto y se han convertido en una de mis consentidas. En parte, ha ayudado que la historia que cuentan la mayoría de sus discos (The Amory Wars) satisface muchas de mis inclinaciones de nerd. Y como buen nerd, por supuesto que quiero analizar a mayor profundidad la obra de esta banda.

En especial, me interesa conocer las tendencias en el contenido de las canciones de Coheed and Cambria, es decir, si su música es triste, feliz, enojada, y si esto ha cambiado con el tiempo. Podemos conocer esto a través de análisis de sentimientos, una técnica que busca clasificar textos a partir de los sentimientos que, en teoría, evoca cada palabra.

Para averiguar esto, revisaremos cómo usar R para hacer web scraping (extracción automatizada de contenido de páginas web), interactuar con APIs (interfaces de programación de aplicaciones, en este caso, en línea) y realizar minería de textos para producir un análisis de sentimientos. Así que, idealmente, todos aprenderemos algo en el proceso.

¡Empecemos preparando nuestro entorno de trabajo!

Paquetes necesarios para el análisis

Necesitaremos los siguientes paquetes:

library(rvest)
library(httr)
library(xml2)
library(jsonlite)
library(tidyverse)
library(tidytext)
library(lubridate)
library(scales)

Como siempre, puedes instalar los paquetes que te falten usando install.packages(). Armados con estas herramientas, comencemos con el web scraping.

Web scraping: lectura de HTML de una página web

Para nuestro análisis, lo primero que necesitamos obtener es el nombre de todos los discos y canciones de Coheed and Cambria, así como la fecha en que cada disco fue publicado. Esta información nos servirá para extraer información desde internet y también para poder combinar distintos datos que obtendremos más adelante. Podríamos escribir manualmente esta información, pero hay maneras más eficientes e interesantes de obtenerla.

Nuestra fuente de información será el sitio MusicBrainz, un portal que compila metadatos musicales. Desde aquí podemos recuperar los metadatos de la discografía de Coheed and Cambria, es decir: la lista de canciones, el título del disco, y la fecha de publicación.

Usamos la función read_html() de rvest, que lee el código html de una página web, a partir de su URL. Asignaremos al objeto musicbrainz_html el resultado de leer el código HTML de la página en la que se encuentran los metadatos del disco “The Afterman: Descencion”.

musicbrainz_html <- read_html("https://musicbrainz.org/release/5e5dad52-a3cf-4cf7-a222-6f4bca6b17ef")

Obtenemos lo siguiente, un objeto con la estructura de un documento XML.

musicbrainz_html
## {xml_document}
## <html lang="en">
## [1] <head>\n<meta http-equiv="Content-Type" content="text/html; charset= ...
## [2] <body>\n<div class="header">\n<a class="logo" href="/" title="MusicB ...

Necesitamos transformar este objeto a otro más apropiado para su análisis.

Para ello utilizaremos la función html_nodes(), que toma como argumentos los selectores CSS de un documento HTML o XML, y devuelve el contenido que corresponde a ellos. Lo anterior dará como resultado otro documento XML, similar a musicbrainz_html, pero sólo con la información que nos interesa.

Una vez con este objeto, utilizamos la función html_text() que transforma el contenido de un documento XML a un vector de cadenas de texto.

Podemos ubicar los selectores CSS relevantes con la función Inspeccionar elemento con la que cuentan todos los navegadores web modernos, por ejemplo, Firefox. Esto requiere un poco de familiaridad con CSS y paciencia, pero es relativamente sencillo.

En nuestro caso, el selector “tbody tr” contiene la lista de canciones del disco, así que este será el argumento de html_nodes.

musicbrainz_html %>%
  html_nodes(css = "tbody tr") %>%
  html_text()
##  [1] "#\n      \n      Title\n      \n      Rating\n      Length\n    "                                                                                            
##  [2] "\n      1\n    \n    \n    Pretelethal      \n        \n  \n      \n    \n    \n    \n      \n        \n\n    \n    3:28\n  "                                
##  [3] "\n      2\n    \n    \n    Key Entity Extraction V: Sentry the Defiant      \n        \n  \n      \n    \n    \n    \n      \n        \n\n    \n    5:17\n  "
##  [4] "\n      3\n    \n    \n    The Hard Sell      \n        \n  \n      \n    \n    \n    \n      \n        \n\n    \n    4:39\n  "                              
##  [5] "\n      4\n    \n    \n    Number City      \n        \n  \n      \n    \n    \n    \n      \n        \n\n    \n    3:52\n  "                                
##  [6] "\n      5\n    \n    \n    Gravity’s Union      \n        \n  \n      \n    \n    \n    \n      \n        \n\n    \n    6:45\n  "                            
##  [7] "\n      6\n    \n    \n    Away We Go      \n        \n  \n      \n    \n    \n    \n      \n        \n\n    \n    3:45\n  "                                 
##  [8] "\n      7\n    \n    \n    Iron Fist      \n        \n  \n      \n    \n    \n    \n      \n        \n\n    \n    4:51\n  "                                  
##  [9] "\n      8\n    \n    \n    Dark Side of Me      \n        \n  \n      \n    \n    \n    \n      \n        \n\n    \n    4:20\n  "                            
## [10] "\n      9\n    \n    \n    2’s My Favorite 1      \n        \n  \n      \n    \n    \n    \n      \n        \n\n    \n    3:23\n  "

Aunque el resultado obtenido aún requiere procesamiento, este tipo de objeto es mucho más fácil de modificar. Con algunas transformaciones podemos obtener texto limpio, apropiado para el análisis.

musicbrainz_html %>%
  html_nodes(css = "tbody tr") %>%
  html_text() %>%
  str_split(pattern = "\\n", simplify = T) %>%
  data.frame() %>%
  tbl_df() %>%
  slice(-1) %>%
  select(song = X5) %>%
  mutate_all(trimws)
## # A tibble: 9 x 1
##   song                                       
##   <chr>                                      
## 1 Pretelethal                                
## 2 Key Entity Extraction V: Sentry the Defiant
## 3 The Hard Sell                              
## 4 Number City                                
## 5 Gravity’s Union                            
## 6 Away We Go                                 
## 7 Iron Fist                                  
## 8 Dark Side of Me                            
## 9 2’s My Favorite 1

Con el mismo procedimiento, podemos obtener el título del disco, ubicado en el selector “.releaseheader h1”.

musicbrainz_html %>%
  html_nodes(css = ".releaseheader h1") %>%
  html_text()
## [1] "The Afterman: Descension"

También podemos extraer la fecha en la que este disco en particular fue publicado, con el selector “.release-date”.

musicbrainz_html %>%
  html_nodes(css = ".release-date") %>%
  html_text()
## [1] "2013-02-05"

Dado que haremos lo anterior para todos los discos de Coheed and Cambria, definimos una función que automatice el proceso. Llamamos a esta función obtener_canciones.

obtener_canciones <- function(musicbrainz_url) {
  mi_html <-
    musicbrainz_url %>%
    read_html()

  nombre_album <-
    mi_html %>%
    html_nodes(css = ".releaseheader h1") %>%
    html_text()

  fecha_album <-
    mi_html %>%
    html_nodes(css = ".release-date") %>%
    html_text()

  canciones <-
    mi_html %>%
    html_nodes(css = "tbody tr") %>%
    html_text() %>%
    str_split(pattern = "\\n", simplify = T) %>%
    data.frame() %>%
    tbl_df() %>%
    slice(-1) %>%
    select(cancion = X5) %>%
    mutate_all(trimws)

  canciones %>%
    mutate(album = nombre_album, fecha = fecha_album)
}

Probemos nuestra función con una URL diferente.

obtener_canciones("https://musicbrainz.org/release/50714cf9-0f08-4632-b7ed-ea33cd05cc92")
## # A tibble: 12 x 3
##    cancion                             album                     fecha    
##    <chr>                               <chr>                     <chr>    
##  1 One                                 Year of the Black Rainbow 2010-04-~
##  2 The Broken                          Year of the Black Rainbow 2010-04-~
##  3 Guns of Summer                      Year of the Black Rainbow 2010-04-~
##  4 Here We Are Juggernaut              Year of the Black Rainbow 2010-04-~
##  5 Far                                 Year of the Black Rainbow 2010-04-~
##  6 This Shattered Symphony             Year of the Black Rainbow 2010-04-~
##  7 World of Lines                      Year of the Black Rainbow 2010-04-~
##  8 Made Out of Nothing (All That I Am) Year of the Black Rainbow 2010-04-~
##  9 Pearl of the Stars                  Year of the Black Rainbow 2010-04-~
## 10 In the Flame of Error               Year of the Black Rainbow 2010-04-~
## 11 When Skeletons Live                 Year of the Black Rainbow 2010-04-~
## 12 The Black Rainbow                   Year of the Black Rainbow 2010-04-~

Muy bien, ahora creamos una lista con los urls que contienen la discografía de Coheed and Cambria, de “The Second Stage Turbine Blade” a “The Color Before the Sun”.

lista_urls <-
  c(
    "https://musicbrainz.org/release/b3074be9-5d8e-4996-a68d-c8f824a2a6e6",
    "https://musicbrainz.org/release/a4bbe913-9728-44af-9edc-83f2080038cb",
    "https://musicbrainz.org/release/c80821ec-61bd-398a-9f93-60daa0387b52",
    "https://musicbrainz.org/release/88b19eac-a7bd-4f46-ba0b-dff3a1f27057",
    "https://musicbrainz.org/release/5e5dad52-a3cf-4cf7-a222-6f4bca6b17ef",
    "https://musicbrainz.org/release/50714cf9-0f08-4632-b7ed-ea33cd05cc92",
    "https://musicbrainz.org/release/a87344ff-39ab-4889-a834-51db7b828ae8",
    "https://musicbrainz.org/release/b9c9bc5d-24fc-4340-a941-c5e1ab0ff011"
    )

Con la función map() de purrr aplicamos la función obtener_canciones() a cada uno de los elementos de lista_url. Después, con reduce() de purrr, aplicamos bind_rows() de dplyr a la lista resultante, para obtener un data frame con tres columnas: nombres de canciones, nombres de discos, y fechas de publicación.

coheed_cambria <-
  map(lista_urls, obtener_canciones) %>%
  reduce(bind_rows)

Nuestro resultado ahora tiene una forma rectangular, que es muy conveniente para el análisis.

coheed_cambria
## # A tibble: 101 x 3
##    cancion                        album                          fecha    
##    <chr>                          <chr>                          <chr>    
##  1 Second Stage Turbine Blade     The Second Stage Turbine Blade 2002-03-~
##  2 Time Consumer                  The Second Stage Turbine Blade 2002-03-~
##  3 Devil in Jersey City           The Second Stage Turbine Blade 2002-03-~
##  4 Everything Evil                The Second Stage Turbine Blade 2002-03-~
##  5 Delirium Trigger               The Second Stage Turbine Blade 2002-03-~
##  6 Hearshot Kid Disaster          The Second Stage Turbine Blade 2002-03-~
##  7 33                             The Second Stage Turbine Blade 2002-03-~
##  8 Junesong Provision             The Second Stage Turbine Blade 2002-03-~
##  9 Neverender                     The Second Stage Turbine Blade 2002-03-~
## 10 God Send Conspirator / IRO-Bot The Second Stage Turbine Blade 2002-03-~
## # ... with 91 more rows

EL siguiente paso es conseguir la letra de cada canción.

Obtención de letras de canciones usando un API

Podemos obtener las letras de las canciones a partir de los nombres de las canciones. Para esta tarea, utilizaremos un API de un portal dedicado a compilar letras de canciones, alojado en Apiseeds.

Para usar este API necesitamos registrar una clave (“key”) de usuario. Este es un proceso gratuito que realizamos en la siguiente página:

Nuestra clave es una serie larga de letras y números que es única para cada usuario. Para nuestra comodidad, la asignamos a un objeto:

mi_api_key <- # Tu API key

Ahora, usamos la función GET() de httr para hacer peticiones a la API. Para obtener letras de canciones, esta API nos pide que proporcionemos un URL con la siguiente estructura:

Es decir, necesitamos darle un URL para cada canción de la que deseemos recuperar una letra.

Por ejemplo, recuperemos la letra de la canción “Everything Evil”, generando el URL apropiado. Nota que en este API podemos usar URLs con espacios.

# Generamos el URL
url_prueba <- paste0(
  "https://orion.apiseeds.com/api/music/lyric/", 
  "Coheed and Cambria/",
  "Everything Evil",
  "?apikey=",
  mi_api_key
  )

# Veamos lo que hemos generado
url_prueba

# Hagamos la petición
everything_evil <-  GET(url = url_prueba)

Lo anterior nos da como resultado un objeto de tipo response que contiene la letra que deseamos. Necesitamos transformar este objeto a uno de un tipo más convencional, y para ello recurrimos a la función content() de httr.

content(everything_evil, as = "text", encoding = "UTF-8")
## [1] "{\"result\":{\"artist\":{\"name\":\"Coheed and Cambria\"},\"track\":{\"name\":\"Everything Evil\",\"text\":\"Wait for everything evil in you comes out\\r\\nI'll stay when we'll only motivate sound instead, sergeant\\r\\nMake for the table in hopes that I won't be afraid again\\r\\nCall when enabled and send the leader out against\\r\\nI will - Stage a reenactment in a false pretense, exist, inflict\\r\\nUnworthy unconsciousness\\r\\nWhy debate when the action's suppressed? Then kill the acquitted.\\r\\nListen to the sounds that remain in question\\r\\nIn hopes... to solidify a truce amongst the children\\r\\nAnd the jury that stands the verdict, alive here among the dead.\\r\\n\\r\\nEvolve Monstar\\r\\nShow me the things that I've never wanted done\\r\\nEvil monster\\r\\nDo to me the things I never wanted done\\r\\n\\r\\nI felt much better than this before\\r\\nIf they find out to avoid then the accidents kept hidden away\\r\\nBut if they stay\\r\\n\\r\\nBlood hungry cannibalistic unfit family ties\\r\\nIn a series of knocks to the young girl's head side\\r\\nCome write me a letter and paste it on my refrigerator door\\r\\nInspected, Inspector, I think we've found something over here\\r\\n\\r\\nI felt much better than this before\\r\\nIf they find out to avoid then the accidents kept hidden away\\r\\nBut if they stay\\r\\n\\r\\nJesse! Just come look at what your brother did\\r\\nHere he did away with me\\r\\nJesse! Just come look at what your brother did\\r\\nHere he did away with me\\r\\n\\r\\nStay until wednesday\\r\\nAnd write me a child-like letter pretending\\r\\nAt war here in thursday\\r\\nLet's make this our last day at home by the Fence\\r\\n\\r\\nWould you run?\\r\\nWould you run?\\r\\nWould you run down past the Fence?\\r\\nWould you run?\\r\\nWould you run?\\r\\nWould you run down past the Fence?\\r\\n\\r\\nK.B.I.!!!\\r\\nAnd she screamed, \\\"Claudio, oh! Dear Claudio, oh!\\r\\nI wish, goddamn it! We'll make it if you believe...\\\"\\r\\nAnd she screamed, \\\"Claudio, oh! Dear Claudio, oh!\\r\\nI wish, goddamn it! We'll make it if you believe...\\\"\\r\\nK.B.I.\\r\\nAnd she screamed, \\\"Claudio, oh! Dear Claudio, oh!\\r\\nI wish, goddamn it! We'll make it if you believe...\\\"\\r\\nK.B.I\\r\\nAnd she screamed, \\\"Claudio, oh! Dear Claudio, oh!\\r\\nI wish, goddamn it! We'll make it if you believe...\\\"\\r\\nK.B.I\",\"lang\":{\"code\":\"en\",\"name\":\"English\"}},\"copyright\":{\"notice\":\"Everything Evil lyrics are property and copyright of their owners. Commercial use is not allowed.\",\"artist\":\"Copyright Coheed and Cambria\",\"text\":\"All lyrics provided for educational purposes and personal use only.\"},\"probability\":100,\"similarity\":1}}"

Requiere procesamiento, pero es un buen inicio.

Automatización de obtención de letras

En apariencia, el proceso anterior nos deja en la misma situación que nos ha motivado a utilizar un API. Estamos obligados a escribir un montón de URLs, uno por canción, y después usar la función GET() para obtener letras, muy parecido a lo que hicimos para obtener los metadatos de los discos.

Sin embargo, como habrás notado, la estructura de los URLs que acepta este API se presta a que automaticemos la generación de URLs.

Nosotros queremos las letras de Coheed and Cambria, así que esa parte del URL siempre será igual, lo mismo que nuestra la parte que pide nuestra API key. Lo único que tenemos que cambiar es la parte que pide el nombre de la canción.

Dado que en el objeto coheed_and_cambria ya contamos con todos los nombres de canciones, entonces sólo es cuestión de reemplazar esa parte del URL.

Hagamos esto usando map() y una función anónima, dentro de la cual generamos un URL por canción y después llamamos a GET().

cc_letras_lista <-
  map(coheed_cambria[["cancion"]], function(x){
    ruta <-  paste0(
      "https://orion.apiseeds.com/api/music/lyric/Coheed and Cambria/",
      x,
      "?apikey=",
      mi_api_key
    )

    GET(url = ruta)
  })

Tenemos una lista de objetos de tipo response, de los cuales podemos extraer la letras usando content().

content(cc_letras_lista[[4]], as = "text", encoding = "UTF-8")
## [1] "{\"result\":{\"artist\":{\"name\":\"Coheed and Cambria\"},\"track\":{\"name\":\"Everything Evil\",\"text\":\"Wait for everything evil in you comes out\\r\\nI'll stay when we'll only motivate sound instead, sergeant\\r\\nMake for the table in hopes that I won't be afraid again\\r\\nCall when enabled and send the leader out against\\r\\nI will - Stage a reenactment in a false pretense, exist, inflict\\r\\nUnworthy unconsciousness\\r\\nWhy debate when the action's suppressed? Then kill the acquitted.\\r\\nListen to the sounds that remain in question\\r\\nIn hopes... to solidify a truce amongst the children\\r\\nAnd the jury that stands the verdict, alive here among the dead.\\r\\n\\r\\nEvolve Monstar\\r\\nShow me the things that I've never wanted done\\r\\nEvil monster\\r\\nDo to me the things I never wanted done\\r\\n\\r\\nI felt much better than this before\\r\\nIf they find out to avoid then the accidents kept hidden away\\r\\nBut if they stay\\r\\n\\r\\nBlood hungry cannibalistic unfit family ties\\r\\nIn a series of knocks to the young girl's head side\\r\\nCome write me a letter and paste it on my refrigerator door\\r\\nInspected, Inspector, I think we've found something over here\\r\\n\\r\\nI felt much better than this before\\r\\nIf they find out to avoid then the accidents kept hidden away\\r\\nBut if they stay\\r\\n\\r\\nJesse! Just come look at what your brother did\\r\\nHere he did away with me\\r\\nJesse! Just come look at what your brother did\\r\\nHere he did away with me\\r\\n\\r\\nStay until wednesday\\r\\nAnd write me a child-like letter pretending\\r\\nAt war here in thursday\\r\\nLet's make this our last day at home by the Fence\\r\\n\\r\\nWould you run?\\r\\nWould you run?\\r\\nWould you run down past the Fence?\\r\\nWould you run?\\r\\nWould you run?\\r\\nWould you run down past the Fence?\\r\\n\\r\\nK.B.I.!!!\\r\\nAnd she screamed, \\\"Claudio, oh! Dear Claudio, oh!\\r\\nI wish, goddamn it! We'll make it if you believe...\\\"\\r\\nAnd she screamed, \\\"Claudio, oh! Dear Claudio, oh!\\r\\nI wish, goddamn it! We'll make it if you believe...\\\"\\r\\nK.B.I.\\r\\nAnd she screamed, \\\"Claudio, oh! Dear Claudio, oh!\\r\\nI wish, goddamn it! We'll make it if you believe...\\\"\\r\\nK.B.I\\r\\nAnd she screamed, \\\"Claudio, oh! Dear Claudio, oh!\\r\\nI wish, goddamn it! We'll make it if you believe...\\\"\\r\\nK.B.I\",\"lang\":{\"code\":\"en\",\"name\":\"English\"}},\"copyright\":{\"notice\":\"Everything Evil lyrics are property and copyright of their owners. Commercial use is not allowed.\",\"artist\":\"Copyright Coheed and Cambria\",\"text\":\"All lyrics provided for educational purposes and personal use only.\"},\"probability\":100,\"similarity\":1}}"

Este resultado, en realidad, puede ser interpretado como JSON. De modo que podemos aprovechar que tiene este formato para recuperar las partes de nuestro interés de una manera relativamente sencilla.

jsonlite para leer JSON

La función fromJSON() de jsonlite nos permite obtener la estructura del resultado de content().

content(cc_letras_lista[[4]], as = "text", encoding = "UTF-8") %>% 
  fromJSON()
## $result
## $result$artist
## $result$artist$name
## [1] "Coheed and Cambria"
## 
## 
## $result$track
## $result$track$name
## [1] "Everything Evil"
## 
## $result$track$text
## [1] "Wait for everything evil in you comes out\r\nI'll stay when we'll only motivate sound instead, sergeant\r\nMake for the table in hopes that I won't be afraid again\r\nCall when enabled and send the leader out against\r\nI will - Stage a reenactment in a false pretense, exist, inflict\r\nUnworthy unconsciousness\r\nWhy debate when the action's suppressed? Then kill the acquitted.\r\nListen to the sounds that remain in question\r\nIn hopes... to solidify a truce amongst the children\r\nAnd the jury that stands the verdict, alive here among the dead.\r\n\r\nEvolve Monstar\r\nShow me the things that I've never wanted done\r\nEvil monster\r\nDo to me the things I never wanted done\r\n\r\nI felt much better than this before\r\nIf they find out to avoid then the accidents kept hidden away\r\nBut if they stay\r\n\r\nBlood hungry cannibalistic unfit family ties\r\nIn a series of knocks to the young girl's head side\r\nCome write me a letter and paste it on my refrigerator door\r\nInspected, Inspector, I think we've found something over here\r\n\r\nI felt much better than this before\r\nIf they find out to avoid then the accidents kept hidden away\r\nBut if they stay\r\n\r\nJesse! Just come look at what your brother did\r\nHere he did away with me\r\nJesse! Just come look at what your brother did\r\nHere he did away with me\r\n\r\nStay until wednesday\r\nAnd write me a child-like letter pretending\r\nAt war here in thursday\r\nLet's make this our last day at home by the Fence\r\n\r\nWould you run?\r\nWould you run?\r\nWould you run down past the Fence?\r\nWould you run?\r\nWould you run?\r\nWould you run down past the Fence?\r\n\r\nK.B.I.!!!\r\nAnd she screamed, \"Claudio, oh! Dear Claudio, oh!\r\nI wish, goddamn it! We'll make it if you believe...\"\r\nAnd she screamed, \"Claudio, oh! Dear Claudio, oh!\r\nI wish, goddamn it! We'll make it if you believe...\"\r\nK.B.I.\r\nAnd she screamed, \"Claudio, oh! Dear Claudio, oh!\r\nI wish, goddamn it! We'll make it if you believe...\"\r\nK.B.I\r\nAnd she screamed, \"Claudio, oh! Dear Claudio, oh!\r\nI wish, goddamn it! We'll make it if you believe...\"\r\nK.B.I"
## 
## $result$track$lang
## $result$track$lang$code
## [1] "en"
## 
## $result$track$lang$name
## [1] "English"
## 
## 
## 
## $result$copyright
## $result$copyright$notice
## [1] "Everything Evil lyrics are property and copyright of their owners. Commercial use is not allowed."
## 
## $result$copyright$artist
## [1] "Copyright Coheed and Cambria"
## 
## $result$copyright$text
## [1] "All lyrics provided for educational purposes and personal use only."
## 
## 
## $result$probability
## [1] 100
## 
## $result$similarity
## [1] 1

Así, por fin, podemos obtener el texto de la letra de cada canción. Hagaos la prueba con “Everything Evil”.

everything_evil_json <- 
  content(cc_letras_lista[[4]], as = "text", encoding = "UTF-8") %>% 
  fromJSON()

everything_evil_json$result$track$text
## [1] "Wait for everything evil in you comes out\r\nI'll stay when we'll only motivate sound instead, sergeant\r\nMake for the table in hopes that I won't be afraid again\r\nCall when enabled and send the leader out against\r\nI will - Stage a reenactment in a false pretense, exist, inflict\r\nUnworthy unconsciousness\r\nWhy debate when the action's suppressed? Then kill the acquitted.\r\nListen to the sounds that remain in question\r\nIn hopes... to solidify a truce amongst the children\r\nAnd the jury that stands the verdict, alive here among the dead.\r\n\r\nEvolve Monstar\r\nShow me the things that I've never wanted done\r\nEvil monster\r\nDo to me the things I never wanted done\r\n\r\nI felt much better than this before\r\nIf they find out to avoid then the accidents kept hidden away\r\nBut if they stay\r\n\r\nBlood hungry cannibalistic unfit family ties\r\nIn a series of knocks to the young girl's head side\r\nCome write me a letter and paste it on my refrigerator door\r\nInspected, Inspector, I think we've found something over here\r\n\r\nI felt much better than this before\r\nIf they find out to avoid then the accidents kept hidden away\r\nBut if they stay\r\n\r\nJesse! Just come look at what your brother did\r\nHere he did away with me\r\nJesse! Just come look at what your brother did\r\nHere he did away with me\r\n\r\nStay until wednesday\r\nAnd write me a child-like letter pretending\r\nAt war here in thursday\r\nLet's make this our last day at home by the Fence\r\n\r\nWould you run?\r\nWould you run?\r\nWould you run down past the Fence?\r\nWould you run?\r\nWould you run?\r\nWould you run down past the Fence?\r\n\r\nK.B.I.!!!\r\nAnd she screamed, \"Claudio, oh! Dear Claudio, oh!\r\nI wish, goddamn it! We'll make it if you believe...\"\r\nAnd she screamed, \"Claudio, oh! Dear Claudio, oh!\r\nI wish, goddamn it! We'll make it if you believe...\"\r\nK.B.I.\r\nAnd she screamed, \"Claudio, oh! Dear Claudio, oh!\r\nI wish, goddamn it! We'll make it if you believe...\"\r\nK.B.I\r\nAnd she screamed, \"Claudio, oh! Dear Claudio, oh!\r\nI wish, goddamn it! We'll make it if you believe...\"\r\nK.B.I"

Definimos una función para realizar esta tarea de extraer letras, además del nombre de la canción, pues lo necesitaremos para unir estos datos con los que obtuvimos de MusicBrainz.

Incluimos un par de gsub() que nos servirán para quitar los caracteres de control como \n y \t, así como para quitar de las letras las indicaciones las partes de la canción que se repiten (“[chorus]”, “[bridge]”, etc.)

Ponemos un if para los casos en que el contenido este vacio, pues nos será conveniente más adelante

extraer_letra <- function(contenido){
  if(!is.na(contenido)) {
    cont_json <- fromJSON(contenido)
    c(cancion = cont_json$result$track$name,
      letra = cont_json$result$track$text) %>%
      gsub("[[:cntrl:]]", " ", .) %>%
      gsub("\\[.*?\\]", " ", .) %>%
      trimws()
  } else {
    c(cancion = NA, letra = NA)
  }
}

El paso siguiente consiste en más procesamiento de las letras.

Procesamiento del texto de las letras

La base de datos que consultamos no está del todo completa, así que tenemos algunas peticiones que no pudieron ser cumplidas.

content(cc_letras_lista[[1]], as = "text", encoding = "UTF-8")
## [1] "{\"error\":\"Lyric no found, try again later.\"}"
content(cc_letras_lista[[10]], as = "text", encoding = "UTF-8")
## [1] "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<title>Error</title>\n</head>\n<body>\n<pre>Cannot GET /api/music/lyric/Coheed%20and%20Cambria/God%20Send%20Conspirator%20/%20IRO-Bot</pre>\n</body>\n</html>\n"

Todos los casos en los que estos problemas se presentaron nos muestran los mismos mensajes de error. Así que podemos filtrarlos usando el texto que aparece en ellos y la función grepl().

Combinamos todo lo anterior para generar un data frame que contenga las letras de las canciones de Coheed and Cambria, con sus nombres.

mis_letras_df <-
  cc_letras_lista %>%
  map(~content(., as = "text", encoding = "UTF-8")) %>%
  map(~ifelse(grepl("error|Bad Request|html", .), NA, .)) %>%
  map(extraer_letra) %>%
  do.call(what = bind_rows)

Este es nuestro resultado.

mis_letras_df
## # A tibble: 101 x 2
##    cancion               letra                                            
##    <chr>                 <chr>                                            
##  1 <NA>                  <NA>                                             
##  2 Time Consumer (live)  The young stale memories of, play the role to yo~
##  3 <NA>                  <NA>                                             
##  4 Everything Evil       "Wait for everything evil in you comes out  I'll~
##  5 <NA>                  <NA>                                             
##  6 Hearshot Kid Disaster Still searching for your call, today Sit down, a~
##  7 33                    It's not what you have learned But what they sai~
##  8 Junesong Provision    "Good morning, sunshine Awake when the sun hits ~
##  9 Neverender            "When you've gone about things all wrong Bury th~
## 10 <NA>                  <NA>                                             
## # ... with 91 more rows
mis_letras_df <-
  cc_letras_lista %>%
  map(~content(., as = "text", encoding = "UTF-8")) %>%
  map(~ifelse(grepl("error|Bad Request|html", .), NA, .)) %>%
  map(function(x) {
    if(!is.na(x)) {
      y <- fromJSON(x)
      c(cancion = y$result$track$name,
        letra = y$result$track$text) %>%
        gsub("[[:cntrl:]]", " ", .) %>%
        gsub("\\[.*?\\]", " ", .) %>%
        trimws()
    } else {
      c(cancion = NA, letra = NA)
    }
  }) %>%
  do.call(what = bind_rows)

Finalmente, unimos la letra de las canciones con el objeto coheed_and_cambria, para así tener identificada cada letra con su nombre, disco al que pertenece y fecha en que fue publicada.

coheed_cambria_df <-
  coheed_cambria %>%
  left_join(., mis_letras_df, by = "cancion")

Este es nuestro resultado.

coheed_cambria_df
## # A tibble: 101 x 4
##    cancion                        album  fecha letra                      
##    <chr>                          <chr>  <chr> <chr>                      
##  1 Second Stage Turbine Blade     The S~ 2002~ <NA>                       
##  2 Time Consumer                  The S~ 2002~ <NA>                       
##  3 Devil in Jersey City           The S~ 2002~ <NA>                       
##  4 Everything Evil                The S~ 2002~ "Wait for everything evil ~
##  5 Delirium Trigger               The S~ 2002~ <NA>                       
##  6 Hearshot Kid Disaster          The S~ 2002~ Still searching for your c~
##  7 33                             The S~ 2002~ It's not what you have lea~
##  8 Junesong Provision             The S~ 2002~ "Good morning, sunshine Aw~
##  9 Neverender                     The S~ 2002~ "When you've gone about th~
## 10 God Send Conspirator / IRO-Bot The S~ 2002~ <NA>                       
## # ... with 91 more rows

Revisemos de cuantas canciones tenemos letra, por disco

coheed_cambria_df %>% 
  filter(!is.na(letra)) %>% 
  count(album)
## # A tibble: 8 x 2
##   album                                                                  n
##   <chr>                                                              <int>
## 1 Good Apollo I’m Burning Star IV, Volume One: From Fear Through th~     8
## 2 Good Apollo I’m Burning Star IV, Volume Two: No World for Tomorrow    10
## 3 In Keeping Secrets of Silent Earth: 3                                  9
## 4 The Afterman: Ascension                                                4
## 5 The Afterman: Descension                                               2
## 6 The Color Before the Sun                                               3
## 7 The Second Stage Turbine Blade                                         5
## 8 Year of the Black Rainbow                                              9

Definitivamente tenemos menos información de los tres discos más recientes de Coheed and Cambria, sin embargo, con lo que tenemos será suficiente para identificar tendencias.

¡Es hora del análisis de sentimiento! bueno, en realidad, de más procesamiento

Simplificación de nombres y creación de fechas

Algunos de los discos de Coheed and Cambria tienen nombres maravillosos. Pero son muy largos y eso los hace incómodos para crear visualizaciones. Por ejemplo, “Good Apollo I’m Burning Star IV, Volume Two: No World for Tomorrow”, es un poco difícil colocar en una gráfica y que luzca bien.

Cambiemos algunos nombres con case_when() de dplyr.

coheed_cambria_df <- 
  coheed_cambria_df %>% 
  mutate(album = case_when(
    album == "Good Apollo I’m Burning Star IV, Volume One: From Fear Through the Eyes of Madness" ~ "From Fear Through the Eyes of Madness",
    album == "Good Apollo I’m Burning Star IV, Volume Two: No World for Tomorrow" ~ "No World for Tomorrow",
    TRUE ~ as.character(album)    )
  )

La columna fecha es de tipo carácter. La convertimos a una de tipo fecha con la función ymd() de lubridate. Hecho esto, reordenamos la columna con los nombres de los discos por fecha con reorder().

coheed_cambria_df <- 
  coheed_cambria_df %>% 
  mutate(fecha = ymd(fecha),
         album = reorder(as.factor(album), fecha))

Es hora de iniciar el análisis de sentimientos.

Análisis de sentimientos

Categorización de palabras por sentimiento

Para realizar el análisis de sentimientos necesitamos separar el texto de las letras por palabra y asignarles un sentimiento a todas las que sean relevantes.

En esta ocasión, usaremos el léxico NRC, que categoriza palabras en los siguientes sentimientos:

  • anger (enojo)
  • disgust (desagrado)
  • fear (miedo)
  • joy (alegría)
  • sadness (tristeza)
  • trust (confianza)
  • surprise (sorpresa)
  • anticipation (anticipación)
  • negative (negativo)
  • positive (positivo)

Una misma palabra puede estar asociada a más de un sentimiento. Por ejemplo, “abandon” está asociada con miedo y tristeza.

Esta ocasión no nos interesa el continuo negativo-positivo, así que omitiremos esas dos categorías al categorizar nuestras palabras. También quitaremos “trust”, “surprise” y “anticipation”, para dejar sólo las cinco “emociones básicas” reconocidas en psicología (que te pueden parecer conocidas de cierta película).

Usamos la función unnest_tokens() de tidytext para separar el texto de las letras en palabras, la función get_sentiments() de tidytext para obtener el léxico NRC.

Después, usamos inner_join() y filter()de dplyr para unir coheed_cambria_df con el léxico y omitir las categorías “positive” y “negative.”

coheed_cambria_tokens <- 
  coheed_cambria_df %>%
  unnest_tokens(input = "letra", output = "word") %>%
  inner_join(., get_sentiments(lexicon = "nrc"), by = "word") %>%
  filter(!sentiment %in% c("positive", "negative", "trust", "surprise", "anticipation"))

Este es nuestro resultado.

coheed_cambria_tokens
## # A tibble: 2,034 x 5
##    cancion         album                      fecha      word    sentiment
##    <chr>           <fct>                      <date>     <chr>   <chr>    
##  1 Everything Evil The Second Stage Turbine ~ 2002-03-05 evil    anger    
##  2 Everything Evil The Second Stage Turbine ~ 2002-03-05 evil    disgust  
##  3 Everything Evil The Second Stage Turbine ~ 2002-03-05 evil    fear     
##  4 Everything Evil The Second Stage Turbine ~ 2002-03-05 evil    sadness  
##  5 Everything Evil The Second Stage Turbine ~ 2002-03-05 afraid  fear     
##  6 Everything Evil The Second Stage Turbine ~ 2002-03-05 inflict anger    
##  7 Everything Evil The Second Stage Turbine ~ 2002-03-05 inflict fear     
##  8 Everything Evil The Second Stage Turbine ~ 2002-03-05 inflict sadness  
##  9 Everything Evil The Second Stage Turbine ~ 2002-03-05 unwort~ disgust  
## 10 Everything Evil The Second Stage Turbine ~ 2002-03-05 kill    fear     
## # ... with 2,024 more rows

El siguiente paso nos ayudará a obtener resultados más claros.

Eliminando palabras ambiguas

Demos un vistazo a cuáles han sido las palabras más frecuentes en cada sentimiento.

coheed_cambria_tokens %>% 
  group_by(sentiment) %>% 
  count(word, sort = T) %>% 
  top_n(15) %>% 
  ggplot() +
  aes(word, n, fill = sentiment) +
  geom_col() +
  scale_y_continuous(expand = c(0, 0)) +
  coord_flip() +
  facet_wrap(~sentiment, scales = "free_y") +
  theme(legend.position = "none")
## Selecting by n