1 Introducción

En este taller realizo un análisis computacional de Cien Años de Soledad de Gabriel García Márquez. Aplico tres técnicas complementarias de procesamiento de lenguaje natural:

  1. Análisis de sentimientos multidimensional adaptado al realismo mágico
  2. Análisis de redes de co-ocurrencia mediante skipgramas
  3. Modelado de tópicos latentes con LDA (Latent Dirichlet Allocation)

1.1 Reflexión metodológica personal

Considero fundamental reconocer que un análisis de sentimientos tradicional (positivo/negativo) resulta inadecuado para literatura de realismo mágico. En la obra de García Márquez, conceptos como “muerte”, “soledad” o “guerra” no son simplemente “negativos”: operan como símbolos de ciclos históricos, destino familiar y transformación mítica.

Por esta razón, diseño un enfoque multidimensional temático que captura siete categorías narrativas específicas del universo de Macondo, siguiendo principios de análisis literario computacional.

1.2 Limitaciones que reconozco explícitamente

Mi análisis presenta las siguientes limitaciones estructurales:

1.2.1 1. División artificial de capítulos

Al no disponer del texto con marcadores estructurales explícitos (como “Capítulo I”, “Capítulo II”), divido la novela en 20 segmentos aproximadamente iguales. Esta es una limitación metodológica seria que puede:

  • Mezclar eventos narrativos distintos dentro de un mismo “capítulo”
  • Afectar la coherencia de los tópicos identificados por LDA
  • Distorsionar las métricas de sentimiento por segmento

1.2.2 2. Pérdida de contexto sintáctico

El análisis léxico que realizo (basado en tokenización por palabras) no captura: - Negaciones (“no es feliz” detecta “feliz”) - Ironía narrativa - Contexto narrativo complejo

1.2.3 3. Sensibilidad al preprocesamiento

La remoción de acentos y stopwords puede eliminar información semántica relevante, aunque intento mitigarlo usando stringi para preservar la ñ.

1.2.4 4. Límites del análisis cuantitativo

El realismo mágico opera con múltiples capas de significado simbólico que el análisis léxico no captura completamente.

Reconozco estas limitaciones abiertamente y las mitigo mediante interpretaciones contextualizadas de los resultados cuantitativos.

2 Carga de librerías

Cargo todas las librerías necesarias para el análisis:

# Manipulación de datos
library(readr)
library(tidyverse)
library(magrittr)
library(dplyr)

# Minería de texto
library(tidytext)
library(tm)
library(SnowballC)
library(ngram)
library(stringi)

# Análisis de redes
library(igraph)
library(ggraph)
library(widyr)

# Visualización
library(ggplot2)
library(gridExtra)
library(wordcloud)
library(RColorBrewer)
library(reshape2)
library(scales)

# Modelado de tópicos
library(topicmodels)

# Tablas profesionales
library(knitr)
library(kableExtra)

3 Importación y preparación del texto

3.1 Lectura del archivo

Importo el texto completo de la novela desde el archivo .txt:

# Leer el archivo de texto
# IMPORTANTE: Ajustar esta ruta según la ubicación real del archivo
texto_completo <- read_lines("gabriel_garcia_marquez_cien_annos_soledad.txt")

# LIMPIEZA CRÍTICA: Eliminar líneas repetitivas de metadatos editoriales
# Esto elimina encabezados de página que contaminan el análisis
patron_metadatos <- "Gabriel García Márquez|Cien años de soledad|EDITADO POR|ediciones la cueva|Para Jomi|María Luisa Elio"

texto_completo <- texto_completo[!grepl(patron_metadatos, texto_completo, ignore.case = TRUE)]

# Eliminar líneas vacías o con solo espacios
texto_completo <- texto_completo[nchar(trimws(texto_completo)) > 0]

# Convertir a vector de caracteres limpio
texto_completo <- unlist(c(texto_completo))
names(texto_completo) <- NULL

cat("\nTotal de líneas en el archivo (después de limpieza):", length(texto_completo), "\n")
## 
## Total de líneas en el archivo (después de limpieza): 9158

3.2 División en capítulos (aproximada)

Como reconozco en las limitaciones, al no tener marcadores estructurales, divido el texto en 20 segmentos de tamaño aproximadamente igual:

# Convertir a data frame con índice de línea
texto_df <- tibble(
  line = seq_along(texto_completo),
  text = texto_completo
)

# División aproximada en 20 "capítulos"
lineas_totales <- nrow(texto_df)
lineas_por_capitulo <- lineas_totales / 20

texto_df <- texto_df %>%
  mutate(
    capitulo = ceiling(line / lineas_por_capitulo),
    capitulo_label = paste0("Cap_", sprintf("%02d", capitulo))
  )

# Verifico la distribución
tabla_capitulos <- texto_df %>%
  group_by(capitulo_label) %>%
  summarise(
    `Líneas` = n(),
    .groups = "drop"
  )

# Muestro la tabla con formato profesional
kable(tabla_capitulos, 
      caption = "Tabla 1. Distribución de líneas por capítulo (división aproximada)",
      align = "lc",
      booktabs = TRUE) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center",
    font_size = 12
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#3498db") %>%
  column_spec(1, bold = TRUE, width = "8em") %>%
  column_spec(2, width = "6em")
Tabla 1. Distribución de líneas por capítulo (división aproximada)
capitulo_label Líneas
Cap_01 457
Cap_02 458
Cap_03 458
Cap_04 458
Cap_05 458
Cap_06 458
Cap_07 458
Cap_08 458
Cap_09 458
Cap_10 458
Cap_11 457
Cap_12 458
Cap_13 458
Cap_14 458
Cap_15 458
Cap_16 458
Cap_17 458
Cap_18 458
Cap_19 458
Cap_20 458

3.3 Construcción del corpus

Ahora agrupo el texto por capítulo para crear el corpus de trabajo:

# Agrupar texto por capítulo
corpus_capitulos <- texto_df %>%
  group_by(capitulo_label) %>%
  summarise(
    text = paste(text, collapse = " "),
    n_lineas = n(),
    .groups = "drop"
  ) %>%
  rename(capitulo = capitulo_label)

cat(" Corpus construido con", nrow(corpus_capitulos), "capítulos.\n")
##  Corpus construido con 20 capítulos.
cat(" Palabras aproximadas por capítulo:", 
    mean(str_count(corpus_capitulos$text, "\\S+")), "\n")
##  Palabras aproximadas por capítulo: 6906.2

4 Tokenización y normalización

4.1 Tokenización básica

Realizo la tokenización utilizando unnest_tokens() de tidytext:

# Tokenizar el texto en palabras individuales
tokens_capitulos <- corpus_capitulos %>%
  unnest_tokens(output = word, input = text) %>%
  filter(!is.na(word))

cat(" Total de tokens generados:", nrow(tokens_capitulos), "\n")
##  Total de tokens generados: 138115
cat(" Primeros 10 tokens:\n")
##  Primeros 10 tokens:
print(head(tokens_capitulos, 10))
## # A tibble: 10 × 3
##    capitulo n_lineas word   
##    <chr>       <int> <chr>  
##  1 Cap_01        457 para   
##  2 Cap_01        457 j      
##  3 Cap_01        457 omi    
##  4 Cap_01        457 garcía 
##  5 Cap_01        457 ascot  
##  6 Cap_01        457 muchos 
##  7 Cap_01        457 años   
##  8 Cap_01        457 después
##  9 Cap_01        457 frente 
## 10 Cap_01        457 al

