Texto como dato

Ir al Capítulo anterior.
Ir al Capítulo siguiente

Cargar librerías

library(dplyr)
library(ggplot2)
library(forcats)
library(tidytext)

Leer datos

climate_text <- read.csv(file = "data/climate_text.csv")

Observar los datos

glimpse(climate_text)
## Rows: 593
## Columns: 4
## $ station   <chr> "MSNBC", "MSNBC", "CNN", "CNN", "MSNBC", "MSNBC", "CNN", "CN…
## $ show      <chr> "Morning Meeting", "Morning Meeting", "CNN Newsroom", "Ameri…
## $ show_date <chr> "2009-09-22 13:00:00", "2009-10-23 13:00:00", "2009-12-03 20…
## $ text      <chr> "the interior positively oozes class raves car magazine slic…
climate_text %>% tibble()
## # A tibble: 593 × 4
##    station show                                 show_date           text        
##    <chr>   <chr>                                <chr>               <chr>       
##  1 MSNBC   Morning Meeting                      2009-09-22 13:00:00 the interio…
##  2 MSNBC   Morning Meeting                      2009-10-23 13:00:00 corporation…
##  3 CNN     CNN Newsroom                         2009-12-03 20:00:00 he says he …
##  4 CNN     American Morning                     2009-12-07 11:00:00 especially …
##  5 MSNBC   Morning Meeting                      2009-12-08 14:00:00 lots more c…
##  6 MSNBC   Countdown With Keith Olbermann       2009-12-10 06:00:00 so they're …
##  7 CNN     Sanjay Gupta MD                      2009-12-12 12:30:00 let me ask …
##  8 CNN     The Situation Room With Wolf Blitzer 2009-12-16 21:00:00 other impor…
##  9 MSNBC   Countdown With Keith Olbermann       2009-12-19 01:00:00 let democra…
## 10 MSNBC   The Rachel Maddow Show               2010-01-08 04:00:00 you know th…
## # ℹ 583 more rows

Procesado datos

climate_text <- climate_text %>% 
  as_tibble()

climate_text %>% 
  filter(station == "CNN") %>% 
  count(show, sort = T) 
## # A tibble: 44 × 2
##    show                                                  n
##    <chr>                                             <int>
##  1 CNN Newsroom                                         18
##  2 New Day                                              17
##  3 Anderson Cooper 360                                   9
##  4 Early Start With John Berman and Christine Romans     9
##  5 Fareed Zakaria GPS                                    7
##  6 Piers Morgan Tonight                                  7
##  7 The Situation Room With Wolf Blitzer                  7
##  8 Erin Burnett OutFront                                 6
##  9 CNN Newsroom With Brooke Baldwin                      5
## 10 CNN Tonight With Don Lemon                            4
## # ℹ 34 more rows
climate_text %>% 
  filter(station == "CNN") %>% 
  count(station, show, sort = T) %>% 
  summarize(total_shows = n())
## # A tibble: 1 × 1
##   total_shows
##         <int>
## 1          44
by_station <- climate_text %>% 
  group_by(station)

by_station %>% 
  group_keys()            #-> ver las agrupaciones
## # A tibble: 3 × 1
##   station 
##   <chr>   
## 1 CNN     
## 2 FOX News
## 3 MSNBC
by_station %>% 
  select(station, show) %>%   
  distinct() %>%
  summarize(total = n())
## # A tibble: 3 × 2
##   station  total
##   <chr>    <int>
## 1 CNN         44
## 2 FOX News    40
## 3 MSNBC       51
by_station %>% 
  select(station, show) %>%   
  distinct() %>%
  summarize(total = n()) %>% 
  summarize(station = n(), 
            mean_shows = mean(total), 
            max_shows = max(total),
            min_shows = min(total))
## # A tibble: 1 × 4
##   station mean_shows max_shows min_shows
##     <int>      <dbl>     <int>     <int>
## 1       3         45        51        40
by_station %>% 
  select(station, show) %>%   
  distinct() %>%
  count()
