Versión PDF: Github
Más sobre ciencia de datos: cienciadedatos.net
Twitter es actualmente una dinámica e ingente fuente de contenidos que, dada su popularidad e impacto, se ha convertido en la principal fuente de información para estudios de Social Media Analytics. Análisis de reputación de empresas, productos o personalidades, estudios de impacto relacionados con marketing, extracción de opiniones y predicción de tendencias son sólo algunos ejemplos de aplicaciones. Este tutorial pretende servir de introducción al análisis de texto y procesamiento de lenguaje natural con R
. Para ello, se analizan las publicaciones que han hecho en Twitter diferentes personalidades. Los puntos tratados son:
Automatización de la extracción de datos de Twitter.
Análisis de las palabras empleadas por cada uno de los usuarios.
Experimentos básicos de clasificación y predicción de la autoría.
Análisis de sentimientos
Si bien Python
es el lenguaje de programación que domina en este ámbito, existen nuevas librerías en R
que facilitan y extienden sus capacidades como herramienta de análisis de texto. Las que se emplean en este tutorial son:
stringr
: paquete desarrollado por Hadley Wickham con multitud de funciones (división, búsqueda, reemplazo…) para trabajar con strings.
tidytext
: paquete desarrollado por Julia Silge y David Robinson. Los autores proponen una forma de trabajar con texto que sigue los principios de tidy data, lo que hace muy sencillo combinarlo con otros paquetes tales como dplyr
, broom
, tidyr
y ggplot2
.
quanteda
: paquete con multitud de funciones orientadas a text mining, algunas de ellas permiten crear Term-Document Matrices
purrr
: permite aplicar funciones a elementos de un vector o lista, por ejemplo, a los elementos de una columna de un dataframe.
Como ocurre en muchas redes sociales, la propia plataforma pone a disposición de los usuarios una API que permite extraer información. Aunque en la mayoría de casos se trata de web services API, con frecuencia existen librerías que permiten interactuar con la API desde diversos lenguajes de programación. En este caso, se emplea la librería rtweet
, un wrapper R
que se comunica con la API de Twitter.
Para poder extraer datos de Twitter es necesario estar registrado en la plataforma y, a partir de la cuenta, crear una Twitter App asociada. Twitter App es el mecanismo que proporciona Twitter para desarrolladores que quieran acceder a los contenidos de Twitter a través de programas. Al crear una Twitter App, Twitter proporciona una serie de claves y tokens de identificación que permiten acceder a la aplicación y extraer información. Se puede encontrar información detallada de cómo crear una APP en https://apps.twitter.com/app/new y de cómo acceder a ella a través de R
en rtweet vignettes. Twitter tiene una normativa que regula la frecuencia máxima de peticiones, así como la cantidad máxima de tweets que se pueden extraer rate limiting. Es importante cumplir estos límites para evitar ser bloqueado.
A lo largo de este tutorial se analizan los tweets de:
Elon Musk (@elonmusk) y Bill Gates (@BillGates), dos directivos de empresas tecnológicas.
Mayor Ed Lee (@mayoredlee) alcalde de la ciudad de San Francisco.
Para el trabajo de análisis que se quiere realizar, es conveniente recuperar tantos tweets como sea posible, o al menos unos 1000. Como el número máximo de tweets recuperados por consulta es de 200 y existe una limitación de tiempo mínimo entre consulta y consulta, se sigue la siguiente estrategia:
Todo tweet tiene un ID global numérico que sigue un orden temporal, lo que permite identificar si un tweet es más reciente que otro.
Entre los argumentos de api.GetUserTimeline()
se puede especificar el max_id para recuperar únicamente tweets más antiguos.
Antes de cada consulta, se lee el fichero donde se están almacenando los tweets y se identifica el ID del último tweet recuperado. Si no existe fichero de almacenamiento para el usuario en cuestión, se crea uno.
Se realiza una nueva consulta empleando como argumento max_id el ID recuperado en el paso anterior.
Se incorporar los nuevos datos al archivo de almacenamiento.
library(rtweet)
library(tidyverse)
library(knitr)
# Identificación y obtención de tokens
appname <- "extrac-------eets"
key <- "WXkVypH-----------------mY0"
secret <- "EeMD2-------qYbYw5EFTKR7i3M"
twitter_token <- create_token(app = appname, consumer_key = key,
consumer_secret = secret)
extraccion_tweets <- function(usuario, maxtweets = 100, output_file_name = NULL){
# Esta función extrae los tweets publicados por un usuario y los almacena en
# un fichero csv. Si existe un fichero con el mismo nombre, lo lee, concatena
# los nuevos tweets y lo sobrescribe.
#
# Argumentos:
# usuario: identificador del usuario de twitter
# maxtweets: número de tweets que se recuperan
# output_file_name: nombre del fichero de escritura
# Si no se especifica el nombre del archivo de almacenamiento, se crea un
# nombre por defecto
if(is.null(output_file_name)){
output_file_name <- paste0("datos_tweets_", usuario, ".csv")
}
# Si no existe el fichero de almacenamiento, se crea uno nuevo con los
# resultados de la primera recuperación
if(!(output_file_name %in% list.files())){
datos_new <- get_timeline(user = usuario, n = maxtweets, parse = TRUE,
check = TRUE, include_rts = FALSE)
write_csv(x = datos_new, path = output_file_name, col_names = TRUE)
print("Nuevo fichero creado")
}else{
# Se leen los datos antiguos
datos_old <- read_csv(file = output_file_name)
# Se identifica el último Id recuperado
ultimo_id <- tail(datos_old, 1)["status_id"] %>% pull()
# Para no recuperar de nuevo el último tweet de la consulta anterior
# se incrementa en 1 el Id
ultimo_id = ultimo_id + 1
# Para que no haya errores de compatibilidad, se convierten todas las
# columnas numéricas a character
datos_old <- map_if(.x = datos_old, .p = is.numeric, .f = as.character)
# Extracción de nuevos tweets
datos_new <- get_timeline(user = usuario, n = maxtweets, max_id = ultimo_id,
parse = TRUE, check = TRUE, include_rts = FALSE)
datos_new <- map_if(.x = datos_new, .p = is.numeric, .f = as.character)
# Concatenación de los datos nuevos, viejos y escritura en disco
datos_concatenados <- bind_rows(datos_old, datos_new)
write_csv(x = datos_concatenados, path = output_file_name, col_names = TRUE)
print(paste("Número total de tweets:", nrow(datos_concatenados)))
print(paste("Número de tweets nuevos:", nrow(datos_new)))
}
}
Cada vez que se ejecuta la función extraccion_tweets()
se recuperan nuevos tweets y se almacenan junto a los extraídos previamente.
extraccion_tweets(usuario = "@elonmusk", maxtweets = 200)
extraccion_tweets(usuario = "@BillGates", maxtweets = 200)
extraccion_tweets(usuario = "@mayoredlee", maxtweets = 200)
Los datos utilizados en este análisis se han obtenido mediante la función definida en el apartado anterior. Los ficheros .csv pueden encontrarse en mi repositorio de github.
tweets_elon <- read_csv(file = "./datos/datos_tweets_@elonmusk.csv",
col_names = TRUE)
tweets_BillGates <- read_csv(file = "./datos/datos_tweets_@BillGates.csv",
col_names = TRUE)
tweets_mayoredlee <- read_csv(file = "./datos/datos_tweets_@mayoredlee.csv",
col_names = TRUE)
# Se unen todos los tweets en un único dataframe
tweets <- bind_rows(tweets_elon, tweets_BillGates, tweets_mayoredlee)
tweets %>% group_by(screen_name) %>% summarise(numero_tweets = n())
## # A tibble: 3 x 2
## screen_name numero_tweets
## <chr> <int>
## 1 BillGates 2087
## 2 elonmusk 2678
## 3 mayoredlee 2447
El número de tweets recuperados es superior a 2000 para todos los usuarios.
De entre toda la información disponible, en este análisis únicamente se emplea: autor del tweet, fecha de publicación, identificador del tweet y contenido.
colnames(tweets)
## [1] "screen_name" "user_id"
## [3] "created_at" "status_id"
## [5] "text" "retweet_count"
## [7] "favorite_count" "is_quote_status"
## [9] "quote_status_id" "is_retweet"
## [11] "retweet_status_id" "in_reply_to_status_status_id"
## [13] "in_reply_to_status_user_id" "in_reply_to_status_screen_name"
## [15] "lang" "source"
## [17] "media_id" "media_url"
## [19] "media_url_expanded" "urls"
## [21] "urls_display" "urls_expanded"
## [23] "mentions_screen_name" "mentions_user_id"
## [25] "symbols" "hashtags"
## [27] "coordinates" "place_id"
## [29] "place_type" "place_name"
## [31] "place_full_name" "country_code"
## [33] "country" "bounding_box_coordinates"
## [35] "bounding_box_type"
# Selección de variables
tweets <- tweets %>% select(screen_name, created_at, status_id, text)
# Se renombran las variables con nombres más prácticos
tweets <- tweets %>% rename(autor = screen_name, fecha = created_at,
texto = text, tweet_id = status_id)
head(tweets)
## # A tibble: 6 x 4
## autor fecha tweet_id texto
## <chr> <dttm> <dbl> <chr>
## 1 elonmusk 2017-11-09 17:28:57 9.29e17 "\"If one day, my words are again…
## 2 elonmusk 2017-11-09 17:12:46 9.29e17 "I placed the flowers\n\nThree br…
## 3 elonmusk 2017-11-08 18:55:13 9.28e17 Atatürk Anıtkabir https://t.co/al…
## 4 elonmusk 2017-11-07 19:48:45 9.28e17 @Bob_Richards One rocket, slightl…
## 5 elonmusk 2017-10-28 21:36:18 9.24e17 @uncover007 500 ft so far. Should…
## 6 elonmusk 2017-10-28 21:30:41 9.24e17 Picture of The Boring Company LA …
El proceso de limpieza de texto, dentro del ámbito de text mining, consiste en eliminar del texto todo aquello que no aporte información sobre su temática, estructura o contenido. No existe una única forma de hacerlo, depende en gran medida de la finalidad del análisis y de la fuente de la que proceda el texto. Por ejemplo, en las redes sociales los usuarios pueden escribir de la forma que quieran, lo que suele resultar en un uso elevado de abreviaturas y signos de puntuación. En este ejercicio, dado que los principales objetivos son estudiar el perfil lingüístico de los tres usuarios, identificar la autoría de los tweets y analizar el sentimiento que transmiten, se procede a eliminar:
Patrones no informativos (urls de páginas web)
Signos de puntuación
Etiquetas HTML
Caracteres sueltos
Números
Tokenizar un texto consiste en dividir el texto en las unidades que lo conforman, entendiendo por unidad el elemento más sencillo con significado propio para el análisis en cuestión, en este caso, las palabras.
Existen múltiples librerías que automatizan en gran medida la limpieza y tokenización de texto, por ejemplo, tokenizers
o quanteda
. Sin embargo, creo que se entiende mejor el proceso implemento una función propia que, si bien puede estar menos optimizada, es más transparente. Definir una función que contenga cada uno de los pasos de limpieza tiene la ventaja de poder adaptarse fácilmente dependiendo del tipo de texto analizado.
limpiar_tokenizar <- function(texto){
# El orden de la limpieza no es arbitrario
# Se convierte todo el texto a minúsculas
nuevo_texto <- tolower(texto)
# Eliminación de páginas web (palabras que empiezan por "http." seguidas
# de cualquier cosa que no sea un espacio)
nuevo_texto <- str_replace_all(nuevo_texto,"http\\S*", "")
# Eliminación de signos de puntuación
nuevo_texto <- str_replace_all(nuevo_texto,"[[:punct:]]", " ")
# Eliminación de números
nuevo_texto <- str_replace_all(nuevo_texto,"[[:digit:]]", " ")
# Eliminación de espacios en blanco múltiples
nuevo_texto <- str_replace_all(nuevo_texto,"[\\s]+", " ")
# Tokenización por palabras individuales
nuevo_texto <- str_split(nuevo_texto, " ")[[1]]
# Eliminación de tokens con una longitud < 2
nuevo_texto <- keep(.x = nuevo_texto, .p = function(x){str_length(x) > 1})
return(nuevo_texto)
}
test = "Esto es 1 ejemplo de l'limpieza de6 TEXTO https://t.co/rnHPgyhx4Z @JoaquinAmatRodrigo #textmining"
limpiar_tokenizar(texto = test)
## [1] "esto" "es" "ejemplo"
## [4] "de" "limpieza" "de"
## [7] "texto" "joaquinamatrodrigo" "textmining"
Puede observarse que la función limpiar_tokenizar()
elimina el símbolo @ y # de las palabra a las que acompañan. En Twitter, los usuarios se identifican de esta forma, por lo que @ y # pertenecen al nombre. Aunque es importante tener en cuenta las eliminaciones del proceso de limpieza, el impacto en este caso no es demasiado alto, ya que, si un documento se caracteriza por tener la palabra #datascience, también será detectado fácilmente mediante la palabra datascience.
# Se aplica la función de limpieza y tokenización a cada tweet
tweets <- tweets %>% mutate(texto_tokenizado = map(.x = texto,
.f = limpiar_tokenizar))
tweets %>% select(texto_tokenizado) %>% head()
## # A tibble: 6 x 1
## texto_tokenizado
## <list>
## 1 <chr [13]>
## 2 <chr [15]>
## 3 <chr [2]>
## 4 <chr [6]>
## 5 <chr [18]>
## 6 <chr [9]>
Gracias a la característica de las tibble de poder contener cualquier tipo de elemento en sus columnas (siempre que sea el mismo para toda la columna), se puede almacenar el texto tokenizado. Cada elemento de la columna texto_tokenizado es una lista con un vector de tipo character que contiene los tokens generados.
tweets %>% slice(1) %>% select(texto_tokenizado) %>% pull()
## [[1]]
## [1] "if" "one" "day" "my" "words" "are" "against"
## [8] "science" "choose" "science" "mustafa" "kemal" "atatürk"
En R
, las estructuras por excelencia para el análisis exploratorio son el DataFrame y la Tibble, que es la forma en la que se encuentra almacenada ahora la información de los tweets. Sin embargo, al realizar la tokenización, ha habido un cambio importante. Previamente a la división del texto, los elementos de estudio (observaciones) eran los tweets, y cada uno se encontraba en una fila, cumplimento así la condición de tidy data: una observación, una fila. Al realizar la tokenización, el elemento de estudio ha pasado a ser cada token (palabra), incumpliendo así la condición de tidy data. Para volver de nuevo a la estructura ideal se tiene que expandir cada lista de tokens, duplicando el valor de las otras columnas tantas veces como sea necesario. Ha este proceso se le conoce como expansión o unnest.
tweets_tidy <- tweets %>% select(-texto) %>% unnest()
tweets_tidy <- tweets_tidy %>% rename(token = texto_tokenizado)
head(tweets_tidy)
## # A tibble: 6 x 4
## autor fecha tweet_id token
## <chr> <dttm> <dbl> <chr>
## 1 elonmusk 2017-11-09 17:28:57 9.29e17 if
## 2 elonmusk 2017-11-09 17:28:57 9.29e17 one
## 3 elonmusk 2017-11-09 17:28:57 9.29e17 day
## 4 elonmusk 2017-11-09 17:28:57 9.29e17 my
## 5 elonmusk 2017-11-09 17:28:57 9.29e17 words
## 6 elonmusk 2017-11-09 17:28:57 9.29e17 are
Ahora que los la información está en formato tidy, se pueden realizar filtrados, sumatorios y representaciones con gran facilidad.
La función unnest_tokens()
del paquete tidytext
permite, entre otras cosas, automatizar el proceso tokenización y almacenamiento en formato tidy en un único paso.
Dado que cada usuario puede haber iniciado su actividad en Twitter en diferente momento, es interesante explorar si los tweets recuperados solapan en el tiempo.
library(lubridate)
ggplot(tweets, aes(x = as.Date(fecha), fill = autor)) +
geom_histogram(position = "identity", bins = 20, show.legend = FALSE) +
scale_x_date(date_labels = "%m-%Y", date_breaks = "5 month") +
labs(x = "fecha de publicación", y = "número de tweets") +
facet_wrap(~ autor, ncol = 1) +
theme_bw() +
theme(axis.text.x = element_text(angle = 90))
tweets_mes_anyo <- tweets %>% mutate(mes_anyo = format(fecha, "%Y-%m"))
tweets_mes_anyo %>% group_by(autor, mes_anyo) %>% summarise(n = n()) %>%
ggplot(aes(x = mes_anyo, y = n, color = autor)) +
geom_line(aes(group = autor)) +
labs(title = "Número de tweets publicados", x = "fecha de publicación",
y = "número de tweets") +
theme_bw() +
theme(axis.text.x = element_text(angle = 90, size = 6),
legend.position = "bottom")
Puede observarse un perfil de actividad distinto para cada usuario. Bill Gates ha mantenido una actividad constante de en torno a 30 tweets por mes durante todo el periodo estudiado. Elon Musk muestra una actividad inicial por debajo de la de Bill Gates pero, a partir de febrero de 2016 incrementó sustancialmente el número de tweets publicados. Ed Lee tiene una actividad muy alta sobre todo en el periodo 2017. Debido a las limitaciones que impone Twitter en las recuperaciones, cuanto más activo es un usuario, menor es el intervalo de tiempo para el que se recuperan tweets. En el caso de Ed Lee, dado que publica con mucha más frecuencia que el resto, con la misma cantidad de tweets recuperados se abarca menos de la mitad del rango temporal que con los otros.
A la hora de entender que caracteriza la escritura de cada usuario, es interesante estudiar qué palabras emplea, con qué frecuencia, así como el significado de las mismas.
tweets_tidy %>% group_by(autor) %>% summarise(n = n())
## # A tibble: 3 x 2
## autor n
## <chr> <int>
## 1 BillGates 31572
## 2 elonmusk 33584
## 3 mayoredlee 41787
tweets_tidy %>% ggplot(aes(x = autor)) + geom_bar() + coord_flip() + theme_bw()
tweets_tidy %>% select(autor, token) %>% distinct() %>% group_by(autor) %>%
summarise(palabras_distintas = n())
## # A tibble: 3 x 2
## autor palabras_distintas
## <chr> <int>
## 1 BillGates 4510
## 2 elonmusk 6552
## 3 mayoredlee 5471
tweets_tidy %>% select(autor, token) %>% distinct() %>%
ggplot(aes(x = autor)) + geom_bar() + coord_flip() + theme_bw()
Aunque Elon Musk no es el que más palabras totales ha utilizado, bien porque ha publicado menos tweets o porque estos son más cortos, es el que más palabras distintas emplea.
tweets_tidy %>% group_by(autor, tweet_id) %>% summarise(longitud = n()) %>% group_by(autor) %>% summarise(media_longitud = mean(longitud),
sd_longitud = sd(longitud))
## # A tibble: 3 x 3
## autor media_longitud sd_longitud
## <chr> <dbl> <dbl>
## 1 BillGates 15.2 3.35
## 2 elonmusk 12.6 6.90
## 3 mayoredlee 17.1 3.45
tweets_tidy %>% group_by(autor, tweet_id) %>% summarise(longitud = n()) %>% group_by(autor) %>%
summarise(media_longitud = mean(longitud),
sd_longitud = sd(longitud)) %>%
ggplot(aes(x = autor, y = media_longitud)) +
geom_col() +
geom_errorbar(aes(ymin = media_longitud - sd_longitud,
ymax = media_longitud + sd_longitud)) +
coord_flip() + theme_bw()
El tipo de tweet de Bill Gates y Mayor Ed Lee es similar en cuanto a longitud media y desviación. Elon Musk alterna más entre tweets cortos y largos, siendo su media inferior a la de los otros dos.
tweets_tidy %>% group_by(autor, token) %>% count(token) %>% group_by(autor) %>%
top_n(10, n) %>% arrange(autor, desc(n)) %>% print(n=30)
## # A tibble: 30 x 3
## # Groups: autor [3]
## autor token n
## <chr> <chr> <int>
## 1 BillGates the 1195
## 2 BillGates to 1117
## 3 BillGates of 670
## 4 BillGates in 591
## 5 BillGates is 453
## 6 BillGates and 439
## 7 BillGates for 363
## 8 BillGates this 345
## 9 BillGates we 334
## 10 BillGates on 333
## 11 elonmusk the 988
## 12 elonmusk to 916
## 13 elonmusk of 638
## 14 elonmusk is 543
## 15 elonmusk in 478
## 16 elonmusk for 400
## 17 elonmusk and 367
## 18 elonmusk it 351
## 19 elonmusk on 344
## 20 elonmusk that 315
## 21 mayoredlee to 1693
## 22 mayoredlee the 1355
## 23 mayoredlee amp 1212
## 24 mayoredlee our 1104
## 25 mayoredlee sf 944
## 26 mayoredlee for 822
## 27 mayoredlee of 819
## 28 mayoredlee we 782
## 29 mayoredlee in 780
## 30 mayoredlee is 452
En la tabla anterior puede observarse que los términos más frecuentes en todos los usuarios se corresponden con artículos, preposiciones, pronombres…, en general, palabras que no aportan información relevante sobre el texto. Ha estas palabras se les conoce como stopwords. Para cada idioma existen distintos listados de stopwords, además, dependiendo del contexto, puede ser necesario adaptar el listado. En la tabla anterior aparece el término amp que procede de la etiqueta html &. Con frecuencia, a medida que se realiza un análisis se encuentran palabras que deben incluirse en el listado de stopwords.
Dentro del paquete tidytext
y tokenizers
pueden encontrarse varios listados de stopwords para los idiomas “en”, “da”, “de”, “el”, “es”, “fr”, “it”, “ru”. En este caso, se emplea un listado de stopwords obtenido de la librería de Python nltk.corpus
.
lista_stopwords <- c('me', 'my', 'myself', 'we', 'our', 'ours', 'ourselves',
'you','your', 'yours', 'yourself', 'yourselves', 'he', 'him','his',
'himself', 'she', 'her', 'hers', 'herself', 'it', 'its', 'itself',
'they', 'them', 'their', 'theirs', 'themselves', 'what', 'which',
'who', 'whom', 'this', 'that', 'these', 'those', 'am', 'is', 'are',
'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
'having', 'do', 'does', 'did', 'doing', 'a', 'an', 'the', 'and',
'but', 'if', 'or', 'because', 'as', 'until', 'while', 'of', 'at',
'by', 'for', 'with', 'about', 'against', 'between', 'into',
'through', 'during', 'before', 'after', 'above', 'below', 'to',
'from', 'up', 'down', 'in', 'out', 'on', 'off', 'over', 'under',
'again', 'further', 'then', 'once', 'here', 'there', 'when',
'where', 'why', 'how', 'all', 'any', 'both', 'each', 'few', 'more',
'most', 'other', 'some', 'such', 'no', 'nor', 'not', 'only', 'own',
'same', 'so', 'than', 'too', 'very', 's', 't', 'can', 'will',
'just', 'don', 'should', 'now', 'd', 'll', 'm', 'o', 're', 've',
'y', 'ain', 'aren', 'couldn', 'didn', 'doesn', 'hadn', 'hasn',
'haven', 'isn', 'ma', 'mightn', 'mustn', 'needn', 'shan',
'shouldn', 'wasn', 'weren', 'won', 'wouldn','i')
# Se añade el término amp al listado de stopwords
lista_stopwords <- c(lista_stopwords, "amp")
# Se filtran las stopwords
tweets_tidy <- tweets_tidy %>% filter(!(token %in% lista_stopwords))
tweets_tidy %>% group_by(autor, token) %>% count(token) %>% group_by(autor) %>%
top_n(10, n) %>% arrange(autor, desc(n)) %>%
ggplot(aes(x = reorder(token,n), y = n, fill = autor)) +
geom_col() +
theme_bw() +
labs(y = "", x = "") +
theme(legend.position = "none") +
coord_flip() +
facet_wrap(~autor,scales = "free", ncol = 1, drop = TRUE)
Los resultados obtenidos tienen sentido si ponemos en contexto la actividad profesional de los usuarios analizados. Mayor Ed Lee es alcalde de San Francisco (sf), por lo que sus tweets están relacionados con la ciudad, residentes, familias, casas… Elon Musk dirige varias empresas tecnológicas entre las que destacan Tesla y SpaceX, dedicadas a los coches y a la aeronáutica. Por último, Bill Gates, además de propietario de microsoft, dedica parte de su capital a fundaciones de ayuda, de ahí las palabras mundo, polio, ayuda…
Otra forma visual de representar las palabras más frecuentes es mediante nubes de palabras (word clouds). En esta representación, las palabras más importantes tienen mayor tamaño.
library(wordcloud)
library(RColorBrewer)
wordcloud_custom <- function(grupo, df){
print(grupo)
wordcloud(words = df$token, freq = df$frecuencia,
max.words = 400, random.order = FALSE, rot.per = 0.35,
colors = brewer.pal(8, "Dark2"))
}
df_grouped <- tweets_tidy %>% group_by(autor, token) %>% count(token) %>%
group_by(autor) %>% mutate(frecuencia = n / n()) %>%
arrange(autor, desc(frecuencia)) %>% nest()
walk2(.x = df_grouped$autor, .y = df_grouped$data, .f = wordcloud_custom)
## [1] "BillGates"
## [1] "elonmusk"