4.2 Construcción de stopwords personalizadas

Decisión metodológica importante: Creo stopwords personalizadas ampliadas específicas para García Márquez. Esto es necesario porque:

  1. Nombres de personajes: Aureliano, Buendía, Arcadio, Úrsula, José, Amaranta, etc. tienen frecuencias extremadamente altas (>400 apariciones) y oscurecen patrones temáticos más relevantes
  2. Metadatos editoriales: El texto incluye repeticiones de “Gabriel García Márquez” y “Cien Años de Soledad” en encabezados de página que contaminan el análisis
  3. Palabras narrativas genéricas: Verbos como “dijo”, “hizo”, “era”, “fue” y adverbios como “después”, “entonces”, “tan” no aportan valor semántico para análisis de tópicos
  4. Descriptores comunes: Palabras como “casa”, “tiempo”, “día”, “noche” son tan frecuentes que no distinguen períodos narrativos específicos

Esta decisión se basa en los resultados preliminares del análisis, donde observé que las palabras más frecuentes eran principalmente nombres propios y términos genéricos.

# Stopwords base en español del paquete tm
stopwords_es <- tibble(
  word = tm::stopwords("spanish")
)

cat("\nStopwords base en español:", nrow(stopwords_es), "\n")
## 
## Stopwords base en español: 308
# Creo stopwords PERSONALIZADAS AMPLIADAS para García Márquez
stopwords_custom <- tibble(
  word = c(
    # ============ METADATOS EDITORIALES (CRÍTICO) ============
    "gabriel", "garcia", "marquez", "cien", "anos", "annos", "soledad",
    "editado", "ediciones", "editorial", "pagina", "capitulo", "cueva",
    "edicion", "jomi", "ascot", "maria", "luisa", "elio",
    
    # ============ NOMBRES DE PERSONAJES ============
    # Familia Buendía (TODOS LOS NOMBRES Y VARIANTES)
    "aureliano", "arcadio", "ursula", "buendia", "jose", "amaranta", 
    "remedios", "fernanda", "petra", "pilar", "rebeca", "santa", 
    "sofia", "renata", "meme", "mauricio", "babilonia",
    
    # Otros personajes principales
    "melquiades", "gerineldo", "crespi", "pietro", "catalan",
    "apolinar", "moscote", "nicanor", "prudencio", "aguilar",
    "carpio", "gaston", "nigromanta", "amaranto", "carmelita",
    "cotes", "brown", "catarino", "roque", "carnicero",
    "visitacion", "cataure", "aquilar", "centeno",
    
    # Nombres genéricos y títulos
    "coronel", "don", "dona", "general", "capitan", "senor", "senora",
    
    # ============ LUGARES ============
    "macondo",
    
    # ============ VERBOS NARRATIVOS MUY FRECUENTES ============
    # Ser/Estar/Haber
    "ser", "era", "fue", "sido", "sea", "estar", "estaba","habian", "estuvo", "estado",
    "haber", "habia", "hubo", "habria", "hay", "han",
    
    # Verbos de acción común
    "hacer", "hizo", "hacia", "hecho", "haria", "hace", "hacen",
    "decir", "dijo", "decia", "dice", "dicho", "diciendo",
    "ir", "iba", "fue", "ido", "van", "va",
    "tener", "tuvo", "tenia", "tenido", "tiene", "tienen",
    "poner", "puso", "ponia", "puesto", "pone",
    "ver", "vio", "veia", "visto", "ve", "ven",
    "dar", "dio", "daba", "dado", "da", "dan",
    "saber", "supo", "sabia", "sabido", "sabe", "saben",
    "poder", "pudo", "podia", "podido", "puede", "pueden",
    "querer", "quiso", "queria", "querido", "quiere", "quieren",
    "llegar", "llego", "llegaba", "llegado", "llega", "llegan",
    "volver", "volvio", "volvia", "vuelto", "vuelve", "vuelven",
    "quedar", "quedo", "quedaba", "quedado", "queda", "quedan",
    "seguir", "siguio", "seguia", "seguido", "sigue", "siguen",
    "encontrar", "encontro", "encontraba", "encontrado", "encuentra",
    "parecer", "parecio", "parecia", "parecido", "parece", "parecen",
    "creer", "creyo", "creia", "creido", "cree", "creen",
    "pensar", "penso", "pensaba", "pensado", "piensa", "piensan",
    "sentir", "sintio", "sentia", "sentido", "siente", "sienten",
    "mirar", "miro", "miraba", "mirado", "mira", "miran",
    "pasar", "paso", "pasaba", "pasado", "pasa", "pasan",
    "comenzar", "comenzo", "comenzaba", "comenzado", "comienza",
    "empezar", "empezo", "empezaba", "empezado", "empieza",
    "acabar", "acabo", "acababa", "acabado", "acaba", "acaban",
    "terminar", "termino", "terminaba", "terminado", "termina",
    "salir", "salio", "salia", "salido", "sale", "salen",
    "entrar", "entro", "entraba", "entrado", "entra", "entran",
    "venir", "vino", "venia", "venido", "viene", "vienen",
    "pedir", "pidio", "pedia", "pedido", "pide", "piden",
    "preguntar", "pregunto", "preguntaba", "preguntado", "pregunta",
    "responder", "respondio", "respondia", "respondido", "responde",
    "contestar", "contesto", "contestaba", "contestado", "contesta",
    "llamar", "llamo", "llamaba", "llamado", "llama", "llaman",
    "dejar", "dejo", "dejaba", "dejado", "deja", "dejan",
    "llevar", "llevo", "llevaba", "llevado", "lleva", "llevan",
    "traer", "trajo", "traia", "traido", "trae", "traen",
    "comprender", "comprendio", "comprendia", "comprendido", "comprende",
    "recordar", "recordo", "recordaba", "recordado", "recuerda",
    "olvidar", "olvido", "olvidaba", "olvidado", "olvida", "olvidan",
    "morir", "murio", "moria", "muerto", "muere", "mueren",
    "vivir", "vivio", "vivia", "vivido", "vive", "viven",
    "abrir", "abrio", "abria", "abierto", "abre", "abren",
    "cerrar", "cerro", "cerraba", "cerrado", "cierra", "cierran",
    "escribir", "escribio", "escribia", "escrito", "escribe",
    "leer", "leyo", "leia", "leido", "lee", "leen",
    "conocer", "conocio", "conocia", "conocido", "conoce", "conocen",
    "perder", "perdio", "perdia", "perdido", "pierde", "pierden",
    "buscar", "busco", "buscaba", "buscado", "busca", "buscan",
    "encontrar", "hallo", "hallaba", "hallado", "halla", "hallan",
    "subir", "subio", "subia", "subido", "sube", "suben",
    "bajar", "bajo", "bajaba", "bajado", "baja", "bajan",
    "caer", "cayo", "caia", "caido", "cae", "caen",
    "levantar", "levanto", "levantaba", "levantado", "levanta",
    "sentar", "sento", "sentaba", "sentado", "sienta", "sientan",
    "dormir", "durmio", "dormia", "dormido", "duerme", "duermen",
    "despertar", "desperto", "despertaba", "despertado", "despierta",
    
    # ============ ADVERBIOS Y CONECTORES ============
    "despues", "entonces", "luego", "ahora", "pronto", "tarde", "temprano",
    "alli", "alla", "aqui", "ahi", "donde", "cuando", "mientras",
    "siempre", "nunca", "jamas", "aun", "todavia", "ya",
    "vez", "veces", "dia", "dias", "noche", "noches", "manana",
    "ano", "anos", "mes", "meses", "semana", "semanas",
    "tiempo", "momento", "instante", "hora", "horas",
    "antes", "durante", "apenas", "incluso", "hasta",
    "mas", "menos", "mucho", "poco", "demasiado", "bastante",
    "muy", "tan", "tanto", "tanta", "tantos", "tantas",
    "casi", "apenas", "solo", "solamente", "unicamente",
    "tambien", "tampoco", "ademas", "incluso",
    
    # ============ DETERMINANTES Y CUANTIFICADORES ============
    "mismo", "misma", "mismos", "mismas",
    "otro", "otra", "otros", "otras",
    "todo", "toda", "todos", "todas",
    "cada", "cualquier", "cualquiera",
    "algun", "alguna", "algunos", "algunas", "algo", "alguien",
    "ningun", "ninguna", "nada", "nadie",
    "primer", "primera", "primero", "primeros", "primeras",
    "segundo", "segunda", "segundos", "segundas",
    "tercero", "tercera", "ultimo", "ultima", "ultimos", "ultimas",
    "unico", "unica", "unicos", "unicas",
    "cierto", "cierta", "ciertos", "ciertas",
    "varios", "varias", "ambos", "ambas",
    "un", "una", "uno", "unos", "unas",
    "dos", "tres", "cuatro", "cinco",
    
    # ============ PRONOMBRES ============
    "yo", "tu", "el", "ella", "nosotros", "vosotros", "ellos", "ellas",
    "me", "te", "se", "nos", "os", "les", "le", "lo", "la", "los", "las",
    "mi", "ti", "si", "conmigo", "contigo", "consigo",
    "mio", "tuyo", "suyo", "nuestro", "vuestro",
    "este", "esta", "esto", "estos", "estas",
    "ese", "esa", "eso", "esos", "esas",
    "aquel", "aquella", "aquello", "aquellos", "aquellas",
    "quien", "quienes", "cual", "cuales", "cuyo", "cuya", "cuyos", "cuyas",
    
    # ============ CONJUNCIONES Y PREPOSICIONES ============
    "y", "e", "o", "u", "ni",
    "pero", "mas", "sino", "aunque", "sin", "embargo",
    "porque", "pues", "asi", "como", "segun", "puesto",
    "si", "a", "ante", "bajo", "con", "contra", "de", "desde",
    "en", "entre", "hacia", "para", "por", "segun", "sobre", "tras",
    
    # ============ PALABRAS DESCRIPTIVAS GENÉRICAS ============
    "casa", "casas", "puerta", "puertas", "ventana", "ventanas",
    "habitacion", "cuarto", "sala", "patio", "calle", "calles",
    "lado", "lados", "parte", "partes", "lugar", "lugares",
    "cosa", "cosas", "manera", "maneras", "forma", "formas", "modo",
    "hombre", "hombres", "mujer", "mujeres",
    "nino", "ninos", "nina", "ninas",
    "hijo", "hijos", "hija", "hijas",
    "padre", "padres", "madre", "madres",
    "mano", "manos", "cara", "caras", "ojos", "ojo",
    "cabeza", "cabezas", "cuerpo", "pie", "pies",
    "brazo", "brazos", "pierna", "piernas",
    "palabra", "palabras", "voz", "voces",
    "mundo", "tierra", "aire", "agua", "fuego",
    "luz", "sombra", "sombras", "color", "colores",
    "vida", "muerte", "vez", "veces",
    "bien", "mal", "mejor", "peor",
    "grande", "gran", "pequeno", "pequena",
    "nuevo", "nueva", "viejo", "vieja",
    "bueno", "buena", "malo", "mala",
    "largo", "larga", "corto", "corta",
    "alto", "alta", "bajo", "baja",
    
    # ============ OTRAS PALABRAS COMUNES ============
    "siendo", "dado", "pudo", "decian", "pregunto",
    "sino", "menos", "verdad", "vez", "fin",
    "nombre", "nombres", "ano", "anos",
    "noche", "dia", "tarde", "manana",
    "gente", "personas", "familia", "familias"
  )
)

