1 Introducción

En esta actividad se realiza un análisis de minería de texto sobre la obra Don Quijote de la Mancha. El propósito es examinar las relaciones entre palabras, bigramas y patrones lingüísticos que reflejan las estructuras semánticas del texto.

1.1 Preparación del entorno

##Carga de librerías Se cargan las librerías necesarias para la ejecución del análisis.

library(pdftools)
library(dplyr)
library(tidytext)
library(stringr)
library(ggplot2)
library(tidyr)
library(widyr)
library(ggraph)
library(igraph)
library(stopwords)
library(wordcloud)
library(RColorBrewer)
library(tidygraph)

1.2 Carga y limpieza del texto

  • Se cargan los archivos PDF con la función pdf_text() del paquete pdftools, convirtiéndolos en texto plano.
  • Se unifican las páginas de cada parte del libro.
  • Se limpia el texto eliminando saltos de línea, caracteres no deseados y espacios redundantes.
# Lectura de los PDF
texto01 <- pdf_text("DONQUIJOTE_PARTE1.pdf")
texto02 <- pdf_text("DONQUIJOTE_PARTE2.pdf")

# Unir las páginas dentro de cada parte
texto01 <- paste(texto01, collapse = " ")
texto02 <- paste(texto02, collapse = " ")

1.3 Función de limpieza del texto

  • Se realiza una limpieza de los textos en cada documento, donde se efectuan las siguientes accioens:

  • Eliminación de contenido no literario

  • Se eliminan enlaces, nombres editoriales y frases fuera del cuerpo narrativo.

  • Limpieza de caracteres de control

    • Se quitan saltos de línea, retornos de carro y saltos de página.
  • Normalización de signos y puntuación

    • Se corrigen guiones, comillas, puntos suspensivos y se eliminan números.
  • Ajuste de estructura textual

    • Se formatean títulos de capítulos y se eliminan paréntesis innecesarios.
  • Formato final y espaciado

    • Se eliminan espacios redundantes y se separan oraciones con saltos de línea.
# Función de limpieza
limpiar_texto <- function(texto) {
# Se realizan la siguiente limpieza en el texto.
  # Frases de origen
  texto <- gsub("http://www\\.educa\\.jcyl\\.es", "", texto)
  texto <- gsub("Portal Educativo EducaCYL", "", texto)
  # Saltos y caracteres de control
  texto <- gsub("\f", " ", texto)
  texto <- gsub("\\r|\\n", " ", texto)
  # Sustitución y normalización
  texto <- gsub("—|–", " ", texto)
  texto <- gsub("\\.{2,}", ".", texto)
  texto <- gsub("\\d+", "", texto)
  # Comillas, apóstrofos y espacios
  texto <- gsub("[‘’´`]", "'", texto)
  texto <- gsub("[“”]", "\"", texto)
  texto <- gsub("\\(\\s+", "(", texto)
  texto <- gsub("\\s+\\)", ")", texto)
  texto <- gsub("\"\\s+", "\"", texto)
  texto <- gsub("\\s+\"", "\"", texto)
  # Encabezados y títulos
  texto <- gsub("Miguel de Cervantes Saavedra", "", texto)
  #texto <- gsub("El Ingenioso Hidalgo Don Quijote de la Mancha", "", texto)
  texto <- gsub("PRIMERA PARTE|SEGUNDA PARTE", "\n", texto)
  texto <- gsub("CAP[IÍ]TULO\\s*\\d+:?", "\n\nCAPÍTULO ", texto)
  # Paréntesis y puntuación
  texto <- gsub("\\(\\)", "", texto)
  texto <- gsub("\\(.*?\\)", "", texto)
  texto <- gsub("\\s+([,;:.!?])", "\\1", texto)
  # Limpieza de frases editoriales
  texto <- gsub("Cuenta Cide Hamete Benengeli.*?historia", "", texto)
  # Espacios múltiples y formato final
  texto <- gsub("\\s{2,}", " ", texto)
  texto <- trimws(texto)
  # Saltos de línea entre oraciones
  texto <- gsub("([.!?])\\s+", "\\1\n", texto)

  return(texto)
}
# Se aplica limpieza a cada parte
texto01_limpio <- limpiar_texto(texto01)
texto02_limpio <- limpiar_texto(texto02)
# Se unen las partes limpias ===
texto <- paste(texto01_limpio, texto02_limpio, collapse = "\n\n")

