Introducción

El objetivo de este documento es analizar la frecuencia y los sentimientos de las palabras de 18 mil tweets que contengan la keyword “BlackLivesMatter”.

Para poder hacer esto vamos a tener que conectar R con Twitter y aplicar técnicas de text mining.

Librerías

Estos son los paquetes que vamos a utilizar en este documento:

library(rtweet)
library(tidytext)
library(dplyr)
library(tidyverse)
library(wordcloud)
library(reshape2)
library(RColorBrewer)

Crear una Twitter APP

El paquete rtweet permite descargar tweets mediante la API de Twitter. Para poder interactuar con la API se debe cumplir con ciertos requisitos:

Para crear la aplicacion hay que completar una serie de campos que pueden ser confusos, les recomiendo leer esta guia https://rtweet.info/articles/auth.html de Michael W. Kearney para que puedan hacerlo sin problemas.

Conectando R a Twitter

Para poder utilizar rtweet tenemos que cargar las claves de autorización, estas las pueden ver una vez que hayan creado la app en Twitter.

Las claves son confidenciales y propios de cada usuario, por lo que voy a utilizar un ejemplo (ustedes tienen que usar las claves que les aparece en su App).

api_key <- "la_clave_alfanumerica_de_su_app"
api_secret_key <- "la_clave_alfanumerica_de_su_app"
access_token <- "la_clave_alfanumerica_de_su_app"
access_token_secret <- "la_clave_alfanumerica_de_su_app"
app_name <- "el_nombre_de_su_app"

Después hay que utilizar la función create_token() para realizar la autentificación vía web y guardar el token de acceso en nuestra sesión de R.

token <- create_token(
  app = app_name,
  consumer_key = api_key,
  consumer_secret = api_secret_key,
  access_token = access_token,
  access_secret = access_token_secret)

Extracción de datos

Para obtener los tweets hay que utilizar la función search_twitter(), para ello hay que definir varios argumentos, generalmente utilizo estos:

Para esta ocasión, voy a extraer 18000 tweets que contengan la keyword “BlackLivesMatter”, en idioma ingles y ningún rt.

tweets <- search_tweets(q="blacklivesmatter",
                        n= 18000,
                        include_rts = FALSE,
                        lang = "en")

Explorando el data frame

La función dim() indica la cantidad de filas y columnas que tiene el data frame, en este caso el df tweets tiene 17952 filas y 90 columnas

dim(tweets)
## [1] 17952    90

La función names() indica el nombre de las variables (es decir las columnas) del data frame.

names(tweets)
##  [1] "user_id"                 "status_id"              
##  [3] "created_at"              "screen_name"            
##  [5] "text"                    "source"                 
##  [7] "display_text_width"      "reply_to_status_id"     
##  [9] "reply_to_user_id"        "reply_to_screen_name"   
## [11] "is_quote"                "is_retweet"             
## [13] "favorite_count"          "retweet_count"          
## [15] "quote_count"             "reply_count"            
## [17] "hashtags"                "symbols"                
## [19] "urls_url"                "urls_t.co"              
## [21] "urls_expanded_url"       "media_url"              
## [23] "media_t.co"              "media_expanded_url"     
## [25] "media_type"              "ext_media_url"          
## [27] "ext_media_t.co"          "ext_media_expanded_url" 
## [29] "ext_media_type"          "mentions_user_id"       
## [31] "mentions_screen_name"    "lang"                   
## [33] "quoted_status_id"        "quoted_text"            
## [35] "quoted_created_at"       "quoted_source"          
## [37] "quoted_favorite_count"   "quoted_retweet_count"   
## [39] "quoted_user_id"          "quoted_screen_name"     
## [41] "quoted_name"             "quoted_followers_count" 
## [43] "quoted_friends_count"    "quoted_statuses_count"  
## [45] "quoted_location"         "quoted_description"     
## [47] "quoted_verified"         "retweet_status_id"      
## [49] "retweet_text"            "retweet_created_at"     
## [51] "retweet_source"          "retweet_favorite_count" 
## [53] "retweet_retweet_count"   "retweet_user_id"        
## [55] "retweet_screen_name"     "retweet_name"           
## [57] "retweet_followers_count" "retweet_friends_count"  
## [59] "retweet_statuses_count"  "retweet_location"       
## [61] "retweet_description"     "retweet_verified"       
## [63] "place_url"               "place_name"             
## [65] "place_full_name"         "place_type"             
## [67] "country"                 "country_code"           
## [69] "geo_coords"              "coords_coords"          
## [71] "bbox_coords"             "status_url"             
## [73] "name"                    "location"               
## [75] "description"             "url"                    
## [77] "protected"               "followers_count"        
## [79] "friends_count"           "listed_count"           
## [81] "statuses_count"          "favourites_count"       
## [83] "account_created_at"      "verified"               
## [85] "profile_url"             "profile_expanded_url"   
## [87] "account_lang"            "profile_banner_url"     
## [89] "profile_background_url"  "profile_image_url"