## # A tibble: 3 × 2
## # Groups:   station [3]
##   station      n
##   <chr>    <int>
## 1 CNN         44
## 2 FOX News    40
## 3 MSNBC       51
by_station %>% 
  select(station, show) %>%   
  distinct() %>%
  count(sort = T)
## # A tibble: 3 × 2
## # Groups:   station [3]
##   station      n
##   <chr>    <int>
## 1 MSNBC       51
## 2 CNN         44
## 3 FOX News    40
by_station %>% 
  select(station, show) %>%   
  distinct() %>%
  count() %>% 
  arrange(desc(n))
## # A tibble: 3 × 2
## # Groups:   station [3]
##   station      n
##   <chr>    <int>
## 1 MSNBC       51
## 2 CNN         44
## 3 FOX News    40
by_station %>% 
  select(station, show) %>%   
  distinct() %>%
  count() %>% 
  arrange(n)
## # A tibble: 3 × 2
## # Groups:   station [3]
##   station      n
##   <chr>    <int>
## 1 FOX News    40
## 2 CNN         44
## 3 MSNBC       51

Los resúmenes agrupados son muy útiles para explorar y trabajar con datos. Normalmente, se agrupan según datos categóricos y se resumen los valores numéricos con funciones como mean, maxo min. Pero ¿qué pasa si queremos resumir datos categóricos? Esta es una pregunta clave, ya que los datos de texto son categóricos.

En este momento, no podemos contar datos categóricos porque el texto no está estructurado. Necesitamos una forma de darle estructura, idealmente siguiendo los principios de tidyverse, para poder seguir usando las funciones que conocemos del universo tidyverse.

Tokenización

Después de indicar el data frame de entrada, se especifica el nombre de la nueva columna donde estarán las palabras obtenidas al tokenizar (word), seguido del nombre de la columna que contiene el texto original. En lugar de tener una reseña por fila, ahora tenemos una palabra por fila.

tidy_climate <- climate_text %>% 
  select(-show_date) %>% 
  unnest_tokens(word, text)

tidy_climate
## # A tibble: 41,076 × 3
##    station show            word      
##    <chr>   <chr>           <chr>     
##  1 MSNBC   Morning Meeting the       
##  2 MSNBC   Morning Meeting interior  
##  3 MSNBC   Morning Meeting positively
##  4 MSNBC   Morning Meeting oozes     
##  5 MSNBC   Morning Meeting class     
##  6 MSNBC   Morning Meeting raves     
##  7 MSNBC   Morning Meeting car       
##  8 MSNBC   Morning Meeting magazine  
##  9 MSNBC   Morning Meeting slick     
## 10 MSNBC   Morning Meeting and       
## # ℹ 41,066 more rows

Como ventaja extra, unnest_tokens() también limpia el texto: elimina la puntuación, pone todas las palabras en minúsculas y quita los espacios en blanco. Al tener una palabra por fila, el número de filas en el conjunto de datos ha pasado de 593 a 41076.

Ahora que el texto tiene una estructura ordenada (tidy), podemos contar las palabras usando la función count(). No debería sorprendernos que las palabras más frecuentes sean muy comunes, como the, que no aportan mucho sobre el tratamiento de la información. Necesitamos hacer una limpieza adicional antes de que el conteo de palabras sea realmente útil.

tidy_climate %>% 
  count(word, sort = TRUE)
## # A tibble: 4,218 × 2
##    word        n
##    <chr>   <int>
##  1 the      1913
##  2 climate  1627
##  3 change   1615
##  4 is       1033
##  5 to       1028
##  6 of        845
##  7 a         823
##  8 and       802
##  9 that      652
## 10 in        535
## # ℹ 4,208 more rows

Estas palabras comunes y poco informativas se llaman stop words, y queremos eliminarlas de nuestro data frame ya ordenado. Para eso, podemos usar funciones del paquete dplyr conocidas como joins, que sirven para unir dos data frames según una o más columnas en común. En este caso, usaremos anti_join(). Esta función conserva las filas del data frame de la izquierda siempre que el valor en la columna coincidente no aparezca en el data frame de la derecha.