1.4 Estructuración y limpieza del texto

  • Se separa el texto por frases donde haya puntos.
  • Se crea un data frame llamado df_texto.
  • Se eliminan los espacios al inicio y final de las frases.
# Se separa el texto por oraciones y crear un DataFrame
frases <- unlist(strsplit(texto, "\\."))
df_texto <- data.frame(frase = frases, stringsAsFactors = FALSE)
# Se limpian espacios al inicio y final de cada frase
df_texto$frase <- stringr::str_trim(df_texto$frase)

1.5 Tokenización y eliminación de stopwords

  • Las stopwords son palabras muy comunes en cualquier texto, independientemente del tema, que generalmente no aportan un significado relevante al análisis. Ejemplos de estas palabras son los artículos, preposiciones, conjunciones y pronombres.
  • Por lo tanto se eliminan porque aportan poco valor semántico y pueden introducir ruido en el procesamiento del lenguaje natural. El proceso realizado es el siguiente:
    • Se define una lista de stopwords en español.
    • Se tokeniza el data frame df_texto, es decir, se separan las frases en palabras individuales y se almacenan en un nuevo data frame llamado df_tokens, en una columna denominada “word”.
    • Se garantiza que las palabras contenidas en df_tokens sean exclusivamente alfabéticas, eliminando caracteres especiales o símbolos innecesarios.
# Stopwords en español
stopwords_es <- stopwords("es")
# Tokenización
df_tokens <- df_texto %>%
unnest_tokens(word, frase) %>%
filter(!word %in% stopwords_es,
str_detect(word, "[a-záéíóúñ]"))

1.6 Análisis de frecuencia de palabras

  • Se genera un gráfico de barras con las palabras más frecuentes del texto después de eliminar las stopwords.
word_counts <- df_tokens %>%
  count(word, sort = TRUE)

ggplot(head(word_counts, 20), aes(x = reorder(word, n), y = n)) +
  geom_col(fill = "steelblue") +
  geom_text(aes(label = n), hjust = -0.1, size = 3) +  # Etiquetas
  coord_flip() +
  labs(
    title = "Palabras más frecuentes (sin stopwords)",
    x = "Palabra", 
    y = "Frecuencia"
  ) +
  theme_minimal() +
  theme(plot.title = element_text(hjust = 0.5)) +      # Centrar título
  expand_limits(y = max(head(word_counts$n, 20)) * 1.1)  # Deja espacio para etiquetas

1.7 Nube de palabras

  • Se genera un gráfico tipo nube de palabras, que permite visualizar de forma práctica y estética las palabras más frecuentes del texto.
  • Las palabras con mayor frecuencia aparecen en tamaño más grande, mientras que las menos comunes se muestran con un tamaño más pequeño, facilitando la identificación visual de los términos más relevantes en el contenido analizado.
set.seed(123)
wordcloud(words = word_counts$word,
freq = word_counts$n,
min.freq = 20,
max.words = 200,
random.order = FALSE,
rot.per = 0.3,
colors = brewer.pal(8, "Dark2"))

1.8 Generación y análisis de bigramas

  • En este bloque se inicia el análisis de bigramas, cuyo objetivo es identificar las combinaciones de palabras que aparecen con mayor frecuencia de forma conjunta en el texto, por ejemplo, el bigrama “don quijote”.
  • Se procesa el data frame df_texto, y el resultado se almacena en un nuevo data frame denominado bigrams.
  • El texto se divide en bigramas (pares de palabras consecutivas). Por ejemplo, la frase “de nuestro hidalgo” se divide en los bigramas (de : nuestro) y (nuestro : hidalgo).
  • El data frame bigrams se separa en dos columnas: “word1” y “word2”, correspondientes a la primera y segunda palabra de cada par.
  • Se eliminan los bigramas que contengan stopwords; en el ejemplo anterior, (de : nuestro) se descarta porque “de” es una stopword.
  • Finalmente, se cuenta la frecuencia de cada bigrama y se ordenan de mayor a menor frecuencia para identificar los pares de palabras más representativos del texto.