Tokenizacion de palabras

Tokenizacion es el proceso de dividir el texto en tokens. Un token es una unidad de texto, estas pueden ser palabras individuales, oraciones, párrafos, n-grams, etc.

La función unnest_tokens() permite realizar este proceso transformando todas las cadenas de texto (es decir las palabras de los tweets) en una palabra por fila.

Las ventajas de utilizar unnest_tokens() es que conservamos la información de las columnas del data frame (los tokens van a conservar todos los datos de las palabras tokenizadas), se eliminan los signos de puntuación y todas las mayúsculas se convierten a minúsculas.

Función unnest_tokens()

Para utilizar esta función hay que definir una serie de argumentos:

tweets_token <- unnest_tokens(tbl=tweets,
                              output = "word",
                              input = "text",
                              token = "words")

Observando el nuevo data frame se puede observar que tiene 486891 filas y 90 columnas.

dim(tweets_token)
## [1] 486891     90

Limpieza de datos

Una de las ventajas del paquete tyditext es que viene con stop_words, un conjunto de datos que contiene palabras comunes del vocabulario inglés como “the”, “what”, “a” o “to”.

Estas palabras comunes se pueden remover del data frame tweets utilizando la función anti_join.

Este join devuelve una tabla con todas las palabras que no coincidan con la lista de stop_words.

tweets_token <- anti_join(x=tweets_token,
                          y=stop_words,
                          by="word")

Para contar las palabras de mayor frecuencia se puede utilizar la función count().

count(tweets_token,
      word,
      sort = TRUE) #Ordena de mayor a menor
## Warning: `...` is not empty.
## 
## We detected these problematic arguments:
## * `needs_dots`
## 
## These dots only exist to allow future extensions and should be empty.
## Did you misspecify an argument?
## # A tibble: 48,345 x 2
##    word                 n
##    <chr>            <int>
##  1 blacklivesmatter 18079
##  2 t.co             12512
##  3 https            12508
##  4 black             3517
##  5 people            2079
##  6 amp               1846
##  7 blm               1604
##  8 fight             1406
##  9 lives             1337
## 10 white             1261
## # ... with 48,335 more rows

Se observan palabras frecuentes que no sirven para el análisis, se pueden remover con la función filter().

tweets_token <- filter(tweets_token, word!="amp" & word!="https" & word!="t.co" & word!="it’s" & word!="don’t")

Al tokenizar el data frame tweets se obtuvo 486891 filas, luego de realizar la limpieza solo quedan 236909 filas.

Recuerden que cada fila es una palabra.

dim(tweets_token)
## [1] 236909     90

Visualización de las palabras más frecuentes

Con un conjunto de datos ordenado y luego de realizar una limpieza de datos, realizar un gráfico con ggplot es más sencillo.

