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.
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)
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.
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)
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")
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 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.
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
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
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:
count(): Contar las palabras de mayor a menor.filter(): Filtrar las 500 palabras que más se repiten.mutate(): Para modificar una columna.reorder(): Ordena una variable según el valor de otra variable (generalmente numérica).geom_text(): Asigna una etiqueta a los datos.xlab(): Elimina el nombre del eje x (así los valores del eje “x” ocupan un mayor espacio en el grafico).coord_flip(): Invierte los ejes (sirve cuando el nombre de los valores son muy largos y resultan ilegibles).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()
El paquete tidytext contiene varios léxicos, algunos de estos son bing y nrc
bing: Este léxico clasifica las palabras según el sentimiento, puede ser negativo o positivo.
nrc: Este léxico clasifica las palabras segun el sentimiento (positivo y negativo) y la emoción (anticipation, disgust, fear, joy, sadness, surprise y trust).
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
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)
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")))
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)
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.
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