bigrams <- df_texto %>%
unnest_tokens(bigram, frase, token = "ngrams", n = 2) %>%
separate(bigram, into = c("word1", "word2"), sep = " ") %>%
filter(!word1 %in% stopwords_es,
!word2 %in% stopwords_es) %>%
count(word1, word2, sort = TRUE)

head(bigrams, 15)
##         word1    word2    n
## 1         don  quijote 2186
## 2        dijo      don  310
## 3   respondió   sancho  310
## 4      sancho    panza  279
## 5   respondió      don  267
## 6        dijo   sancho  246
## 7       vuesa   merced  245
## 8       señor      don  183
## 9  caballeros andantes  131
## 10  caballero  andante  116
## 11        don fernando  114
## 12     sancho     dijo  106
## 13     merced    señor  100
## 14     señora dulcinea   96
## 15     muchas    veces   81

1.9 Visualización de red de bigramas

  • Se genera una visualización en forma de grafo de los bigramas, con el objetivo de representar de manera gráfica los pares de palabras que se repiten con mayor frecuencia en el texto.
  • Los grupos de palabras se conectan mediante líneas, donde el grosor y el color indican la frecuencia de aparición: las líneas más gruesas y de color verde oscuro representan una mayor frecuencia, mientras que las más delgadas y de tono más claro indican menor recurrencia.
  • En este caso, las líneas de mayor grosor y color verde intenso corresponden a los bigramas con una frecuencia de aparición igual o superior a 2000.
# Filtrar bigramas más frecuentes

bigrams_grafo <- bigrams %>%
filter(n > 100) %>%
graph_from_data_frame()

ggraph(bigrams_grafo, layout = "fr") +
geom_edge_link(aes(edge_alpha = n, edge_width = n), color = "darkcyan") +
geom_node_point(size = 5, color = "lightblue") +
geom_node_text(aes(label = name), repel = TRUE) +
theme_void() +
labs(title = "Red de bigramas más frecuentes")

1.10 Coocurrencia entre palabras

  • Este bloque de código tiene como objetivo analizar las coocurrencias de palabras dentro de grupos de frases del texto. En otras palabras, busca identificar qué palabras tienden a aparecer juntas dentro de un mismo contexto textual.
  • El texto tokenizado se divide en 10 grupos de frases para facilitar el análisis contextual.
  • Se crean dos columnas:
    • “grupo”, que indica la segmentación de los bloques de texto.
    • “word”, que contiene cada palabra tokenizada.
  • Se eliminan las stopwords y se filtran únicamente los caracteres alfabéticos, excluyendo números o signos.
  • La función pairwise_count() del paquete widyr calcula cuántas veces dos palabras aparecen juntas dentro del mismo grupo (considerado como una unidad de análisis, como un párrafo o bloque de texto).
    • Es importante destacar que estas palabras no necesariamente están una al lado de la otra en el texto original; por tanto, no forman bigramas ni n-gramas, sino que coocurren dentro del mismo contexto.
  • El resultado genera tres columnas:
    • “item1”: primera palabra del par.
    • “item2”: segunda palabra del par.
    • “n”: número de coocurrencias entre ambas palabras dentro de los grupos analizados.
# Crear grupos de 10 frases para mejorar las coocurrencias
df_tokens_coocurencia_grupos <- df_texto %>%
mutate(grupo = rep(1:ceiling(nrow(df_texto)/10), each = 10, length.out = nrow(df_texto))) %>%
unnest_tokens(word, frase) %>%
filter(!word %in% stopwords_es,
str_detect(word, "[a-záéíóúñ]"))

# Se Calculan las coocurrencias por grupo
word_pairs <- df_tokens_coocurencia_grupos %>%
pairwise_count(word, grupo, sort = TRUE)

