Relaciones entre palabras

Ir al Capítulo anterior.
Ir al Case study: Netflix.

Este script asume que se han instalado los paquetes igraph y ggraph.

Hasta ahora hemos considerado las palabras como unidades individuales y hemos analizado sus relaciones con el tono o sentimiento del documento. Sin embargo, muchos análisis de texto interesantes se basan en las relaciones entre palabras, ya sea las que tienden a presentarse inmediatamente a otras, o cuáles tienden a coexistir dentro de los mismos documentos.

Cargar librerías

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

Leer datos

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

Tokenización por secuencias de palabras (bigramas)

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
# En este caso, cada token corresponde a una secuencia de dos palabras (bigrama)
bigram_climate <- climate_text %>% 
  select(-show_date) %>% 
  unnest_tokens(input = text, output = bigram, token = "ngrams", n = 2) %>% 
  tibble()

bigram_climate
## # A tibble: 40,483 × 3
##    station show            bigram             
##    <chr>   <chr>           <chr>              
##  1 MSNBC   Morning Meeting the interior       
##  2 MSNBC   Morning Meeting interior positively
##  3 MSNBC   Morning Meeting positively oozes   
##  4 MSNBC   Morning Meeting oozes class        
##  5 MSNBC   Morning Meeting class raves        
##  6 MSNBC   Morning Meeting raves car          
##  7 MSNBC   Morning Meeting car magazine       
##  8 MSNBC   Morning Meeting magazine slick     
##  9 MSNBC   Morning Meeting slick and          
## 10 MSNBC   Morning Meeting and sensuous       
## # ℹ 40,473 more rows

Muchos bigramas no son informativos (e.g., “of the”, “on the”, “is a”, …). Esto motiva aplicar nuevamente filtros con stop words. En este paso obtendremos las combinaciones de bigramas, identificando aquellos que se producen con más frecuencia.

bigram_climate %>% 
  count(bigram, sort = TRUE)
## # A tibble: 18,336 × 2
##    bigram             n
##    <chr>          <int>
##  1 climate change  1584
##  2 change is        237
##  3 of the           178
##  4 on climate       132
##  5 on the           121
##  6 of climate       118
##  7 in the           115
##  8 is a              94
##  9 about climate     93
## 10 this is           84
## # ℹ 18,326 more rows
bigram_counts <- bigram_climate %>% 
  select(-show) %>% 
  separate(col = bigram, into = c("word1", "word2")) %>% 
  
  # Eliminar stop words en ambas posiciones
  filter(!word1 %in% stop_words$word) %>%
  filter(!word2 %in% stop_words$word) %>%
  
  # Evitar "global warming"
  filter(word1 != "global" ) %>% 
  
  # Combinaciones más frecuentes 
  count(word1, word2, sort = TRUE) %>% 
  rename(weight = n)

bigram_counts
## # A tibble: 3,046 × 3
##    word1     word2   weight
##    <chr>     <chr>    <int>
##  1 climate   change    1584
##  2 donald    trump       40
##  3 al        gore        39
##  4 change    climate     34
##  5 president obama       25
##  6 james     cameron     19
##  7 sea       level       19
##  8 white     house       17
##  9 causing   climate     16
## 10 level     rise        16
## # ℹ 3,036 more rows

Una vez se ha determinado la frecuencia de bigramas, podemos definir la red de frecuencia.

library(igraph)
## 
## Attaching package: 'igraph'
## The following object is masked from 'package:tidyr':
## 
##     crossing
## The following objects are masked from 'package:dplyr':
## 
##     as_data_frame, groups, union
## The following objects are masked from 'package:stats':
## 
##     decompose, spectrum
## The following object is masked from 'package:base':
## 
##     union
# Crear un grafo no dirigido con los pares query-subject
g <- bigram_counts %>% 
  filter(weight > 10) %>%
  graph_from_data_frame(directed = FALSE)

# Visualización
library(ggraph)

set.seed(2025)
a <- grid::arrow(type = "closed", length = unit(.15, "inches"))

