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:
(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
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.
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:
¿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:
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:
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
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
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.
Repetir el ejercicio comparando las cartas con los libros
###