# Se muestran los pares con más de 100 coocurrencias
head(word_pairs[word_pairs$n > 100, ], 15)
## # A tibble: 15 × 3
##    item1   item2       n
##    <chr>   <chr>   <dbl>
##  1 dijo    si        610
##  2 si      dijo      610
##  3 quijote don       572
##  4 don     quijote   572
##  5 dijo    don       553
##  6 don     dijo      553
##  7 si      don       549
##  8 don     si        549
##  9 dijo    quijote   518
## 10 quijote dijo      518
## 11 si      quijote   516
## 12 quijote si        516
## 13 si      así       515
## 14 así     si        515
## 15 dijo    así       507

1.11 Correlaciones entre palabras

  • Este bloque de código tiene como propósito calcular la correlación de coocurrencia entre pares de palabras utilizando el coeficiente phi.
  • El objetivo es identificar qué palabras tienden a aparecer juntas o en contextos similares, no solo por coincidencia dentro de un grupo, sino también considerando su frecuencia relativa de aparición en todo el texto.
    • Preprocesamiento del texto
      • El texto contenido en df_texto se divide en grupos de 10 frases, generando así una estructura segmentada para el análisis.
      • Cada grupo se tokeniza, creando un nuevo conjunto de datos con dos columnas:
        • “grupo”: el número de bloque o segmento de texto.
        • “word”: las palabras tokenizadas.
    • A continuación, se eliminan las stopwords (palabras funcionales sin valor semántico relevante) y se filtran únicamente los términos que contienen caracteres alfabéticos, eliminando signos o números.
    • Cálculo de la correlación
      • Al conjunto df_tokens_grupos se le aplica un filtrado para excluir las palabras con menos de 20 apariciones, con el fin de eliminar términos poco frecuentes o sin relevancia analítica.
      • Luego, se utiliza la función pairwise_cor() del paquete widyr, la cual calcula el coeficiente de correlación phi (φ) entre pares de palabras según su patrón de aparición en los grupos definidos.
      • Este método permite identificar relaciones semánticas o contextuales entre palabras que coexisten dentro de un mismo segmento narrativo.
      • Finalmente, se muestran los pares de palabras con mayor correlación, es decir, aquellas que aparecen de forma consistente en contextos similares. Cabe destacar que las palabras con correlaciones más altas suelen corresponder a nombres propios o entidades que comparten un contexto narrativo recurrente. Por ejemplo, aunque la coocurrencia directa de “don” y “quijote” es alta, otras combinaciones como “cide” y “hamete” presentan una correlación más fuerte y precisa, ya que reflejan vínculos contextuales más definidos dentro del relato, en lugar de una simple frecuencia de aparición.
# Se crean grupos de 10 frases para analizar coocurrencias en segmentos más largos
df_tokens_grupos <- df_texto %>%
  mutate(grupo = rep(1:ceiling(nrow(df_texto)/10),
  each = 10, length.out = nrow(df_texto))) %>%
  unnest_tokens(word, frase) %>%
  filter(!word %in% stopwords_es,
         str_detect(word, "[a-záéíóúñ]"))
# Se calcula la correlación entre palabras (phi de coocurrencia)
word_cors <- df_tokens_grupos %>%
  group_by(word) %>%
  filter(n() >= 20) %>%                 # solo palabras con al menos 20 apariciones
  pairwise_cor(word, grupo, sort = TRUE)
# Se muestran las correlaciones más fuertes
head(word_cors, 15)
## # A tibble: 15 × 3
##    item1      item2      correlation
##    <chr>      <chr>            <dbl>
##  1 hamete     cide             0.983
##  2 cide       hamete           0.983
##  3 camila     lotario          0.965
##  4 lotario    camila           0.965
##  5 marcela    grisóstomo       0.953
##  6 grisóstomo marcela          0.953
##  7 camila     anselmo          0.923
##  8 anselmo    camila           0.923
##  9 tosilos    lacayo           0.903
## 10 lacayo     tosilos          0.903
## 11 lotario    anselmo          0.890
## 12 anselmo    lotario          0.890
## 13 quijote    don              0.844
## 14 don        quijote          0.844
## 15 sansón     carrasco         0.791