cat("\nStopwords personalizadas:", nrow(stopwords_custom), "\n")
## 
## Stopwords personalizadas: 739
# Combino ambas listas
stopwords_completas <- bind_rows(stopwords_es, stopwords_custom)

# Elimino duplicados
stopwords_completas <- stopwords_completas %>% distinct(word)

cat("\nTotal de stopwords a remover (sin duplicados):", nrow(stopwords_completas), "\n")
## 
## Total de stopwords a remover (sin duplicados): 923

4.3 Normalización del texto

Aplico los siguientes pasos de normalización:

# 1. Remover tokens que contienen números
tokens_limpios <- tokens_capitulos %>%
  filter(!grepl(pattern = "[0-9]", x = word))

cat("\nDespués de remover números:", nrow(tokens_limpios), "tokens\n")
## 
## Después de remover números: 137914 tokens
# 2. CRÍTICO: Normalizar acentos PRIMERO (antes del anti_join)
# Esto asegura que "Úrsula" se convierta en "ursula" y coincida con stopwords
tokens_limpios <- tokens_limpios %>%
  mutate(word = stri_trans_general(word, "Latin-ASCII"))

cat("\nDespués de normalizar acentos:", nrow(tokens_limpios), "tokens\n")
## 
## Después de normalizar acentos: 137914 tokens
# 3. Ahora sí: remover stopwords (con acentos ya normalizados)
tokens_limpios <- tokens_limpios %>%
  anti_join(stopwords_completas, by = "word")

cat("\nDespués de remover stopwords:", nrow(tokens_limpios), "tokens\n")
## 
## Después de remover stopwords: 45462 tokens
cat(" Tokens finales limpios:", nrow(tokens_limpios), "\n")
##  Tokens finales limpios: 45462
cat(" Tokens únicos:", n_distinct(tokens_limpios$word), "\n")
##  Tokens únicos: 14790

4.4 Palabras más frecuentes

Identifico las palabras más frecuentes después de la normalización:

# Calculo las top 15 palabras más frecuentes
top_palabras <- tokens_limpios %>%
  count(word, sort = TRUE) %>%
  head(15) %>%
  mutate(
    Palabra = word,
    Frecuencia = n
  ) %>%
  select(Palabra, Frecuencia)

# Muestro la tabla con formato profesional
kable(top_palabras,
      caption = "Tabla 2. Palabras más frecuentes después de normalización",
      align = "lc",
      booktabs = TRUE) %>%
  kable_styling(
    bootstrap_options = c("striped", "hover", "condensed", "responsive"),
    full_width = FALSE,
    position = "center",
    font_size = 12
  ) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#2ecc71") %>%
  column_spec(1, bold = TRUE, width = "10em") %>%
  column_spec(2, width = "8em")