ggraph(g, layout = "fr") +
  geom_edge_link(aes(edge_alpha = weight), show.legend = FALSE,
                 arrow = a, end_cap = circle(.07, 'inches')) +
  geom_node_point(color = "lightblue", size = 5) +
  geom_node_text(aes(label = name), vjust = 1, hjust = 1) +
  theme_void() 

Common bigrams in climate change narratives, showing those that occurred more than 10 times and where neither word was a stop word.

It may take some experimentation with ggraph to get your networks into a presentable format like this, but the network structure is useful and flexible way to visualize relational tidy data.

bigram_MSNBC <- climate_text %>% 
  select(-show_date) %>% 
  unnest_tokens(input = text, output = bigram, token = "ngrams", n = 2) %>% 
  filter(station == "MSNBC") %>% 
  select(-show) %>% 
  
  separate(col = bigram, into = c("word1", "word2")) %>% 
  
  # Eliminar stop words en ambas posiciones
  filter(!word1 %in% stop_words$word) %>%
  filter(!word2 %in% stop_words$word) %>%
  
  # Evitar "global warming"
  filter(word1 != "global") %>% 
  
  # Combinaciones más frecuentes 
  count(word1, word2, sort = TRUE) %>% 
  rename(weight = n) %>% 


  tibble() 
## Warning: Expected 2 pieces. Additional pieces discarded in 1248 rows [44, 45, 172, 173,
## 314, 315, 356, 357, 386, 387, 416, 417, 447, 448, 460, 461, 593, 594, 598, 599,
## ...].
bigram_FOX <- climate_text %>% 
  select(-show_date) %>% 
  unnest_tokens(input = text, output = bigram, token = "ngrams", n = 2) %>% 
  filter(station == "FOX News") %>% 
  select(-show) %>% 
  
  separate(col = bigram, into = c("word1", "word2")) %>% 
  
  # Eliminar stop words en ambas posiciones
  filter(!word1 %in% stop_words$word) %>%
  filter(!word2 %in% stop_words$word) %>%
  
  # Evitar "global warming"
  filter(word1 != "global") %>% 
  
  # Combinaciones más frecuentes 
  count(word1, word2, sort = TRUE) %>% 
  rename(weight = n) %>% 


  tibble() 
## Warning: Expected 2 pieces. Additional pieces discarded in 688 rows [16, 17, 24, 25, 32,
## 33, 83, 84, 161, 162, 165, 166, 203, 204, 227, 228, 236, 379, 380, 415, ...].
# Crear un grafo no dirigido con los pares query-subject
g_msnbc <- bigram_MSNBC %>% 
  filter(weight > 10) %>%
  graph_from_data_frame(directed = FALSE)

g_fox <- bigram_FOX %>% 
  filter(weight > 6) %>%
  graph_from_data_frame(directed = FALSE)




# Visualización
set.seed(2025)
a <- grid::arrow(type = "closed", length = unit(.10, "inches"))

ggraph(g_msnbc, layout = "fr") +
  geom_edge_link(aes(edge_alpha = weight), show.legend = FALSE,
                 arrow = a, end_cap = circle(.07, 'inches')) +
  geom_node_point(color = "red3", size = 5) +
  geom_node_text(aes(label = name), vjust = 1, hjust = 1) +
  theme_void() + 
  labs(
    title = "Red de relaciones MSNBC",
    subtitle = "Basado en pesos de conexión",
    caption = "Fuente: Datos de MSNBC")

ggraph(g_fox, layout = "fr") +
  geom_edge_link(aes(edge_alpha = weight), show.legend = FALSE,
                 arrow = a, end_cap = circle(.07, 'inches')) +
  geom_node_point(color = "dodgerblue", size = 5) +
  geom_node_text(aes(label = name), vjust = 1, hjust = 1) +
  theme_void() +
  labs(title = "Red de relaciones MSNBC",
       subtitle = "Basado en pesos de conexión",
       caption = "Fuente: Datos de MSNBC")