1.12 Scatterplot - pares de palabras más correlacionadas

En el siguiente código se presenta una visualización de las 15 correlaciones más altas entre palabras. Tal como se identificó previamente en el análisis, los términos “hamete” y “cide” destacan como el par con la correlación más fuerte, evidenciando su estrecha relación contextual dentro de la narrativa.

# Seleccionamos los pares con mayor correlación
word_cors_top <- word_cors %>%
  filter(correlation > 0.25) %>%
  arrange(desc(correlation)) %>%
  head(15)

# Graficar correlaciones como puntos
ggplot(word_cors_top, aes(x = reorder(paste(item1, item2, sep = " - "), correlation),
                          y = correlation)) +
  geom_point(color = "darkgreen", size = 3) +
  coord_flip() +
  labs(title = "Pares de palabras con mayor correlación",
       x = "Par de palabras", y = "Correlación (phi)") +
  theme_minimal(base_size = 13)

1.13 Gráfico de correlaciones entre palabras (Top 15 y Top 150)

  • Se presentan dos gráficos para visualizar las correlaciones entre palabras.
    • El primero ofrece una visualización más clara y ordenada, mostrando las 15 palabras más correlacionadas, lo que facilita la interpretación de las relaciones más fuertes.
    • El segundo gráfico proporciona una visión más amplia con las 150 correlaciones más relevantes. Aunque su lectura es menos precisa debido al mayor número de conexiones, permite observar un panorama general de las asociaciones y cómo ciertas palabras se correlacionan con múltiples términos dentro del texto.
# Función auxiliar para graficar correlaciones

graficar_red <- function(data, titulo, subtitulo) {
  set.seed(123)
  data %>%
    graph_from_data_frame() %>%
    ggraph(layout = "fr") +
    geom_edge_link(aes(edge_alpha = correlation, edge_width = correlation), color = "darkgreen") +
    geom_node_point(size = 4, color = "lightgreen") +
    geom_node_text(aes(label = name), repel = TRUE, size = 3) +
    theme_void() +
    labs(title = titulo,
         subtitle = subtitulo,
         caption = "Fuente: Análisis de texto del Don Quijote de la Mancha") +
    theme(plot.title = element_text(size = 14, face = "bold"),
          plot.subtitle = element_text(size = 11))
}
# Se filtra y grafica el TOP 15 de correlaciones
word_cors_top15 <- word_cors %>%
  filter(correlation > 0.25) %>%
  arrange(desc(correlation)) %>%
  slice_head(n = 15)

grafico_top15 <- graficar_red(
  word_cors_top15,
  titulo = "Red de correlaciones entre palabras",
  subtitulo = "Top 15 correlaciones > 0.25"
)

# Se muestra el grafico
print(grafico_top15)

# Se filtra y grafica el TOP 150 de correlaciones

word_cors_top200 <- word_cors %>%
  filter(correlation > 0.25) %>%
  arrange(desc(correlation)) %>%
  slice_head(n = 200)

grafico_top200 <- graficar_red(
  word_cors_top200,
  titulo = "Red de correlaciones entre palabras",
  subtitulo = "Top 150 correlaciones > 0.25"
)

print(grafico_top200)

1.14 Correlaciones en gráficos de barras

  • En el siguiente código se presentan dos gráficos de barras que analizan la correlación entre una palabra específica y las palabras que más se relacionan con ella.
    • En primer lugar, se examina la correlación de la palabra “renegado” con otros términos. En la red de correlaciones se observa que presenta múltiples asociaciones directas (hasta con cinco palabras), lo cual refleja tanto la fuerza de sus correlaciones como la cercanía contextual dentro del texto.
    • Posteriormente, se analiza la correlación de la palabra “hamete”, que, como se identificó previamente, muestra una correlación muy alta con “cide”, pero valores bajos con el resto de las palabras. Esto se evidencia también en el gráfico de red de correlaciones, donde solo se visualiza una conexión directa entre “hamete” y “cide”, indicando una relación fuerte y específica entre ambos términos.
