En este documento revisaremos cómo realizar análisis de sentimientos usando R y el léxico Afinn.

Nos enfocaremos en algunas de las opciones que tenemos para analizar sentimientos usando R más que en los resultados específicos de los datos que usaremos, pero en el proceso veremos maneras para contestar ciertas preguntas:

Como datos usaremos la actividad de Twitter de los candidatos a la presidencia de México durante el 2018, hasta el 11 de Abril. Pero antes, preparemos nuestro entorno de trabajo.

Preparación

Los paquetes más importantes que usaremos son tidyverse, que nos permite importar multiples paquetes que nos facilitarán el análisis y manipulación de datos, y tidytext, que contiene las herramientas para manipular texto. Además usaremos tm contiene herramientas de mineria de textos, lubridate para fechas de manera consistente, y zoo y scales que contienen funciones para realizar tareas comunes de análisis y presentación de datos. Si no cuentas con estos paquetes, puedes obtenerlos usando la función install.packages()

library(tidyverse)
library(tidytext)
library(tm)
library(lubridate)
library(zoo)
library(scales)

Definimos un tema para facilitar la visualización de nuestros resultados.

tema_graf <-
  theme_minimal() +
  theme(text = element_text(family = "serif"),
        panel.grid.minor = element_blank(),
        strip.background = element_rect(fill = "#EBEBEB", colour = NA),
        legend.position = "none",
        legend.box.background = element_rect(fill = "#EBEBEB", colour = NA))

Importando los datos

Descargamos los datos con los tuits de los candidatos a la presidencia desde la siguiente dirección, estos han sido obtenidos usando la API de Twitter.

download.file("https://raw.githubusercontent.com/jboscomendoza/rpubs/master/sentimientos_afinn/tuits_candidatos.csv",
              "tuits_candidatos.csv")

Leemos los tuits usando read.csv(). El argumento fileEncoding = "latin1" es importante para mostrar correctamente las vocales con tildes, la ñ y otro caracteres especiales.

tuits <- read.csv("tuits_candidatos.csv", stringsAsFactors = F, fileEncoding = "latin1") %>% 
  tbl_df()

Nuestros datos lucen así:

tuits
## # A tibble: 2,660 x 4
##    status_id created_at       screen_name   text                          
##        <dbl> <chr>            <chr>         <chr>                         
##  1   7.30e17 09/05/2016 03:03 lopezobrador_ Proceso habla de vilezas de E~
##  2   7.30e17 10/05/2016 02:06 lopezobrador_ "MORENA llegó como bendición ~
##  3   7.30e17 10/05/2016 12:00 lopezobrador_ Muchas felicidades a las madr~
##  4   7.31e17 12/05/2016 04:46 lopezobrador_ "En Chihuahua, Javier Félix M~
##  5   7.31e17 13/05/2016 02:54 lopezobrador_ Están desatados priístas, pan~
##  6   7.31e17 13/05/2016 19:13 lopezobrador_ No sé a ustedes, pero a mí lo~
##  7   7.32e17 15/05/2016 03:27 lopezobrador_ Luego de la explosión en Paja~
##  8   7.32e17 16/05/2016 02:20 lopezobrador_ Hoy 15 de mayo nuestro sincer~
##  9   7.32e17 17/05/2016 02:56 lopezobrador_ "El periódico Financial Times~
## 10   7.33e17 18/05/2016 17:57 lopezobrador_ Sandra Ávila Beltrán, conocid~
## # ... with 2,650 more rows

Para este análisis de sentimiento usaremos el léxico Afinn. Este es un conjunto de palabras, puntuadas de acuerdo a qué tan positivamente o negativamente son percibidas. Las palabras que son percibidas de manera positiva tienen puntuaciones de -4 a -1; y las positivas de 1 a 4.

La versión que usaremos es una traducción automática, de inglés a español, de la versión del léxico presente en el conjunto de datos sentiments de tidytext, con algunas correcciones manuales. Por supuesto, esto quiere decir que este léxico tendrá algunos defectos, pero será suficiente para nuestro análisis.

Descargamos este léxico de la siguiente dirección:

download.file("https://raw.githubusercontent.com/jboscomendoza/rpubs/master/sentimientos_afinn/lexico_afinn.en.es.csv",
              "lexico_afinn.en.es.csv")

De nuevo usamos la función read.csv() para importar los datos.

afinn <- read.csv("lexico_afinn.en.es.csv", stringsAsFactors = F, fileEncoding = "latin1") %>% 
  tbl_df()

Este léxico luce así:

afinn
## # A tibble: 2,476 x 3
##    Palabra     Puntuacion Word      
##    <chr>            <int> <chr>     
##  1 a bordo              1 aboard    
##  2 abandona            -2 abandons  
##  3 abandonado          -2 abandoned 
##  4 abandonar           -2 abandon   
##  5 abatido             -2 dejected  
##  6 abatido             -3 despondent
##  7 aborrece            -3 abhors    
##  8 aborrecer           -3 abhor     
##  9 aborrecible         -3 abhorrent 
## 10 aborrecido          -3 abhorred  
## # ... with 2,466 more rows