El paquete tidytext incluye un data frame llamado stop_words. Si usamos el operador pipe para pasar los datos tokenizados de review_data a anti_join(), colocando review_data como el data frame de la izquierda y stop_words como el de la derecha, eliminaremos esas palabras comunes. Como resultado, el número de filas se reduce notablemente (~13%).

# reconstruir el data frame 'limpio'
tidy_climate %>% 
  anti_join(stop_words) %>% 
  count(word, sort = T)
## Joining with `by = join_by(word)`
## # A tibble: 3,699 × 2
##    word          n
##    <chr>     <int>
##  1 climate    1627
##  2 change     1615
##  3 people      139
##  4 real        125
##  5 president   112
##  6 global      107
##  7 issue        87
##  8 trump        86
##  9 warming      85
## 10 issues       69
## # ℹ 3,689 more rows

Ahora, tenemos nuevo bag of words donde las palabras más frecuentes son climate, y change, que sobre el tema específico del climate change quizás tampoco sean informativas. Podemos actualizar las stop words a nuestros casos concretos de la siguiente forma :

custom_stop_words <- tribble(~ word, ~lexicon,
                             "climate", "CUSTOM", 
                             "change", "CUSTOM")

stop_words2 <- stop_words %>% 
  bind_rows(custom_stop_words)

tidy_climate %>% 
  anti_join(stop_words2) %>% 
  count(word, sort = T)
## # A tibble: 3,697 × 2
##    word          n
##    <chr>     <int>
##  1 people      139
##  2 real        125
##  3 president   112
##  4 global      107
##  5 issue        87
##  6 trump        86
##  7 warming      85
##  8 issues       69
##  9 talk         68
## 10 world        65
## # ℹ 3,687 more rows
# reconstruir el data frame 'limpio'
tidy_climate <- tidy_climate %>% 
  anti_join(stop_words2) %>% 
  tibble()

tidy_climate
## # A tibble: 13,354 × 3
##    station show            word      
##    <chr>   <chr>           <chr>     
##  1 MSNBC   Morning Meeting interior  
##  2 MSNBC   Morning Meeting positively
##  3 MSNBC   Morning Meeting oozes     
##  4 MSNBC   Morning Meeting class     
##  5 MSNBC   Morning Meeting raves     
##  6 MSNBC   Morning Meeting car       
##  7 MSNBC   Morning Meeting magazine  
##  8 MSNBC   Morning Meeting slick     
##  9 MSNBC   Morning Meeting sensuous  
## 10 MSNBC   Morning Meeting boasts    
## # ℹ 13,344 more rows

En ocasiones nos interesará grabar un objeto que hemos generado durante el análisis para recuperarlo más adelante en otra sesión de trabajo.

save(tidy_climate, file = "res/tidy_climate.rda")

Visualización de palabras

tidy_climate %>% 
  count(word, sort = T) %>% 
  ggplot(aes(x = word, y = n)) + 
  geom_col()

El primer problema es que estamos representando demasiadas palabras a la vez. Normalmente estaremos interesados en las palabras más frecuentes. Vamos a mantener aquellas palabras que superan un umbral establecido. Podemos usar filter().

Mejorar la figura

tidy_climate %>% 
  count(word, sort = T) %>% 
  filter(n > 50)  %>% 
  arrange(desc(n)) 
## # A tibble: 16 × 2
##    word          n
##    <chr>     <int>
##  1 people      139
##  2 real        125
##  3 president   112
##  4 global      107
##  5 issue        87
##  6 trump        86
##  7 warming      85
##  8 issues       69
##  9 talk         68
## 10 world        65
## 11 time         58
## 12 obama        57
## 13 threat       57
## 14 lot          54
## 15 country      52
## 16 science      51

El segundo problema era que las palabras se superponen y resultan difíciles de leer en el eje x. Podemos usar coord_flip().