Tabla 2. Palabras más frecuentes después de normalización
Palabra Frecuencia
guerra 152
pueblo 115
dormitorio 96
cama 94
amor 87
cuenta 83
realidad 83
corazon 79
frente 77
oro 71
bella 67
piedad 67
dios 66
regreso 63
lluvia 60

5 Análisis multidimensional de sentimientos

5.1 Justificación del enfoque multidimensional

Reflexión crítica: En literatura de realismo mágico, un análisis binario de sentimientos (positivo/negativo) es metodológicamente inadecuado. Palabras como “muerte”, “soledad” o “sangre” no son simplemente “negativas” en García Márquez: operan como símbolos de ciclos históricos, destino familiar y transformación mítica.

Por ello, construyo un lexicón literario multidimensional con 7 categorías temáticas que capturan los campos semánticos específicos del universo narrativo de Macondo.

5.2 Construcción del lexicón literario

Creo manualmente un lexicón con siete dimensiones literarias:

# 1. REALISMO MÁGICO: Lo sobrenatural, mítico y simbólico
realismo_magico <- tibble(
  word = c(
    "fantasma", "espectro", "aparicion", "milagro", "magia",
    "hechizo", "premonicion", "profecia", "alquimia", "pergamino",
    "gitano", "melquiades", "milagroso", "sobrenatural",
    "levitacion", "vision", "augurio", "destino", "misterio",
    "mariposas", "lluvia", "diluvio", "sombra", "eterno",
    "infinito", "laberinto", "tiempo", "memoria", "olvido"
  ),
  dimension = "Realismo Mágico",
  valor = 1
)

# 2. FATALISMO: Decadencia, destino trágico, soledad existencial
fatalismo <- tibble(
  word = c(
    "soledad", "muerte", "olvido", "ruina", "abandono",
    "destino", "condena", "tristeza", "desesperacion",
    "silencio", "derrota", "fracaso", "amargura",
    "destruccion", "ceniza", "melancolia", "oscuridad",
    "tormenta", "sepultura", "vacio", "angustia", "pesadumbre"
  ),
  dimension = "Fatalismo",
  valor = -1
)

# 3. VIOLENCIA POLÍTICA: Guerras civiles, conflicto armado
violencia_politica <- tibble(
  word = c(
    "guerra", "coronel", "fusilamiento", "ejercito",
    "liberales", "conservadores", "batalla", "revolucion",
    "matanza", "violencia", "soldados", "gobierno",
    "capitan", "dictadura", "disparo", "sangre",
    "cadaver", "masacre", "militares", "armas", "combate"
  ),
  dimension = "Violencia Política",
  valor = -1
)

# 4. SENSORIAL TROPICAL: Ambiente físico caribeño con carga simbólica
sensorial_tropical <- tibble(
  word = c(
    "calor", "lluvia", "banana", "tierra", "viento",
    "rio", "pantano", "selva", "humedad", "polvo",
    "flores", "mar", "sol", "tormenta", "pescado",
    "animal", "olor", "sudor", "fruta", "vegetacion",
    "caribe", "tropical", "cielo", "nube", "horizonte"
  ),
  dimension = "Sensorial Tropical",
  valor = 0
)

# 5. LINAJE FAMILIAR: Repetición generacional, memoria colectiva
linaje <- tibble(
  word = c(
    "familia", "apellido", "hijo", "madre", "padre",
    "abuela", "gemelos", "herencia", "sangre", "descendencia",
    "repeticion", "memoria", "nombre", "generacion",
    "estirpe", "ancestro", "clan", "parentesco"
  ),
  dimension = "Linaje",
  valor = 1
)

# 6. MÍSTICO-RELIGIOSO: Espiritualidad, fe, simbolismo sagrado
mistico_religioso <- tibble(
  word = c(
    "dios", "iglesia", "pecado", "alma", "cielo",
    "milagro", "bendicion", "maldicion", "rezar",
    "santo", "virgen", "espiritu", "eterno",
    "infierno", "profecia", "sacerdote", "cruz",
    "fe", "divino", "sagrado"
  ),
  dimension = "Místico-Religioso",
  valor = 1
)

# 7. AMOR OBSESIVO: Pasión, deseo, abandono
amor_obsesivo <- tibble(
  word = c(
    "amor", "deseo", "pasion", "obsesion", "celos",
    "amante", "cuerpo", "piel", "beso", "sexo",
    "espera", "nostalgia", "locura", "anhelo",
    "corazon", "ternura", "abrazo"
  ),
  dimension = "Amor y Deseo",
  valor = 1
)

# Combino todos los lexicones
lexicon_literario <- bind_rows(
  realismo_magico,
  fatalismo,
  violencia_politica,
  sensorial_tropical,
  linaje,
  mistico_religioso,
  amor_obsesivo
)

# Resumen del lexicón que construí
resumen_lexicon <- lexicon_literario %>%
  count(dimension, name = "Palabras") %>%
  arrange(desc(Palabras))

kable(resumen_lexicon,
      caption = "Tabla 3. Lexicón literario multidimensional que construí",
      align = "lc") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center")
Tabla 3. Lexicón literario multidimensional que construí
dimension Palabras
Realismo Mágico 29
Sensorial Tropical 25
Fatalismo 22
Violencia Política 21
Místico-Religioso 20
Linaje 18
Amor y Deseo 17
cat("\n Total de palabras en el lexicón:", nrow(lexicon_literario), "\n")
## 
##  Total de palabras en el lexicón: 152

5.3 Análisis dimensional por capítulo

Calculo la frecuencia de cada dimensión en cada capítulo:

# Cruzo los tokens con el lexicón literario
dimensiones_caps <- tokens_limpios %>%
  inner_join(lexicon_literario, by = "word") %>%
  count(capitulo, dimension) %>%
  group_by(capitulo) %>%
  mutate(proporcion = n / sum(n)) %>%
  ungroup()

# Creo una matriz de dimensiones por capítulo
matriz_dimensiones <- dimensiones_caps %>%
  select(capitulo, dimension, n) %>%
  pivot_wider(names_from = dimension, values_from = n, values_fill = 0)

# Muestro los primeros 5 capítulos
tabla_top5 <- matriz_dimensiones %>%
  head(5)

kable(tabla_top5,
      caption = "Tabla 4. Frecuencia de dimensiones literarias (primeros 5 capítulos)",
      align = "lcccccccc") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = TRUE,
                font_size = 11) %>%
  scroll_box(width = "100%")
Tabla 4. Frecuencia de dimensiones literarias (primeros 5 capítulos)
capitulo Amor y Deseo Fatalismo Linaje Místico-Religioso Realismo Mágico Sensorial Tropical Violencia Política
Cap_01 7 12 8 9 22 34 11
Cap_02 23 10 3 6 12 21 4
Cap_03 9 9 10 5 14 9 9
Cap_04 31 6 2 10 10 23 9
Cap_05 4 5 4 16 5 16 50

5.4 Visualización: Evolución dimensional

Visualizo cómo evoluciona cada dimensión a través de los capítulos:

# Gráfico de evolución de dimensiones
dimensiones_caps %>%
  mutate(indice_cap = as.numeric(str_extract(capitulo, "[0-9]+"))) %>%
  ggplot(aes(x = indice_cap, y = n, color = dimension, group = dimension)) +
  geom_line(size = 1.2, alpha = 0.8) +
  geom_point(size = 2.5) +
  scale_color_brewer(palette = "Set2") +
  labs(
    title = "Evolución de dimensiones literarias a través de Cien Años de Soledad",
    subtitle = "Frecuencia de palabras asociadas a cada campo semántico por capítulo",
    x = "Capítulo",
    y = "Frecuencia de palabras",
    color = "Dimensión literaria"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    legend.position = "bottom",
    legend.text = element_text(size = 10)
  )

5.5 Visualización: Composición dimensional

Muestro la composición proporcional de dimensiones por capítulo:

# Stacked area chart de proporciones
dimensiones_caps %>%
  mutate(indice_cap = as.numeric(str_extract(capitulo, "[0-9]+"))) %>%
  ggplot(aes(x = indice_cap, y = proporcion, fill = dimension)) +
  geom_area(alpha = 0.8, position = "fill") +
  scale_fill_brewer(palette = "Set2") +
  scale_y_continuous(labels = percent) +
  labs(
    title = "Composición dimensional de Cien Años de Soledad",
    subtitle = "Proporción relativa de cada dimensión literaria por capítulo",
    x = "Capítulo",
    y = "Proporción",
    fill = "Dimensión"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    legend.position = "bottom"
  )

5.6 Heatmap dimensional

Creo un mapa de calor para visualizar la intensidad de cada dimensión:

# Heatmap de intensidad dimensional
dimensiones_caps %>%
  mutate(indice_cap = as.numeric(str_extract(capitulo, "[0-9]+"))) %>%
  ggplot(aes(x = indice_cap, y = dimension, fill = n)) +
  geom_tile(color = "white") +
  scale_fill_gradient2(
    low = "white",
    mid = "lightblue",
    high = "darkblue",
    midpoint = 5
  ) +
  labs(
    title = "Mapa de calor: Intensidad dimensional por capítulo",
    subtitle = "Distribución de campos semánticos a lo largo de la novela",
    x = "Capítulo",
    y = "Dimensión literaria",
    fill = "Frecuencia"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    axis.text.y = element_text(size = 11)
  )

5.7 Dimensión dominante por capítulo

Identifico qué dimensión predomina en cada capítulo:

# Calculo la dimensión dominante por capítulo
dominancia_dimensional <- dimensiones_caps %>%
  group_by(capitulo) %>%
  slice_max(n, n = 1) %>%
  ungroup() %>%
  mutate(indice_cap = as.numeric(str_extract(capitulo, "[0-9]+"))) %>%
  arrange(indice_cap) %>%
  select(`Capítulo` = capitulo, `Dimensión dominante` = dimension, `Frecuencia` = n)

kable(dominancia_dimensional,
      caption = "Tabla 5. Dimensión literaria dominante por capítulo",
      align = "lcr") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  scroll_box(height = "400px")
Tabla 5. Dimensión literaria dominante por capítulo
Capítulo Dimensión dominante Frecuencia
Cap_01 Sensorial Tropical 34
Cap_02 Amor y Deseo 23
Cap_03 Realismo Mágico 14
Cap_04 Amor y Deseo 31
Cap_05 Violencia Política 50
Cap_06 Violencia Política 38
Cap_07 Violencia Política 60
Cap_08 Violencia Política 66
Cap_09 Violencia Política 49
Cap_10 Violencia Política 23
Cap_11 Sensorial Tropical 21
Cap_12 Violencia Política 27
Cap_13 Fatalismo 20
Cap_14 Amor y Deseo 28
Cap_15 Violencia Política 39
Cap_16 Sensorial Tropical 52
Cap_17 Sensorial Tropical 27
Cap_18 Fatalismo 14
Cap_19 Amor y Deseo 36
Cap_20 Amor y Deseo 26

6 Análisis de redes: Skipgramas

6.1 Tokenización en skipgramas

Ahora tokenizo el texto en skipgramas (pares de palabras no necesariamente consecutivas):

# Tokenizar en skipgramas usando unnest_tokens
skipgramas_caps <- corpus_capitulos %>%
  unnest_tokens(input = text, output = skipgram, token = "skip_ngrams", n = 2) %>%
  filter(!is.na(skipgram))

# Contar palabras en cada skipgrama
skipgramas_caps$num_words <- skipgramas_caps$skipgram %>%
  map_int(.f = ~ wordcount(.x))

# Mantener solo bigramas (2 palabras)
skipgramas_caps <- skipgramas_caps %>%
  filter(num_words == 2) %>%
  select(-num_words)

cat(" Skipgramas totales generados:", nrow(skipgramas_caps), "\n")
##  Skipgramas totales generados: 276170

6.2 Limpieza de skipgramas

Aplico el mismo proceso de limpieza que a los tokens individuales:

# Separo los skipgramas en dos palabras
skipgramas_sep <- skipgramas_caps %>%
  separate(skipgram, c("word1", "word2"), sep = " ")

# Limpio y normalizo (ORDEN CRÍTICO)
skipgramas_limpios <- skipgramas_sep %>%
  # 1. Remover números
  filter(!grepl(pattern = "[0-9]", x = word1),
         !grepl(pattern = "[0-9]", x = word2)) %>%
  # 2. CRÍTICO: Normalizar acentos PRIMERO (antes del filtro de stopwords)
  mutate(
    word1 = stri_trans_general(word1, "Latin-ASCII"),
    word2 = stri_trans_general(word2, "Latin-ASCII")
  ) %>%
  # 3. Ahora sí: remover stopwords (con acentos ya normalizados)
  filter(!word1 %in% stopwords_completas$word,
         !word2 %in% stopwords_completas$word) %>%
  # 4. Remover NA
  filter(!is.na(word1), !is.na(word2))

cat(" Skipgramas limpios:", nrow(skipgramas_limpios), "\n")
##  Skipgramas limpios: 24566

6.3 Frecuencias con umbral alto

Decisión metodológica: Aplico un umbral de frecuencia 3 para reducir ruido en las redes. Esto produce redes más interpretables y densas, evitando la fragmentación excesiva que observaría con umbrales más bajos.

# Cuento frecuencias y aplico UMBRAL = 3
skipgramas_count <- skipgramas_limpios %>%
  count(capitulo, word1, word2, sort = TRUE) %>%
  rename(weight = n) %>%
  filter(weight >= 3)  # UMBRAL ALTO para reducir ruido

cat(" Skipgramas frecuentes (≥3):", nrow(skipgramas_count), "\n")
##  Skipgramas frecuentes (≥3): 65
# Top 15 skipgramas globales
top_skipgramas <- skipgramas_count %>%
  group_by(word1, word2) %>%
  summarise(total = sum(weight), .groups = "drop") %>%
  arrange(desc(total)) %>%
  head(15) %>%
  mutate(
    Bigrama = paste(word1, "-", word2),
    Frecuencia = total
  ) %>%
  select(Bigrama, Frecuencia)

kable(top_skipgramas,
      caption = "Tabla 6. Skipgramas más frecuentes en la novela",
      align = "lr") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center")
Tabla 6. Skipgramas más frecuentes en la novela
Bigrama Frecuencia
compania - bananera 29
medicos - invisibles 7
peloton - fusilamiento 7
cuento - gallo 6
darse - cuenta 6
gallo - capon 6
hermano - gemelo 6
aquiles - ricardo 5
consejo - guerra 5
contara - cuento 5
doctor - noguera 5
mariposas - amarillas 5
mister - herbert 5
palmas - funebres 5
veinte - centavos 5