palabra_base <- "renegado"   # Palabra a filtrar

# Se filtran  las correlaciones relacionadas con esa palabra
cor_palabra <- word_cors %>%
  filter(item1 == palabra_base) %>%
  arrange(desc(correlation)) %>%
  head(10)
# Gráfico de barras de correlaciones
ggplot(cor_palabra, aes(x = reorder(item2, correlation), y = correlation)) +
  geom_col(fill = "steelblue") +
  coord_flip() +
  labs(title = paste("Palabras más correlacionadas con:", palabra_base),
       x = "Palabra asociada",
       y = "Correlación (phi)") +
  theme_minimal(base_size = 13)

#-----------------------------------------------------------------------
palabra_base <- "hamete"   # Palabra a filtrar

# Filtra las correlaciones relacionadas con esa palabra
cor_palabra <- word_cors %>%
  filter(item1 == palabra_base) %>%
  arrange(desc(correlation)) %>%
  head(10)

# Gráfico de barras de correlaciones
ggplot(cor_palabra, aes(x = reorder(item2, correlation), y = correlation)) +
  geom_col(fill = "steelblue") +
  coord_flip() +
  labs(title = paste("Palabras más correlacionadas con:", palabra_base),
       x = "Palabra asociada",
       y = "Correlación (phi)") +
  theme_minimal(base_size = 13)

2 Conclusiones

Técnicas de minería de texto empleadas

En el desarrollo del análisis de “Don Quijote de la Mancha”, se aplicaron diversas técnicas de minería de texto orientadas a extraer patrones, asociaciones y relaciones semánticas entre palabras del texto. Estas metodologías permitieron limpiar, estructurar y representar gráficamente la información contenida en los archivos importados, facilitando la identificación de términos clave y su comportamiento conjunto dentro del texto. A continuación se listan un grupo de procedimientos o técnicas utilizadas:

  • Tokenización y limpieza: separación del texto en frases y luego en palabras, eliminación de stopwords (palabras sin contexto) y filtrado de tokens con caracteres no alfabéticos.

  • Frecuencia de palabras y bigramas: conteo de palabras más comunes y de pares de palabras consecutivas (bigramas) para identificar asociaciones frecuentes.

  • Coocurrencia por grupos: agrupación del texto en bloques de 10 frases y uso de pairwise_count() para contar cuántas veces dos palabras aparecen juntas dentro del mismo bloque.

  • Correlación de coocurrencia: aplicación de pairwise_cor() para calcular el coeficiente phi entre pares de palabras, identificando aquellas que muestran una asociación estadística más fuerte.

  • Visualización gráfica: uso de gráficos de barras, nubes de palabras y redes para ilustrar frecuencias y relaciones entre términos.

Análisis de correlaciones entre bigramas

El análisis de correlaciones permite identificar qué palabras dentro del texto tienden a aparecer juntas con mayor fuerza estadística, revelando vínculos semánticos o narrativos relevantes. A través del cálculo del coeficiente phi, se miden asociaciones que van más allá de la simple frecuencia, mostrando patrones consistentes de aparición conjunta entre términos significativos del texto, ya que se encuentra una relación ligada a segmentos del texto lo que permite identificar relaciones narrativas.

  • En el informe, los pares con mayor correlación phi incluyen “hamete – cide”, “camila – lotario” y otros nombres propios.

  • Estas correlaciones elevadas indican que esas palabras aparecen consistentemente juntas en los mismos bloques contextuales, casi nunca por separado.

  • A diferencia de las coocurrencias puras (que sólo cuentan frecuencia conjunta), la correlación ajusta por las frecuencias individuales de cada término, enfatizando asociaciones narrativas.

Conclusiones breves

  • Las palabras que dominan en el texto reflejan los personajes y temas centrales (ej. don, quijote, sancho, caballero).

  • Las correlaciones más fuertes entre nombres propios (como hamete con cide) revelan relaciones narrativas más precisas y distintivas que las meras coocurrencias.

  • El método de correlación distingue entre términos frecuentes con alta coincidencia y términos con asociación contextual significativa.