Voy a realizar un gráfico de barras visualizando las palabras que se repiten más de 500 veces, para ello utilizare las siguientes funciones:

  1. count(): Contar las palabras de mayor a menor.
  2. filter(): Filtrar las 500 palabras que más se repiten.
  3. mutate(): Para modificar una columna.
  4. reorder(): Ordena una variable según el valor de otra variable (generalmente numérica).
  5. geom_text(): Asigna una etiqueta a los datos.
  6. xlab(): Elimina el nombre del eje x (así los valores del eje “x” ocupan un mayor espacio en el grafico).
  7. coord_flip(): Invierte los ejes (sirve cuando el nombre de los valores son muy largos y resultan ilegibles).
  8. theme_minimal(): Aplica un tipo de fondo al gráfico.
tweets_token %>%
  count(word, sort = TRUE) %>%
  filter(n > 500) %>%
  mutate(word = reorder(word, n)) %>%
  ggplot(aes(word, n)) +
  geom_text(aes(label=n), hjust= -0.2) +
  geom_col() +
  xlab(NULL) +
  coord_flip()+
  theme_minimal()

Análisis de sentimientos: Join de tablas

El paquete tidytext contiene varios léxicos, algunos de estos son bing y nrc

Se pueden descargar estos vocabularios con la función get_sentiments() (en algunos casos les van a pedir que acepten la licencia de uso).

get_sentiments("bing")
## Warning: `...` is not empty.
## 
## We detected these problematic arguments:
## * `needs_dots`
## 
## These dots only exist to allow future extensions and should be empty.
## Did you misspecify an argument?
## # A tibble: 6,786 x 2
##    word        sentiment
##    <chr>       <chr>    
##  1 2-faces     negative 
##  2 abnormal    negative 
##  3 abolish     negative 
##  4 abominable  negative 
##  5 abominably  negative 
##  6 abominate   negative 
##  7 abomination negative 
##  8 abort       negative 
##  9 aborted     negative 
## 10 aborts      negative 
## # ... with 6,776 more rows
get_sentiments("nrc")
## Warning: `...` is not empty.
## 
## We detected these problematic arguments:
## * `needs_dots`
## 
## These dots only exist to allow future extensions and should be empty.
## Did you misspecify an argument?
## # A tibble: 13,901 x 2
##    word        sentiment
##    <chr>       <chr>    
##  1 abacus      trust    
##  2 abandon     fear     
##  3 abandon     negative 
##  4 abandon     sadness  
##  5 abandoned   anger    
##  6 abandoned   fear     
##  7 abandoned   negative 
##  8 abandoned   sadness  
##  9 abandonment anger    
## 10 abandonment fear     
## # ... with 13,891 more rows

Al tener el conjunto de datos de forma ordenada (una palabra por fila), se puede asignar el sentimiento a cada una de las palabras mediante la función inner_join().

Este join devuelve una tabla con los datos que coinciden entre la tabla de la izquierda y de la derecha.

tw_bing <- tweets_token %>%
  inner_join(get_sentiments("bing"))

tw_nrc <- tweets_token %>%
  inner_join(get_sentiments("nrc"))

Observamos el resultado con la función count():

tw_bing %>%
  count(word,sentiment,sort=TRUE)
## Warning: `...` is not empty.
## 
## We detected these problematic arguments:
## * `needs_dots`
## 
## These dots only exist to allow future extensions and should be empty.
## Did you misspecify an argument?
## # A tibble: 2,391 x 3
##    word     sentiment     n
##    <chr>    <chr>     <int>
##  1 racism   negative   1164
##  2 support  positive    901
##  3 racist   negative    866
##  4 love     positive    482
##  5 trump    positive    468
##  6 protest  negative    424
##  7 protests negative    347
##  8 killed   negative    304
##  9 hate     negative    267
## 10 free     positive    247
## # ... with 2,381 more rows
tw_nrc %>%
  count(word,sentiment,sort=TRUE)