6.4 Construcción de redes por capítulo

Creo una función para construir redes y las aplico a cada capítulo:

# Función para crear red de co-ocurrencia
crear_red_capitulo <- function(datos, cap, umbral = 3) {
  datos_cap <- datos %>%
    filter(capitulo == cap, weight >= umbral)
  
  if (nrow(datos_cap) == 0) return(NULL)
  
  grafo <- graph_from_data_frame(
    d = datos_cap %>% select(word1, word2, weight),
    directed = FALSE
  )
  
  # Simplifico el grafo (remover loops y aristas múltiples)
  grafo <- igraph::simplify(grafo)
  return(grafo)
}

# Creo redes para todos los capítulos
caps_todos <- unique(skipgramas_count$capitulo)
redes_caps <- map(caps_todos, ~crear_red_capitulo(skipgramas_count, .x, umbral = 3))
names(redes_caps) <- caps_todos

# Verifico cuántas redes fueron creadas exitosamente
redes_validas <- sum(!sapply(redes_caps, is.null))
cat(" Redes válidas creadas:", redes_validas, "de", length(redes_caps), "\n")
##  Redes válidas creadas: 20 de 20

6.5 Métricas de red por capítulo

Calculo métricas estructurales para cada red:

# Función para calcular métricas de red
calcular_metricas <- function(grafo) {
  if (is.null(grafo) || vcount(grafo) < 3) {
    return(tibble(
      orden = NA, tamano = NA, densidad = NA,
      grado_medio = NA, transitividad = NA, componentes = NA
    ))
  }
  
  tibble(
    orden = vcount(grafo),
    tamano = ecount(grafo),
    densidad = edge_density(grafo),
    grado_medio = mean(degree(grafo)),
    transitividad = transitivity(grafo, type = "global"),
    componentes = components(grafo)$no
  )
}

# Aplico la función a todas las redes
metricas_redes <- map_dfr(redes_caps, calcular_metricas, .id = "capitulo") %>%
  mutate(indice_cap = as.numeric(str_extract(capitulo, "[0-9]+"))) %>%
  filter(!is.na(orden))

# Tabla de métricas (primeros 10 capítulos)
tabla_metricas <- metricas_redes %>%
  head(10) %>%
  mutate(
    Capítulo = capitulo,
    Nodos = orden,
    Aristas = tamano,
    Densidad = round(densidad, 4),
    `Grado medio` = round(grado_medio, 2),
    Transitividad = round(transitividad, 4),
    Componentes = componentes
  ) %>%
  select(Capítulo, Nodos, Aristas, Densidad, `Grado medio`, Transitividad, Componentes)

kable(tabla_metricas,
      caption = "Tabla 7. Métricas de redes de co-ocurrencia por capítulo",
      align = "lcccccc") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = TRUE,
                font_size = 11)
Tabla 7. Métricas de redes de co-ocurrencia por capítulo
Capítulo Nodos Aristas Densidad Grado medio Transitividad Componentes
Cap_15 8 4 0.1429 1.00 NaN 4
Cap_03 8 5 0.1786 1.25 0 3
Cap_05 8 4 0.1429 1.00 NaN 4
Cap_08 10 5 0.1111 1.00 NaN 5
Cap_10 6 3 0.2000 1.00 NaN 3
Cap_11 6 3 0.2000 1.00 NaN 3
Cap_14 10 5 0.1111 1.00 NaN 5
Cap_17 6 3 0.2000 1.00 NaN 3
Cap_09 8 4 0.1429 1.00 NaN 4
Cap_13 8 4 0.1429 1.00 NaN 4

6.6 Visualización de redes seleccionadas

Visualizo las redes de los 4 capítulos con mayor densidad:

# Selecciono los 4 capítulos con mayor densidad
top_dense_caps <- metricas_redes %>%
  arrange(desc(densidad)) %>%
  head(4) %>%
  pull(capitulo)

# Configuro el área de gráficos
par(mfrow = c(2, 2), mar = c(2, 2, 3, 2))

# Grafico cada red
for (cap in top_dense_caps) {
  grafo <- redes_caps[[cap]]
  if (!is.null(grafo) && vcount(grafo) > 0) {
    # Extraigo componente conexa más grande
    V(grafo)$cluster <- components(grafo)$membership
    gcc <- induced_subgraph(
      graph = grafo,
      vids = which(V(grafo)$cluster == which.max(components(grafo)$csize))
    )
    
    if (vcount(gcc) >= 3) {
      V(gcc)$strength <- strength(gcc)
      
      set.seed(1702)
      plot(
        gcc,
        layout = layout_with_fr,
        vertex.color = adjustcolor("steelblue", 0.3),
        vertex.frame.color = "steelblue",
        vertex.size = 3 * sqrt(V(gcc)$strength),
        vertex.label.color = "black",
        vertex.label.cex = 0.7,
        vertex.label.dist = 0.5,
        edge.width = 2 * E(gcc)$weight / max(E(gcc)$weight),
        edge.color = adjustcolor("gray60", 0.5),
        main = cap
      )
    }
  }
}

6.7 Detección de comunidades

Aplico el algoritmo Fast Greedy para detectar comunidades:

# Función para detectar comunidades
detectar_comunidades <- function(grafo) {
  if (is.null(grafo) || vcount(grafo) < 3) {
    return(list(n_comunidades = NA, modularidad = NA))
  }
  
  comunidades <- cluster_fast_greedy(grafo)
  list(
    n_comunidades = length(comunidades),
    modularidad = modularity(comunidades)
  )
}

# Aplico a todas las redes
comunidades_caps <- map_dfr(redes_caps, detectar_comunidades, .id = "capitulo") %>%
  mutate(indice_cap = as.numeric(str_extract(capitulo, "[0-9]+"))) %>%
  filter(!is.na(n_comunidades))

# Tabla de comunidades (primeros 10)
tabla_comunidades <- comunidades_caps %>%
  head(10) %>%
  mutate(
    Capítulo = capitulo,
    `N° Comunidades` = n_comunidades,
    Modularidad = round(modularidad, 4)
  ) %>%
  select(Capítulo, `N° Comunidades`, Modularidad)

kable(tabla_comunidades,
      caption = "Tabla 8. Comunidades detectadas por capítulo (Fast Greedy)",
      align = "lcr") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center")
Tabla 8. Comunidades detectadas por capítulo (Fast Greedy)
Capítulo N° Comunidades Modularidad
Cap_15 4 0.6667
Cap_03 3 0.4832
Cap_05 4 0.7378
Cap_08 5 0.7900
Cap_10 3 0.6446
Cap_11 3 0.6528
Cap_14 5 0.7889
Cap_17 3 0.6528
Cap_09 4 0.7456
Cap_13 4 0.7456

7 Análisis de tópicos con LDA

7.1 Matriz documento-término

Construyo la matriz documento-término (DTM) necesaria para LDA:

# Creo DTM usando cast_dtm de tidytext
palabras_caps <- tokens_limpios %>%
  count(capitulo, word, sort = TRUE)

dtm_caps <- palabras_caps %>%
  cast_dtm(document = capitulo, term = word, value = n)

