library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.1.3     ✔ readr     2.1.4
## ✔ forcats   1.0.0     ✔ stringr   1.5.0
## ✔ ggplot2   3.4.3     ✔ tibble    3.2.1
## ✔ lubridate 1.9.2     ✔ tidyr     1.3.0
## ✔ purrr     1.0.2     
## ── 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)

Introducción

Un problema muy importante en NLP es tratar de generar alguna forma de input sobre el contenido de un texto sin leerlo en su totalidad (“lectura distante”). Esto supone intentar cuantificar los contenidos de un texto. Vamos a ir viendo unas cuantas técnicas relativamente sofisticadas vinculadas al modelado de tópicos y a la constitución de embeddings de palabras más adelante, pero por ahora vamos a seguir contando palabras pero de forma más inteligente. ¿Podemos a partir del conteo de palabras llegar a tener una idea del contenido de un texto? Como vimos, una primera métrica de la importancia de una palabra es la llamada term frequency (\(tf\)): la frecuencia con la que aparece una palabra en un documento.

Sin embargo, hay palabras en un documento que aparecen muchas veces pero que no son importantes; en castellano, probablemente palabras como “el”, “es”, “de”, etc. Podríamos adoptar el enfoque de agregar palabras como estas a una lista de palabras vacías (stopwords) y eliminarlas antes del análisis, pero es posible que algunas de estas palabras sean más importantes en algunos documentos que en otros. Una lista de stopwords puede ser un primer paso pero no es el único ni, necesariamente, el más importante para ajustar la frecuencia de los términos para palabras de uso común.

Vamos a trabajar con dos métricas. Podemos analizar la “informatividad” de un término observando la inverse document frequency (\(idf\)). Esta métrica disminuye el peso de las palabras de uso común y aumenta el peso de las palabras que no se usan mucho en una colección de documentos.

Finalmente, podemos calcular la \(tf-idf\) que combina ambas métricas multiplicándolas: es la frecuencia de cada término, ajustada o ponderada por la importancia que tiene en el corpus.

Term frequency en los diarios de Renzi

Vamos a seguir con los 3 tomos de los diarios de Renzi. Examinemos primero \(tf\) y luego \(tf-idf\). Lo interesante de todo esto es que si nos mantenemos en nuestro formato “tidy” vamos a poder comenzar simplemente usando verbos dplyr como group_by() y join(). ¿Cuáles son las palabras más utilizadas en los diarios? (También vamos a calcular el total de palabras en cada novela aquí, para uso posterior).

renzi <- read_csv('../data/renzi.csv') %>%
        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',
        ))
## 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.
book_words <- renzi %>%
        mutate(entry_number = row_number()) %>%
        unnest_tokens(output = word, 
                      input = entry) %>%
        group_by(tomo, word) %>%
        summarise(n = n()) %>%
        arrange(desc(n)) %>%
        ungroup()
## `summarise()` has grouped output by 'tomo'. You can override using the
## `.groups` argument.
total_words <- book_words %>% 
  group_by(tomo) %>% 
  summarize(total = sum(n))

book_words <- book_words %>%
                left_join(total_words) %>%
                ungroup() %>%
                arrange(desc(n))
## Joining with `by = join_by(tomo)`
book_words
## # A tibble: 42,541 × 4
##    tomo                  word      n  total
##    <chr>                 <chr> <int>  <int>
##  1 II-Los años felices   de     8362 151724
##  2 I-Años de formación   de     6811 133476
##  3 II-Los años felices   la     6606 151724
##  4 I-Años de formación   la     5792 133476
##  5 III-Un día en la vida de     5561 101377
##  6 II-Los años felices   que    4998 151724
##  7 I-Años de formación   que    4541 133476
##  8 II-Los años felices   en     4440 151724
##  9 III-Un día en la vida la     4368 101377
## 10 II-Los años felices   el     4248 151724
## # ℹ 42,531 more rows

Esta tibble book_words presenta una fila para cada combinación de tomo y palabras. Tiene dos columnas (además de tomo y word que ya las conocemos):

  • n es el número de veces que se usa esa palabra en ese tomo
  • total es el total de palabras en ese libro.

Vemos las palabras que eliminaríamos mediante una lista como stopwords: “de”, “la”, “que”, etc.

En el siguiente gráfico veamos la distribución porcentual (la calculamos como n / total) para cada tomo, el número de veces que aparece una palabra en un tomo dividido por el número total de términos (palabras) en ese tomo. Hemos cálculado la term frequency (\(tf\)).

book_words %>%
        mutate(tf = n/total) %>%
        ggplot(aes(tf, fill = tomo)) +
                geom_histogram(show.legend = FALSE) +
                xlim(NA, 0.0002) +
                facet_wrap(~tomo) +
                theme_minimal()
## `stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
## Warning: Removed 1606 rows containing non-finite values (`stat_bin()`).
## Warning: Removed 3 rows containing missing values (`geom_bar()`).

Hay colas muy largas a la derecha para esos tomos (son muy raras) que no hemos mostrado en estos tramas. En general, los gráficos son similares en los tres tomos. Pocas palabras que ocurren mucho y muchas palabras que ocurrren poco.

Calculando todas las métricas: la función bind_tfi_idf()

Repitámoslo una vez más: la idea de la métrica \(tf-idf\) es encontrar las palabras importantes para el contenido de cada documento disminuyendo el peso de las palabras de uso común y aumentando el peso de las palabras que no se usan mucho en una colección o corpus de documentos. El cálculo de \(tf-idf\) intenta encontrar las palabras que son importantes (es decir, comunes) en un texto, pero informativas (no demasiado comunes).

