1 Introducción

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:

  1. Análisis de sentimientos por capítulos
  2. Análisis de redes de co-ocurrencia (skipgramas)
  3. Análisis de tópicos con LDA

La metodología sigue los principios de Procesamiento de Lenguaje Natural (NLP) presentados en las notas de clase, adaptados al análisis literario.

2 Librerías

# 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)))

3 Importación del texto

3.1 Lectura del archivo

# 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

3.2 Identificación de capítulos

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:
table(texto_df$capitulo)
## 
## 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

3.3 Corpus por capítulos

# 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
head(corpus_capitulos, 3)
## # 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…

4 Tokenización

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
head(tokens_capitulos, 10)
## # 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

5 Normalización del texto

5.1 Remover números

# 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
head(tokens_con_numeros, 10)
## # 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

5.2 Remover stopwords

# 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

5.3 Remover acentos

# 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
head(tokens_limpios, 10)
## # 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

6 Palabras más frecuentes

6.1 Top palabras por capítulo

# 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

6.2 Visualización de palabras frecuentes

# 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)
  )

6.3 Nubes de palabras por capítulo

# 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)
}

7 Análisis de sentimientos

Siguiendo la metodología de las notas de clase, construiremos un lexicón de sentimientos en español.

7.1 Lexicón de sentimientos

# 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:
cat("Palabras positivas:", sum(lexicon_sentimientos$valor == 1), "\n")
## Palabras positivas: 45
cat("Palabras negativas:", sum(lexicon_sentimientos$valor == -1), "\n")
## Palabras negativas: 55

7.2 Sentimientos por capítulo

# 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:
print(sentimientos_caps)
## # 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>

7.3 Visualización de sentimientos

# 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"
  )

7.4 Palabras con sentimiento más frecuentes

# 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)
  )

8 Análisis de redes: Skipgramas

Siguiendo la metodología de las notas de clase, utilizaremos skipgramas para construir redes de co-ocurrencia.

8.1 Tokenización en skipgramas

# 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
head(skipgramas_caps, 10)
## # 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

8.2 Remover unigramas y limpiar

# 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

8.3 Frecuencias de skipgramas por capítulo

# 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 skipgramas globales
cat("\nTop 20 skipgramas más frecuentes:\n")
## 
## 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

8.4 Construcción de redes por capítulo

# 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

8.5 Visualización de redes

# 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
    )
  }
}

8.6 Métricas de red por capítulo

# 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:
print(metricas_redes)
## # 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>

8.7 Evolución de métricas de red

# 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)
  )

8.8 Detección de comunidades

# 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:
print(comunidades_caps)
## # 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)

8.9 Palabras más importantes (Centralidad de eigenvector)

# 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

9 Análisis de tópicos con LDA

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:

  1. Cada documento es una mezcla de tópicos
  2. Cada tópico es una mezcla de palabras

9.1 Matriz documento-término

# 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:
cat("Documentos (capítulos):", dtm_caps$nrow, "\n")
## Documentos (capítulos): 20
cat("Términos (palabras únicas):", dtm_caps$ncol, "\n")
## 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 %

9.2 Ajuste del modelo LDA

# 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

9.3 Word-Topic Probabilities (Beta)

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:
print(head(topicos_beta, 10))
## # 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:
print(top_terminos)
## # 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)
  )

9.4 Greatest Difference in Beta (Log Ratio)

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:
print(beta_spread %>% arrange(desc(abs(log_ratio))) %>% head(15))
## # 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))

9.5 Document-Topic Probabilities (Gamma)

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:
print(topicos_gamma %>% head(20))
## # 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
# Resumen estadístico por tópico
cat("\nEstadísticas de gamma por tópico:\n")
## 
## 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)
  )

9.6 Per-Document Classification

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:
print(clasificacion_caps %>% select(document, topic, gamma))
## # 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
# Distribución de capítulos por tópico
cat("\nNúmero de capítulos asignados a cada tópico:\n")
## 
## Número de capítulos asignados a cada tópico:
print(clasificacion_caps %>% count(topic, sort = TRUE))
## # 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"
  )

9.7 By-Word Assignments: augment()

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:
print(head(asignaciones, 10))
## # 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

9.8 Consensus Topics

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:
print(topicos_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

9.9 Confusion Matrix of Assignments

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()
  )

9.10 Words Assigned to Wrong Topics

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

9.11 Perplejidad del modelo

# 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
cat("\nInterpretación:\n")
## 
## Interpretación:
cat("- Menor perplejidad = mejor ajuste\n")
## - Menor perplejidad = mejor ajuste
cat("- Perplejidad mide qué tan bien el modelo predice nuevos datos\n")
## - Perplejidad mide qué tan bien el modelo predice nuevos datos

9.12 Interpretación de tópicos

cat("=" %>% strrep(70), "\n")
## ======================================================================
cat("INTERPRETACIÓN DE TÓPICOS IDENTIFICADOS\n")
## INTERPRETACIÓN DE TÓPICOS IDENTIFICADOS
cat("=" %>% strrep(70), "\n\n")
## ======================================================================
# 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%
cat("=" %>% strrep(70), "\n")
## ======================================================================

10 Comparación integrada

10.1 Integración de todos los análisis

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:
print(analisis_completo)
## # 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>

10.2 Visualización integrada

# 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")

10.3 Relación entre sentimiento 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:
print(sentimiento_por_topico)
## # 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"
  )

10.4 Relación entre complejidad de red y tópicos

# 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:
print(red_por_topico)
## # 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"
  )

10.5 Matriz de correlaciones

# 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)
  )

10.6 Análisis de clusters de capítulos

# 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)

11 Conclusiones

11.1 Resumen ejecutivo

cat("=" %>% strrep(60), "\n")
## ============================================================
cat("RESUMEN EJECUTIVO DEL ANÁLISIS\n")
## RESUMEN EJECUTIVO DEL ANÁLISIS
cat("=" %>% strrep(60), "\n\n")
## ============================================================
cat("1. ANÁLISIS DE SENTIMIENTOS:\n")
## 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)
cat(sprintf("   Capítulo más negativo: %s (%.3f)\n", cap_neg$capitulo, cap_neg$sentimiento_promedio))
##    Capítulo más negativo: Capitulo_06 (-0.407)
cat("\n2. ANÁLISIS DE REDES (SKIPGRAMAS):\n")
## 
## 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)
cat(sprintf("   Mayor transitividad: %s (%.4f)\n", red_trans$capitulo, red_trans$transitividad))
##    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
cat("\n3. ANÁLISIS DE TÓPICOS (LDA):\n")
## 
## 3. ANÁLISIS DE TÓPICOS (LDA):
cat(sprintf("   Número de tópicos: %d\n", k_topicos))
##    Número de tópicos: 6
cat(sprintf("   Perplejidad: %.2f\n", perplejidad))
##    Perplejidad: 3836.15
cat("\n   Tópicos principales (top 5 palabras):\n")
## 
##    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
cat("\n", "=" %>% strrep(60), "\n")
## 
##  ============================================================

11.2 Interpretación narrativa

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.