cat(" DTM construida:\n")
##  DTM construida:
cat("   Documentos:", dtm_caps$nrow, "\n")
##    Documentos: 20
cat("   Términos:", dtm_caps$ncol, "\n")
##    Términos: 14790
cat("   Sparsity:", round(100 * (1 - length(dtm_caps$i) / (dtm_caps$nrow * dtm_caps$ncol)), 1), "%\n")
##    Sparsity: 88.3 %

7.2 Ajuste del modelo LDA

Justificación metodológica: Selecciono k = 6 tópicos como compromiso entre granularidad interpretativa y coherencia temática para una novela de 20 capítulos con estructura narrativa compleja.

# Número de tópicos
k_topicos <- 6

# Ajusto el modelo LDA con método Gibbs
set.seed(1702)
modelo_lda <- LDA(
  dtm_caps,
  k = k_topicos,
  method = "Gibbs",
  control = list(
    seed = 1702,
    burnin = 1000,
    iter = 2000,
    thin = 100
  )
)

cat(" Modelo LDA ajustado con k =", k_topicos, "tópicos\n")
##  Modelo LDA ajustado con k = 6 tópicos
# Calculo perplejidad
perplejidad <- perplexity(modelo_lda, dtm_caps)
cat(" Perplejidad del modelo:", round(perplejidad, 2), "\n")
##  Perplejidad del modelo: 6573.23
cat("   (Menor perplejidad = mejor ajuste)\n")
##    (Menor perplejidad = mejor ajuste)

7.3 Tópicos identificados (Beta)

Extraigo la matriz β (probabilidad palabra-tópico):

# Extraigo beta usando tidy()
topicos_beta <- tidy(modelo_lda, matrix = "beta")

# Top 10 palabras por tópico
top_terminos <- topicos_beta %>%
  group_by(topic) %>%
  slice_max(beta, n = 10) %>%
  ungroup() %>%
  arrange(topic, -beta)

# Creo tabla formateada
tabla_topicos <- top_terminos %>%
  mutate(
    Tópico = paste0("T", topic),
    Palabra = term,
    Probabilidad = round(beta, 4)
  ) %>%
  select(Tópico, Palabra, Probabilidad) %>%
  group_by(Tópico) %>%
  mutate(Ranking = row_number()) %>%
  ungroup() %>%
  pivot_wider(
    names_from = Tópico,
    values_from = c(Palabra, Probabilidad)
  ) %>%
  select(Ranking, starts_with("Palabra"), starts_with("Probabilidad"))

kable(tabla_topicos %>% head(10),
      caption = "Tabla 9. Top 10 palabras más probables por tópico (β)",
      align = "c") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = TRUE,
                font_size = 10) %>%
  scroll_box(width = "100%")
Tabla 9. Top 10 palabras más probables por tópico (β)
Ranking Palabra_T1 Palabra_T2 Palabra_T3 Palabra_T4 Palabra_T5 Palabra_T6 Probabilidad_T1 Probabilidad_T2 Probabilidad_T3 Probabilidad_T4 Probabilidad_T5 Probabilidad_T6
1 guerra cama piedad marido bella pueblo 0.0179 0.0065 0.0081 0.0062 0.0079 0.0086
2 gobierno dormitorio compania gitanos oro ocasion 0.0042 0.0062 0.0066 0.0053 0.0054 0.0045
3 cafe realidad amor aldea triste par 0.0039 0.0058 0.0056 0.0045 0.0052 0.0043
4 orden cuenta tren lluvia amor can 0.0035 0.0056 0.0053 0.0034 0.0051 0.0041
5 armas frente bananera animales recuerdos paredes 0.0033 0.0052 0.0048 0.0032 0.0035 0.0040
6 liberales dios pergaminos oro taller taller 0.0032 0.0043 0.0046 0.0030 0.0034 0.0036
7 militar corazon hicieron hielo trataba cienaga 0.0029 0.0040 0.0041 0.0030 0.0027 0.0035
8 noticias punto muertos laboratorio reina memoria 0.0028 0.0040 0.0037 0.0029 0.0026 0.0035
9 amanecer seis trabajadores mar corazon vestido 0.0028 0.0040 0.0029 0.0029 0.0025 0.0030
10 moneada ternera iban volvieron forasteros boda 0.0027 0.0038 0.0029 0.0029 0.0024 0.0029

7.4 Visualización de tópicos

Visualizo las palabras características de cada tópico:

# Gráfico de barras por tópico
top_terminos %>%
  mutate(term = reorder_within(term, beta, topic)) %>%
  ggplot(aes(x = term, y = beta, fill = factor(topic))) +
  geom_col(show.legend = FALSE) +
  facet_wrap(~topic, scales = "free", ncol = 3, 
             labeller = labeller(topic = function(x) paste("Tópico", x))) +
  coord_flip() +
  scale_x_reordered() +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Palabras características de cada tópico (LDA)",
    subtitle = "Probabilidad β: relevancia de cada palabra en el tópico",
    x = NULL,
    y = "Probabilidad β"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    strip.text = element_text(face = "bold", size = 11)
  )

7.5 Distribución de tópicos por capítulo (Gamma)

Extraigo la matriz γ (probabilidad tópico-documento):

# Extraigo gamma
topicos_gamma <- tidy(modelo_lda, matrix = "gamma") %>%
  mutate(indice_cap = as.numeric(str_extract(document, "[0-9]+")))

# Tabla gamma (primeros 5 capítulos)
tabla_gamma <- topicos_gamma %>%
  arrange(indice_cap, topic) %>%
  head(30) %>%
  mutate(
    Capítulo = document,
    Tópico = paste0("T", topic),
    `Proporción γ` = round(gamma, 3)
  ) %>%
  select(Capítulo, Tópico, `Proporción γ`) %>%
  pivot_wider(
    names_from = Tópico,
    values_from = `Proporción γ`
  )

kable(tabla_gamma,
      caption = "Tabla 10. Distribución de tópicos (γ) - Primeros 5 capítulos",
      align = "lcccccc") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = TRUE,
                font_size = 11)
Tabla 10. Distribución de tópicos (γ) - Primeros 5 capítulos
Capítulo T1 T2 T3 T4 T5 T6
Cap_01 0.097 0.208 0.041 0.507 0.067 0.080
Cap_02 0.037 0.356 0.047 0.356 0.059 0.145
Cap_03 0.060 0.204 0.043 0.114 0.067 0.512
Cap_04 0.047 0.402 0.035 0.064 0.078 0.374
Cap_05 0.376 0.229 0.025 0.076 0.056 0.239

7.6 Tópico dominante por capítulo

Identifico el tópico con mayor probabilidad en cada capítulo:

# Clasificación por tópico dominante
clasificacion_caps <- topicos_gamma %>%
  group_by(document) %>%
  slice_max(gamma, n = 1) %>%
  ungroup() %>%
  arrange(indice_cap)

# Tabla de clasificación
tabla_clasificacion <- clasificacion_caps %>%
  mutate(
    Capítulo = document,
    `Tópico dominante` = paste0("T", topic),
    `Fuerza (γ)` = round(gamma, 3)
  ) %>%
  select(Capítulo, `Tópico dominante`, `Fuerza (γ)`)