## Warning: `...` is not empty.
## 
## We detected these problematic arguments:
## * `needs_dots`
## 
## These dots only exist to allow future extensions and should be empty.
## Did you misspecify an argument?
## # A tibble: 7,058 x 3
##    word   sentiment        n
##    <chr>  <chr>        <int>
##  1 black  negative      3517
##  2 black  sadness       3517
##  3 fight  anger         1406
##  4 fight  fear          1406
##  5 fight  negative      1406
##  6 white  anticipation  1261
##  7 white  joy           1261
##  8 white  positive      1261
##  9 white  trust         1261
## 10 police fear          1220
## # ... with 7,048 more rows

Visualización de sentimientos

Anteriormente realice un gráfico con las palabras de mayor frecuencia, ahora se puede hacer exactamente lo mismo, pero según el sentimiento.

Para visualizar las palabras más repetidas según el sentimiento conviene utilizar facet_wrap().

Esta función permite representar en un gráfico todos los factores de una variable, como en este caso la variable a utilizar es “sentiment”, el resultado va a ser dos gráficos, uno con las palabras de sentimiento negativo y el otro con las palabras de sentimiento positivo.

tw_bing %>%                                   #Empezamos con el léxico bing
  count(word,sentiment,sort=TRUE) %>%         #Contamos palabras según el sentimiento      
  group_by(sentiment) %>%                     #Agrupamos por variable "sentiment"     
  top_n(15) %>%                               #Seleccionamos el top 15            
  ungroup() %>%                               #Siempre conviene desagrupar luego de un agrupado     
  mutate(word=reorder(word,n)) %>%            #Reordenamos la variable "word" según variable "n"        
  ggplot(aes(word,n,fill=sentiment))+         #Fill asigna un color a cada factor de "sentiment"        
  geom_col(show.legend = FALSE)+              #Ocultamos la leyenda
  geom_text(aes(label=n), hjust= 1.2) +       #Agregamos una etiqueta a los valores del eje
  facet_wrap(~sentiment,scales = "free_y") +  #Facetamos según "sentiment"  
  coord_flip() +                              #Invertimos los ejes
  xlab(NULL)                                  #Ocultamos el nombre del eje x

El mismo grafico se puede realizar para el léxico nrc, sin embargo, recuerden que este clasifica las palabras según el sentimiento y las emociones, veamos:

unique(tw_nrc$sentiment)
##  [1] "anger"        "fear"         "negative"     "surprise"     "positive"    
##  [6] "trust"        "anticipation" "joy"          "disgust"      "sadness"

Para realizar un gráfico con el sentimiento y el otro con las emociones se puede utilizar la función filter().

tw_nrc %>%
  filter(sentiment!="negative" & sentiment!="positive") %>%
  count(word,sentiment,sort=TRUE) %>%             
  group_by(sentiment) %>%                        
  top_n(15) %>%                                                     
  ungroup() %>%                                   
  mutate(word=reorder(word,n)) %>%                
  ggplot(aes(word,n,fill=sentiment))+           
  geom_col(show.legend = FALSE) +
  geom_text(aes(label=n), hjust= 0) +
  facet_wrap(~sentiment,scales = "free_y")+  
  coord_flip() +
  xlab(NULL)

tw_nrc %>%
  filter(sentiment=="negative" | sentiment=="positive") %>%
  count(word,sentiment,sort=TRUE) %>%             
  group_by(sentiment) %>%                        
  top_n(15) %>% #Seleccionamos el top 15                                                    
  ungroup() %>%                                   
  mutate(word=reorder(word,n)) %>%                
  ggplot(aes(word,n,fill=sentiment))+           
  geom_col(show.legend = FALSE) +
  geom_text(aes(label=n), hjust= 0) +
  facet_wrap(~sentiment,scales = "free_y")+  
  coord_flip() +
  xlab(NULL)

Nube de Palabras

Otra forma de visualizar frecuencia y sentimiento de palabras es con una wordcloud, son muy fáciles de hacer y tienen la ventaja de ser intuitivas.

Se pueden aplicar varios argumentos, algunos de estos son:

tweets_token %>%
  count(word) %>%
  with(wordcloud(words=word,
                 freq=n,
                 max.words = 250,
                 scale = c(3,1),
                 rot.per = 0.3,
                 random.order = FALSE,
                 colors=brewer.pal(6,"Dark2")))

