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:
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.
Mi análisis presenta las siguientes limitaciones estructurales:
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:
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
La remoción de acentos y stopwords puede eliminar información
semántica relevante, aunque intento mitigarlo usando
stringi para preservar la ñ.
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.
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)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
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")| 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 |
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.
## Palabras aproximadas por capítulo: 6906.2
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
## Primeros 10 tokens:
## # 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
Decisión metodológica importante: Creo stopwords personalizadas ampliadas específicas para García Márquez. Esto es necesario porque:
- 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
- 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
- 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
- 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
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
## Tokens finales limpios: 45462
## Tokens únicos: 14790
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")| 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 |
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.
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")| 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 |
##
## Total de palabras en el lexicón: 152
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%")| 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 |
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)
)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"
)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)
)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")| 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 |
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
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
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")| 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 |
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
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)| 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 |
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
)
}
}
}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")| 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 |
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:
## Documentos: 20
## Términos: 14790
cat(" Sparsity:", round(100 * (1 - length(dtm_caps$i) / (dtm_caps$nrow * dtm_caps$ncol)), 1), "%\n")## Sparsity: 88.3 %
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
## (Menor perplejidad = mejor ajuste)
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%")| 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 |
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)
)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)| 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 |
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")| 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 |
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"
)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)| 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 |
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)
)
}En mi análisis identifico los siguientes patrones:
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.
Estructura de redes: Las redes de co-ocurrencia revelan núcleos semánticos densos que corresponden a períodos narrativos específicos.
Tópicos latentes: El modelo LDA identifica 6 temas principales que capturan líneas narrativas centrales de la obra.
Este trabajo demuestra que:
Como señalé en la introducción, mi análisis tiene limitaciones importantes:
Futuros análisis deberían:
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.