kable(tabla_clasificacion,
      caption = "Tabla 11. Tópico dominante por capítulo y fuerza de asociación",
      align = "lcr") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE,
                position = "center") %>%
  scroll_box(height = "400px")
Tabla 11. Tópico dominante por capítulo y fuerza de asociación
Capítulo Tópico dominante Fuerza (γ)
Cap_01 T4 0.507
Cap_02 T2 0.356
Cap_02 T4 0.356
Cap_03 T6 0.512
Cap_04 T2 0.402
Cap_05 T1 0.376
Cap_06 T1 0.354
Cap_07 T1 0.435
Cap_08 T1 0.446
Cap_09 T1 0.290
Cap_10 T5 0.366
Cap_11 T5 0.334
Cap_12 T5 0.362
Cap_13 T2 0.338
Cap_14 T2 0.371
Cap_15 T3 0.402
Cap_16 T2 0.229
Cap_17 T3 0.283
Cap_18 T2 0.321
Cap_19 T3 0.382
Cap_20 T3 0.355

7.7 Visualización de composición de tópicos

Visualizo cómo se distribuyen los tópicos a través de los capítulos:

# Stacked bar chart de gamma
ggplot(topicos_gamma, aes(x = indice_cap, y = gamma, fill = factor(topic))) +
  geom_col(position = "fill") +
  scale_fill_brewer(palette = "Set2", labels = paste0("T", 1:k_topicos)) +
  scale_y_continuous(labels = percent) +
  labs(
    title = "Composición de tópicos a lo largo de Cien Años de Soledad",
    subtitle = "Cada capítulo como mezcla de tópicos latentes",
    x = "Capítulo",
    y = "Proporción γ",
    fill = "Tópico"
  ) +
  theme_minimal() +
  theme(
    plot.title = element_text(face = "bold", size = 14),
    legend.position = "bottom"
  )

8 Análisis integrado

8.1 Tabla de resumen integrado

Combino los resultados de los tres análisis:

# Integro: tópicos + dimensiones + redes
analisis_completo <- clasificacion_caps %>%
  select(capitulo = document, indice_cap, topic_dominante = topic, gamma_max = gamma) %>%
  left_join(
    metricas_redes %>% select(capitulo, densidad, transitividad),
    by = "capitulo"
  ) %>%
  left_join(
    dimensiones_caps %>%
      group_by(capitulo) %>%
      slice_max(n, n = 1) %>%
      ungroup() %>%
      select(capitulo, dimension_dominante = dimension),
    by = "capitulo"
  ) %>%
  arrange(indice_cap)

# Tabla integrada (primeros 10)
tabla_final <- analisis_completo %>%
  head(10) %>%
  mutate(
    Capítulo = capitulo,
    `Tópico` = paste0("T", topic_dominante),
    `γ` = round(gamma_max, 3),
    `Dimensión` = dimension_dominante,
    `Densidad` = round(densidad, 4),
    `Transitividad` = round(transitividad, 4)
  ) %>%
  select(Capítulo, Tópico, γ, Dimensión, Densidad, Transitividad)

kable(tabla_final,
      caption = "Tabla 12. Resumen integrado: Tópicos, dimensiones y redes",
      align = "lcclcc") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = TRUE,
                font_size = 11)
Tabla 12. Resumen integrado: Tópicos, dimensiones y redes
Capítulo Tópico γ Dimensión Densidad Transitividad
Cap_01 T4 0.507 Sensorial Tropical NA NA
Cap_02 T2 0.356 Amor y Deseo 0.2000 NaN
Cap_02 T4 0.356 Amor y Deseo 0.2000 NaN
Cap_03 T6 0.512 Realismo Mágico 0.1786 0
Cap_04 T2 0.402 Amor y Deseo NA NA
Cap_05 T1 0.376 Violencia Política 0.1429 NaN
Cap_06 T1 0.354 Violencia Política 0.1111 NaN
Cap_07 T1 0.435 Violencia Política 0.3333 NaN
Cap_08 T1 0.446 Violencia Política 0.1111 NaN
Cap_09 T1 0.290 Violencia Política 0.1429 NaN

8.2 Matriz de correlaciones

Analizo las relaciones entre las diferentes métricas:

# Selecciono variables numéricas
vars_numericas <- analisis_completo %>%
  select(gamma_max, densidad, transitividad) %>%
  na.omit()

if (nrow(vars_numericas) > 0) {
  # Calculo correlaciones
  matriz_cor <- cor(vars_numericas, use = "complete.obs")
  matriz_cor_long <- melt(matriz_cor)
  
  # Visualizo
  ggplot(matriz_cor_long, aes(x = Var1, y = Var2, fill = value)) +
    geom_tile(color = "white") +
    geom_text(aes(label = round(value, 2)), size = 4) +
    scale_fill_gradient2(
      low = "blue", mid = "white", high = "red",
      midpoint = 0, limits = c(-1, 1)
    ) +
    labs(
      title = "Matriz de correlaciones entre métricas",
      subtitle = "Relaciones entre tópicos, redes y dimensiones literarias",
      x = NULL, y = NULL,
      fill = "Correlación"
    ) +
    theme_minimal() +
    theme(
      plot.title = element_text(face = "bold", size = 14),
      axis.text.x = element_text(angle = 45, hjust = 1)
    )
}

9 Conclusiones

9.1 Hallazgos principales

En mi análisis identifico los siguientes patrones:

  1. Dimensiones literarias: Observo una alternancia entre momentos de realismo mágico, fatalismo y violencia política, que parecen reflejar los ciclos históricos de Macondo descritos por García Márquez.

  2. Estructura de redes: Las redes de co-ocurrencia revelan núcleos semánticos densos que corresponden a períodos narrativos específicos.

  3. Tópicos latentes: El modelo LDA identifica 6 temas principales que capturan líneas narrativas centrales de la obra.

9.2 Reflexión metodológica

Este trabajo demuestra que:

  • El análisis cuantitativo de literatura requiere adaptación metodológica al género y estilo del autor
  • Los lexicones literarios multidimensionales son superiores a los binarios para realismo mágico
  • La combinación de técnicas proporciona una visión más completa

9.3 Limitaciones que reconozco

Como señalé en la introducción, mi análisis tiene limitaciones importantes:

  1. División artificial de capítulos (sin marcadores estructurales reales)
  2. Pérdida de contexto sintáctico (negaciones, ironía)
  3. Limitaciones del análisis léxico para simbolismo complejo

9.4 Trabajo futuro

Futuros análisis deberían:

  • Utilizar ediciones con marcadores estructurales reales
  • Incorporar análisis sintáctico y contextual
  • Validar hallazgos cuantitativos con análisis literario cualitativo

Reflexión final: Este análisis demuestra el potencial del procesamiento de lenguaje natural para reinterpretar literatura latinoamericana, pero también sus límites frente a la riqueza simbólica del realismo mágico de Gabriel García Márquez.

10 Referencias

  • García Márquez, G. (1967). Cien años de soledad. Editorial Sudamericana.
  • Silge, J., & Robinson, D. (2017). Text Mining with R: A Tidy Approach. O’Reilly Media.
  • Newman, M. E. J. (2010). Networks: An Introduction. Oxford University Press.
  • Blei, D. M., Ng, A. Y., & Jordan, M. I. (2003). Latent Dirichlet Allocation. Journal of Machine Learning Research, 3, 993-1022.