En la anterior wordcloud visualizamos todas las palabras, se puede realizar la misma nube, pero según el sentimiento.

Veamos una wordcloud de las palabras con sentimiento positivo según el lexico bing:

tw_bing%>%
  count(word,sentiment) %>%
  filter(sentiment=="positive") %>%  
  with(wordcloud(words=word,
                 freq=n,
                 max.words = 250,
                 scale = c(3,1),
                 rot.per = 0.3,
                 random.order = FALSE,
                 colors=brewer.pal(6,"Dark2")))

Ahora según el sentimiento negativo:

tw_bing%>%
  count(word,sentiment) %>%
  filter(sentiment=="negative") %>%  
  with(wordcloud(words=word,
                 freq=n,
                 max.words = 250,
                 scale = c(3,1),
                 rot.per = 0.3,
                 random.order = FALSE,
                 colors=brewer.pal(6,"Dark2")))

Comparision Cloud

La función comparision.cloud() permite crear una wordcloud con diferentes dimensiones, por ejemplo el sentimiento o las emociones.

Esto es muy util dado que se puede observar en la misma wordcloud las palabras más frecuentes de cada dimensión.

Para lograr esto se debe utilizar la función acast() del paquete reshape2.

tw_bing %>%
  count(word,sentiment,sort=TRUE) %>%
  acast(word~sentiment,value.var = "n", fill = 0) %>%
  comparison.cloud(colors = c("red","green"), 
                   max.words = 300,
                   title.size = 2)

Se puede realizar la misma nube, pero con las emociones como dimensiones del léxico nrc.

tw_nrc %>%
  count(word,sentiment,sort=TRUE) %>%
  filter(sentiment!="positive" & sentiment!="negative") %>% 
  acast(word~sentiment,value.var = "n", fill = 0) %>%
  comparison.cloud(title.size = 1.5)

Conclusión

Una diferencia entre el léxico nrc y bing¨ es que el primero le asigna sentimiento y emoción a los colores, en este caso “blanco” y “negro”.

A la palabra “black” la clasifica como negativa y triste, mientras que a “white” la clasifica como positiva, alegre, de confianza y anticipación (ansiedad o esperar por el futuro).

tw_nrc %>%
  filter(word=="white" | word=="black") %>%
  count(word,sentiment)
## # A tibble: 6 x 3
##   word  sentiment        n
##   <chr> <chr>        <int>
## 1 black negative      3517
## 2 black sadness       3517
## 3 white anticipation  1261
## 4 white joy           1261
## 5 white positive      1261
## 6 white trust         1261

Otra cosa interesante que pude encontrar, es que ambos léxicos le asignaron un sentimiento a la palabra “trump”. Teniendo en cuenta el contexto actual de Estados Unidos, todos podemos inferir que hacen referencia al presidente y no a otra cosa.

tw_nrc %>%
  filter(word=="trump") %>%
  count(word,sentiment)
## # A tibble: 1 x 3
##   word  sentiment     n
##   <chr> <chr>     <int>
## 1 trump surprise    468
tw_bing %>%
  filter(word=="trump") %>%
  count(word,sentiment)
## # A tibble: 1 x 3
##   word  sentiment     n
##   <chr> <chr>     <int>
## 1 trump positive    468

Al dividir la cadena de texto en palabras individuales se pierde el contexto de la oración, por lo que se puede cometer sesgo en el análisis.

Por ejemplo, la palabra “hope” puede ser clasificada como positiva, pero si el tweet llegara a decir “there is no hope” estaríamos en presencia de una expresión negativa.

Next Steps

En este documento vimos una simple introducción al text mining con R, todavía quedan temas muy importantes para analizar como ley de Zipf’s, n-grams o topic modeling.

Si quieren profundizar su conocimiento sobre análisis de texto les recomiendo leer el libro Text Mining with R de Silge y Robinson, todo lo que hice anteriormente lo aprendí leyendo este libro.

 


Elaborado por Jonathan Rzezak