La idea de “datos ordenados” o tidy data es una forma de manejar de forma efectiva y eso aplica también para el caso de los datos textuales. Según Hadley Wickham (2004) los datos “tidy” tiene tres características:

  • Cada variable (o atributo) es una columna
  • Cada unidad (u observación) es una fila
  • Cada tipo de unidad observacional es una tabla

(Si en esta definición les resuena aquello que en las materias de Metodología de la Investigación se llamaba “estructura tripartita del dato” o lo que Juan Samaja llamaba “estructura cuatripartita del dato” están bien encaminades).

En el contexto de NLP, los datos ordenados van a tener la siguente estructura: un token por fila. Un token es una unidad conceptual y/o analíticamente signifactivas con las que dividimos un documento. Un token puede ser una palabra (ese será el caso más frecuente en este curso) pero también podrían ser n-gramas, oraciones e incluso párrafos. De hecho, un primer paso en el preprocesamiento de texto es dividir el corpus en tokens. Para lo cual, es necesario, entonces, definir qué va a ser un token.

Como puede verse esta estructura difiere de otras formas de almacenar el texto crudo

  • Cadena: el texto puede, por supuesto, almacenarse como cadenas, es decir, vectores de caracteres, dentro de R y, a menudo, los datos de texto se leen primero en la memoria de esta forma.
  • Corpus: estos tipos de objetos suelen contener cadenas sin procesar anotadas con metadatos y detalles adicionales.
  • Matriz documento-término: esta es una matriz dispersa que describe una colección (es decir, un corpus) de documentos con una fila para cada documento y una columna para cada término. El valor de la matriz suele ser el recuento de palabras o tf-idf.

Como iremos viendo, va a ser muy fácil pasar del texto en formato tidy a otros formatos. Particularmente, vamos a estar yendo y viniendo entre textos en formato tidy y no-tidy para diferentes tareas. Así, cuando nos toque construir modelos seguramento vamos a tener que pasar a un formato Matrix-documento-término (TFM). Pero cuando querramos generar gráficos o métricas que nos permitan interpretar ese modelo, seguramente vamos a tener que volver a formato tidy.

Primer ejemplo

Vamos a empezar con un ejemplo mínimo: un párrafo de Marx. En el famoso “Prólogo a la Contribución a la Crítica de la Economía Política” de 1859 escribió el siguiente párrafo que ha generado infinitas interpretaciones y polémicas. Vamos a guardarlo en un objeto character que se va a llamar marx.

marx <- c("El conjunto de estas relaciones de producción forma la estructura económica de la sociedad, la base real sobre la que se levanta la superestructura jurídica y política y a la que corresponden determinadas formas de conciencia social. El modo de producción de la vida material condiciona el proceso de la vida social política y espiritual en general. No es la conciencia del hombre la que determina su ser sino, por el contrario, el ser social es lo que determina su conciencia. Al llegar a una fase determinada de desarrollo las fuerzas productivas materiales de la sociedad entran en contradicción con las relaciones de producción existentes o, lo que no es más que la expresión jurídica de esto, con las relaciones de propiedad dentro de las cuales se han desenvuelto hasta allí. De formas de desarrollo de las fuerzas productivas, estas relaciones se convierten en trabas suyas, y se abre así una época de revolución social. Al cambiar la base económica se transforma, más o menos rápidamente, toda la inmensa superestructura erigida sobre ella.")

marx
## [1] "El conjunto de estas relaciones de producción forma la estructura económica de la sociedad, la base real sobre la que se levanta la superestructura jurídica y política y a la que corresponden determinadas formas de conciencia social. El modo de producción de la vida material condiciona el proceso de la vida social política y espiritual en general. No es la conciencia del hombre la que determina su ser sino, por el contrario, el ser social es lo que determina su conciencia. Al llegar a una fase determinada de desarrollo las fuerzas productivas materiales de la sociedad entran en contradicción con las relaciones de producción existentes o, lo que no es más que la expresión jurídica de esto, con las relaciones de propiedad dentro de las cuales se han desenvuelto hasta allí. De formas de desarrollo de las fuerzas productivas, estas relaciones se convierten en trabas suyas, y se abre así una época de revolución social. Al cambiar la base económica se transforma, más o menos rápidamente, toda la inmensa superestructura erigida sobre ella."

