Hasta este punto, hemos considerado las palabras como unidades individuales y hemos examinado sus relaciones con los sentimientos o los documentos. Sin embargo, muchos análisis de texto interesantes se basan en las relaciones entre palabras, ya sea al examinar qué palabras tienden a seguir a otras inmediatamente, o qué palabras tienden a co-ocurrir dentro de los mismos documentos.
En este capítulo, exploraremos algunos de los métodos que ofrece
tidytext
para calcular y visualizar relaciones entre
palabras en tu conjunto de datos de texto. Esto incluye el argumento
token = "ngrams"
, que realiza una tokenización de pares de
palabras adyacentes en lugar de palabras individuales. También
introduciremos dos paquetes nuevos: ggraph
, que extiende
ggplot2
para construir gráficos de red, y
widyr
, que calcula correlaciones y distancias entre pares
dentro de un marco de datos ordenado. Estas herramientas amplían nuestra
caja de herramientas para explorar el texto dentro del marco de datos
ordenado.
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.2 ✔ readr 2.1.4
## ✔ forcats 1.0.0 ✔ stringr 1.5.0
## ✔ ggplot2 3.4.2 ✔ tibble 3.2.1
## ✔ lubridate 1.9.2 ✔ tidyr 1.3.0
## ✔ purrr 1.0.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(tidytext)
Ya tenemos organizado los tres tomos en un dataset con información sobre el tomo, el captítulo y la entrada del diario. Vamos a mejorar un poco las cateogrías de los tomos:
renzi <- read_csv('../data/renzi.csv')
## Rows: 1997 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (2): tomo, entry
## dbl (1): chapter
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
renzi <- renzi %>%
mutate(tomo = case_when(
tomo == '1_diarios_renzi_años_de_formacion.txt' ~ 'I-Años de formación',
tomo == '2_diarios_renzi_los_años_felices.txt' ~ 'II-Los años felices',
tomo == '3_diarios_renzi_un_dia_en_la_vida.txt' ~ 'III-Un día en la vida',
))
Vamos a eliminar acentos:
renzi <- renzi %>%
mutate(entry = stringi::stri_trans_general(str = entry,
id = "Latin-ASCII"))
Hasta ahora, hemos estado utilizando la función
unnest_tokens
para tokenizar por palabra, o a veces por
oración, lo cual es útil para los tipos de análisis de sentimiento y
frecuencia que hemos estado haciendo hasta este punto. Pero también
podemos utilizar esta función para tokenizar en secuencias consecutivas
de palabras, lo que llamamos n-gramas. Al observar con qué frecuencia la
palabra X es seguida por la palabra Y, podemos construir un modelo de
sus relaciones.
Para lograr esto, agregamos la opción token = "ngrams"
a
la función unnest_tokens()
, y establecemos el valor de
n
en el número de palabras que deseamos capturar en cada
n-grama. Cuando establecemos n
en 2, estamos examinando
pares de dos palabras consecutivas, a menudo llamadas “bigramas”.
renzi_bigrams <- renzi %>%
unnest_tokens(bigram, entry, token = "ngrams", n = 2) %>%
filter(!is.na(bigram))
renzi_bigrams
## # A tibble: 384,583 × 3
## tomo chapter bigram
## <chr> <dbl> <chr>
## 1 I-Años de formación 1 capitulo 1
## 2 I-Años de formación 1 1 en
## 3 I-Años de formación 1 en el
## 4 I-Años de formación 1 el umbral
## 5 I-Años de formación 1 umbral desde
## 6 I-Años de formación 1 desde chico
## 7 I-Años de formación 1 chico repito
## 8 I-Años de formación 1 repito lo
## 9 I-Años de formación 1 lo que
## 10 I-Años de formación 1 que no
## # ℹ 384,573 more rows
Esta estructura de datos sigue siendo una variación del formato de texto ordenado. Está estructurada con un token por fila (con metadatos adicionales, como el libro, todavía conservados), pero cada token representa un bigrama.
Es importante notar que estos bigramas se superponen: “el umbral” es un token, mientras que “umbral desde” es otro.
Nuestras herramientas habituales de tidy data se aplican igualmente
bien al análisis de n-gramas. Podemos examinar los bigramas más comunes
utilizando la función count()
de dplyr:
renzi_bigrams %>%
count(bigram, sort = TRUE)
## # A tibble: 166,824 × 2
## bigram n
## <chr> <int>
## 1 de la 3343
## 2 en el 2527
## 3 en la 2420
## 4 a la 1688
## 5 lo que 1140
## 6 de los 994
## 7 que se 894
## 8 en un 695
## 9 de un 657
## 10 que no 637
## # ℹ 166,814 more rows
Como uno podría esperar, muchas de las bigramas más comunes son pares de palabras comunes (poco interesantes), como “de la” y “en el”: lo que llamamos “stopword”.
Este es un momento adecuado para usar la función
separate()
de tidyr
, que divide una columna en
varias basadas en un delimitador. Esto nos permite separarla en dos
columnas, “palabra1” y “palabra2”, momento en el cual podemos eliminar
los casos en los que cualquiera de ellas sea una stopword.
Para ello, cargamos nuestro diccionario de stopwords y eliminamos acentos:
stop_words <- read_csv('../data/stop_words_complete.csv')
## Rows: 1766 Columns: 2
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (2): word, lexicon
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
stop_words <- stop_words %>%
mutate(word = stringi::stri_trans_general(str = word,
id = "Latin-ASCII")) %>%
add_row(word=c("capitulo", "uno", "dos", "tres", "cuatro", "cinco",
"seis", "siete", "ocho", "nueve", "diez", "siempre", "solo"),
lexicon=c(rep("custom", 13)))
bigrams_separated <- renzi_bigrams %>%
separate(bigram, c("word1", "word2"), sep = " ")
bigrams_filtered <- bigrams_separated %>%
filter(!word1 %in% stop_words$word) %>%
filter(!word2 %in% stop_words$word)
# new bigram counts:
bigram_counts <- bigrams_filtered %>%
count(word1, word2, sort = TRUE)
bigram_counts
## # A tibble: 52,221 × 3
## word1 word2 n
## <chr> <chr> <int>
## 1 buenos aires 195
## 2 mil pesos 126
## 3 cada vez 116
## 4 mismo tiempo 66
## 5 primera vez 64
## 6 puede ser 56
## 7 jorge alvarez 55
## 8 tal vez 49
## 9 quiere decir 43
## 10 debe ser 41
## # ℹ 52,211 more rows
Podemos ver que hay bastantes nombres de lugares (buenos_aires), de personas (jorge_alvarez), números (mil_pesos), etc.
En otros análisis, es posible que deseemos trabajar con las palabras
recombinadas. La función unite()
de tidyr
es
el inverso de separate()
, y nos permite combinar las
columnas en una sola. Así, “separate/filter/count/unite” nos permite
encontrar las bigramas más comunes que no contienen stopwords.
bigrams_united <- bigrams_filtered %>%
unite(bigram, word1, word2, sep = " ")
bigrams_united
## # A tibble: 63,201 × 3
## tomo chapter bigram
## <chr> <dbl> <chr>
## 1 I-Años de formación 1 chico repito
## 2 I-Años de formación 1 reia retrospectivo
## 3 I-Años de formación 1 radiante emilio
## 4 I-Años de formación 1 emilio renzi
## 5 I-Años de formación 1 abuelo emilio
## 6 I-Años de formación 1 emilio sentado
## 7 I-Años de formación 1 cuero ausente
## 8 I-Años de formación 1 ojos fijos
## 9 I-Años de formación 1 misterioso objeto
## 10 I-Años de formación 1 objeto rectangular
## # ℹ 63,191 more rows
En otros análisis, es posible que estés interesado en los trigramas
más comunes, que son secuencias consecutivas de 3 palabras. Podemos
encontrar esto estableciendo n = 3
:
renzi %>%
unnest_tokens(trigram, entry, token = "ngrams", n = 3) %>%
filter(!is.na(trigram)) %>%
separate(trigram, c("word1", "word2", "word3"), sep = " ") %>%
filter(!word1 %in% stop_words$word,
!word2 %in% stop_words$word,
!word3 %in% stop_words$word) %>%
count(word1, word2, word3, sort = TRUE)
## # A tibble: 19,786 × 4
## word1 word2 word3 n
## <chr> <chr> <chr> <int>
## 1 veinte mil pesos 17
## 2 cada vez menos 15
## 3 cincuenta mil pesos 13
## 4 dipi di paola 12
## 5 cien mil pesos 10
## 6 quinientos mil pesos 10
## 7 mata hari 55 9
## 8 dia tras dia 8
## 9 editorial tiempo contemporaneo 8
## 10 nestor garcia canclini 7
## # ℹ 19,776 more rows
Este formato de un bigrama por fila es útil para análisis exploratorios del texto. Como ejemplo sencillo, podríamos estar interesados en la forma en que Renzi menciona la palabra “literatura”:
bigrams_filtered %>%
filter(word1 == "literatura" | word2 == "literatura") %>%
count(tomo, chapter,word1, word2, sort = TRUE)
## # A tibble: 182 × 5
## tomo chapter word1 word2 n
## <chr> <dbl> <chr> <chr> <int>
## 1 II-Los años felices 3 literatura norteamericana 5
## 2 I-Años de formación 18 literatura argentina 4
## 3 II-Los años felices 3 literatura argentina 4
## 4 I-Años de formación 11 literatura contemporanea 3
## 5 II-Los años felices 1 literatura argentina 3
## 6 II-Los años felices 1 literatura contemporanea 3
## 7 III-Un día en la vida 5 literatura argentina 3
## 8 I-Años de formación 16 literatura argentina 2
## 9 I-Años de formación 18 literatura latinoamericana 2
## 10 I-Años de formación 18 literatura norteamericana 2
## # ℹ 172 more rows
Un bigrama también puede ser tratado como un término en un documento de la misma manera que tratamos palabras individuales. Por ejemplo, podemos analizar el tf-idf de bigramas en los diarios de Piglia. Estos valores de tf-idf pueden visualizarse dentro de cada tomo, al igual que hicimos para las palabras.
bigram_tf_idf <- bigrams_united %>%
count(tomo, bigram) %>%
bind_tf_idf(bigram, tomo, n) %>%
arrange(desc(tf_idf))
bigram_tf_idf
## # A tibble: 55,677 × 6
## tomo bigram n tf idf tf_idf
## <chr> <chr> <int> <dbl> <dbl> <dbl>
## 1 I-Años de formación moneda griega 10 0.000486 1.10 0.000534
## 2 II-Los años felices norberto soares 12 0.000468 1.10 0.000514
## 3 III-Un día en la vida mil dolares 21 0.00124 0.405 0.000502
## 4 II-Los años felices serie negra 10 0.000390 1.10 0.000428
## 5 II-Los años felices vino david 10 0.000390 1.10 0.000428
## 6 III-Un día en la vida anita barrenechea 6 0.000354 1.10 0.000388
## 7 III-Un día en la vida penso renzi 6 0.000354 1.10 0.000388
## 8 II-Los años felices cosas concretas 9 0.000351 1.10 0.000385
## 9 I-Años de formación dijo despues 7 0.000340 1.10 0.000374
## 10 I-Años de formación dijo lucia 7 0.000340 1.10 0.000374
## # ℹ 55,667 more rows
Similar a lo que descubrimos las clases anteriores, aparecen muchos nombres propios. También aparecen cuestiones monetarias, libros y revistas.
Existen ventajas y desventajas en examinar el tf-idf de bigramas en lugar de palabras individuales. Pares de palabras consecutivas pueden capturar estructuras que no están presentes cuando solo se cuentan palabras individuales, y pueden proporcionar contexto que hace que los tokens sean más comprensibles (por ejemplo, “serie negra” en “II - Los años felices” es más informativo que “serie”).
Sin embargo, las métricas por bigrama también son más dispersas: un par de palabras típico es más raro que cualquiera de sus palabras componentes. Por lo tanto, los bigramas pueden ser especialmente útiles cuando tienes un conjunto de datos de texto muy grande.
Nuestro enfoque de análisis de sentimiento que vimos la clase pasada simplemente contó la aparición de palabras positivas o negativas, de acuerdo con un léxico de referencia. Uno de los problemas con este enfoque es que el contexto de una palabra puede ser tan importante como su presencia. Por ejemplo, las palabras “feliz” y “gustar” serán contadas como positivas, incluso en una frase como “¡No estoy feliz y no me gusta!”
Ahora que tenemos los datos organizados en bigramas, es fácil determinar con qué frecuencia las palabras están precedidas por una palabra como “no”:
bigrams_separated %>%
filter(word1 == "no") %>%
count(word1, word2, sort = TRUE)
## # A tibble: 819 × 3
## word1 word2 n
## <chr> <chr> <int>
## 1 no se 353
## 2 no me 204
## 3 no es 192
## 4 no hay 148
## 5 no lo 90
## 6 no puedo 89
## 7 no le 76
## 8 no puede 62
## 9 no tiene 57
## 10 no habia 51
## # ℹ 809 more rows
Realizando análisis de sentimiento en los datos de bigramas, podemos examinar con qué frecuencia las palabras asociadas al sentimiento son precedidas por “no” u otras palabras negadoras. Esto nos permitiría ignorar o incluso revertir su contribución al puntaje de sentimiento.
Utilizaremos el lexicon elaborado por el LIIA-UBA para el análisis de sentimiento, que recordarás otorga un valor numérico entre 1 y 3 de “likeness” para cada palabra.
sentiment_words_liia <- read_csv('../data/sentiment_lexicon_liia.csv')
## Rows: 2880 Columns: 3
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (1): word
## dbl (2): mean_likeness, std_likeness
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
sentiment_words_liia <- sentiment_words_liia %>%
mutate(sentiment = case_when(
round(mean_likeness) == 1 ~ 'negativo',
round(mean_likeness) == 2 ~ 'neutral',
round(mean_likeness) == 3 ~ 'positivo',
))
sentiment_words_liia <- sentiment_words_liia %>%
mutate(word = stringi::stri_trans_general(str = word,
id = "Latin-ASCII"))
Luego podemos examinar las palabras más frecuentes que fueron precedidas por “no” y estaban asociadas con un sentimiento.
not_words <- bigrams_separated %>%
filter(word1 == "no") %>%
inner_join(sentiment_words_liia, by = c(word2 = "word")) %>%
count(word2, mean_likeness, sort = TRUE)
## Warning in inner_join(., sentiment_words_liia, by = c(word2 = "word")): Detected an unexpected many-to-many relationship between `x` and `y`.
## ℹ Row 23 of `x` matches multiple rows in `y`.
## ℹ Row 2543 of `y` matches multiple rows in `x`.
## ℹ If a many-to-many relationship is expected, set `relationship =
## "many-to-many"` to silence this warning.
not_words
## # A tibble: 146 × 3
## word2 mean_likeness n
## <chr> <dbl> <int>
## 1 solo 2 72
## 2 habia 2.4 51
## 3 era 2.4 47
## 4 solo 1.4 36
## 5 pensar 2.8 27
## 6 bien 3 24
## 7 tenia 1 24
## 8 tener 2.6 23
## 9 ficcion 2.6 18
## 10 recuerdo 2.2 17
## # ℹ 136 more rows
Por ejemplo, la palabra asociada con el sentimiento más común que sigue a “no” es “solo”, que normalmente tendría una puntuación neutral.
“No” no es el único término que proporciona algún contexto para la palabra siguiente. Podríamos elegir cuatro palabras comunes (o más) que nieguen el término subsiguiente, y utilizar el mismo enfoque de unión y conteo para examinar todas ellas a la vez.
negation_words <- c("no", "sin", "nunca", "jamas")
negated_words <- bigrams_separated %>%
filter(word1 %in% negation_words) %>%
inner_join(sentiment_words_liia, by = c(word2 = "word")) %>%
count(word1, word2, mean_likeness, sort = TRUE)
## Warning in inner_join(., sentiment_words_liia, by = c(word2 = "word")): Detected an unexpected many-to-many relationship between `x` and `y`.
## ℹ Row 27 of `x` matches multiple rows in `y`.
## ℹ Row 2543 of `y` matches multiple rows in `x`.
## ℹ If a many-to-many relationship is expected, set `relationship =
## "many-to-many"` to silence this warning.
negated_words
## # A tibble: 383 × 4
## word1 word2 mean_likeness n
## <chr> <chr> <dbl> <int>
## 1 no solo 2 72
## 2 no habia 2.4 51
## 3 no era 2.4 47
## 4 no solo 1.4 36
## 5 sin saber 2.8 31
## 6 sin saber 3 31
## 7 sin plata 2.8 28
## 8 no pensar 2.8 27
## 9 no bien 3 24
## 10 no tenia 1 24
## # ℹ 373 more rows
Luego podríamos visualizar cuáles son las palabras más comunes que siguen a cada negación en particular. Si bien “no solo” y “no era” siguen siendo los dos ejemplos más comunes, también podemos ver combinaciones como “sin saber” y “sin plata”.
Podríamos combinar esto con los enfoques de la clase pasada para revertir los valores del lexicon de cada palabra que sigue a una negación. Estos son solo algunos ejemplos de cómo encontrar palabras consecutivas puede proporcionar contexto a los métodos de minería de texto.
ggraph
Podríamos estar interesados en visualizar todas las relaciones entre palabras de manera simultánea, en lugar de solo las principales algunas veces. Como una visualización común, podemos organizar las palabras en una red o “grafo”. En este caso, nos referiremos a un “grafo” no en el sentido de una visualización, sino como una combinación de nodos conectados. Un grafo puede construirse a partir de un objeto tidy, ya que tiene tres variables:
- from: el nodo desde el cual parte un borde - to: el nodo hacia el cual se dirige un borde - weight: un valor numérico asociado con cada borde
El paquete igraph
tiene muchas funciones poderosas para
manipular y analizar redes. Una forma de crear un objeto igraph a partir
de datos tidy es la función graph_from_data_frame()
, que
toma un data frame de bordes con columnas para “from”, “to” y atributos
de los bordes (en este caso, n):
library(igraph)
##
## Attaching package: 'igraph'
## The following objects are masked from 'package:lubridate':
##
## %--%, union
## The following objects are masked from 'package:dplyr':
##
## as_data_frame, groups, union
## The following objects are masked from 'package:purrr':
##
## compose, simplify
## The following object is masked from 'package:tidyr':
##
## crossing
## The following object is masked from 'package:tibble':
##
## as_data_frame
## The following objects are masked from 'package:stats':
##
## decompose, spectrum
## The following object is masked from 'package:base':
##
## union
# Recuentos originales
bigram_counts
## # A tibble: 52,221 × 3
## word1 word2 n
## <chr> <chr> <int>
## 1 buenos aires 195
## 2 mil pesos 126
## 3 cada vez 116
## 4 mismo tiempo 66
## 5 primera vez 64
## 6 puede ser 56
## 7 jorge alvarez 55
## 8 tal vez 49
## 9 quiere decir 43
## 10 debe ser 41
## # ℹ 52,211 more rows
# Filtrar solo combinaciones relativamente comunes
bigram_graph <- bigram_counts %>%
filter(n > 15) %>%
graph_from_data_frame()
bigram_graph
## IGRAPH 115e9cd DN-- 113 73 --
## + attr: name (v/c), n (e/n)
## + edges from 115e9cd (vertex names):
## [1] buenos ->aires mil ->pesos cada ->vez
## [4] mismo ->tiempo primera ->vez puede ->ser
## [7] jorge ->alvarez tal ->vez quiere ->decir
## [10] debe ->ser santa ->fe jose ->sazbon
## [13] alguna ->vez literatura->argentina veinte ->anos
## [16] muchas ->veces nueva ->york roberto ->arlt
## [19] varias ->veces abuelo ->emilio aquel ->tiempo
## [22] miguel ->briante hace ->anos siglo ->xix
## + ... omitted several edges
igraph
tiene funciones de graficación incorporadas, pero
muchos otros paquetes han desarrollado métodos de visualización para
objetos de grafo. El paquete ggraph
implementa estas
visualizaciones en términos de la gramática de gráficos, con la que ya
estamos familiarizados por ggplot2
.
Podemos convertir un objeto igraph en un ggraph con la función
ggraph
, después de lo cual agregamos capas a él, de manera
similar a cómo se agregan capas en ggplot2
. Por ejemplo,
para un gráfico básico necesitamos agregar tres capas: nodos, bordes y
texto.
library(ggraph)
set.seed(2017)
ggraph(bigram_graph, layout = "fr") +
geom_edge_link() +
geom_node_point() +
geom_node_text(aes(label = name), vjust = 1, hjust = 1)
## Warning: Using the `size` aesthetic in this geom was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` in the `default_aes` field and elsewhere instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
Más arriba, podemos visualizar algunos detalles de la estructura del
texto. Aparecen algunos clústers con claro sentido sintáctico:
(parece/debe/puede-se). Luego, el término “tiempo” aparece como el
centro de un nodo importante.
Una vez más, aparecen numerosos nombres propios que son nombres de autores y/o de personajes (“fierro-martín-san”) y nodos de palabras asociadas a la “teoría literaria”: “novola-policial-género”.
Concluimos con algunas operaciones de pulido para hacer un gráfico de mejor apariencia:
Agregamos el atributo edge_alpha a la capa de enlaces para hacer los enlaces transparentes según la frecuencia del bigrama (común o raro)
Agregamos direccionalidad con una flecha, construida usando
grid::arrow()
, incluyendo la opción end_cap
que indica que la flecha debe terminar antes de tocar el nodo
Jugamos con las opciones de la capa de nodos para hacer los nodos más atractivos (puntos más grandes y azules)
Agregamos un tema útil para trazar redes,
theme_void()
set.seed(2020)
a <- grid::arrow(type = "closed", length = unit(.15, "inches"))
ggraph(bigram_graph, layout = "fr") +
geom_edge_link(aes(edge_alpha = n), 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()
Puede requerir algo de experimentación con ggraph
para
lograr que las redes tengan un formato presentable como este, pero la
estructura de red es una forma útil y flexible de visualizar datos
relacionales tidy.
Para hacer que la visualización sea interpretable, elegimos mostrar solo las conexiones más comunes de palabra a palabra, pero uno podría imaginar un grafo enorme que represente todas las conexiones que ocurren en el texto.
La tokenización por n-grama es una forma útil de explorar pares de palabras adyacentes. Sin embargo, también podríamos estar interesados en palabras que tienden a coocurrir dentro de documentos particulares o capítulos particulares, incluso si no ocurren una al lado de la otra.
Los datos tidy son una estructura útil para comparar entre variables o agrupar por filas, pero puede ser desafiante comparar entre filas: por ejemplo, contar la cantidad de veces que dos palabras aparecen en el mismo documento o ver cuán correlacionadas están. La mayoría de las operaciones para encontrar recuentos o correlaciones por pares necesitan convertir los datos en una matriz primero.
Examinaremos algunas de las formas en que el texto tidy se puede
convertir en una matriz dispersa más adelante, pero en este caso no es
necesario. El paquete widyr
facilita operaciones como
calcular recuentos y correlaciones, al simplificar el patrón de
“expandir datos, realizar una operación y luego reorganizar datos”. Nos
centraremos en un conjunto de funciones que hacen comparaciones por
pares entre grupos de observaciones (por ejemplo, entre documentos o
secciones de texto).
Consideremos el primer tomo de los diarios de Renzi, “I-Años de formación”, dividido en entradas. Es posible que estemos interesados en las palabras que tienden a aparecer dentro de la misma sección.
renzi_section_words <- renzi %>%
filter(tomo == "I-Años de formación") %>%
mutate(entry_code = row_number()) %>%
#filter(section > 0) %>%
unnest_tokens(word, entry) %>%
filter(!word %in% stop_words$word)
renzi_section_words
## # A tibble: 63,857 × 4
## tomo chapter entry_code word
## <chr> <dbl> <int> <chr>
## 1 I-Años de formación 1 1 1
## 2 I-Años de formación 1 1 umbral
## 3 I-Años de formación 1 1 chico
## 4 I-Años de formación 1 1 repito
## 5 I-Años de formación 1 1 entiendo
## 6 I-Años de formación 1 1 reia
## 7 I-Años de formación 1 1 retrospectivo
## 8 I-Años de formación 1 1 radiante
## 9 I-Años de formación 1 1 emilio
## 10 I-Años de formación 1 1 renzi
## # ℹ 63,847 more rows
Una función útil de widyr es pairwise_count()
. El
prefijo pairwise_
significa que esta función resultará en
una fila por cada par de palabras en la variable “word”. Esto nos
permite contar pares comunes de palabras que coaparecen en la misma
sección:
library(widyr)
# cuenta palabras que co-ocurren en cada entrada
word_pairs <- renzi_section_words %>%
pairwise_count(word, entry_code, sort = TRUE)
word_pairs
## # A tibble: 13,019,932 × 3
## item1 item2 n
## <chr> <chr> <dbl>
## 1 mismo tiempo 70
## 2 tiempo mismo 70
## 3 vida tiempo 69
## 4 tiempo vida 69
## 5 mismo vida 69
## 6 vida mismo 69
## 7 vez mismo 68
## 8 mismo vez 68
## 9 ahora vida 64
## 10 ser vida 64
## # ℹ 13,019,922 more rows
Es importante notar que mientras la entrada tenía una fila por cada par de un documento (una entrada) y una palabra, la salida tiene una fila por cada par de palabras. Esto también es un formato tidy, pero de una estructura muy diferente que podemos usar para responder nuevas preguntas.
Por ejemplo, podemos ver que el par de palabras más común en una sección es “mismo” y “tiempo”. También podemos encontrar fácilmente las palabras que más a menudo ocurren con “tiempo”:
word_pairs %>%
filter(item1 == "tiempo")
## # A tibble: 9,809 × 3
## item1 item2 n
## <chr> <chr> <dbl>
## 1 tiempo mismo 70
## 2 tiempo vida 69
## 3 tiempo hace 60
## 4 tiempo ahora 58
## 5 tiempo vez 58
## 6 tiempo ser 56
## 7 tiempo despues 54
## 8 tiempo noche 52
## 9 tiempo hacer 48
## 10 tiempo historia 48
## # ℹ 9,799 more rows
Pares como “siempre” y “solo” son las palabras que más comúnmente coocurren, pero eso no es particularmente significativo ya que también son las palabras individuales más comunes. En cambio, es posible que deseemos examinar la correlación entre palabras, lo que indica con qué frecuencia aparecen juntas en comparación con cuántas veces aparecen por separado.
En particular, aquí nos centraremos en el coeficiente phi, una medida común para la correlación binaria. El foco del coeficiente phi es cuánto más probable es que tanto la palabra X como la palabra Y aparezcan, o que ninguna aparezca, en comparación con que una aparezca sin la otra.
Considera la siguiente tabla:
Tiene la palabra Y | No iene la palabra Y | Total | |
---|---|---|---|
Tiene la palabra X | \(n_{11}\) | \(n_{10}\) | \(n_{1.}\) |
No tiene la palabra X | \(n_{01}\) | \(n_{00}\) | \(n_{0.}\) |
Total | \(n_{.1}\) | \(n_{.0}\) | \(n\) |
Por ejemplo, que \(n_{11}\), representa la cantidad de documentos en los que tanto la palabra X como la palabra Y aparecen, \(n_{00}\) es el número en el que ninguna de las dos aparece, y \(n_{10}\) y \(n_{01}\) son los casos en los que una aparece sin la otra.
En términos de esta tabla, el coeficiente phi es:
\[\phi=\frac{n_{11}n_{00}-n_{10}n_{01}}{\sqrt{n_{1\cdot}n_{0\cdot}n_{\cdot0}n_{\cdot1}}}\]
El coeficiente phi es equivalente a la correlación de Pearson, que tal vez hayas escuchado en otros contextos cuando se aplica a datos binarios.
La función pairwise_cor()
en widyr nos permite encontrar
el coeficiente phi entre palabras basado en cuán frecuentemente aparecen
juntas en la misma sección. Su sintaxis es similar a
pairwise_count()
.
# we need to filter for at least relatively common words first
word_cors <- renzi_section_words %>%
group_by(word) %>%
filter(n() >= 20) %>%
pairwise_cor(word, entry_code, sort = TRUE)
word_cors
## # A tibble: 336,980 × 3
## item1 item2 correlation
## <chr> <chr> <dbl>
## 1 buenos aires 0.944
## 2 aires buenos 0.944
## 3 pesos mil 0.751
## 4 mil pesos 0.751
## 5 alvarez jorge 0.638
## 6 jorge alvarez 0.638
## 7 renzi emilio 0.592
## 8 emilio renzi 0.592
## 9 abuelo emilio 0.515
## 10 emilio abuelo 0.515
## # ℹ 336,970 more rows
Este ouput es útil para hacer un análisis exploratorio. Por ejemplo, podemos encontrar las palabras mas correlaciones con una palabra como “vinas”, referida a David Viñas, usando una operación de filtrado:
word_cors %>%
filter(item1 == "vinas")
## # A tibble: 580 × 3
## item1 item2 correlation
## <chr> <chr> <dbl>
## 1 vinas peronismo 0.235
## 2 vinas beatriz 0.235
## 3 vinas posible 0.199
## 4 vinas narra 0.199
## 5 vinas zona 0.183
## 6 vinas escritura 0.176
## 7 vinas prosa 0.167
## 8 vinas concepto 0.161
## 9 vinas varios 0.154
## 10 vinas luego 0.152
## # ℹ 570 more rows
Esto nos permite elegir palabras específicas interesantes y encontrar las otras palabras más asociadas con ellas.
word_cors %>%
filter(item1 %in% c("novela", "literatura", "escritor", "libro")) %>%
group_by(item1) %>%
slice_max(correlation, n = 10) %>%
ungroup() %>%
mutate(item2 = reorder(item2, correlation)) %>%
ggplot(aes(item2, correlation)) +
geom_bar(stat = "identity") +
facet_wrap(~ item1, scales = "free") +
coord_flip() +
theme_minimal()
Así como utilizamos ggraph para visualizar los bigrams, podemos usarlo
para visualizar las correlaciones y agrupaciones de palabras que fueron
encontradas por el paquete
widyr
:
set.seed(2016)
word_cors %>%
filter(correlation > .39) %>%
graph_from_data_frame() %>%
ggraph(layout = "fr") +
geom_edge_link(aes(edge_alpha = correlation), show.legend = FALSE) +
geom_node_point(color = "lightblue", size = 2) +
geom_node_text(aes(label = name), repel = TRUE) +
theme_void()
Es importante destacar que, a diferencia del análisis de bigramas, las
relaciones aquí son simétricas en lugar de direccionales (no hay
flechas). También podemos observar que, aunque las combinaciones de
nombres y títulos que dominaban las parejas de bigrams son comunes, como
‘buenos_aires’, también podemos ver combinaciones de palabras que
aparecen cerca una de la otra, como ‘cartas’, ‘abuelo’, ‘guerra’ o
‘adrogué’.
Este clase demostró cómo el enfoque de texto ordenado es útil no solo para analizar palabras individuales, sino también para explorar las relaciones y conexiones entre palabras. Tales relaciones pueden involucrar n-gramas, que nos permiten ver qué palabras tienden a aparecer después de otras, o coocurrencias y correlaciones, para palabras que aparecen en proximidad una de la otra. La clase también demostró el paquete ggraph para visualizar ambos tipos de relaciones como redes. Estas visualizaciones de redes son una herramienta flexible para explorar relaciones.