La función bind_tf_idf() en el paquete tidytext toma un dataset de texto tidy como entrada con una fila por token (término), por documento. Una columna (word en nuestro caso) contiene los términos / tokens, otra columna contiene los documentos (tomo, en este caso) y la última columna necesaria contiene los recuentos, cuántas veces cada documento contiene cada término (n en este ejemplo). Calculamos un total para cada tomo para nuestras exploraciones en las secciones anteriores, pero no es necesario para la función bind_tf_idf (); la tabla solo debe contener todas las palabras de cada documento.

book_tf_idf <- book_words %>%
  bind_tf_idf(word, tomo, n)

head(book_tf_idf)
## # A tibble: 6 × 7
##   tomo                  word      n  total     tf   idf tf_idf
##   <chr>                 <chr> <int>  <int>  <dbl> <dbl>  <dbl>
## 1 II-Los años felices   de     8362 151724 0.0551     0      0
## 2 I-Años de formación   de     6811 133476 0.0510     0      0
## 3 II-Los años felices   la     6606 151724 0.0435     0      0
## 4 I-Años de formación   la     5792 133476 0.0434     0      0
## 5 III-Un día en la vida de     5561 101377 0.0549     0      0
## 6 II-Los años felices   que    4998 151724 0.0329     0      0

Es importante notar que idf y, por lo tanto, tf-idf son cero para estas palabras extremadamente comunes (que en el ejercicio anterior, habíamos eliminado como stopwords). Todas estas son palabras que aparecen en los tres tomos de los diarios, por lo que el término idf (que entonces será el logaritmo natural de 1) es cero.

La frecuencia inversa del documento (y por tanto tf-idf) es muy baja (cercana a cero) para las palabras que aparecen en muchos de los documentos de una colección; así es como este enfoque reduce el peso de las palabras comunes. La idf será un número mayor para las palabras que aparecen en una menor cantidad de documentos de la colección.

Ordenemos la tabla de forma diferente para identificar las palabras más importantes:

book_tf_idf %>%
  select(-total) %>%
  arrange(desc(tf_idf))
## # A tibble: 42,541 × 6
##    tomo                  word          n       tf   idf   tf_idf
##    <chr>                 <chr>     <int>    <dbl> <dbl>    <dbl>
##  1 III-Un día en la vida carola       41 0.000404  1.10 0.000444
##  2 II-Los años felices   schmucler    42 0.000277  1.10 0.000304
##  3 II-Los años felices   toto         39 0.000257  1.10 0.000282
##  4 III-Un día en la vida érica        25 0.000247  1.10 0.000271
##  5 III-Un día en la vida pomaire      20 0.000197  1.10 0.000217
##  6 III-Un día en la vida lafuente     19 0.000187  1.10 0.000206
##  7 I-Años de formación   rovel        25 0.000187  1.10 0.000206
##  8 II-Los años felices   lola         27 0.000178  1.10 0.000196
##  9 III-Un día en la vida tardewski    17 0.000168  1.10 0.000184
## 10 I-Años de formación   capitulo     20 0.000150  1.10 0.000165
## # ℹ 42,531 more rows

Aquí vemos que varios nombres y apodos (carola, schucler, toto, etc.) son importantes en los tres tomos, aunque no se repiten a lo largo de los tomos.

Visualicemos estos términos. Vamos a quedarnos (para cada tomo) con los principales 20 términos según tf-idf.

library(forcats)

book_tf_idf %>%
  group_by(tomo) %>%
        slice_max(tf_idf, n = 20) %>%
        ungroup() %>%
        ggplot(aes(tf_idf, fct_reorder(word, tf_idf), fill = tomo)) +
        geom_col(show.legend = FALSE) +
        facet_wrap(~tomo, ncol = 2, scales = "free") +
        labs(x = "tf-idf", y = NULL) +
        theme_minimal()

Es muy interesante ver como los nombres aparecen pero son diferentes en cada etapa de la vida de Renzi. A su vez, en los años de formación se ve como la vida académica de Renzi (o sea, Piglia) tenía su importancia: términos como “concursos”, “sinóptico” o “monografía” rankean entre los más imporantes.

A medida que avanza la vida de Piglia/Renzi vemos como otros personajes como “Toto Schmucler” toma importancia en su vida y es en el segundo tomo en el que viaja a China y por eso palabras como “china” o “mao” resultan relevantes.

Finalmente, en el período final toman importancia los años “1976” y “1977”, los años de encierro de la dictadura. También su migración a Estados Unidos aparece capturada por el término “princeton”, la universidad en la que enseñó muchos años. Es la época en que termina de escribir y publica la novela [Respiración Artificial](https://es.wikipedia.org/wiki/Respiraci%C3%B3n_artificial_(novela)) en la que “arocena” es un personaje importante (el censor y el espía que lee las cartas).

Actividad

A partir del corpus de textos de Marx y Engels trabajado la clase pasada, identificar y graficar las 20 principales palabras en las cartas, las notas y los libros.

###

Resumen

El uso de tf-idf nos permite encontrar palabras que son características de un documento dentro de una colección de documentos, ya sea que ese documento sea una novela, un diario un texto físico o una página web. Explorar la frecuencia de los términos por sí solo puede darnos una idea de cómo se usa el lenguaje en una colección de lenguaje natural, y los verbos dplyr como count() y rank() nos brindan herramientas para razonar sobre la frecuencia de los términos. El paquete tidytext utiliza una implementación de tf-idf consistente con los principios de tidy data que nos permite ver cómo diferentes palabras son importantes en los documentos dentro de una colección o corpus de documentos.