¿Qué formato de los que vimos hasta aquí sería este?

Para poder analizarlo como datos tidy, primero tenemos que llevarlo a un dataframe.

marx_df <- tibble(line = 1, text = marx)
marx_df
## # A tibble: 1 × 2
##    line text                                                                    
##   <dbl> <chr>                                                                   
## 1     1 El conjunto de estas relaciones de producción forma la estructura econó…

¿Qué significa que este dataframe se ha impreso como una “tibble”? Una tibble es una clase de dataframe más “moderna” dentro de R, disponible en los paquetes dplyr y tibble. Las tibbles tienen un método de impresión conveniente para lo que queremos hacer: no convierten a las cadenas/strings en factores de forma automática y no usa nombres de fila (`ronames``). Las tibbles son ideales para usar con herramientas tidy.

Sin embargo, tengamos en cuenta que este dataframe que contiene texto aún no es compatible con un análisis de texto tidy. No podemos filtrar las palabras ni contar las que ocurren con mayor frecuencia, ya que cada fila está formada por varias palabras combinadas. Necesitamos convertir esto para que tenga un token por documento por fila.

¿Cuántos documentos tenemos en este ejemplo?

Dentro de nuestro dataframe de texto ordenado, necesitamos dividir el texto en tokens individuales (un proceso llamado tokenización) y transformarlo en una estructura de datos ordenada. Para hacer esto, usamos la función unnest_tokens() de tidytext.

library(tidytext)

marx_df %>%
  unnest_tokens(output=word, input=text)
## # A tibble: 173 × 2
##     line word      
##    <dbl> <chr>     
##  1     1 el        
##  2     1 conjunto  
##  3     1 de        
##  4     1 estas     
##  5     1 relaciones
##  6     1 de        
##  7     1 producción
##  8     1 forma     
##  9     1 la        
## 10     1 estructura
## # ℹ 163 more rows

Usamos aquí dos argumentos básicos:

  • el nombre de la columna de salida que se creará cuando el texto no esté anidado (palabra, en este caso), y luego
  • la columna de entrada de la que proviene el texto (texto, en este caso).

¿Qué formato tiene ahora nuestro párrafo?


Otros tokens Es importante recordar que marx_df arriba tiene una columna llamada text que contiene los datos de interés. A su vez unnest_tokens() realiza la tokenización por defecto usando palabras. Esto puede cambiarse sin problemas, cambiando el parámetro token. Por ejemplo,

marx_df %>%
  unnest_tokens(word, text, token='sentences')
## # A tibble: 6 × 2
##    line word                                                                    
##   <dbl> <chr>                                                                   
## 1     1 el conjunto de estas relaciones de producción forma la estructura econó…
## 2     1 el modo de producción de la vida material condiciona el proceso de la v…
## 3     1 no es la conciencia del hombre la que determina su ser sino, por el con…
## 4     1 al llegar a una fase determinada de desarrollo las fuerzas productivas …
## 5     1 de formas de desarrollo de las fuerzas productivas, estas relaciones se…
## 6     1 al cambiar la base económica se transforma, más o menos rápidamente, to…

Ahora, tenemos una tibble en la que cada línea es una oración (“sentence”) y no una palabra.

A su vez, han pasado otras varias cosas al ejecutar unnest_tokens que es importante marcar:

  • Se conservan otras columnas, como el número de línea de donde proviene cada palabra.
  • Se ha eliminado la puntuación.
  • De forma predeterminada, unnest_tokens() convierte los tokens a minúsculas, lo que los hace más fáciles de comparar o combinar con otros conjuntos de datos. (Esto puede modificarse utilizando el argumento to_lower = FALSE)

Un diagrama del flujo de trabajo puede verse a continuación:

Ordenando algunos textos de Marx y Engels

Vamos a trabajar con un dataset que el capo de Diego Kozlowski escrapeó de la sección en español del Marxist Internet Archive.

Cargamos los datos:

marx_engels <- read_csv('../data/marx_engels.csv')
## Rows: 145 Columns: 4
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (4): tipo, autor, titulo, texto
## 
## ℹ 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.
marx_engels
## # A tibble: 145 × 4
##    tipo   autor       titulo                                               texto
##    <chr>  <chr>       <chr>                                                <chr>
##  1 libros marx engels Contribucion al problema de la vivienda              Enge…
##  2 libros marx engels Feuerbach Oposicion entre las concepciones material… Marx…
##  3 libros marx engels La guerra civil en Francia                           Marx…
##  4 libros marx engels La Guerra de los Campesinos en Alemania              A li…
##  5 libros marx engels La ideologia alemana                                 K Ma…
##  6 libros marx engels La revolucion de la ciencia de Eugenio Duhring Anti… Enge…
##  7 libros marx engels La Sagrada Familia o Critica de la criticia critica… K Ma…
##  8 libros marx engels Las luchas de clases en Francia de 1848 a 1850       K Ma…
##  9 libros marx engels Ludwig Feuerbach y el fin de la filosofia clasica a… F En…
## 10 libros marx engels Manuscritos economicos y filosoficos de 1844         K Ma…
## # ℹ 135 more rows

¿Qué estructura tiene este dataset?

Vamos a transformarlo en un formato tidy:

marx_engels_tidy <- marx_engels %>%
        unnest_tokens(word, texto)
marx_engels_tidy
## # A tibble: 1,260,450 × 4
##    tipo   autor       titulo                                  word        
##    <chr>  <chr>       <chr>                                   <chr>       
##  1 libros marx engels Contribucion al problema de la vivienda engels      
##  2 libros marx engels Contribucion al problema de la vivienda 1873        
##  3 libros marx engels Contribucion al problema de la vivienda contribucion
##  4 libros marx engels Contribucion al problema de la vivienda al          
##  5 libros marx engels Contribucion al problema de la vivienda problema    
##  6 libros marx engels Contribucion al problema de la vivienda de          
##  7 libros marx engels Contribucion al problema de la vivienda la          
##  8 libros marx engels Contribucion al problema de la vivienda vivienda    
##  9 libros marx engels Contribucion al problema de la vivienda indice      
## 10 libros marx engels Contribucion al problema de la vivienda f           
## # ℹ 1,260,440 more rows

Eliminando stopwords

El siguiente paso es la eliminación de las llamadas stopwords. Se trata de palabras que o bien por su función sintáctica (pronombres, preposiciones, adverbios, etc.) o por su frecuencia (aparecen en gran frecuencia) aportan poca información semántica al texto.

En general, la forma estándar y más inmediata de lidiar con ellas es mediante su eliminación a través de una lista. La idea es matchear las palabras en nuestro corpus con las que estén en esa lista y eliminar las que coinciden. Carguemos la lista con las stopwords, al mismo tiempo, vamos a eliminar los acentos de esta tabla.

stop_words <- read_csv('https://raw.githubusercontent.com/Alir3z4/stop-words/master/spanish.txt', col_names=FALSE) %>%
        rename(word = X1) %>%
        mutate(word = stringi::stri_trans_general(word, "Latin-ASCII"))
## Rows: 608 Columns: 1
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (1): X1
## 
## ℹ 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.

Ahora sí, podemos removerlas usando la funcion anti_join:

marx_engels_tidy <- marx_engels_tidy %>%
  anti_join(stop_words)
## Joining with `by = join_by(word)`

Fíjense cómo pasamos de aprimadamente 1.000.000 de palabras a 529.000 luego de eliminar las stop_words.

Ahora bien, ¿cuáles son las palabras más usadas por Marx y Engels?

marx_engels_tidy %>%
        count(word, sort=TRUE)
## # A tibble: 45,439 × 2
##    word           n
##    <chr>      <int>
##  1 senor       2130
##  2 produccion  1994
##  3 propiedad   1894
##  4 sociedad    1891
##  5 hombre      1880
##  6 pag         1815
##  7 marx        1639
##  8 clase       1567
##  9 general     1545
## 10 critica     1538
## # ℹ 45,429 more rows

Actividad

Escribir el código para replicar la tabla anterior usando los comandos del tidyverse

###

Debido a que hemos estado usando herramientas del estilo tidy, nuestros recuentos de palabras se almacenan en una tibble en ese formato (tidy data). Esto nos permite pasar casi sin mediaciones a nuestro viejo amigo ggplot2, por ejemplo, para crear una visualización de las palabras más comunes:

marx_engels_tidy %>%
        count(word, sort=TRUE) %>%
        filter(n > 600) %>%
        mutate(word = reorder(word, n)) %>%
        ggplot(aes(n, word)) +
                geom_col() +
                labs(y = NULL)

Podríamos evaluar ahora si Marx y Engels usan diferentes palabras en las cargas y notas y en sus libros. Para ello vamos a tener que procesar un poco el campo de titulo:

marx_engels_tidy <- marx_engels_tidy %>%
        mutate(tipo = case_when(
                str_detect(titulo, 'Carta') ~ 'cartas',
                TRUE ~ tipo
        )) 

Utilizamos la función str_detect del paquete stringr para testear si cierto patrón está en un determinado texto. Más precisamente, en este caso buscamos para cada fila si la palabra Carta aparece en las filas de título.

Combinamos esta función con case_when y mutate para modificar la columna tipo y agregar una categoría de cartas.

A partir de ahora podemos utilizar las herramientas del tidyverse para contar y manipular palabras.

freqs <- marx_engels_tidy %>%
        mutate(word = str_extract(word, "[a-z']+")) %>% #Nos quedamos con las letras
        group_by(tipo, word) %>% #Agrupamos por tipo y word
        summarise(n = n()) %>%
        mutate(
                total = sum(n),
                prop = n/total*100
                ) %>%
        ungroup() %>%
        select(tipo, word, prop) %>%
        pivot_wider(names_from = tipo, values_from = prop) 
## `summarise()` has grouped output by 'tipo'. You can override using the
## `.groups` argument.
freqs
## # A tibble: 44,434 × 4
##    word         cartas   libros     notas
##    <chr>         <dbl>    <dbl>     <dbl>
##  1 a           0.0139  0.00782   0.00535 
##  2 abajo       0.0139  0.0138    0.0113  
##  3 abandonado  0.00695 0.00541   0.00476 
##  4 abandonados 0.00695 0.00180   0.00297 
##  5 abandonan   0.00348 0.000902  0.000595
##  6 abandonar   0.0209  0.00662   0.00654 
##  7 abandonasen 0.00348 0.000301 NA       
##  8 abarca      0.00348 0.00782   0.00238 
##  9 abarcar     0.00348 0.000902  0.00119 
## 10 abarque     0.00348 0.00120   0.000595
## # ℹ 44,424 more rows

Y ahora podemos hacer un gráfico en el que comparamos la frecuencia de uso de las diferentes palabras en los libros y las notas:

freqs %>%
ggplot( aes(notas, libros)) +
  geom_jitter(alpha = 0.05, size = 2.5, width = 0.25, height = 0.25) +
  geom_text(aes(label = word), check_overlap = TRUE, vjust = 1.5) +
  scale_x_log10() +
  scale_y_log10() +
  geom_abline(color = "red") +
  theme_minimal()
## Warning: Removed 27811 rows containing missing values (`geom_point()`).
## Warning: Removed 27812 rows containing missing values (`geom_text()`).

Las palabras que están cerca de la línea en estos gráficos tienen frecuencias similares en ambos conjuntos de textos, por ejemplo, tanto en los libros de Marx y Engels como en las aparecen con frecuencias simialres: burguesa, acción, campesinos, abolición, comuna, producción, clase. En cambio, en los libros parecen aparecer palabras como autoconciencia, during, espípirtu, matemática. En las notas periodísitcas aparecen palabras ligadas a la acción política: congreso, consejo, estatutos, conferencia, etc.


Actividad

Repetir el ejercicio comparando las cartas con los libros

###