install.packages(c("quanteda", "readtext", "dplyr", "stringi", "quanteda.textplots", "ggplot2", "stringr", "tm", "topicmodels"))
Carga y procesamiento de textos
📦Preparando los paquetes
Para seguir este tutorial por primera vez, deberás instalar una serie de paquetes1 que emplearemos: quanteda
, readtext
, dplyr
y stringr
. Esto se hace a través del comando: install.packages()
:
¿Para qué sirve cada uno?
Paquete | Descripción |
---|---|
quanteda |
Paquete de análisis de textos, incluyendo tokenización, conteo y limpieza de textos |
readtext |
Permite importar archivos de texto en varios formatos, facilitando la carga de datos |
dplyr |
Herramienta para manipulación y transformación de datos, útil para filtrar y organizar datos |
stringi |
Conjunto de funciones para trabajar con texto, especialmente útil para limpieza y manejo de expresiones regulares2 |
quanteda.textplots |
Extensión de quanteda para crear visualizaciones como nubes de palabras y gráficos de dispersión léxica. |
ggplot2 |
Paquete de visualización de datos que permite crear gráficos personalizados y atractivos. |
stringr |
Facilita la manipulación de cadenas de texto con funciones simples y potentes, parte del tidyverse . |
tm |
Paquete clásico para el análisis de texto y minería de textos, incluye herramientas para el procesamiento de textos y la creación de matrices de documentos. |
topicmodels |
Se utiliza para el modelado de temas latentes, permitiendo ajustar modelos como LDA para identificar patrones temáticos. |
¿Sabías qué…? ️🤓☝
La tokenización es el proceso de dividir un texto en unidades más pequeñas llamdas tokens. Estas unidades pueden ser palabras, símbolos, frases o incluso caracteres dependiendo del tipo de análisis que se vaya a realizar. En quanteda
consideramos la palabra como la unidad mínima de trabajo. Imagínate que tienes la siguiente oración:
“Hola, ¿cómo estás?”
La tokenización de esta frase podría dar como resultado los siguientes tokens: “Hola”, “¿”, “cómo”, “estás”, “?”
Esto es especialmente útil cuando estamos trabajando con estudios relacionados con frecuencias de palabras.
📄➡️🖥️Importación de datos de texto
Vamos a vincular el archivo de texto en la aplicación de RStudios. Para ello vamos a cargar los paquetes que hemos instalado anteriormente con el comando library("name")
library(quanteda)
Package version: 4.1.0
Unicode version: 15.1
ICU version: 74.1
Parallel computing: 8 of 8 threads used.
See https://quanteda.io for tutorials and examples.
library(readtext)
Adjuntando el paquete: 'readtext'
The following object is masked from 'package:quanteda':
texts
library(dplyr)
Adjuntando el paquete: 'dplyr'
The following objects are masked from 'package:stats':
filter, lag
The following objects are masked from 'package:base':
intersect, setdiff, setequal, union
library(stringi)
Es importante resaltar que, si no llamamos antes el paquete, los comandos que introduzcamos después no funcionarán o nos darán error. Asegúrate de cargar siempre la librería antes de empezar a trabajar.
Una vez cargados, el programa estará listo para leer nuestro archivo de texto. La formula que vamos a escribir para decirle a quanteda
que archivo analizar será el siguiente:
<- as.character(readtext("NAVALNY.txt")) navalny_raw
❗ATENCIÓN: Si por alguna razón hiciesemos algún cambio en el contenido del archivo, deberemos de aplicar el paso anterior de nuevo. Cuando cargamos un archivo en R, se guarda una copia y cualquier cambio en el original no se refleja automáticamente.
🤔¿Qué hemos hecho?
Este comando carga el archivo NAVALNY.txt
en el objeto navalny_raw
, el cual contiene el contenido del texto. Vamos a desgranar este prompt para que pueda entenderse más facil:
navalny_raw
es un objeto en R que almacena el texto como una cadena de caracteres (character vector). EnR
hablamos de objetos para referirnos a los contenedores donde almacenamos datos e información. En el caso anterior, el objetodata_char_navalny
almacena el texto plano que vamos a utilizar. Existen distintos objetos con diferentes datos almacenados: matrices, números, listas jerarquizadas, así como un sin fin de combinaciones. A lo largo de este caso práctico trabajaremos con ellos para gestionar más facilmente el análisis cuantitativo.<-
emula a una flecha y básicamente indica la dirección de la acción. Al objetonavalny_raw
estamos aplicándole una funciónas.character()
: Se trata de la función que estamos aplicando. Esta función convierte todo lo que se contiene dentro de ella en carácter. EnR
toda función viene seguida por unos paréntesis dentro de los cuáles se incluyen los parámetros de dicha función. Si no hay contenido, se aplican los parámetros que la función trae por defecto.readtext
: Aquí estamos empleando una función expecífica del paquetereadtext
. Si no hubiésemos cargado el paquete anteriormente, podríamos invocarlo específicamente para activar esta función con la notaciónpaquete::función
. Aquí, lo que le estamos diciendo a RStudio es que, del paquetereadtext
, aplique específicamente la función lectura que casualmente también se llamareadtext
. Dentro indicamos entrecomillada la ruta del archivo a importar. Un problema muy común que puede surgir a la hora de introducir la URL de la ubicación del archivo es expresarlo con barras laterales izquierdas ” \ “, tal y como viene en la barra de dirección del explorador de archivos de Windows, en vez de la derecha” / “. Si tienes problemas leer tu .txt ¡prueba con hacer este cambio!names(navalny_raw) <- "navalny"
: Asigna un nombre al objeto que contiene el texto, facilitando su identificación en futuros análisis.
🔍 Comprobaciones
En el análisis cuantitativo toda precaución es poca. Vamos a verificar que el texto ha sido leído por el programa. Usaremos el paquete stringi
para ver los primeros 75 caracteres de nuestro archivo y confirmar que los datos se cargaron bien.
# Comprobar los primeros 75 caracteres del texto
stri_sub(navalny_raw, 1, 75)
[1] "2022\nJanuary 17th\nExactly one year ago today I came home, to Russia.\nI didn"
Si hemos hecho los pasos bien, tendréis que haber recibir este texto de vuelta:
[1] "2022\nJanuary 17th\nExactly one year ago today I came home, to Russia.\nI didn"
🗃️Acotando el texto a analizar
Nuestro siguiente objetivo es seleccionar qué partes del texto vamos aplicar el análisis cuantitativo. Puede que nuestro foco de interés sea algun apartado concreto de nuestro material, por lo que vamos a crear un objeto que albergue un rango determinado dentro de nuestro fichero txt
. Con esto, nos quitaremos toda la información innecesaria que puede ensuciar nuestros resultados.
El proceso que vamos a realizar a continuación es muy útil cuando los archivos que manejamos tienen ligados metadatos. Normalmente, esta metadata suele ser más un dolor de cabeza que otra cosa y es recomendable realizar una limpieza previa para que esos datos no se mezclen con el contenido de nuestro análisis. En este apartado seguiremos trabajando con el paquete stringi
.
Si el texto contiene secciones que no necesitamos para el análisis, podemos filtrarlas o limpiarlas en esta etapa.
PASO 1: Identificación de comienzo y fin del texto
Para crear el objeto que albergue el rango de texto a analizar deberemos empezar indicando donde empieza y termina nuestra selección. Imaginemos que sólo nos interesa analizar los últimos años de vida de Navalny. Para ello, crearemos dos valores de posición: start_v
y end_v
, donde start_
será: “2023, January 12th” y end_v
“Alexei Navalny died”.
Localizado el rango que queremos, la forma de expresarlo en el programa sería el siguiente:
<- stri_locate_first_fixed(navalny_raw, "2023\nJanuary 12th")[1]) (start_v
[1] 23654
<- stri_locate_last_fixed(navalny_raw, "Alexei Navalny died")[1]) (end_v
[1] 44099
Si lo hemos aplicado bien, la función debería de devolver los siguientes resultados
Para
start_value
[1] 23654
Para
end_value
[1] 44099
🤔¿Qué hemos hecho?
Tanto
start_v
comoend_v
son nombres que hemos asignado a la posición específicas del texto. En sí, no significan nada. Solo decimos, a través de “<-” que dichos nombres albergan una función de posicionamiento.Las funciones del paquete
stringi
:stri_locate_first_fixed
ystri_locate_last_fixed
buscan y encuentran la primera coincidencia del valor entrecomillado que precede a nuestro objetodata_char_NALVANY
El [1] es un indicador que le estamos dando a la función para que escoja la primera posición donde aparezca el texto que hayamos escogido.
Así, cuando vemos devuelto las respuestas
[1] 23653
y[1] 44141
quiere decir que parastart_v
yend_v
está asignado el primer valor donde aparece dichas expresiones , localizadas por primera vez en la posición 23653 y 44141 de nuestro texto.
PASO 2: Nuevo objeto
Creado nuestro punto de inicio y final de nuestra zona de trabajo, haremos un objeto que alberge dicho rango. Lo llamaremos: navalny_fix
<- stri_sub(navalny_raw, start_v, end_v)
navalny_fix length(navalny_fix)
[1] 1
Al iniciar el código el valor que os ha tenido que recuperar, además de almacenar el objeto en la pestaña Environment
de RStuido, es:
[1] 1
🤔¿Qué hemos hecho?
navalny_fix
es el nombre del objeto que almacena la función que ha sido asignada. En este caso, a través destri_sub
, estamos extrayendo una parte del textonavalny_fix
. A diferencia del caso anterior, aquí le estamos pidiendo que, en vez de que recuper un número detemrinado de caracteres, escoja todos los que hay comprendidos entre las posiciones que hemos dado astart_v
y aend_v
. Con esto nos aseguramos que el objetonavalny_fix
siempre trabaje en los rangos que nos interesa analizar.length(navalny_fix)
es una expresión que usamos para comprobar cuandos valores existen en nuestro objeto. Es una forma de asegurarnos de que nuestro objeto solo tiene un vector y no es un conjunto de fragmentos de texto. Por eso, al introducirlo, el programa nos devuelve el valor 1 porque solo hay 1 valor dentro de nuestro objeto.
🗑️ Limpiando datos
El fichero de texto con el que estamos trabajando ya está libre de ruido, por lo que continuaremos el resto del ejemplo trabajando con el objeto navalny_raw
.
Ya tenemos nuestro set de datos, nos toca empezar a limpiar antes de tokenizar y empezar a analizar.
PASO 1: Comprobar la estructura del texto
Vamos a abrir un momento nuestro archivo para ver lo que contiene. Aquí te enseño las primeras líneas:
2022
January 17th
Exactly one year ago today I came home, to Russia.
I didn’t manage to take a single step on the soil of my country as a free man: I
was arrested even before border control.
The hero of one of my favorite books,“Resurrection,” by Leo Tolstoy, says,“Yes,
the only suitable place for an honest man in Russia at the present time is
prison.”
It sounds fine, but it was wrong then, and it’s even more wrong now.
There are a lot of honest people in Russia—tens of millions. There are far more
than is commonly believed.
The authorities, however, who were repugnant then and are even more so now,
Aquí queda más clara la estructura del texto:
Cada entrada del diario empieza con el año en la primera línea
Dentro de cada año, se dividen por días las entradas
Los párrafos están separados por saltos de línea
Las entradas están separadas entre sí también por una linea en blanco.
A continuación lo que vamos a hacer es crear un objeto lista3, en la que cada elemento será una entrada del diario.
PASO 2: Conversión del texto en vectores
Ahora que comprendemos la estructura, vamos a separar el texto en entradas diarias. Primero dividimos el texto en líneas, donde cada línea se convierte en un elemento de un vector. Esto nos permite identificar las líneas que contienen fechas y separar las entradas.
# Convertir el texto en un vector de líneas
<- unlist(strsplit(navalny_raw, '\n')) # \n indica un salto de línea lines
🤔¿Qué hemos hecho?
strsplit()
divide el texto por saltos de línea ("\n"
), creando un vector en el que cada línea es un elemento independiente.Usamos
unlist()
para simplificar la estructura y trabajar con un vector plano.
head(lines) # Veamos las primeras seis líneas de nuestro objeto
NAVALNY.txt1
"2022"
NAVALNY.txt2
"January 17th"
NAVALNY.txt3
"Exactly one year ago today I came home, to Russia."
NAVALNY.txt4
"I didn’t manage to take a single step on the soil of my country as a free man: I"
NAVALNY.txt5
"was arrested even before border control."
NAVALNY.txt6
""
PASO 3: Creación de índices para identificar entradas
En este paso, vamos a crear los índices que nos permitirán identificar las líneas en el texto que corresponden a cada año y a cada día. Esto nos ayudará a estructurar las entradas en el próximo paso.
- Utilizando expresiones regulares, vamos a identificar las líneas que contengan sólo el año. Estas líneas marcan el inicio de cada conjunto de entradas anuales
<- grep("^\\d{4}$", lines)
year_indices
print(year_indices) # Muestra las líneas que contienen el año
[1] 1 420 714
- Hemos identificado tres líneas que incluyen el año. Ahora haremos lo mismo para las líneas que encuentren el mes y el día
<- grep("^(January|February|March|April|May|June|July|August|September|October|November|December)\\s+\\d{1,2}(st|nd|rd|th)?$", lines)
day_indices
length(day_indices) # Número de entradas identificadas
[1] 16
🤔¿Qué hemos hecho?
year_indices
contiene los índices de las líneas con los años, es decir, las posiciones donde comienza cada año en el texto. La expresión regular^\\d{4}
busca cuatro dígitos al inicio de la línea.month_day_indices
contiene los índices de las líneas con fechas diarias, indicando el inicio de cada día dentro de los años.(January|February|...)
: Busca un mes escrito en inglés.\\s+
: Representa uno o más espacios.\\d{1,2}(st|nd|rd|th)?$
: Busca el día, que tendrá uno o dos dígitos y que puede ir seguido de “st”, “nd”, “rd”, o “th” al final de la línea.
PASO 4: Creación de entradas del diario
Ahora que tenemos los índices para los años y días, podemos organizar el texto en entradas anidadas: cada año será un grupo principal, y dentro de cada año, cada día será una entrada individual.
El siguiente código es un poco tocho, te intento explicar:
- Creamos las entradas por año. Para esto utilizamos el objeto
year_indices
que construimos antes. - Creamos sublistas dentro de cada año con nuestro objeto
month_day_indices
.
# Dividimos el texto en entradas anidadas (por año y día)
<- lapply(
yearly_entries seq_along(year_indices),
function(i) {
<- year_indices[i]
start_year <- if (i < length(year_indices))
end_year + 1] - 1
year_indices[i else
length(lines)
# Extraemos las líneas correspondientes al año actual
<- lines[start_year:end_year]
year_lines
# Encontrar las entradas diarias dentro del año actual
<- grep(
day_indices "^(January|February|March|April|May|June|July|August|September|October|November|December)\\s+\\d{1,2}(st|nd|rd|th)?$",
year_lines
)
# Crear una sublista para cada día dentro del año
<- lapply(seq_along(day_indices), function(j) {
entries <- day_indices[j]
start_day <- if (j < length(day_indices))
end_day + 1] - 1
day_indices[j else
length(year_lines)
:end_day]
year_lines[start_day
})
# Devolver una lista con el año y sus entradas diarias
list(year = year_lines[1], entries = entries)
})
🤔¿Qué hemos hecho?
Cada elemento en
yearly_entries
es un año completoprint(yearly_entries[[1]]$year) # primer año
NAVALNY.txt1 "2022"
Dentro de cada año,
entries
contiene las entradas diarias como sublistas, donde cada día es un vector de párrafoslength(yearly_entries[[1]]$entries)
[1] 5
Para ello, hemos combinado diferentes funciones en un sólo script:
lapply()
sirve para aplicar una función a cada elemento de un vector o lista. En nuestro caso, itera sobre cada índice deyear_indices
, procesando el texto correspondiente a cada ño. Genera una listayearly_indices
donde cada elemento representa un año y sus entradas diarias. La segunda vez que la empleo es para crear sublistas para cada día dentro del año, aplicándola sobreday_indices
.seq_along()
genera una secuencia de números que corresponden a la longitud de un vector o lista. Lo utilizo para generar una secuencia de índices a iterar sobre los años y sobre los días dentro de cada año.grep()
busca patrones específicos dentro de un vector, tal y como hicimos antes.if
dentro delapply()
define los límites de inicio y fin de cada año.list()
crea la lista que almacena todas las entradas.
🗿 ¡A la tokenización!
Ya tenemos nuestro texto bien organizado y estructurado. Toca dividir aún más y limpiar. Para ello vamos a hacer lo siguiente:
- Convertimos el texto en minúsculas y eliminamos los signos de puntuación
- Tokenizamos el texto dividiéndolo en palabras. De manera que nuestra unidad de análisis será la palabra4.
- Eliminamos todas las palabras vacías5 para quedarnos sólo con aquellas relevantes para nuestro análisis.
PASO 1: Texto en minúscula y puntuación fuera
Para asegurarnos que palabras idénticas no se traten como diferentes por su formato, converitmos todo el texto a minúsculas y eliminamos puntuación.
# Convertir cada entrada diaria a minúsculas y eliminar signos de puntuación
<- lapply(yearly_entries, function(year) {
yearly_entries $entries <- lapply(year$entries, function(entry) {
year# Convertir el texto a minúsculas y eliminar puntuación
<- char_tolower(entry)
entry <- gsub("[[:punct:]]", "", entry)
entry
entry
})# Devolver la lista de año modificada
year })
🤔¿Qué hemos hecho?
La función
char_tolower()
convierte el texto en minúsculas.La función
gsub()
sustituye un patrón de texto por otro. En nuestro caso, le hemos pedido que busque cualquier signo de puntuación empleando la expresión regular[[:punct:]]
y la reemplace por nada.Después hemos pedido que incluya estos cambios nuevamente en nuestro objeto
yearly_entries
.
Si observas las primeras líneas de una entrada, verás que todo ha funcionado tal y como esperábamos:
NAVALNY.txt2
"january 17th"
NAVALNY.txt3
"exactly one year ago today i came home to russia"
NAVALNY.txt4
"i didnt manage to take a single step on the soil of my country as a free man i"
NAVALNY.txt5
"was arrested even before border control"
NAVALNY.txt6
""
PASO 2: Tokenización
Para que el programa pueda analizar y realizar manipulaciones sobre las palabras de forma individualizada, vamos a convertir a cada una de ellas en pequeños valores que llamamos tokens.
# Tokenizar cada entrada diaria dentro de cada año
<- lapply(yearly_entries, function(year) {
yearly_entries $entries <- lapply(year$entries, function(entry) {
yeartokens(entry, what = "word")
})# Devolver la lista de año modificada
year })
🤔¿Qué hemos hecho?
Siguiendo la misma estructura de la vez anterior, hemos incorporado la función
tokens()
y lo hemos aplicado al objetoentry
Además, hemos utilizado el parámetro
what=
en el que indicamos el nivel. En nuestro caso tokenizamos por palabras. Otras opciones son por caracteres (character
) y frases (sentence
).
Ahora en lugar de contener un listado de filas por entrada, lo que tengo es una bolsa de palabras:
Tokens consisting of 3 documents.
NAVALNY.txt2 :
[1] "january" "17th"
NAVALNY.txt3 :
[1] "exactly" "one" "year" "ago" "today" "i" "came"
[8] "home" "to" "russia"
NAVALNY.txt4 :
[1] "i" "didnt" "manage" "to" "take" "a" "single" "step"
[9] "on" "the" "soil" "of"
[ ... and 7 more ]
PASO 3: Eliminación de palabras vacías
Para centrarnos en las palabras significativas, eliminamos las palabras vacías (stopwords), que suelen ser términos comunes y poco informativos, como “el”, “de”, “y”. Esto permite que el análisis se centre en términos con más contenido semántico.
# Eliminar palabras vacías en inglés en cada entrada diaria
<- lapply(yearly_entries, function(year) {
yearly_entries $entries <- lapply(year$entries, function(entry) {
yeartokens_remove(entry, pattern = stopwords("en"))
})# Devolver la lista de año modificada
year })
🤔¿Qué hemos hecho?
- Aquí empleamos la función
tokens_remove()
otra vez aplicada al objetoentry
, en este caso empleamos el parámetropattern =
para indicar que eliminaremos los tokens que representen palabras vacías, en paréntesis incluimos la lengua a través de su código ISO, en nuestro caso el inglés. Aquí tienes el listado completo de idiomas. El paquetestopwords
permite asimismo crear y/o añadir tus propias palabras vacías.
Fíjate cómo, en comparación con el fragmento anterior, se han eliminado palabras como “in”, “my” u “only”.
Tokens consisting of 3 documents.
NAVALNY.txt2 :
[1] "january" "17th"
NAVALNY.txt3 :
[1] "exactly" "one" "year" "ago" "today" "came" "home"
[8] "russia"
NAVALNY.txt4 :
[1] "didnt" "manage" "take" "single" "step" "soil" "country"
[8] "free" "man"
Como habrás notado, es posible juntar todos estos pasos en uno sólo. Aquí lo mostramos por trozos para que vayas comprendiendo el proceso, pero podríamos hacer todo esto de una vez.
¿Sabías qué…? ️🤓☝
En inglés, es posible que el texto venga acompañado de apóstrofes como en los casos de don't
y he's
. Aquí, quanteda
no tomará las 't
ni las 's
como elementos aislados, sino que lo mantendrá unida a la palabra para respetar el significado original.
Footnotes
R
es un lenguaje de programación abierto y colaborativo que sigue una estructura totalmente descentralizada. Cuando instalamosR
por primera vez, sólo instalamos sus funcionalidades básicas. Todas aquellas funcionalidades adicionales llevadas a cabo por terceras personas deben instalarse en lo que se denominan paquetes o libraries en inglés.Cada vez que vayas a emplear un paquete, debes cargarlo, por defecto, cada vez que abres
R
estos paquetes no están cargados.Una expresión regular es un patrón de búsqueda utilizado para manipular texto específico en una cadena. Facilita tareas como eliminar caracteres no deseados o extraer información específica (e.g., fechas o números). Es especialmente útil en la limpieza y preprocesamiento de datos textuales.
Una lista es una estructura de datos que puede contener elementos de diferentes tipos (numérico, caractéres, vectores o incluso otras listas) en un solo objeto. Cada elemento en una lista se puede acceder de forma individual usando índices. Esto se hace utilizando corchetes dobles. Por ejemplo si queremos ver el segundo elemento de la lista
lista
, lo indicaremos así:lista[[2]]
.Aquí es importante diferenciar entre análisis textual y un análisis semántico. En el análisis de textos examinamos cuestiones como la frecuencia de las palabras, patrones o estructura, sin considerar el significado de cada palabra. Se trataría de un paso previo al análisis semántico donde nos centramos en el significado y el contexto de las palabras.
Las palabras vacías son términos comunes (como “el”, “de”, “y”) que suelen aparecer con mucha frecuencia en el texto, pero aportan poco significado o valor informativo al análisis. Estas palabras se eliminan generalmente para centrar el análisis en los términos más relevantes.