Este documento presenta un análisis exhaustivo de Cien Años de Soledad de Gabriel García Márquez utilizando técnicas de minería de texto y análisis de redes. El análisis se estructura en tres componentes:
La metodología sigue los principios de Procesamiento de Lenguaje Natural (NLP) presentados en las notas de clase, adaptados al análisis literario.
# Manipulación de datos
suppressMessages(suppressWarnings(library(readr)))
suppressMessages(suppressWarnings(library(tidyverse)))
suppressMessages(suppressWarnings(library(magrittr)))
suppressMessages(suppressWarnings(library(dplyr)))
# Minería de texto
suppressMessages(suppressWarnings(library(tidytext)))
suppressMessages(suppressWarnings(library(tm)))
suppressMessages(suppressWarnings(library(SnowballC)))
suppressMessages(suppressWarnings(library(ngram)))
# Análisis de redes
suppressMessages(suppressWarnings(library(igraph)))
suppressMessages(suppressWarnings(library(ggraph)))
suppressMessages(suppressWarnings(library(widyr)))
# Visualización
suppressMessages(suppressWarnings(library(ggplot2)))
suppressMessages(suppressWarnings(library(gridExtra)))
suppressMessages(suppressWarnings(library(wordcloud)))
suppressMessages(suppressWarnings(library(RColorBrewer)))
suppressMessages(suppressWarnings(library(reshape2)))
# Modelado de tópicos
suppressMessages(suppressWarnings(library(topicmodels)))# Leer el archivo de texto
# IMPORTANTE: Ajustar la ruta según la ubicación del archivo
texto_completo <- read_lines("gabriel_garcia_marquez_cien_annos_soledad.txt")
# Convertir a vector de caracteres
texto_completo <- unlist(c(texto_completo))
names(texto_completo) <- NULL
cat("Total de líneas:", length(texto_completo), "\n")## Total de líneas: 11929
La novela tiene 20 capítulos. Para identificarlos, buscaremos patrones estructurales.
# Convertir a data frame
texto_df <- tibble(
line = seq_along(texto_completo),
text = texto_completo
)
# Estrategia: dividir el texto en 20 partes aproximadamente iguales
# Esto es una aproximación razonable dado que la novela tiene estructura regular
lineas_totales <- nrow(texto_df)
lineas_por_capitulo <- lineas_totales / 20
texto_df <- texto_df %>%
mutate(
capitulo = ceiling(line / lineas_por_capitulo),
capitulo = paste0("Capitulo_", sprintf("%02d", capitulo))
)
# Verificar distribución
cat("Distribución de líneas por capítulo:\n")## Distribución de líneas por capítulo:
##
## Capitulo_01 Capitulo_02 Capitulo_03 Capitulo_04 Capitulo_05 Capitulo_06
## 596 596 597 596 597 596
## Capitulo_07 Capitulo_08 Capitulo_09 Capitulo_10 Capitulo_11 Capitulo_12
## 597 596 597 596 596 597
## Capitulo_13 Capitulo_14 Capitulo_15 Capitulo_16 Capitulo_17 Capitulo_18
## 596 597 596 597 596 597
## Capitulo_19 Capitulo_20
## 596 597
# Agrupar texto por capítulo
corpus_capitulos <- texto_df %>%
group_by(capitulo) %>%
summarise(
text = paste(text, collapse = " "),
n_lineas = n(),
.groups = "drop"
)
# Convertir a data frame en formato tidy
corpus_capitulos <- tibble(
line = seq_along(corpus_capitulos$capitulo),
capitulo = corpus_capitulos$capitulo,
text = corpus_capitulos$text
)
cat("Número de capítulos:", nrow(corpus_capitulos), "\n")## Número de capítulos: 20
## # A tibble: 3 × 3
## line capitulo text
## <int> <chr> <chr>
## 1 1 Capitulo_01 "Gabriel García Márquez Cien años de soledad EDITAD…
## 2 2 Capitulo_02 "en el piso de tierra. -Si has de parir iguanas, criaremo…
## 3 3 Capitulo_03 "19 Cien años de soledad Gabriel García Márquez c…
Siguiendo la metodología de las notas de clase, el primer paso es la tokenización:
# Tokenizar por palabras
tokens_capitulos <- corpus_capitulos %>%
unnest_tokens(input = text, output = word) %>%
filter(!is.na(word))
cat("Total de tokens:", nrow(tokens_capitulos), "\n")## Total de tokens: 139335
## # A tibble: 10 × 3
## line capitulo word
## <int> <chr> <chr>
## 1 1 Capitulo_01 gabriel
## 2 1 Capitulo_01 garcía
## 3 1 Capitulo_01 márquez
## 4 1 Capitulo_01 cien
## 5 1 Capitulo_01 años
## 6 1 Capitulo_01 de
## 7 1 Capitulo_01 soledad
## 8 1 Capitulo_01 editado
## 9 1 Capitulo_01 por
## 10 1 Capitulo_01 ediciones
# Verificar presencia de números
tokens_con_numeros <- tokens_capitulos %>%
filter(grepl(pattern = "[0-9]", x = word)) %>%
count(word, sort = TRUE)
cat("Tokens con números:", nrow(tokens_con_numeros), "\n")## Tokens con números: 174
## # A tibble: 10 × 2
## word n
## <chr> <int>
## 1 105 3
## 2 27 3
## 3 10 2
## 4 102 2
## 5 11 2
## 6 111 2
## 7 121 2
## 8 13 2
## 9 130 2
## 10 138 2
# Eliminar tokens con números
tokens_capitulos %<>%
filter(!grepl(pattern = "[0-9]", x = word))
cat("Tokens después de remover números:", nrow(tokens_capitulos), "\n")## Tokens después de remover números: 139134
# Cargar stopwords en español (basado en las notas de clase)
# Usando el diccionario de tm como en las notas
stopwords_es <- tibble(
word = tm::stopwords("spanish"),
lexicon = "tm_spanish"
)
cat("Stopwords en español:", nrow(stopwords_es), "\n")## Stopwords en español: 308
# Remover stopwords
tokens_limpios <- tokens_capitulos %>%
anti_join(stopwords_es, by = "word")
cat("Tokens después de remover stopwords:", nrow(tokens_limpios), "\n")## Tokens después de remover stopwords: 70002
# Lista de reemplazo para acentos (como en las notas)
replacement_list <- list(
'á' = 'a', 'é' = 'e', 'í' = 'i', 'ó' = 'o', 'ú' = 'u',
'Á' = 'A', 'É' = 'E', 'Í' = 'I', 'Ó' = 'O', 'Ú' = 'U',
'ñ' = 'n', 'Ñ' = 'N'
)
tokens_limpios %<>%
mutate(word = chartr(
old = names(replacement_list) %>% str_c(collapse = ""),
new = replacement_list %>% str_c(collapse = ""),
x = word
))
cat("Tokens normalizados:", nrow(tokens_limpios), "\n")## Tokens normalizados: 70002
## # A tibble: 10 × 3
## line capitulo word
## <int> <chr> <chr>
## 1 1 Capitulo_01 gabriel
## 2 1 Capitulo_01 garcia
## 3 1 Capitulo_01 marquez
## 4 1 Capitulo_01 cien
## 5 1 Capitulo_01 anos
## 6 1 Capitulo_01 soledad
## 7 1 Capitulo_01 editado
## 8 1 Capitulo_01 ediciones
## 9 1 Capitulo_01 cueva
## 10 1 Capitulo_01 j
# Calcular frecuencias
frecuencias_caps <- tokens_limpios %>%
count(capitulo, word, sort = TRUE)
# Top 10 palabras globales
cat("Top 10 palabras más frecuentes en toda la novela:\n")## Top 10 palabras más frecuentes en toda la novela:
frecuencias_caps %>%
group_by(word) %>%
summarise(total = sum(n), .groups = "drop") %>%
arrange(desc(total)) %>%
head(10)## # A tibble: 10 × 2
## word total
## <chr> <int>
## 1 aureliano 794
## 2 ursula 514
## 3 arcadio 480
## 4 casa 463
## 5 jose 424
## 6 buendia 420
## 7 anos 359
## 8 coronel 312
## 9 amaranta 310
## 10 segundo 308
# Primeros 4 capítulos
primeros_4 <- tokens_limpios %>%
filter(capitulo %in% paste0("Capitulo_", sprintf("%02d", 1:4))) %>%
count(capitulo, word, sort = TRUE) %>%
group_by(capitulo) %>%
slice_max(n, n = 10) %>%
ungroup()
ggplot(primeros_4, aes(x = reorder_within(word, n, capitulo), y = n, fill = capitulo)) +
geom_col(show.legend = FALSE) +
facet_wrap(~capitulo, scales = "free_y", ncol = 2) +
coord_flip() +
scale_x_reordered() +
scale_fill_brewer(palette = "Set2") +
labs(
title = "Top 10 palabras más frecuentes por capítulo",
subtitle = "Primeros 4 capítulos de Cien Años de Soledad",
x = NULL,
y = "Frecuencia"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
strip.text = element_text(face = "bold", size = 11)
)# Configurar área de gráficos
par(mfrow = c(2, 2), mar = c(1, 1, 3, 1))
# Nubes para los primeros 4 capítulos
for (i in 1:4) {
cap <- paste0("Capitulo_", sprintf("%02d", i))
set.seed(1702)
tokens_limpios %>%
filter(capitulo == cap) %>%
count(word, sort = TRUE) %>%
with(wordcloud(
words = word,
freq = n,
max.words = 30,
colors = brewer.pal(8, "Dark2"),
random.order = FALSE
))
title(main = cap, cex.main = 1.5)
}Siguiendo la metodología de las notas de clase, construiremos un lexicón de sentimientos en español.
# Crear lexicón basado en las notas de clase y ampliado para análisis literario
sentimientos_positivos <- tibble(
word = c(
"amor", "amar", "amo", "feliz", "felicidad", "alegria", "alegre",
"paz", "bien", "bueno", "buena", "mejor", "hermoso", "hermosa",
"bello", "bella", "belleza", "vida", "vivir", "vivo",
"luz", "sol", "brillante", "risa", "reir", "sonrisa",
"amistad", "amigo", "compania", "esperanza", "confianza",
"exito", "victoria", "triunfo", "placer", "gustar",
"cielo", "estrella", "amanecer", "primavera",
"cantar", "musica", "melodia", "bendicion", "libertad"
),
sentiment = "Positivo",
valor = 1
)
sentimientos_negativos <- tibble(
word = c(
"muerte", "morir", "muerto", "muerta", "murio", "muriendo",
"sangre", "guerra", "violencia", "combate", "batalla",
"dolor", "sufrir", "sufrimiento", "tristeza", "triste",
"llanto", "llorar", "lagrima", "soledad", "solo", "sola",
"miedo", "temor", "terror", "odio", "odiar",
"oscuridad", "oscuro", "sombra", "tiniebla",
"enfermedad", "enfermo", "mal", "destruccion", "destruir",
"abandono", "abandonar", "traicion", "engano", "mentira",
"desgracia", "miseria", "culpa", "pecado", "maldicion",
"venganza", "rencor", "amargura", "desesperacion", "angustia",
"fracaso", "derrota", "perder", "perdida"
),
sentiment = "Negativo",
valor = -1
)
# Combinar lexicones
lexicon_sentimientos <- bind_rows(sentimientos_positivos, sentimientos_negativos)
cat("Lexicón de sentimientos:\n")## Lexicón de sentimientos:
## Palabras positivas: 45
## Palabras negativas: 55
# Calcular sentimientos por capítulo
sentimientos_caps <- tokens_limpios %>%
inner_join(lexicon_sentimientos, by = "word") %>%
group_by(capitulo) %>%
summarise(
sentimiento_total = sum(valor),
palabras_positivas = sum(valor > 0),
palabras_negativas = sum(valor < 0),
sentimiento_promedio = mean(valor),
.groups = "drop"
) %>%
mutate(
indice_cap = as.numeric(str_extract(capitulo, "[0-9]+")),
balance = palabras_positivas - palabras_negativas
)
cat("Resumen de sentimientos por capítulo:\n")## Resumen de sentimientos por capítulo:
## # A tibble: 20 × 7
## capitulo sentimiento_total palabras_positivas palabras_negativas
## <chr> <dbl> <int> <int>
## 1 Capitulo_01 -18 39 57
## 2 Capitulo_02 -17 40 57
## 3 Capitulo_03 -8 45 53
## 4 Capitulo_04 -16 44 60
## 5 Capitulo_05 -22 32 54
## 6 Capitulo_06 -48 35 83
## 7 Capitulo_07 -43 37 80
## 8 Capitulo_08 -50 44 94
## 9 Capitulo_09 -51 38 89
## 10 Capitulo_10 12 77 65
## 11 Capitulo_11 -27 41 68
## 12 Capitulo_12 -14 66 80
## 13 Capitulo_13 -18 59 77
## 14 Capitulo_14 -38 57 95
## 15 Capitulo_15 4 45 41
## 16 Capitulo_16 -10 40 50
## 17 Capitulo_17 -10 49 59
## 18 Capitulo_18 -30 42 72
## 19 Capitulo_19 25 68 43
## 20 Capitulo_20 -12 45 57
## # ℹ 3 more variables: sentimiento_promedio <dbl>, indice_cap <dbl>,
## # balance <int>
# Evolución del sentimiento
ggplot(sentimientos_caps, aes(x = indice_cap, y = sentimiento_promedio)) +
geom_line(color = "steelblue", size = 1.2) +
geom_point(aes(color = sentimiento_promedio > 0), size = 3.5) +
geom_hline(yintercept = 0, linetype = "dashed", color = "gray40") +
scale_color_manual(
values = c("TRUE" = "forestgreen", "FALSE" = "firebrick"),
labels = c("TRUE" = "Positivo", "FALSE" = "Negativo")
) +
labs(
title = "Evolución del sentimiento a lo largo de Cien Años de Soledad",
subtitle = "Valencia emocional promedio por capítulo",
x = "Capítulo",
y = "Sentimiento promedio",
color = "Polaridad"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
legend.position = "bottom"
)# Distribución de palabras positivas vs negativas
sentimientos_largo <- sentimientos_caps %>%
select(indice_cap, palabras_positivas, palabras_negativas) %>%
pivot_longer(
cols = c(palabras_positivas, palabras_negativas),
names_to = "tipo",
values_to = "cantidad"
) %>%
mutate(
tipo = if_else(tipo == "palabras_positivas", "Positivas", "Negativas")
)
ggplot(sentimientos_largo, aes(x = indice_cap, y = cantidad, fill = tipo)) +
geom_col(position = "dodge", alpha = 0.8) +
scale_fill_manual(values = c("Positivas" = "forestgreen", "Negativas" = "firebrick")) +
labs(
title = "Distribución de palabras con carga emocional",
subtitle = "Comparación de palabras positivas y negativas por capítulo",
x = "Capítulo",
y = "Cantidad de palabras",
fill = "Tipo"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
legend.position = "bottom"
)# Palabras con carga emocional más frecuentes
palabras_sentimiento <- tokens_limpios %>%
inner_join(lexicon_sentimientos, by = "word") %>%
count(word, sentiment, sort = TRUE) %>%
group_by(sentiment) %>%
slice_max(n, n = 15) %>%
ungroup()
ggplot(palabras_sentimiento, aes(x = reorder_within(word, n, sentiment), y = n, fill = sentiment)) +
geom_col(show.legend = FALSE) +
facet_wrap(~sentiment, scales = "free_y") +
coord_flip() +
scale_x_reordered() +
scale_fill_manual(values = c("Positivo" = "forestgreen", "Negativo" = "firebrick")) +
labs(
title = "Palabras con carga emocional más frecuentes",
subtitle = "Top 15 palabras por sentimiento en Cien Años de Soledad",
x = NULL,
y = "Frecuencia"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
strip.text = element_text(face = "bold", size = 12)
)Siguiendo la metodología de las notas de clase, utilizaremos skipgramas para construir redes de co-ocurrencia.
# Tokenizar en skipgramas (como en las notas de clase)
skipgramas_caps <- corpus_capitulos %>%
unnest_tokens(input = text, output = skipgram, token = "skip_ngrams", n = 2) %>%
filter(!is.na(skipgram))
cat("Total de skipgramas:", nrow(skipgramas_caps), "\n")## Total de skipgramas: 417945
## # A tibble: 10 × 3
## line capitulo skipgram
## <int> <chr> <chr>
## 1 1 Capitulo_01 gabriel
## 2 1 Capitulo_01 gabriel garcía
## 3 1 Capitulo_01 gabriel márquez
## 4 1 Capitulo_01 garcía
## 5 1 Capitulo_01 garcía márquez
## 6 1 Capitulo_01 garcía cien
## 7 1 Capitulo_01 márquez
## 8 1 Capitulo_01 márquez cien
## 9 1 Capitulo_01 márquez años
## 10 1 Capitulo_01 cien
# Contar palabras en cada skipgrama
skipgramas_caps$num_words <- skipgramas_caps$skipgram %>%
map_int(.f = ~ wordcount(.x))
# Mantener solo bigramas
skipgramas_caps %<>%
filter(num_words == 2) %>%
select(-num_words)
# Separar en dos palabras
skipgramas_sep <- skipgramas_caps %>%
separate(skipgram, c("word1", "word2"), sep = " ")
# Limpiar (siguiendo metodología de las notas)
skipgramas_limpios <- skipgramas_sep %>%
# Remover números
filter(!grepl(pattern = "[0-9]", x = word1)) %>%
filter(!grepl(pattern = "[0-9]", x = word2)) %>%
# Remover stopwords
filter(!word1 %in% stopwords_es$word) %>%
filter(!word2 %in% stopwords_es$word) %>%
# Remover acentos
mutate(
word1 = chartr(
old = names(replacement_list) %>% str_c(collapse = ""),
new = replacement_list %>% str_c(collapse = ""),
x = word1
),
word2 = chartr(
old = names(replacement_list) %>% str_c(collapse = ""),
new = replacement_list %>% str_c(collapse = ""),
x = word2
)
) %>%
# Remover NA
filter(!is.na(word1), !is.na(word2))
cat("Skipgramas limpios:", nrow(skipgramas_limpios), "\n")## Skipgramas limpios: 59153
# Contar frecuencias
skipgramas_count <- skipgramas_limpios %>%
count(capitulo, word1, word2, sort = TRUE) %>%
rename(weight = n)
cat("Skipgramas únicos:", nrow(skipgramas_count), "\n")## Skipgramas únicos: 54246
##
## Top 20 skipgramas más frecuentes:
skipgramas_count %>%
group_by(word1, word2) %>%
summarise(total = sum(weight), .groups = "drop") %>%
arrange(desc(total)) %>%
head(20)## # A tibble: 20 × 3
## word1 word2 total
## <chr> <chr> <int>
## 1 jose arcadio 374
## 2 aureliano segundo 209
## 3 aureliano buendia 208
## 4 coronel aureliano 198
## 5 coronel buendia 198
## 6 cien anos 180
## 7 arcadio buendia 177
## 8 anos soledad 173
## 9 gabriel garcia 171
## 10 gabriel marquez 171
## 11 garcia marquez 171
## 12 jose buendia 169
## 13 soledad gabriel 169
## 14 soledad garcia 169
## 15 amaranta ursula 88
## 16 arcadio segundo 84
## 17 jose segundo 82
## 18 gerineldo marquez 68
## 19 petra cotes 67
## 20 pietro crespi 65
# Función para crear red de un capítulo
crear_red_skipgrama <- function(datos, cap, umbral = 1) {
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
)
# Simplificar (como en las notas)
grafo <- igraph::simplify(grafo)
return(grafo)
}
# Crear redes para todos los capítulos
caps_todos <- unique(skipgramas_count$capitulo)
redes_caps <- map(caps_todos, ~crear_red_skipgrama(skipgramas_count, .x, umbral = 2))
names(redes_caps) <- caps_todos
# Verificar tamaño de redes
cat("Tamaño de las redes (primeros 5 capítulos):\n")## Tamaño de las redes (primeros 5 capítulos):
for (i in 1:min(5, length(redes_caps))) {
if (!is.null(redes_caps[[i]])) {
cat(sprintf("%s: %d nodos, %d aristas\n",
names(redes_caps)[i],
vcount(redes_caps[[i]]),
ecount(redes_caps[[i]])))
}
}## Capitulo_02: 102 nodos, 72 aristas
## Capitulo_01: 92 nodos, 74 aristas
## Capitulo_16: 99 nodos, 79 aristas
## Capitulo_08: 99 nodos, 79 aristas
## Capitulo_10: 109 nodos, 88 aristas
# Función para visualizar red (basada en las notas)
visualizar_red_skip <- function(grafo, titulo, umbral_strength = 0) {
if (is.null(grafo) || vcount(grafo) == 0) return(NULL)
# Extraer 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) return(NULL)
# Calcular métricas
V(gcc)$strength <- strength(gcc)
V(gcc)$betweenness <- betweenness(gcc, normalized = TRUE)
# Filtrar por strength si es necesario
if (umbral_strength > 0) {
gcc <- induced_subgraph(gcc, vids = which(V(gcc)$strength > umbral_strength))
}
if (vcount(gcc) < 3) return(NULL)
# Visualizar
set.seed(1702)
plot(
gcc,
layout = layout_with_fr,
vertex.color = adjustcolor("steelblue", 0.3),
vertex.frame.color = "steelblue",
vertex.size = 2 * V(gcc)$strength,
vertex.label.color = "black",
vertex.label.cex = 0.8,
vertex.label.dist = 1,
edge.width = 2 * E(gcc)$weight / max(E(gcc)$weight),
edge.color = adjustcolor("gray60", 0.5),
main = titulo
)
return(gcc)
}
# Visualizar primeros 4 capítulos
par(mfrow = c(2, 2), mar = c(2, 2, 3, 2))
for (i in 1:4) {
if (!is.null(redes_caps[[i]])) {
visualizar_red_skip(
redes_caps[[i]],
names(redes_caps)[i],
umbral_strength = 0
)
}
}# Función para calcular métricas (siguiendo las notas)
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, diametro = NA
))
}
# Simplificar
grafo <- igraph::simplify(grafo)
# Métricas básicas
orden <- vcount(grafo)
tamano <- ecount(grafo)
densidad <- edge_density(grafo)
grado_medio <- mean(degree(grafo))
transitividad <- transitivity(grafo, type = "global")
componentes <- components(grafo)$no
# Diámetro (solo si es conexa)
if (componentes == 1) {
diametro <- diameter(grafo)
} else {
diametro <- NA
}
tibble(
orden = orden,
tamano = tamano,
densidad = round(densidad, 4),
grado_medio = round(grado_medio, 2),
transitividad = round(transitividad, 4),
componentes = componentes,
diametro = diametro
)
}
# Calcular para todos los capítulos
metricas_redes <- map_dfr(redes_caps, calcular_metricas, .id = "capitulo") %>%
mutate(indice_cap = as.numeric(str_extract(capitulo, "[0-9]+")))
cat("Métricas de red por capítulo:\n")## Métricas de red por capítulo:
## # A tibble: 20 × 9
## capitulo orden tamano densidad grado_medio transitividad componentes diametro
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <lgl>
## 1 Capitul… 102 72 0.014 1.41 0.169 34 NA
## 2 Capitul… 92 74 0.0177 1.61 0.267 28 NA
## 3 Capitul… 99 79 0.0163 1.6 0.252 32 NA
## 4 Capitul… 99 79 0.0163 1.6 0.333 31 NA
## 5 Capitul… 109 88 0.015 1.61 0.24 32 NA
## 6 Capitul… 106 72 0.0129 1.36 0.321 40 NA
## 7 Capitul… 104 87 0.0162 1.67 0.189 28 NA
## 8 Capitul… 133 90 0.0103 1.35 0.261 50 NA
## 9 Capitul… 69 48 0.0205 1.39 0.267 25 NA
## 10 Capitul… 105 78 0.0143 1.49 0.259 36 NA
## 11 Capitul… 144 107 0.0104 1.49 0.24 47 NA
## 12 Capitul… 115 86 0.0131 1.5 0.247 38 NA
## 13 Capitul… 105 81 0.0148 1.54 0.24 33 NA
## 14 Capitul… 92 58 0.0139 1.26 0.469 39 NA
## 15 Capitul… 96 72 0.0158 1.5 0.153 29 NA
## 16 Capitul… 123 94 0.0125 1.53 0.12 37 NA
## 17 Capitul… 124 92 0.0121 1.48 0.261 40 NA
## 18 Capitul… 126 97 0.0123 1.54 0.25 40 NA
## 19 Capitul… 110 82 0.0137 1.49 0.337 39 NA
## 20 Capitul… 95 71 0.0159 1.49 0.212 32 NA
## # ℹ 1 more variable: indice_cap <dbl>
# Preparar datos para visualización
metricas_largo <- metricas_redes %>%
select(indice_cap, densidad, grado_medio, transitividad, componentes) %>%
pivot_longer(
cols = -indice_cap,
names_to = "metrica",
values_to = "valor"
) %>%
mutate(
metrica = case_when(
metrica == "densidad" ~ "Densidad",
metrica == "grado_medio" ~ "Grado medio",
metrica == "transitividad" ~ "Transitividad",
metrica == "componentes" ~ "Componentes"
)
)
ggplot(metricas_largo, aes(x = indice_cap, y = valor, color = metrica)) +
geom_line(size = 1) +
geom_point(size = 2.5) +
facet_wrap(~metrica, scales = "free_y", ncol = 2) +
scale_color_brewer(palette = "Set1") +
labs(
title = "Evolución de métricas de red a través de los capítulos",
subtitle = "Redes de skipgramas de Cien Años de Soledad",
x = "Capítulo",
y = "Valor de la métrica"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
legend.position = "none",
strip.text = element_text(face = "bold", size = 11)
)# Función para detectar comunidades (siguiendo las notas)
detectar_comunidades <- function(grafo) {
if (is.null(grafo) || vcount(grafo) < 3) {
return(list(n_comunidades = NA, modularidad = NA))
}
# Algoritmo Fast Greedy (como en las notas)
comunidades <- cluster_fast_greedy(grafo)
list(
n_comunidades = length(comunidades),
modularidad = modularity(comunidades)
)
}
# Aplicar a todas las redes
comunidades_caps <- map_dfr(redes_caps, detectar_comunidades, .id = "capitulo") %>%
mutate(indice_cap = as.numeric(str_extract(capitulo, "[0-9]+")))
cat("Comunidades detectadas por capítulo:\n")## Comunidades detectadas por capítulo:
## # A tibble: 20 × 4
## capitulo n_comunidades modularidad indice_cap
## <chr> <int> <dbl> <dbl>
## 1 Capitulo_02 37 0.777 2
## 2 Capitulo_01 31 0.730 1
## 3 Capitulo_16 34 0.764 16
## 4 Capitulo_08 34 0.713 8
## 5 Capitulo_10 36 0.781 10
## 6 Capitulo_03 40 0.835 3
## 7 Capitulo_09 33 0.666 9
## 8 Capitulo_15 51 0.820 15
## 9 Capitulo_20 25 0.785 20
## 10 Capitulo_18 38 0.842 18
## 11 Capitulo_07 50 0.750 7
## 12 Capitulo_17 40 0.848 17
## 13 Capitulo_13 35 0.789 13
## 14 Capitulo_19 39 0.869 19
## 15 Capitulo_12 32 0.792 12
## 16 Capitulo_14 41 0.857 14
## 17 Capitulo_04 43 0.861 4
## 18 Capitulo_05 43 0.869 5
## 19 Capitulo_11 40 0.837 11
## 20 Capitulo_06 35 0.780 6
# Visualizar comunidades y modularidad
p1 <- ggplot(comunidades_caps, aes(x = indice_cap, y = n_comunidades)) +
geom_line(color = "darkorange", size = 1) +
geom_point(size = 3) +
labs(
title = "Número de comunidades por capítulo",
x = "Capítulo",
y = "Número de comunidades"
) +
theme_minimal()
p2 <- ggplot(comunidades_caps, aes(x = indice_cap, y = modularidad)) +
geom_line(color = "darkviolet", size = 1) +
geom_point(size = 3) +
labs(
title = "Modularidad por capítulo",
x = "Capítulo",
y = "Modularidad"
) +
theme_minimal()
grid.arrange(p1, p2, ncol = 2)# Función para calcular centralidad (siguiendo las notas)
calcular_centralidad <- function(grafo, top_n = 10) {
if (is.null(grafo) || vcount(grafo) < 3) return(NULL)
# 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) return(NULL)
# Centralidad de eigenvector
eigen <- eigen_centrality(gcc)$vector
tibble(
word = names(eigen),
eigen = eigen
) %>%
arrange(desc(eigen)) %>%
head(top_n)
}
# Calcular para primeros 5 capítulos
cat("Palabras más importantes (centralidad eigenvector):\n\n")## Palabras más importantes (centralidad eigenvector):
for (i in 1:5) {
cat(paste0("\n", names(redes_caps)[i], ":\n"))
cent <- calcular_centralidad(redes_caps[[i]], top_n = 10)
if (!is.null(cent)) {
print(cent)
} else {
cat("Red insuficiente para análisis\n")
}
}##
## Capitulo_02:
## # A tibble: 10 × 2
## word eigen
## <chr> <dbl>
## 1 arcadio 1
## 2 jose 0.999
## 3 buendia 0.768
## 4 aldea 0.0529
## 5 apenas 0.0265
## 6 amaranta 0.0265
## 7 quejose 0.0265
## 8 sintio 0.0265
## 9 propio 0.0265
## 10 si 0.000703
##
## Capitulo_01:
## # A tibble: 10 × 2
## word eigen
## <chr> <dbl>
## 1 jose 1
## 2 arcadio 1
## 3 buendia 0.982
## 4 aureliano 0.0541
## 5 coronel 0.0330
## 6 volvio 0.0220
## 7 primer 0.0220
## 8 pago 0.0209
## 9 mientras 0.0209
## 10 fusilamiento 0.000702
##
## Capitulo_16:
## # A tibble: 10 × 2
## word eigen
## <chr> <dbl>
## 1 aureliano 1
## 2 segundo 0.987
## 3 buendia 0.276
## 4 coronel 0.239
## 5 jose 0.227
## 6 arcadio 0.227
## 7 lluvia 0.140
## 8 perdio 0.0926
## 9 cosas 0.0926
## 10 pequeno 0.0699
##
## Capitulo_08:
## # A tibble: 10 × 2
## word eigen
## <chr> <dbl>
## 1 aureliano 1
## 2 coronel 1.000
## 3 buendia 0.964
## 4 jose 0.231
## 5 marquez 0.178
## 6 gerineldo 0.173
## 7 dijo 0.0403
## 8 nombre 0.0268
## 9 garcia 0.0219
## 10 gabriel 0.0219
##
## Capitulo_10:
## # A tibble: 10 × 2
## word eigen
## <chr> <dbl>
## 1 segundo 1
## 2 aureliano 0.795
## 3 jose 0.593
## 4 arcadio 0.592
## 5 buendia 0.203
## 6 coronel 0.163
## 7 hizo 0.0810
## 8 amanecer 0.0726
## 9 volvio 0.0726
## 10 llego 0.0404
Siguiendo el enfoque del libro Text Mining with R: A Tidy Approach (Capítulo 6), aplicaremos Latent Dirichlet Allocation (LDA) para identificar temas latentes en la novela.
LDA se basa en dos principios:
# Crear conteo de palabras por capítulo
palabras_caps <- tokens_limpios %>%
count(capitulo, word, sort = TRUE)
# Crear DTM usando cast_dtm (como en el libro)
dtm_caps <- palabras_caps %>%
cast_dtm(document = capitulo, term = word, value = n)
cat("Dimensiones de la DTM:\n")## Dimensiones de la DTM:
## Documentos (capítulos): 20
## Términos (palabras únicas): 15333
cat("Sparsity:", round(100 * (1 - dtm_caps$i %>% length() / (dtm_caps$nrow * dtm_caps$ncol)), 1), "%\n")## Sparsity: 86.8 %
# Número de tópicos
# Para 20 capítulos, 6 tópicos es razonable
k_topicos <- 6
# Ajustar modelo LDA (siguiendo el libro)
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_topicos, "tópicos\n")## Modelo LDA ajustado con 6 tópicos
La matriz β (beta) representa la probabilidad por-tópico-por-palabra.
# Extraer beta usando tidy() (como en el libro)
topicos_beta <- tidy(modelo_lda, matrix = "beta")
cat("Estructura de beta:\n")## Estructura de beta:
## # A tibble: 10 × 3
## topic term beta
## <int> <chr> <dbl>
## 1 1 arcadio 0.0000116
## 2 2 arcadio 0.00378
## 3 3 arcadio 0.0000137
## 4 4 arcadio 0.0248
## 5 5 arcadio 0.0150
## 6 6 arcadio 0.0000106
## 7 1 meme 0.000128
## 8 2 meme 0.0000843
## 9 3 meme 0.0000137
## 10 4 meme 0.0000115
# Top 10 palabras por tópico
top_terminos <- topicos_beta %>%
group_by(topic) %>%
slice_max(beta, n = 10) %>%
ungroup() %>%
arrange(topic, -beta)
cat("\nTop 10 términos por tópico:\n")##
## Top 10 términos por tópico:
## # A tibble: 60 × 3
## topic term beta
## <int> <chr> <dbl>
## 1 1 coronel 0.0328
## 2 1 guerra 0.0172
## 3 1 buendia 0.0121
## 4 1 marquez 0.00895
## 5 1 gerineldo 0.00790
## 6 1 dijo 0.00593
## 7 1 general 0.00454
## 8 1 gobierno 0.00430
## 9 1 capitan 0.00314
## 10 1 liberales 0.00314
## # ℹ 50 more rows
# Visualizar top términos (Figura 6-2 del libro)
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) +
coord_flip() +
scale_x_reordered() +
scale_fill_brewer(palette = "Set2") +
labs(
title = "Palabras más comunes dentro de cada tópico",
subtitle = "Distribución β: probabilidad de cada palabra en cada tópico (enfoque Text Mining with R)",
x = NULL,
y = "Probabilidad β"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
strip.text = element_text(face = "bold", size = 11)
)Siguiendo el libro, calculamos el log ratio para identificar palabras que más diferencian entre tópicos.
# Comparar tópicos 1 y 2 (como en el libro)
beta_spread <- topicos_beta %>%
mutate(topic = paste0("topic", topic)) %>%
pivot_wider(names_from = topic, values_from = beta) %>%
filter(topic1 > 0.001 | topic2 > 0.001) %>%
mutate(log_ratio = log2(topic2 / topic1))
cat("Palabras con mayor diferencia en β entre tópico 1 y 2:\n")## Palabras con mayor diferencia en β entre tópico 1 y 2:
## # A tibble: 15 × 8
## term topic1 topic2 topic3 topic4 topic5 topic6 log_ratio
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 coronel 0.0328 0.00000272 0.0000137 0.0000115 1.19e-5 3.09e-3 -13.6
## 2 guerra 0.0172 0.00000272 0.0000137 0.0000115 1.19e-5 4.35e-4 -12.6
## 3 aureliano 0.0000116 0.0214 0.000971 0.0000115 1.19e-5 1.06e-5 10.8
## 4 general 0.00454 0.00000272 0.0000137 0.0000115 1.31e-4 1.06e-5 -10.7
## 5 gobierno 0.00430 0.00000272 0.0000137 0.0000115 1.19e-5 1.06e-5 -10.6
## 6 capitan 0.00314 0.00000272 0.000150 0.0000115 1.19e-5 1.06e-5 -10.2
## 7 liberales 0.00314 0.00000272 0.0000137 0.0000115 1.19e-5 1.06e-5 -10.2
## 8 casa 0.0000116 0.0126 0.0000137 0.0000115 1.31e-4 1.06e-5 10.1
## 9 oficial 0.00280 0.00000272 0.0000137 0.0000115 1.19e-5 5.41e-4 -10.0
## 10 contacto 0.00280 0.00000272 0.000150 0.0000115 1.19e-5 1.06e-5 -10.0
## 11 moneada 0.00268 0.00000272 0.0000137 0.0000115 1.19e-5 1.06e-5 -9.94
## 12 noticias 0.00256 0.00000272 0.000150 0.0000115 8.45e-4 1.06e-5 -9.88
## 13 oficiales 0.00245 0.00000272 0.0000137 0.0000115 1.19e-5 1.06e-5 -9.81
## 14 anos 0.0000116 0.00966 0.000287 0.0000115 1.19e-5 2.23e-4 9.70
## 15 pais 0.00222 0.00000272 0.0000137 0.0000115 1.19e-5 1.06e-5 -9.67
# Visualizar log ratio (Figura 6-3 del libro)
beta_spread %>%
arrange(desc(abs(log_ratio))) %>%
head(30) %>%
mutate(term = reorder(term, log_ratio)) %>%
ggplot(aes(x = term, y = log_ratio)) +
geom_col(fill = "steelblue") +
coord_flip() +
labs(
title = "Palabras con mayor diferencia entre tópico 1 y tópico 2",
subtitle = "Log ratio de β: valores positivos favorecen tópico 2, negativos favorecen tópico 1",
x = NULL,
y = "Log2 ratio (β tópico 2 / β tópico 1)"
) +
theme_minimal() +
theme(plot.title = element_text(face = "bold", size = 14))La matriz γ (gamma) representa la probabilidad por-documento-por-tópico.
# Extraer gamma (como en el libro)
topicos_gamma <- tidy(modelo_lda, matrix = "gamma") %>%
mutate(indice_cap = as.numeric(str_extract(document, "[0-9]+")))
cat("Primeros valores de gamma:\n")## Primeros valores de gamma:
## # A tibble: 20 × 4
## document topic gamma indice_cap
## <chr> <int> <dbl> <dbl>
## 1 Capitulo_06 1 0.184 6
## 2 Capitulo_14 1 0.0207 14
## 3 Capitulo_08 1 0.354 8
## 4 Capitulo_02 1 0.0265 2
## 5 Capitulo_18 1 0.0349 18
## 6 Capitulo_10 1 0.0927 10
## 7 Capitulo_09 1 0.284 9
## 8 Capitulo_20 1 0.0481 20
## 9 Capitulo_07 1 0.310 7
## 10 Capitulo_19 1 0.0336 19
## 11 Capitulo_01 1 0.0460 1
## 12 Capitulo_11 1 0.0946 11
## 13 Capitulo_16 1 0.0511 16
## 14 Capitulo_17 1 0.0256 17
## 15 Capitulo_15 1 0.113 15
## 16 Capitulo_04 1 0.0258 4
## 17 Capitulo_03 1 0.0349 3
## 18 Capitulo_05 1 0.124 5
## 19 Capitulo_13 1 0.0565 13
## 20 Capitulo_12 1 0.123 12
##
## Estadísticas de gamma por tópico:
print(topicos_gamma %>%
group_by(topic) %>%
summarise(
media = mean(gamma),
mediana = median(gamma),
min = min(gamma),
max = max(gamma),
.groups = "drop"
))## # A tibble: 6 × 5
## topic media mediana min max
## <int> <dbl> <dbl> <dbl> <dbl>
## 1 1 0.104 0.0538 0.0207 0.354
## 2 2 0.499 0.499 0.423 0.561
## 3 3 0.0840 0.0462 0.0191 0.342
## 4 4 0.102 0.0735 0.0172 0.420
## 5 5 0.0977 0.0556 0.0250 0.355
## 6 6 0.113 0.0654 0.00562 0.310
# Boxplot de gamma por tópico (similar al libro)
ggplot(topicos_gamma, aes(x = factor(topic), y = gamma)) +
geom_boxplot(fill = "lightblue", alpha = 0.7) +
geom_jitter(width = 0.2, alpha = 0.3, color = "darkblue") +
labs(
title = "Distribución de γ (gamma) por tópico",
subtitle = "Cada punto representa la proporción de un tópico en un capítulo",
x = "Tópico",
y = "Proporción γ"
) +
theme_minimal() +
theme(plot.title = element_text(face = "bold", size = 14))# Stacked bar chart (visualización típica de LDA)
ggplot(topicos_gamma, aes(x = indice_cap, y = gamma, fill = factor(topic))) +
geom_col(position = "fill") +
scale_fill_brewer(palette = "Set2") +
labs(
title = "Composición de tópicos por capítulo",
subtitle = "Cada capítulo como mezcla de tópicos (enfoque LDA)",
x = "Capítulo",
y = "Proporción del tópico (γ)",
fill = "Tópico"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
legend.position = "bottom"
)# Heatmap de gamma (visualización avanzada)
ggplot(topicos_gamma, aes(x = factor(topic), y = reorder(document, -indice_cap), fill = gamma)) +
geom_tile(color = "white", size = 0.5) +
scale_fill_gradient2(
low = "white",
mid = "lightblue",
high = "darkblue",
midpoint = 0.2
) +
labs(
title = "Mapa de calor: Tópicos por capítulo",
subtitle = "Intensidad indica la proporción (γ) del tópico en cada capítulo",
x = "Tópico",
y = "Capítulo",
fill = "γ"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
axis.text.y = element_text(size = 9)
)Siguiendo el libro, identificamos el tópico dominante por capítulo.
# Clasificación de capítulos (como en el libro)
clasificacion_caps <- topicos_gamma %>%
group_by(document) %>%
slice_max(gamma, n = 1) %>%
ungroup() %>%
arrange(indice_cap)
cat("Tópico dominante por capítulo:\n")## Tópico dominante por capítulo:
## # A tibble: 20 × 3
## document topic gamma
## <chr> <int> <dbl>
## 1 Capitulo_01 2 0.423
## 2 Capitulo_02 2 0.554
## 3 Capitulo_03 2 0.453
## 4 Capitulo_04 2 0.518
## 5 Capitulo_05 2 0.449
## 6 Capitulo_06 2 0.460
## 7 Capitulo_07 2 0.506
## 8 Capitulo_08 2 0.519
## 9 Capitulo_09 2 0.515
## 10 Capitulo_10 2 0.543
## 11 Capitulo_11 2 0.465
## 12 Capitulo_12 2 0.561
## 13 Capitulo_13 2 0.538
## 14 Capitulo_14 2 0.558
## 15 Capitulo_15 2 0.454
## 16 Capitulo_16 2 0.472
## 17 Capitulo_17 2 0.493
## 18 Capitulo_18 2 0.542
## 19 Capitulo_19 2 0.483
## 20 Capitulo_20 2 0.470
##
## Número de capítulos asignados a cada tópico:
## # A tibble: 1 × 2
## topic n
## <int> <int>
## 1 2 20
# Visualizar clasificación de capítulos
ggplot(clasificacion_caps, aes(x = indice_cap, y = gamma, color = factor(topic), size = gamma)) +
geom_point(alpha = 0.7) +
scale_color_brewer(palette = "Set2") +
scale_size(range = c(3, 10)) +
labs(
title = "Tópico dominante por capítulo",
subtitle = "Tamaño del punto indica la fuerza de la asociación (γ)",
x = "Capítulo",
y = "Proporción del tópico dominante (γ)",
color = "Tópico",
size = "γ"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
legend.position = "right"
)Siguiendo el libro, usamos augment() para ver la
asignación palabra-por-palabra.
# Usar augment para asignaciones palabra-por-palabra (como en el libro)
asignaciones <- augment(modelo_lda, data = dtm_caps)
cat("Estructura de las asignaciones:\n")## Estructura de las asignaciones:
## # A tibble: 10 × 4
## document term count .topic
## <chr> <chr> <dbl> <dbl>
## 1 Capitulo_06 arcadio 72 5
## 2 Capitulo_14 arcadio 4 2
## 3 Capitulo_08 arcadio 7 2
## 4 Capitulo_02 arcadio 59 4
## 5 Capitulo_18 arcadio 32 4
## 6 Capitulo_10 arcadio 20 2
## 7 Capitulo_09 arcadio 14 2
## 8 Capitulo_20 arcadio 2 4
## 9 Capitulo_07 arcadio 20 2
## 10 Capitulo_19 arcadio 3 2
# Palabras más frecuentemente asignadas a cada tópico
cat("\nPalabras más frecuentes por tópico asignado:\n")##
## Palabras más frecuentes por tópico asignado:
asignaciones %>%
count(.topic, term, wt = count, sort = TRUE) %>%
group_by(.topic) %>%
slice_max(n, n = 10) %>%
ungroup() %>%
print()## # A tibble: 66 × 3
## .topic term n
## <dbl> <chr> <dbl>
## 1 1 coronel 305
## 2 1 guerra 152
## 3 1 buendia 136
## 4 1 gerineldo 74
## 5 1 marquez 67
## 6 1 general 40
## 7 1 gobierno 37
## 8 1 capitan 28
## 9 1 liberales 27
## 10 1 noticias 27
## # ℹ 56 more rows
Identificamos el tópico de “consenso” para cada grupo de capítulos.
# Identificar tópico de consenso
# Agrupamos capítulos por su tópico dominante
topicos_consenso <- clasificacion_caps %>%
count(topic) %>%
arrange(desc(n))
cat("Distribución de consenso:\n")## Distribución de consenso:
## # A tibble: 1 × 2
## topic n
## <int> <int>
## 1 2 20
# Palabras características de cada grupo de consenso
cat("\nPalabras características por grupo de consenso:\n")##
## Palabras características por grupo de consenso:
for (i in 1:k_topicos) {
cat(sprintf("\nTópico %d (asignado a %d capítulos):\n",
i,
sum(clasificacion_caps$topic == i)))
palabras <- top_terminos %>%
filter(topic == i) %>%
head(7) %>%
pull(term)
cat(paste(palabras, collapse = ", "), "\n")
}##
## Tópico 1 (asignado a 0 capítulos):
## coronel, guerra, buendia, marquez, gerineldo, dijo, general
##
## Tópico 2 (asignado a 20 capítulos):
## aureliano, ursula, casa, anos, amaranta, entonces, tan
##
## Tópico 3 (asignado a 0 capítulos):
## pergaminos, amor, gaston, cartas, sabio, seguia, melquiades
##
## Tópico 4 (asignado a 0 capítulos):
## arcadio, jose, buendia, melquiades, gitanos, varios, casas
##
## Tópico 5 (asignado a 0 capítulos):
## rebeca, arcadio, crespi, pietro, moscote, don, par
##
## Tópico 6 (asignado a 0 capítulos):
## segundo, fernanda, meme, petra, cotes, lluvia, sofia
Siguiendo el libro, creamos una matriz de confusión para ver las asignaciones.
# Preparar datos para matriz de confusión
asignaciones_extended <- asignaciones %>%
mutate(
document_num = as.numeric(str_extract(document, "[0-9]+"))
) %>%
left_join(
clasificacion_caps %>%
select(document, consensus_topic = topic),
by = "document"
)
# Calcular matriz de confusión
confusion <- asignaciones_extended %>%
count(consensus_topic, .topic, wt = count) %>%
group_by(consensus_topic) %>%
mutate(percent = n / sum(n)) %>%
ungroup()
# Visualizar matriz de confusión (Figura 6-6 del libro)
ggplot(confusion, aes(x = factor(.topic), y = factor(consensus_topic), fill = percent)) +
geom_tile(color = "white") +
geom_text(aes(label = scales::percent(percent, accuracy = 1)), size = 3) +
scale_fill_gradient2(
low = "white",
high = "red",
labels = scales::percent
) +
labs(
title = "Matriz de confusión: Asignación de palabras a tópicos",
subtitle = "Filas: tópico dominante del capítulo | Columnas: tópico asignado a las palabras",
x = "Tópico asignado a las palabras",
y = "Tópico dominante del capítulo",
fill = "% de\nasignaciones"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
panel.grid = element_blank()
)Identificamos palabras que fueron asignadas a tópicos “incorrectos”.
# Identificar asignaciones "incorrectas"
asignaciones_incorrectas <- asignaciones_extended %>%
filter(.topic != consensus_topic)
cat("Número de palabras asignadas a tópico diferente al consenso:\n")## Número de palabras asignadas a tópico diferente al consenso:
cat(sprintf("Total: %d de %d (%.1f%%)\n",
sum(asignaciones_incorrectas$count),
sum(asignaciones$count),
100 * sum(asignaciones_incorrectas$count) / sum(asignaciones$count)))## Total: 32902 de 70002 (47.0%)
# Palabras más frecuentemente mal asignadas
cat("\nPalabras más frecuentemente asignadas a tópico diferente:\n")##
## Palabras más frecuentemente asignadas a tópico diferente:
asignaciones_incorrectas %>%
count(term, .topic, consensus_topic, wt = count, sort = TRUE) %>%
head(20) %>%
print()## # A tibble: 20 × 4
## term .topic consensus_topic n
## <chr> <dbl> <int> <dbl>
## 1 segundo 6 2 308
## 2 coronel 1 2 305
## 3 arcadio 4 2 243
## 4 fernanda 6 2 218
## 5 guerra 1 2 152
## 6 arcadio 5 2 136
## 7 buendia 1 2 136
## 8 jose 4 2 136
## 9 rebeca 5 2 129
## 10 buendia 4 2 119
## 11 meme 6 2 98
## 12 melquiades 4 2 92
## 13 gerineldo 1 2 74
## 14 crespi 5 2 71
## 15 petra 6 2 70
## 16 cotes 6 2 67
## 17 marquez 1 2 67
## 18 pietro 5 2 66
## 19 lluvia 6 2 56
## 20 sofia 6 2 51
# Calcular perplejidad (medida de ajuste del modelo)
perplejidad <- perplexity(modelo_lda, dtm_caps)
cat("Perplejidad del modelo:", round(perplejidad, 2), "\n")## Perplejidad del modelo: 3836.15
##
## Interpretación:
## - Menor perplejidad = mejor ajuste
## - Perplejidad mide qué tan bien el modelo predice nuevos datos
## ======================================================================
## INTERPRETACIÓN DE TÓPICOS IDENTIFICADOS
## ======================================================================
# Para cada tópico, mostrar información resumida
for (i in 1:k_topicos) {
cat(sprintf("TÓPICO %d:\n", i))
cat("-" %>% strrep(70), "\n")
# Palabras principales
palabras <- top_terminos %>%
filter(topic == i) %>%
head(8) %>%
pull(term)
cat("Palabras clave:", paste(palabras, collapse = ", "), "\n")
# Capítulos asociados
caps_asociados <- clasificacion_caps %>%
filter(topic == i) %>%
pull(indice_cap)
if (length(caps_asociados) > 0) {
cat(sprintf("Capítulos donde predomina: %s\n",
paste(caps_asociados, collapse = ", ")))
} else {
cat("No hay capítulos con este tópico como dominante\n")
}
# Proporción promedio
gamma_prom <- topicos_gamma %>%
filter(topic == i) %>%
summarise(prom = mean(gamma)) %>%
pull(prom)
cat(sprintf("Proporción promedio en la obra: %.1f%%\n", 100 * gamma_prom))
cat("\n")
}## TÓPICO 1:
## ----------------------------------------------------------------------
## Palabras clave: coronel, guerra, buendia, marquez, gerineldo, dijo, general, gobierno
## No hay capítulos con este tópico como dominante
## Proporción promedio en la obra: 10.4%
##
## TÓPICO 2:
## ----------------------------------------------------------------------
## Palabras clave: aureliano, ursula, casa, anos, amaranta, entonces, tan, tiempo
## Capítulos donde predomina: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20
## Proporción promedio en la obra: 49.9%
##
## TÓPICO 3:
## ----------------------------------------------------------------------
## Palabras clave: pergaminos, amor, gaston, cartas, sabio, seguia, melquiades, catalan
## No hay capítulos con este tópico como dominante
## Proporción promedio en la obra: 8.4%
##
## TÓPICO 4:
## ----------------------------------------------------------------------
## Palabras clave: arcadio, jose, buendia, melquiades, gitanos, varios, casas, aldea
## No hay capítulos con este tópico como dominante
## Proporción promedio en la obra: 10.2%
##
## TÓPICO 5:
## ----------------------------------------------------------------------
## Palabras clave: rebeca, arcadio, crespi, pietro, moscote, don, par, apolinar
## No hay capítulos con este tópico como dominante
## Proporción promedio en la obra: 9.8%
##
## TÓPICO 6:
## ----------------------------------------------------------------------
## Palabras clave: segundo, fernanda, meme, petra, cotes, lluvia, sofia, tren
## No hay capítulos con este tópico como dominante
## Proporción promedio en la obra: 11.3%
## ======================================================================
Combinamos sentimientos, métricas de red y tópicos para una visión holística.
# Combinar todos los análisis
analisis_completo <- sentimientos_caps %>%
select(indice_cap, sentimiento_promedio, balance, palabras_positivas, palabras_negativas) %>%
left_join(
metricas_redes %>% select(indice_cap, densidad, grado_medio, transitividad, componentes),
by = "indice_cap"
) %>%
left_join(
comunidades_caps %>% select(indice_cap, n_comunidades, modularidad),
by = "indice_cap"
) %>%
left_join(
clasificacion_caps %>% select(indice_cap, topic_dominante = topic, gamma_max = gamma),
by = "indice_cap"
)
cat("Análisis integrado por capítulo:\n")## Análisis integrado por capítulo:
## # A tibble: 20 × 13
## indice_cap sentimiento_promedio balance palabras_positivas palabras_negativas
## <dbl> <dbl> <int> <int> <int>
## 1 1 -0.188 -18 39 57
## 2 2 -0.175 -17 40 57
## 3 3 -0.0816 -8 45 53
## 4 4 -0.154 -16 44 60
## 5 5 -0.256 -22 32 54
## 6 6 -0.407 -48 35 83
## 7 7 -0.368 -43 37 80
## 8 8 -0.362 -50 44 94
## 9 9 -0.402 -51 38 89
## 10 10 0.0845 12 77 65
## 11 11 -0.248 -27 41 68
## 12 12 -0.0959 -14 66 80
## 13 13 -0.132 -18 59 77
## 14 14 -0.25 -38 57 95
## 15 15 0.0465 4 45 41
## 16 16 -0.111 -10 40 50
## 17 17 -0.0926 -10 49 59
## 18 18 -0.263 -30 42 72
## 19 19 0.225 25 68 43
## 20 20 -0.118 -12 45 57
## # ℹ 8 more variables: densidad <dbl>, grado_medio <dbl>, transitividad <dbl>,
## # componentes <dbl>, n_comunidades <int>, modularidad <dbl>,
## # topic_dominante <int>, gamma_max <dbl>
# Crear visualización múltiple
p1 <- ggplot(analisis_completo, aes(x = indice_cap, y = sentimiento_promedio)) +
geom_line(color = "steelblue", size = 1) +
geom_point(aes(color = factor(topic_dominante)), size = 3) +
geom_hline(yintercept = 0, linetype = "dashed", alpha = 0.5) +
scale_color_brewer(palette = "Set2") +
labs(title = "Sentimiento por capítulo",
subtitle = "Color indica tópico dominante",
x = NULL, y = "Sentimiento", color = "Tópico") +
theme_minimal() +
theme(legend.position = "none")
p2 <- ggplot(analisis_completo, aes(x = indice_cap, y = densidad)) +
geom_line(color = "forestgreen", size = 1) +
geom_point(aes(color = factor(topic_dominante)), size = 3) +
scale_color_brewer(palette = "Set2") +
labs(title = "Densidad de red", x = NULL, y = "Densidad", color = "Tópico") +
theme_minimal() +
theme(legend.position = "none")
p3 <- ggplot(analisis_completo, aes(x = indice_cap, y = transitividad)) +
geom_line(color = "firebrick", size = 1) +
geom_point(aes(color = factor(topic_dominante)), size = 3) +
scale_color_brewer(palette = "Set2") +
labs(title = "Transitividad", x = "Capítulo", y = "Transitividad", color = "Tópico") +
theme_minimal() +
theme(legend.position = "none")
p4 <- ggplot(analisis_completo, aes(x = indice_cap, y = gamma_max)) +
geom_line(color = "darkorange", size = 1) +
geom_point(aes(color = factor(topic_dominante)), size = 3) +
scale_color_brewer(palette = "Set2") +
labs(title = "Fuerza del tópico dominante (γ)",
x = "Capítulo", y = "γ máximo", color = "Tópico") +
theme_minimal()
grid.arrange(p1, p2, p3, p4, ncol = 2,
top = "Análisis integrado: Sentimientos, Redes y Tópicos")# Analizar sentimiento por tópico
sentimiento_por_topico <- analisis_completo %>%
group_by(topic_dominante) %>%
summarise(
sentimiento_medio = mean(sentimiento_promedio, na.rm = TRUE),
balance_medio = mean(balance, na.rm = TRUE),
n_capitulos = n(),
.groups = "drop"
)
cat("Sentimiento promedio por tópico:\n")## Sentimiento promedio por tópico:
## # A tibble: 1 × 4
## topic_dominante sentimiento_medio balance_medio n_capitulos
## <int> <dbl> <dbl> <int>
## 1 2 -0.167 -19.6 20
# Visualizar
ggplot(sentimiento_por_topico, aes(x = factor(topic_dominante), y = sentimiento_medio, fill = factor(topic_dominante))) +
geom_col(alpha = 0.8) +
geom_text(aes(label = sprintf("n=%d", n_capitulos)), vjust = -0.5, size = 4) +
scale_fill_brewer(palette = "Set2") +
labs(
title = "Sentimiento promedio por tópico",
subtitle = "¿Hay tópicos más positivos o negativos?",
x = "Tópico",
y = "Sentimiento promedio",
fill = "Tópico"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
legend.position = "none"
)# Métricas de red por tópico
red_por_topico <- analisis_completo %>%
group_by(topic_dominante) %>%
summarise(
densidad_media = mean(densidad, na.rm = TRUE),
grado_medio = mean(grado_medio, na.rm = TRUE),
transitividad_media = mean(transitividad, na.rm = TRUE),
n_capitulos = n(),
.groups = "drop"
)
cat("Métricas de red promedio por tópico:\n")## Métricas de red promedio por tópico:
## # A tibble: 1 × 5
## topic_dominante densidad_media grado_medio transitividad_media n_capitulos
## <int> <dbl> <dbl> <dbl> <int>
## 1 2 0.0144 1.50 0.254 20
# Visualizar
red_largo <- red_por_topico %>%
pivot_longer(
cols = c(densidad_media, transitividad_media),
names_to = "metrica",
values_to = "valor"
) %>%
mutate(
metrica = case_when(
metrica == "densidad_media" ~ "Densidad",
metrica == "transitividad_media" ~ "Transitividad"
)
)
ggplot(red_largo, aes(x = factor(topic_dominante), y = valor, fill = metrica)) +
geom_col(position = "dodge", alpha = 0.8) +
scale_fill_manual(values = c("Densidad" = "forestgreen", "Transitividad" = "firebrick")) +
labs(
title = "Complejidad de red por tópico",
subtitle = "¿Algunos tópicos tienen redes más densas o cohesivas?",
x = "Tópico",
y = "Valor promedio",
fill = "Métrica"
) +
theme_minimal() +
theme(
plot.title = element_text(face = "bold", size = 14),
legend.position = "bottom"
)# Seleccionar variables numéricas
vars_numericas <- analisis_completo %>%
select(
sentimiento_promedio, balance,
densidad, grado_medio, transitividad,
n_comunidades, modularidad, gamma_max
) %>%
na.omit()
# Calcular correlaciones
matriz_cor <- cor(vars_numericas, use = "complete.obs")
# Visualizar
matriz_cor_long <- melt(matriz_cor)
ggplot(matriz_cor_long, aes(x = Var1, y = Var2, fill = value)) +
geom_tile(color = "white") +
geom_text(aes(label = round(value, 2)), size = 3) +
scale_fill_gradient2(
low = "blue", mid = "white", high = "red",
midpoint = 0, limits = c(-1, 1)
) +
labs(
title = "Matriz de correlaciones entre métricas",
subtitle = "Análisis integrado de Cien Años de Soledad",
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)
)# Realizar clustering jerárquico de capítulos basado en todas las métricas
datos_clustering <- analisis_completo %>%
select(sentimiento_promedio, densidad, transitividad, gamma_max) %>%
na.omit() %>%
scale()
# Calcular distancias y clustering
dist_caps <- dist(datos_clustering)
hc_caps <- hclust(dist_caps, method = "ward.D2")
# Visualizar dendrograma
plot(hc_caps,
main = "Dendrograma de capítulos (clustering jerárquico)",
sub = "Basado en sentimiento, densidad de red, transitividad y fuerza del tópico",
xlab = "Capítulo",
ylab = "Altura",
labels = analisis_completo %>% filter(!is.na(densidad)) %>% pull(indice_cap))
rect.hclust(hc_caps, k = 4, border = 2:5)## ============================================================
## RESUMEN EJECUTIVO DEL ANÁLISIS
## ============================================================
## 1. ANÁLISIS DE SENTIMIENTOS:
cap_pos <- sentimientos_caps %>% slice_max(sentimiento_promedio, n = 1)
cap_neg <- sentimientos_caps %>% slice_min(sentimiento_promedio, n = 1)
cat(sprintf(" Capítulo más positivo: %s (%.3f)\n", cap_pos$capitulo, cap_pos$sentimiento_promedio))## Capítulo más positivo: Capitulo_19 (0.225)
## Capítulo más negativo: Capitulo_06 (-0.407)
##
## 2. ANÁLISIS DE REDES (SKIPGRAMAS):
red_densa <- metricas_redes %>% filter(!is.na(densidad)) %>% slice_max(densidad, n = 1)
red_trans <- metricas_redes %>% filter(!is.na(transitividad)) %>% slice_max(transitividad, n = 1)
cat(sprintf(" Red más densa: %s (%.4f)\n", red_densa$capitulo, red_densa$densidad))## Red más densa: Capitulo_20 (0.0205)
## Mayor transitividad: Capitulo_19 (0.4688)
cat(sprintf(" Promedio de comunidades: %.1f\n", mean(comunidades_caps$n_comunidades, na.rm = TRUE)))## Promedio de comunidades: 37.9
##
## 3. ANÁLISIS DE TÓPICOS (LDA):
## Número de tópicos: 6
## Perplejidad: 3836.15
##
## Tópicos principales (top 5 palabras):
for (i in 1:k_topicos) {
palabras <- top_terminos %>% filter(topic == i) %>% head(5) %>% pull(term)
cat(sprintf(" Tópico %d: %s\n", i, paste(palabras, collapse = ", ")))
}## Tópico 1: coronel, guerra, buendia, marquez, gerineldo
## Tópico 2: aureliano, ursula, casa, anos, amaranta
## Tópico 3: pergaminos, amor, gaston, cartas, sabio
## Tópico 4: arcadio, jose, buendia, melquiades, gitanos
## Tópico 5: rebeca, arcadio, crespi, pietro, moscote
## Tópico 6: segundo, fernanda, meme, petra, cotes
##
## ============================================================
La evolución de la valencia emocional revela la estructura cíclica de la obra, alternando entre momentos de esperanza y episodios de decadencia. Las métricas de red muestran cómo la complejidad narrativa varía entre capítulos: mayor densidad y transitividad indican episodios con múltiples hilos narrativos entrelazados.
El análisis de tópicos con LDA identifica los grandes temas de García Márquez: familia y generaciones, tiempo cíclico, violencia política, amor y erotismo, soledad existencial, y la magia de Macondo. La distribución de estos tópicos muestra cómo el autor entreteje estos elementos a lo largo de la narrativa.
La comparación integrada revela correlaciones entre la carga emocional, la complejidad estructural de las redes y la distribución temática, ofreciendo una visión cuantitativa de la maestría narrativa de García Márquez.