tidy_climate %>% 
  count(word, sort = T) %>% 
  filter(n > 50)  %>% 
  arrange(desc(n)) %>% 
  ggplot(aes(x = forcats::fct_reorder(word, n ), y = n)) + 
  geom_col() + 
  coord_flip() + 
   labs(title = "Word counts in News Stations",
    x = "",
    y = "")


Ejercicio.

# Visualización por station 
tidy_climate %>% 
  filter(station == "MSNBC") %>% 
  count(word, sort = T) %>% 
  slice(1:10) %>% 
  ggplot(aes(x = forcats::fct_reorder(word, n ), y = n)) + 
  geom_col() + 
  coord_flip() + 
   labs(title = "Word counts in MSNBC",
    x = "",
    y = "")

# Idem para 'CNN' y 'FOX


# Alternativa facet_wrap 
tidy_climate %>% 
  count(station, word, name = "total") %>% 
  group_by(station) %>% 
  slice_max(total, n = 10) %>% 
  ungroup() %>%  
  ggplot(aes(x = word, y = total, fill = station)) +
  geom_col(show.legend = FALSE) + 
  coord_flip() + 
  facet_wrap(~station) + 
  labs(title = "Top 10 Word counts by News Station",
    x = "",
    y = "")

Podemos observar que entre las palabras más frecuentes por cadena, algunas se repiten en las tres, como ‘people’ o ‘global’. Sin embargo, también hay términos que parecen ser característicos de cada cadena (al menos, no aparecen entre las 10 más usadas por las otras dos). Por ejemplo, CNN es la única que incluye en su top 10 palabras como ‘reporter’ o ‘happening’; FOX News, por su parte, destaca con ‘threat’ o ‘bob’ (??*); y MSNBC utiliza términos como ‘talk’, ‘debate’ o ‘country’.

Podemos ver las palabras más usadas por cada cadena ordenando dentro de cada grupo con reorder_within del paquete tidytext:

tidy_climate %>% 
  count(station, word, sort = T) %>% 
  group_by(station) %>% 
  slice_max(n, n = 10) %>% 
  arrange(station) %>% 
  mutate(word = reorder_within(word, n, station)) %>% #ordena dentro de cada grupo
  ungroup() %>% 
  ggplot(aes(x = word,  y = n, fill = station)) + 
  geom_col(show.legend = FALSE) + 
  facet_wrap(~ station, scales = "free_y") + 
  coord_flip() + 
  scale_x_reordered() +
  labs(title = "Top 10 Word counts by News Station", 
       x = "Words")

(*) Bob Beckel fue un destacado analista político, comentarista, asesor y colaborador mediático estadounidense. Formó parte del panel original del programa The Five en Fox News desde su inicio en 2011.

library(stringr)
climate_text %>% 
  filter(station == "FOX News", show == "The Five") %>% 
  filter(str_detect(text, "bob")) %>% 
  slice(1)
## # A tibble: 1 × 4
##   station  show     show_date           text                                    
##   <chr>    <chr>    <chr>               <chr>                                   
## 1 FOX News The Five 2011-07-21 21:00:00 greg never mind bob i want to move on a…

Nube de palabras

Un gráfico de barras es probablemente la forma más efectiva de visualizar los recuentos de palabras. Sin embargo, a veces podemos necesitar algo un poco más sugerente.

if("wordcloud" %in% rownames(installed.packages()) == FALSE) {
  install.packages("wordcloud") }

library(wordcloud)

word_counts <- tidy_climate %>% 
  count(word)

set.seed(2025)
wordcloud(words = word_counts$word, 
          freq = word_counts$n, 
          max.words = 25, 
          color = "steelblue")

# size of each word is based on relative word count 
# location of each word in the cloud is random 
  
#probar esta syntaxis : 
# wc = word_counts %>%
# with(wordcloud(word, n, min.freq = 1, max.words = 125, colors=brewer.pal(8, "Dark2")))

Combinar varias gráficas en 1 figura :