options(repr.plot.width = 14, repr.plot.height = 7)
library(gridExtra)
## 
## Attaching package: 'gridExtra'
## The following object is masked from 'package:dplyr':
## 
##     combine
grid.arrange(ggraph(g_msnbc, layout = "fr") +
  geom_edge_link(aes(edge_alpha = weight), show.legend = FALSE,
                 arrow = a, end_cap = circle(.07, 'inches')) +
  geom_node_point(color = "red3", size = 5) +
  geom_node_text(aes(label = name), vjust = 1, hjust = 1) +
  theme_graph() +   
  labs(title = "Red de relaciones MSNBC",
       subtitle = "Basado en pesos de conexión"),
    
  ggraph(g_fox, layout = "fr") +
  geom_edge_link(aes(edge_alpha = weight), show.legend = FALSE,
                 arrow = a, end_cap = circle(.07, 'inches')) +
  geom_node_point(color = "dodgerblue", size = 5) +
  geom_node_text(aes(label = name), vjust = 1, hjust = 1) +
  theme_graph() + 
  labs(title = "Red de relaciones MSNBC",
       subtitle = "Basado en pesos de conexión"),
  
  ncol = 2)

In other analyses, we may want to work with the recombined words. For example, we can look at the tf-idf (Chapter 3) of bigrams across news stations. These tf-idf values can be visualized within each station, just as we did for words.

bigram_climate
## # A tibble: 40,483 × 3
##    station show            bigram             
##    <chr>   <chr>           <chr>              
##  1 MSNBC   Morning Meeting the interior       
##  2 MSNBC   Morning Meeting interior positively
##  3 MSNBC   Morning Meeting positively oozes   
##  4 MSNBC   Morning Meeting oozes class        
##  5 MSNBC   Morning Meeting class raves        
##  6 MSNBC   Morning Meeting raves car          
##  7 MSNBC   Morning Meeting car magazine       
##  8 MSNBC   Morning Meeting magazine slick     
##  9 MSNBC   Morning Meeting slick and          
## 10 MSNBC   Morning Meeting and sensuous       
## # ℹ 40,473 more rows
bigram_tf_idf <- bigram_climate %>%
  count(station, bigram) %>%
  bind_tf_idf(bigram, station, n) %>%
  arrange(desc(tf_idf))

bigram_tf_idf
## # A tibble: 21,163 × 6
##    station  bigram               n       tf   idf   tf_idf
##    <chr>    <chr>            <int>    <dbl> <dbl>    <dbl>
##  1 MSNBC    against climate     22 0.00114   1.10 0.00126 
##  2 MSNBC    cameron about       19 0.000988  1.10 0.00109 
##  3 MSNBC    fight against       19 0.000988  1.10 0.00109 
##  4 MSNBC    his fight           19 0.000988  1.10 0.00109 
##  5 MSNBC    james cameron       19 0.000988  1.10 0.00109 
##  6 CNN      will get            10 0.000947  1.10 0.00104 
##  7 FOX News change if            9 0.000842  1.10 0.000925
##  8 FOX News they will            9 0.000842  1.10 0.000925
##  9 CNN      because romney       8 0.000757  1.10 0.000832
## 10 CNN      change activists     8 0.000757  1.10 0.000832
## # ℹ 21,153 more rows
bigram_tf_idf %>% 
  group_by(station) %>% 
  slice_max(tf_idf, n = 10) %>% 
  mutate(word = reorder_within(bigram, tf_idf, station)) %>% #ordena dentro de cada grupo
  ungroup() %>% 
  ggplot(aes(x = bigram, y = tf_idf, fill = station)) + 
  geom_col(show.legend = FALSE) + 
  facet_wrap(~ station, scales = "free_y") + 
  coord_flip() + 
  labs(title = "tf-idf values per station", 
       y = "tf-idf", x = NULL)

These words are, as measured by tf-idf, the most important to each news station.
This is the point of tf-idf; it identifies words that are important to one document within a collection of documents.