Tenemos tres columnas. Una con palabras en español, su puntuación y una tercera columna con la misma palabra, en inglés.

Hora de preparar nuestros datos para análisis.

Peparando los datos

Fechas

Lo primero que necesitamos es filtrar el objeto tuits para limitar nuestros datos sólo a los del 2018. Manipulamos la columna created_at con la función separate() de tidyr. Separamos esta columna en una fecha y hora del día, y después separaremos la fecha en día, mes y año. Usamos la función ymd() de lubridate para convertir la nueva columna Fecha a tipo de dato fecha.

Por último, usamos filter() de dplyr para seleccionar sólo los tuits hechos en el 2018.

tuits <- 
  tuits %>%
  separate(created_at, into = c("Fecha", "Hora"), sep = " ") %>%
  separate(Fecha, into = c("Dia", "Mes", "Periodo"), sep = "/",
           remove = FALSE) %>%
  mutate(Fecha = dmy(Fecha),
         Semana = week(Fecha) %>% as.factor(),
         text = tolower(text)) %>%
  filter(Periodo == 2018)

Convirtiendo tuits en palabras

Necesitamos separar cada tuit en palabras, para así asignarle a cada palabra relevante una puntuación de sentimiento usando el léxico Afinn. Usamos la función unnest_token() de tidytext, que tomara los tuits en la columna text y los separá en una nueva columna llamada Palabra Hecho esto, usamos left_join() de dplyr, para unir los objetos tuits y afinn, a partir del contenido de la columna Palabra. De este modo, obtendremos un data frame que contiene sólo los tuits con palabras presentes en el léxico Afinn.

Además, aprovechamos para crear una columna con mutate() de dplyr a las palabras como Positiva o Negativa. Llamaremos esta columna Tipo y cambiamos el nombre de la columna screen_name a Candidato.

tuits_afinn <- 
  tuits %>%
  unnest_tokens(input = "text", output = "Palabra") %>%
  inner_join(afinn, ., by = "Palabra") %>%
  mutate(Tipo = ifelse(Puntuacion > 0, "Positiva", "Negativa")) %>% 
  rename("Candidato" = screen_name)

Obtenemos también una puntuación por tuit, usando group_by() y summarise() de dplyr, y la agregamos tuits para usarla después. Tambien asignamos a los tuits sin puntuación positiva o negativa un valor de 0, que indica neutralidad. Por último cambiamos el nombre de la columna screen_name a Candidato

tuits <-
  tuits_afinn %>%
  group_by(status_id) %>%
  summarise(Puntuacion_tuit = mean(Puntuacion)) %>%
  left_join(tuits, ., by = "status_id") %>% 
  mutate(Puntuacion_tuit = ifelse(is.na(Puntuacion_tuit), 0, Puntuacion_tuit)) %>% 
  rename("Candidato" = screen_name)

Con esto estamos listos para empezar.

Explorando los datos, medias por día

Empecemos revisando cuántas palabras en total y cuantas palabras únicas ha usado cada candidato con count(), group_by() y distinct() de dplyr.

# Total
tuits_afinn %>%
  count(Candidato)
## # A tibble: 5 x 2
##   Candidato         n
##   <chr>         <int>
## 1 JaimeRdzNL      525
## 2 JoseAMeadeK     533
## 3 lopezobrador_   183
## 4 Mzavalagc       838
## 5 RicardoAnayaC   617
# Únicas
tuits_afinn %>% 
  group_by(Candidato) %>% 
  distinct(Palabra) %>% 
  count()
## # A tibble: 5 x 2
## # Groups:   Candidato [5]
##   Candidato         n
##   <chr>         <int>
## 1 JaimeRdzNL      117
## 2 JoseAMeadeK     183
## 3 lopezobrador_    73
## 4 Mzavalagc       190
## 5 RicardoAnayaC   153

Y veamos también las palabras positivas y negativas más usadas por cada uno de ellos, usando map() de purr, top_n() de dplyr() y ggplot.

map(c("Positiva", "Negativa"), function(sentimiento) {
  tuits_afinn %>%
    filter(Tipo ==  sentimiento) %>%
    group_by(Candidato) %>%
    count(Palabra, sort = T) %>%
    top_n(n = 10, wt = n) %>%
    ggplot() +
    aes(Palabra, n, fill = Candidato) +
    geom_col() +
    facet_wrap("Candidato", scales = "free") +
    scale_y_continuous(expand = c(0, 0)) +
    coord_flip() +
    labs(title = sentimiento) +
    tema_graf
})
## [[1]]