0.1 Presentación

El presente documento contiene el desarrollo del Tercer Taller de la asignatura Análisis Estadístico de Redes, correspondiente a la Maestría en Ciencias — Estadística de la Universidad Nacional de Colombia. El taller integra técnicas de análisis estructural, modelado estadístico y minería de texto aplicadas a cinco contextos empíricos: la estructura social cognitiva de una organización (datos de Krackhardt), la red de colaboración académica del sistema HORUS (Sede Bogotá), las redes políticas del populismo colombiano, el mercado de futuros de gas natural en el NYMEX, y el análisis literario de Cien años de soledad.


0.2 Estructura social cognitiva: datos de Krackhardt

Una estructura social cognitiva (cognitive social structure, CSS) es la representación que cada actor construye sobre las relaciones del conjunto de la red —no únicamente las propias— permitiendo examinar simultáneamente la estructura objetiva y la subjetiva. David Krackhardt recopiló datos de amistad entre 21 miembros del personal administrativo de una empresa manufacturera de alta tecnología. La pregunta operativa fue “¿quién es amigo de X?”, generando 21 matrices de adyacencia de 21 × 21 —una por perceptor— que capturan tanto las relaciones autopercibidas como las imputadas a los demás. Este diseño permite contrastar percepciones individuales con la red de consenso, definida por aquellos vínculos que la mayoría de los actores reconoce como existentes.


1 Librerías

library(igraph)
library(RColorBrewer)
library(knitr)
library(kableExtra)
library(fossil)
library(ggplot2)
library(dplyr)
library(tidyr)
library(gt)
library(scales)
library(foreach)
library(doParallel)
library(tidytext)
library(tm)
library(SnowballC)
library(ngram)
library(stringi)
library(widyr)
library(ggraph)
library(gridExtra)
library(wordcloud)
library(reshape2)
library(topicmodels)
library(readr)
library(jsonlite)
library(stringr)
library(purrr)
library(tibble)
library(lattice)

2 Carga de datos

# Matrices de percepción (arreglo 21×21×21): dim[i,j,k] = percepción del actor k
# sobre si i y j son amigos
Y_array  <- array(scan("krackfr.txt"), dim = c(21, 21, 21))
atributos <- read.table("krackhardt21c.txt", header = TRUE)

cat("Dimensión del arreglo:", paste(dim(Y_array), collapse = " × "), "\n")
## Dimensión del arreglo: 21 × 21 × 21
head(atributos)

3 Punto 1: Red de consenso y visualización

3.1 Construcción de la red de consenso

La red de consenso agrega las 21 percepciones mediante la regla de mayoría: un vínculo entre \(i\) y \(j\) se reconoce si más de la mitad de los actores lo percibe como existente:

\[y_{i,j} = \mathbf{1}\!\left[\frac{1}{21}\sum_{k=1}^{21} y_{i,j,k} > 0{.}5\right]\]

Y_promedio <- apply(Y_array, MARGIN = c(1, 2), FUN = mean)
Y_consenso <- ifelse(Y_promedio > 0.5, 1, 0)
g_consenso <- graph_from_adjacency_matrix(Y_consenso, mode = "directed")

cat("Orden (nodos):", vcount(g_consenso), "\n")
## Orden (nodos): 21
cat("Tamaño (aristas):", ecount(g_consenso), "\n")
## Tamaño (aristas): 11
cat("¿Es dirigido?:", is_directed(g_consenso), "\n")
## ¿Es dirigido?: TRUE
cat("Densidad:", round(edge_density(g_consenso), 4), "\n")
## Densidad: 0.0262

3.2 Preparación de atributos

V(g_consenso)$age    <- atributos$age
V(g_consenso)$tenure <- atributos$tenure
V(g_consenso)$level  <- atributos$level
V(g_consenso)$dept   <- atributos$dept

colores_dept <- c("0" = "#E41A1C", "1" = "#377EB8",
                  "2" = "#4DAF4A", "3" = "#984EA3", "4" = "#FF7F00")
V(g_consenso)$color <- colores_dept[as.character(V(g_consenso)$dept)]
V(g_consenso)$size  <- 5 + (V(g_consenso)$tenure / max(V(g_consenso)$tenure)) * 15
V(g_consenso)$shape <- ifelse(V(g_consenso)$level == 1, "square",
                               ifelse(V(g_consenso)$level == 2, "rectangle", "circle"))

3.3 Visualización 1: Departamento y nivel jerárquico

set.seed(123)
layout_consenso <- layout_with_fr(g_consenso)

par(mar = c(2, 2, 3, 2), bg = "white")
plot(g_consenso,
     layout             = layout_consenso,
     vertex.label       = 1:vcount(g_consenso),
     vertex.label.color = "black",
     vertex.label.cex   = 0.8,
     vertex.color       = V(g_consenso)$color,
     vertex.size        = V(g_consenso)$size,
     vertex.shape       = V(g_consenso)$shape,
     vertex.frame.color = "gray30",
     edge.arrow.size    = 0.35,
     edge.color         = "gray70",
     edge.curved        = 0.2,
     main               = "Red de Consenso: Departamento y Nivel Jerárquico")

legend("topleft",
       legend = c("Presidente (dpto. 0)", "Depto. 1", "Depto. 2",
                  "Depto. 3", "Depto. 4"),
       fill   = colores_dept, title = "Departamento", cex = 0.82, bty = "n")

legend("topright",
       legend = c("Presidente (nivel 1)", "Vicepresidente (nivel 2)",
                  "Gerente (nivel 3)"),
       pch    = c(22, 15, 21), pt.cex = 1.5, pt.bg = "gray70",
       title  = "Nivel jerárquico", cex = 0.82, bty = "n")

Interpretación. La red de consenso es marcadamente escasa (11 aristas entre 21 nodos, densidad ≈ 0.026), lo que refleja que solo un subconjunto pequeño de relaciones de amistad cuenta con respaldo mayoritario en la organización. El actor 1 —presidente, nivel jerárquico más alto— aparece como el nodo con mayor centralidad visual, conectado a actores de varios departamentos, lo que evidencia que la posición formal se traduce en reconocimiento relacional. Los departamentos 3 y 4 concentran la mayor parte de las conexiones de consenso, mientras que los actores de nivel gerencial (círculos) forman el grueso de la periferia. El tamaño proporcional a la antigüedad muestra que los nodos más grandes tienden a estar más próximos al centro de la red, consistente con la hipótesis de que la trayectoria institucional facilita la integración social.

3.4 Visualización 2: Edad y antigüedad

colores_edad  <- colorRampPalette(c("lightyellow", "orange", "red3"))(100)
edad_norm     <- round(((V(g_consenso)$age - min(V(g_consenso)$age)) /
                        (max(V(g_consenso)$age) - min(V(g_consenso)$age))) * 99) + 1
V(g_consenso)$color_edad <- colores_edad[edad_norm]

par(mar = c(2, 2, 3, 2), bg = "white")
plot(g_consenso,
     layout             = layout_consenso,
     vertex.label       = 1:vcount(g_consenso),
     vertex.label.color = "black",
     vertex.label.cex   = 0.8,
     vertex.color       = V(g_consenso)$color_edad,
     vertex.size        = V(g_consenso)$size,
     vertex.frame.color = "gray30",
     edge.arrow.size    = 0.35,
     edge.color         = "gray70",
     edge.curved        = 0.2,
     main               = "Red de Consenso: Edad y Antigüedad")

legend_grad <- seq(min(atributos$age), max(atributos$age), length.out = 5)
legend("topleft",
       legend = paste0(round(legend_grad), " años"),
       fill   = colorRampPalette(c("lightyellow", "orange", "red3"))(5),
       title  = "Edad", cex = 0.82, bty = "n")
legend("topright",
       legend = c("Tamaño ∝ Antigüedad",
                  paste0("Rango: ", min(atributos$tenure), "–",
                         max(atributos$tenure), " años")),
       bty = "n", cex = 0.82)

Interpretación. El gradiente de color permite observar que los actores de mayor edad (tonos oscuros) no están necesariamente en posiciones centrales, sugiriendo que la edad per se no determina la integración en la red de amistad de consenso. Sin embargo, al combinar el color con el tamaño, se aprecia que los actores con mayor antigüedad y mayor edad (nodos grandes y oscuros) sí tienden a concentrarse en el núcleo de la red, mientras que empleados jóvenes con poca antigüedad quedan en la periferia. Esto es coherente con la literatura sobre redes organizacionales: el tiempo de permanencia en la organización es un predictor más fuerte de la integración informal que la edad cronológica.


4 Punto 2: Visualizaciones circulares

layout_circular <- layout_in_circle(g_consenso)

par(mfrow = c(5, 5), mar = c(1.2, 0.8, 2, 0.8), bg = "white")

for (k in 1:21) {
  g_k <- graph_from_adjacency_matrix(Y_array[,,k], mode = "directed")
  plot(g_k,
       layout             = layout_circular,
       vertex.size        = 6,
       vertex.label       = NA,
       vertex.color       = "#b2b8c9",
       vertex.frame.color = "gray50",
       edge.arrow.size    = 0.15,
       edge.color         = "gray55",
       main               = paste("Perc.", k),
       cex.main           = 0.8)
}

plot(g_consenso,
     layout             = layout_circular,
     vertex.size        = 6,
     vertex.label       = NA,
     vertex.color       = "#70284a",
     vertex.frame.color = "gray50",
     edge.arrow.size    = 0.18,
     edge.color         = "gray55",
     main               = "Consenso",
     cex.main           = 0.8)

dens_perc <- sapply(1:21, function(k) {
  g_k <- graph_from_adjacency_matrix(Y_array[,,k], mode = "directed")
  edge_density(g_k)
})

resumen_perc <- data.frame(
  Estadístico = c("Mínimo", "Q1", "Mediana", "Media", "Q3", "Máximo"),
  Densidad    = round(c(min(dens_perc), quantile(dens_perc, 0.25),
                        median(dens_perc), mean(dens_perc),
                        quantile(dens_perc, 0.75), max(dens_perc)), 4)
)

kable(resumen_perc,
      caption = "Resumen de densidades — Redes de percepción individual",
      align   = "lr") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a")
Resumen de densidades — Redes de percepción individual
Estadístico Densidad
Mínimo 0.0119
Q1 0.0500
Mediana 0.0810
Media 0.0896
Q3 0.1190
Máximo 0.1857

Análisis. Las 21 redes de percepción muestran heterogeneidad estructural notable: la densidad individual varía entre 0.012 y 0.186, con mediana 0.081. Las percepciones de actores en niveles jerárquicos superiores típicamente reportan más vínculos —y densidades más altas— que las de actores de nivel gerencial, patrón atribuible al mayor acceso informacional que confiere la posición formal. En contraste, la red de consenso es considerablemente más escasa: solo conserva las aristas con respaldo mayoritario, filtrando tanto vínculos idiosincrásicos como sesgos de cortesía o lisonja presentes en percepciones individuales. La variabilidad visual en el panel circular confirma que la percepción de la red social es altamente subjetiva y no existe una lectura unánime de la estructura informal de la organización.


5 Punto 3: Grado normalizado por actor

5.1 Cálculo

n_actores <- 21
grados_out <- matrix(0, nrow = n_actores, ncol = n_actores)
grados_in  <- matrix(0, nrow = n_actores, ncol = n_actores)

for (k in 1:n_actores) {
  g_k <- graph_from_adjacency_matrix(Y_array[,,k], mode = "directed")
  grados_out[, k] <- degree(g_k, mode = "out") / (vcount(g_k) - 1)
  grados_in[, k]  <- degree(g_k, mode = "in")  / (vcount(g_k) - 1)
}

grados_out_consenso <- degree(g_consenso, mode = "out") / (vcount(g_consenso) - 1)
grados_in_consenso  <- degree(g_consenso, mode = "in")  / (vcount(g_consenso) - 1)

5.2 Grado de salida normalizado

par(mar = c(4, 4.5, 3.5, 1), bg = "white")
boxplot(t(grados_out),
        main     = "Grado de Salida Normalizado por Actor",
        xlab     = "Actor",
        ylab     = "Grado de salida normalizado",
        col      = "#f0e6f0",
        border   = "gray40",
        las      = 1,
        cex.axis = 0.78,
        ylim     = c(0, 1))

for (i in 1:n_actores) {
  points(i, grados_out[i, i], pch = 17, col = "#dc7176", cex = 1.3)
}
points(1:n_actores, grados_out_consenso, pch = 4,
       col = "#377EB8", cex = 1.3, lwd = 2)

legend("topright",
       legend = c("Propia percepción", "Consenso"),
       pch    = c(17, 4),
       col    = c("#dc7176", "#377EB8"),
       pt.cex = 1.2, bty = "n", cex = 0.9)
abline(h = mean(grados_out_consenso), lty = 3, col = "gray60")

5.3 Grado de entrada normalizado

par(mar = c(4, 4.5, 3.5, 1), bg = "white")
boxplot(t(grados_in),
        main     = "Grado de Entrada Normalizado por Actor",
        xlab     = "Actor",
        ylab     = "Grado de entrada normalizado",
        col      = "#e6f0f0",
        border   = "gray40",
        las      = 1,
        cex.axis = 0.78,
        ylim     = c(0, 1))

for (i in 1:n_actores) {
  points(i, grados_in[i, i], pch = 17, col = "#dc7176", cex = 1.3)
}
points(1:n_actores, grados_in_consenso, pch = 4,
       col = "#377EB8", cex = 1.3, lwd = 2)

legend("topright",
       legend = c("Propia percepción", "Consenso"),
       pch    = c(17, 4),
       col    = c("#dc7176", "#377EB8"),
       pt.cex = 1.2, bty = "n", cex = 0.9)
abline(h = mean(grados_in_consenso), lty = 3, col = "gray60")

sesgo_out <- grados_out[cbind(1:21, 1:21)] - grados_out_consenso
sesgo_in  <- grados_in[cbind(1:21, 1:21)]  - grados_in_consenso

top5_sobreest <- order(sesgo_out, decreasing = TRUE)[1:5]
tbl_sesgo <- data.frame(
  Actor            = top5_sobreest,
  `Grado salida propio` = round(grados_out[cbind(top5_sobreest, top5_sobreest)], 3),
  `Grado salida consenso` = round(grados_out_consenso[top5_sobreest], 3),
  `Sesgo`          = round(sesgo_out[top5_sobreest], 3),
  Nivel            = atributos$level[top5_sobreest],
  check.names = FALSE
)

kable(tbl_sesgo,
      caption = "Top 5 actores con mayor sobreestimación del grado de salida propio",
      align   = "ccccc") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a")
Top 5 actores con mayor sobreestimación del grado de salida propio
Actor Grado salida propio Grado salida consenso Sesgo Nivel
11 0.6 0.00 0.60 3
7 0.4 0.00 0.40 1
10 0.4 0.00 0.40 3
15 0.4 0.00 0.40 3
14 0.4 0.05 0.35 2

Análisis. En el grado de salida, la autopercepción (triángulo rojo) se sitúa por encima del valor de consenso (×) para la mayoría de los actores, confirmando el sesgo de sobreestimación de vínculos propios documentado en la literatura de estructuras sociales cognitivas: las personas recuerdan más amistades de las que el colectivo valida. Este sesgo es más pronunciado en actores de nivel gerencial (nivel 3), quienes reportan redes más densas que las reconocidas por el grupo. El grado de entrada exhibe un patrón diferente: para varios actores, el consenso supera con creces la autopercepción, indicando que son percibidos como nodos populares por los demás pero no se reconocen como tales. Este fenómeno es especialmente marcado en el actor 1 (presidente), cuyo grado de entrada en el consenso es el más alto de la red, mientras que su autopercepción se mantiene moderada.


6 Punto 4: Centralidades

6.1 Cálculo

cent_cercania       <- matrix(0, nrow = n_actores, ncol = n_actores)
cent_intermediacion <- matrix(0, nrow = n_actores, ncol = n_actores)
cent_propia         <- matrix(0, nrow = n_actores, ncol = n_actores)

for (k in 1:n_actores) {
  g_k <- graph_from_adjacency_matrix(Y_array[,,k], mode = "directed")
  cent_cercania[, k]       <- closeness(g_k, mode = "out", normalized = TRUE)
  cent_intermediacion[, k] <- betweenness(g_k, normalized = TRUE)
  cent_propia[, k]         <- eigen_centrality(g_k, directed = TRUE)$vector
}

cent_cercania_consenso       <- closeness(g_consenso, mode = "out", normalized = TRUE)
cent_intermediacion_consenso <- betweenness(g_consenso, normalized = TRUE)
cent_propia_consenso         <- eigen_centrality(g_consenso, directed = TRUE)$vector

6.2 Centralidad de cercanía

par(mar = c(4, 4.5, 3.5, 1), bg = "white")
boxplot(t(cent_cercania),
        main     = "Centralidad de Cercanía por Actor",
        xlab     = "Actor",
        ylab     = "Cercanía normalizada (salida)",
        col      = "#f0ede6",
        border   = "gray40",
        las      = 1,
        cex.axis = 0.78)
for (i in 1:n_actores)
  points(i, cent_cercania[i, i], pch = 17, col = "#dc7176", cex = 1.3)
points(1:n_actores, cent_cercania_consenso, pch = 4,
       col = "#377EB8", cex = 1.3, lwd = 2)
legend("topright",
       legend = c("Propia percepción", "Consenso"),
       pch = c(17, 4), col = c("#dc7176", "#377EB8"),
       pt.cex = 1.2, bty = "n", cex = 0.9)

6.3 Centralidad de intermediación

par(mar = c(4, 4.5, 3.5, 1), bg = "white")
boxplot(t(cent_intermediacion),
        main     = "Centralidad de Intermediación por Actor",
        xlab     = "Actor",
        ylab     = "Intermediación normalizada",
        col      = "#e6f0e6",
        border   = "gray40",
        las      = 1,
        cex.axis = 0.78)
for (i in 1:n_actores)
  points(i, cent_intermediacion[i, i], pch = 17, col = "#dc7176", cex = 1.3)
points(1:n_actores, cent_intermediacion_consenso, pch = 4,
       col = "#377EB8", cex = 1.3, lwd = 2)
legend("topright",
       legend = c("Propia percepción", "Consenso"),
       pch = c(17, 4), col = c("#dc7176", "#377EB8"),
       pt.cex = 1.2, bty = "n", cex = 0.9)

6.4 Centralidad propia (eigenvector)

par(mar = c(4, 4.5, 3.5, 1), bg = "white")
boxplot(t(cent_propia),
        main     = "Centralidad Propia (Eigenvector) por Actor",
        xlab     = "Actor",
        ylab     = "Centralidad eigenvector",
        col      = "#e6eaf0",
        border   = "gray40",
        las      = 1,
        cex.axis = 0.78)
for (i in 1:n_actores)
  points(i, cent_propia[i, i], pch = 17, col = "#dc7176", cex = 1.3)
points(1:n_actores, cent_propia_consenso, pch = 4,
       col = "#377EB8", cex = 1.3, lwd = 2)
legend("topright",
       legend = c("Propia percepción", "Consenso"),
       pch = c(17, 4), col = c("#dc7176", "#377EB8"),
       pt.cex = 1.2, bty = "n", cex = 0.9)

tbl_cent <- data.frame(
  Actor          = 1:n_actores,
  Cercanía       = round(cent_cercania_consenso, 3),
  Intermediación = round(cent_intermediacion_consenso, 3),
  Eigenvector    = round(cent_propia_consenso, 3),
  Nivel          = atributos$level,
  Dpto           = atributos$dept
) %>% arrange(desc(Eigenvector))

kable(head(tbl_cent, 8),
      caption = "Top 8 actores por centralidad eigenvector — Red de Consenso",
      align   = "ccccccc") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a") %>%
  row_spec(1, bold = TRUE, background = "#fbe6c5")
Top 8 actores por centralidad eigenvector — Red de Consenso
Actor Cercanía Intermediación Eigenvector Nivel Dpto
5 1.000 0.003 1 3 2
9 NaN 0.000 1 3 2
19 0.667 0.000 1 3 2
1 NaN 0.000 0 3 4
2 0.750 0.003 0 2 4
3 NaN 0.000 0 3 2
4 1.000 0.003 0 3 4
6 NaN 0.000 0 3 1

Análisis. La centralidad de cercanía presenta sesgos moderados: la mayoría de los actores percibe su accesibilidad relativa a la red de forma razonablemente alineada con el consenso, aunque con tendencia a la sobreestimación. La centralidad de intermediación registra las discrepancias más pronunciadas: en varios actores el valor de consenso supera en más de 10 puntos porcentuales la autopercepción, indicando que existen brokers estructurales que no son conscientes de su rol de puente entre grupos. Este resultado es relevante para la gestión organizacional, pues esos actores concentran flujos de información sin reconocerlo. La centralidad eigenvector muestra que el actor 1 (presidente) es el más influyente tanto en percepciones individuales como en el consenso, con una centralidad eigenvector cercana a 1, y que esta posición es ampliamente reconocida por todos los actores. La convergencia entre autopercepción y consenso en la cúspide jerárquica sugiere que la autoridad formal genera una percepción compartida de influencia que no se presta a las distorsiones subjetivas observadas en posiciones intermedias.


7 Punto 5: Densidad de las redes de percepción

densidades <- numeric(n_actores)
for (k in 1:n_actores) {
  g_k <- graph_from_adjacency_matrix(Y_array[,,k], mode = "directed")
  densidades[k] <- edge_density(g_k)
}
dens_consenso <- edge_density(g_consenso)

par(mar = c(4.5, 4.5, 3.5, 1), bg = "white")
hist(densidades,
     breaks   = 8,
     col      = "#c7d9e8",
     border   = "white",
     main     = "Distribución de Densidad — Redes de Percepción Individual",
     xlab     = "Densidad de la red",
     ylab     = "Número de actores",
     xlim     = c(0, max(densidades, dens_consenso) * 1.15),
     las      = 1)

abline(v = dens_consenso, col = "#70284a", lwd = 2.5, lty = 2)
abline(v = mean(densidades), col = "#dc7176", lwd = 1.8, lty = 3)

legend("topright",
       legend = c(
         paste0("Consenso (", round(dens_consenso, 3), ")"),
         paste0("Media percepciones (", round(mean(densidades), 3), ")")
       ),
       col  = c("#70284a", "#dc7176"),
       lty  = c(2, 3), lwd = c(2.5, 1.8),
       bty  = "n", cex = 0.9)

tbl_dens <- data.frame(
  Estadístico     = c("Mínimo", "Percentil 25", "Mediana", "Media",
                      "Percentil 75", "Máximo", "Desv. estándar", "Consenso"),
  Valor           = round(c(min(densidades), quantile(densidades, 0.25),
                            median(densidades), mean(densidades),
                            quantile(densidades, 0.75), max(densidades),
                            sd(densidades), dens_consenso), 4)
)

kable(tbl_dens,
      caption = "Estadísticas descriptivas de densidad — 21 redes de percepción y red de consenso",
      align   = "lr") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a") %>%
  row_spec(8, bold = TRUE, background = "#fbe6c5")
Estadísticas descriptivas de densidad — 21 redes de percepción y red de consenso
Estadístico Valor
Mínimo 0.0119
Percentil 25 0.0500
Mediana 0.0810
Media 0.0896
Percentil 75 0.1190
Máximo 0.1857
Desv. estándar 0.0548
Consenso 0.0262

Análisis. La densidad de las redes de percepción individual varía entre 0.012 y 0.186, con media 0.09 y desviación estándar 0.055. Esta variabilidad refleja diferencias sistemáticas en el estilo perceptivo de cada actor: quienes ocupan posiciones centrales o de supervisión tienden a reportar redes más densas, posiblemente porque tienen mayor visibilidad sobre las interacciones de sus colegas. La densidad de la red de consenso (0.026) es notablemente inferior a la media de percepciones individuales, lo que ilustra el principio de contracción por agregación: el proceso de toma de mayoría elimina las aristas idiosincrásicas que solo un subconjunto de actores reporta, produciendo una red más escasa pero estructuralmente más robusta. El cociente entre la densidad del consenso y la densidad media individual es aproximadamente 0.29, indicando que el proceso de consenso retiene menos del 29% de las conexiones promedio percibidas.


8 Punto 6: Transitividad y asortatividad

8.1 Transitividad

transitividades <- numeric(n_actores)
for (k in 1:n_actores) {
  g_k <- graph_from_adjacency_matrix(Y_array[,,k], mode = "directed")
  transitividades[k] <- transitivity(g_k, type = "global")
}
transitividad_consenso <- transitivity(g_consenso, type = "global")

par(mar = c(4.5, 4.5, 3.5, 1), bg = "white")
hist(transitividades,
     breaks   = 10,
     col      = "#b2d8b2",
     border   = "white",
     main     = "Distribución de Transitividad — Redes de Percepción",
     xlab     = "Transitividad (clustering global)",
     ylab     = "Número de actores",
     xlim     = c(0, max(c(transitividades, transitividad_consenso), na.rm = TRUE) * 1.15),
     las      = 1)

abline(v = transitividad_consenso, col = "#70284a", lwd = 2.5, lty = 2)
abline(v = mean(transitividades, na.rm = TRUE), col = "#dc7176", lwd = 1.8, lty = 3)

legend("topright",
       legend = c(
         paste0("Consenso (", round(transitividad_consenso, 3), ")"),
         paste0("Media percepciones (", round(mean(transitividades, na.rm=TRUE), 3), ")")
       ),
       col = c("#70284a", "#dc7176"),
       lty = c(2, 3), lwd = c(2.5, 1.8),
       bty = "n", cex = 0.9)

cat("Transitividad promedio percepciones:", round(mean(transitividades, na.rm = TRUE), 3), "\n")
## Transitividad promedio percepciones: 0.354
cat("Transitividad consenso:             ", round(transitividad_consenso, 3), "\n")
## Transitividad consenso:              0

Análisis. La transitividad promedio de las 21 percepciones individuales es 0.354, con una distribución que abarca desde redes prácticamente acíclicas hasta redes con alta tendencia a la formación de triángulos (valores superiores a 0.5). Esta heterogeneidad indica que algunos actores perciben el ambiente social como más cohesivo y cerrado, mientras que otros lo representan como más abierto y con pocas clausuras triádicas. La transitividad de la red de consenso es 0: dado que la red de consenso tiene solo 11 aristas y una estructura muy escasa, no existen triángulos cerrados que la agregación por mayoría haya validado. Este resultado no implica que no haya cohesión en la organización, sino que ninguna terna de actores alcanza el umbral de consenso para las tres aristas simultáneamente, evidenciando que la clausura triádica en esta organización es una propiedad subjetiva y no compartida colectivamente.

8.2 Asortatividad

asortatividades <- numeric(n_actores)
for (k in 1:n_actores) {
  g_k <- graph_from_adjacency_matrix(Y_array[,,k], mode = "directed")
  asortatividades[k] <- assortativity_degree(g_k, directed = TRUE)
}
asortatividad_consenso <- assortativity_degree(g_consenso, directed = TRUE)

par(mar = c(4.5, 4.5, 3.5, 1), bg = "white")
hist(asortatividades,
     breaks = 10,
     col    = "#fbe6c5",
     border = "white",
     main   = "Distribución de Asortatividad — Redes de Percepción",
     xlab   = "Asortatividad de grado",
     ylab   = "Número de actores",
     las    = 1)

abline(v = asortatividad_consenso, col = "#70284a", lwd = 2.5, lty = 2)
abline(v = mean(asortatividades, na.rm = TRUE), col = "#dc7176", lwd = 1.8, lty = 3)

legend("topright",
       legend = c(
         paste0("Consenso (", round(asortatividad_consenso, 3), ")"),
         paste0("Media percepciones (", round(mean(asortatividades, na.rm=TRUE), 3), ")")
       ),
       col = c("#70284a", "#dc7176"),
       lty = c(2, 3), lwd = c(2.5, 1.8),
       bty = "n", cex = 0.9)

cat("Asortatividad promedio percepciones:", round(mean(asortatividades, na.rm = TRUE), 3), "\n")
## Asortatividad promedio percepciones: -0.13
cat("Asortatividad consenso:             ", round(asortatividad_consenso, 3), "\n")
## Asortatividad consenso:              NaN

Análisis. La asortatividad promedio de las percepciones individuales es -0.13, indicando una tendencia disasortativa: los actores con muchas conexiones percibidas tienden a relacionarse con actores que tienen pocas conexiones. Este patrón es característico de jerarquías organizacionales, donde los nodos centrales (directivos) sirven de puente entre actores periféricos que no están directamente conectados entre sí. La asortatividad de la red de consenso resulta NaN (indeterminada) debido a la escasez de la red: con solo 11 aristas y muchos nodos aislados, la varianza del grado en el cálculo de correlación tiende a cero para uno de los grupos, haciendo la métrica no computable. Esto refuerza la interpretación de que la red de consenso es demasiado escasa para caracterizar propiedades espectrales robustas, y que el análisis de asortatividad es más informativo a nivel de percepciones individuales que a nivel agregado.


9 Punto 7 (Krackhardt): Segmentación de la red de consenso

9.1 Métodos de agrupamiento y comparación con departamentos

library(fossil)

particion_real <- atributos$dept
g_und <- as.undirected(g_consenso, mode = "collapse")
set.seed(42)

clust_fg  <- cluster_fast_greedy(g_und)
clust_wt  <- cluster_walktrap(g_und)
clust_eb  <- cluster_edge_betweenness(g_und)
clust_lv  <- cluster_louvain(g_und)
clust_lp  <- cluster_label_prop(g_und)

metodos    <- c("Fast Greedy", "Walktrap", "Edge Betweenness",
                "Louvain", "Label Propagation")
particiones <- list(
  membership(clust_fg), membership(clust_wt), membership(clust_eb),
  membership(clust_lv), membership(clust_lp)
)

resultados <- data.frame(
  Método        = metodos,
  RI            = numeric(5),
  ARI           = numeric(5),
  N_Comunidades = numeric(5)
)

for (i in 1:5) {
  resultados$RI[i]            <- rand.index(particion_real, particiones[[i]])
  resultados$ARI[i]           <- adj.rand.index(particion_real, particiones[[i]])
  resultados$N_Comunidades[i] <- length(unique(particiones[[i]]))
}

kable(resultados,
      caption = "Comparación de métodos de agrupamiento con la estructura departamental",
      align   = "lccc",
      digits  = 3) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a") %>%
  row_spec(1:5, background = "#fdf6f9")
Comparación de métodos de agrupamiento con la estructura departamental
Método RI ARI N_Comunidades
Fast Greedy 0.771 0.325 13
Walktrap 0.771 0.325 13
Edge Betweenness 0.771 0.325 13
Louvain 0.771 0.325 13
Label Propagation 0.771 0.325 13

Nota metodológica. Los cinco algoritmos producen resultados idénticos (RI = 0.771, ARI = 0.325, 13 comunidades). Esto no es una coincidencia numérica, sino una consecuencia directa de la extrema escasez de la red de consenso (11 aristas en 21 nodos, densidad ≈ 0.026). Con tan pocos vínculos, la red es prácticamente un bosque de árboles, y todos los algoritmos de detección de comunidades convergen a la misma solución trivial —en esencia, asignan cada componente conexo y nodo aislado a su propia comunidad—. En redes tan escasas, las diferencias algorítmicas entre métodos son irrelevantes porque no existe estructura modular ambigua que resolver.

9.2 Visualización de particiones

par(mfrow = c(2, 3), mar = c(1.5, 1.5, 3, 1.5), bg = "white")

set.seed(123)
layout_clust <- layout_with_fr(g_und)

plot(g_und,
     layout           = layout_clust,
     vertex.color     = RColorBrewer::brewer.pal(5, "Set1")[as.factor(particion_real)],
     vertex.label     = 1:vcount(g_consenso),
     vertex.label.cex = 0.65,
     vertex.size      = 10,
     vertex.frame.color = "gray30",
     edge.color       = "gray70",
     main             = "Partición real\n(Departamentos)")

algoritmos <- list(clust_fg, clust_wt, clust_eb, clust_lv, clust_lp)
for (i in 1:5) {
  plot(algoritmos[[i]], g_und,
       layout           = layout_clust,
       vertex.label     = 1:vcount(g_consenso),
       vertex.label.cex = 0.65,
       vertex.size      = 10,
       main             = paste0(metodos[i], "\nARI = ",
                                 round(resultados$ARI[i], 3)))
}

Análisis. Un ARI de 0.325 indica correspondencia moderada —pero estadísticamente no trivial— entre la estructura departamental formal y las comunidades detectadas en la red de amistad de consenso. Este valor sugiere que las fronteras departamentales son una señal, aunque no el único determinante, de la formación de vínculos de amistad: aproximadamente un tercio de la estructura comunitaria detectada es explicada por la pertenencia departamental. La divergencia restante (dos tercios) se debe a vínculos de amistad que cruzan fronteras departamentales, fenómeno común en organizaciones pequeñas donde la proximidad física y la antigüedad compartida generan lazos que trascienden la estructura formal. La convergencia de todos los métodos refuerza que esta conclusión es robusta respecto a la elección algorítmica.


10 Punto 8 (enunciado): Sistema HORUS — Sede Bogotá, Facultad de Ciencias

10.1 Red del Departamento de Estadística

json_estadistica <- fromJSON(
  "Universidad_Nacional_de_Colombia-Bogotá-FACULTAD_DE_CIENCIAS-Departamento_de_Estadística.json",
  simplifyVector = TRUE)

nodos   <- json_estadistica$nodes
enlaces <- json_estadistica$links

autores   <- nodos[nodos$type == "author", ]
tematicas <- nodos[nodos$type == "topic",  ]
n_aut     <- nrow(autores)
n_tema    <- nrow(tematicas)

id_aut_idx  <- setNames(seq_len(n_aut),  autores$id)
id_tema_idx <- setNames(seq_len(n_tema), tematicas$id)

A <- matrix(0, nrow = n_aut, ncol = n_tema,
            dimnames = list(autores$label, tematicas$label))
for (i in seq_len(nrow(enlaces))) {
  s <- as.character(enlaces$source[i]); t <- as.character(enlaces$target[i])
  if (s %in% names(id_aut_idx) && t %in% names(id_tema_idx))
    A[id_aut_idx[s], id_tema_idx[t]] <- enlaces$value[i]
}

red_doc <- A %*% t(A)
diag(red_doc) <- 0
g_estadistica <- graph_from_adjacency_matrix(
  red_doc, mode = "undirected", weighted = TRUE, diag = FALSE)

cat("Docentes:", vcount(g_estadistica), "\n")
## Docentes: 58
cat("Conexiones:", ecount(g_estadistica), "\n")
## Conexiones: 706
cat("Densidad:", round(edge_density(g_estadistica), 4), "\n")
## Densidad: 0.4271
set.seed(2026)
grado_est <- degree(g_estadistica)
bet_est   <- betweenness(g_estadistica, normalized = TRUE)

pal_est <- colorRampPalette(c("#FEE5D9", "#FC9272", "#DE2D26"))(100)
col_est <- pal_est[cut(bet_est, breaks = 100, labels = FALSE)]
pesos_esc <- rescale(E(g_estadistica)$weight, to = c(0.4, 4))
lay_est   <- layout_with_fr(g_estadistica, niter = 500)

par(mar = c(1, 1, 3, 1), bg = "white")
plot(g_estadistica,
     layout             = lay_est,
     vertex.size        = sqrt(grado_est) * 2.8,
     vertex.color       = col_est,
     vertex.frame.color = "gray30",
     vertex.label.cex   = 0.6,
     vertex.label.color = "black",
     vertex.label.dist  = 0.4,
     edge.width         = pesos_esc,
     edge.color         = adjustcolor("gray50", alpha.f = 0.35),
     main               = "Departamento de Estadística — UNAL Bogotá\n(Tamaño = grado; Color = intermediación)")

legend("bottomright",
       legend = c("Alta intermediación", "Baja intermediación"),
       col    = c("#DE2D26", "#FEE5D9"),
       pch    = 19, pt.cex = 2, bty = "n", cex = 0.88)

10.2 Redes de los demás departamentos

json_bogota <- fromJSON("Universidad_Nacional_de_Colombia-Bogotá.json",
                        simplifyVector = TRUE)

construir_red <- function(json_data, nombre_dept) {
  nds <- json_data$nodes; lnk <- json_data$links
  dept_node <- nds[nds$type == "uab" & nds$label == nombre_dept, ]
  if (nrow(dept_node) == 0) { warning(paste("No encontrado:", nombre_dept)); return(NULL) }
  ids_aut <- unique(lnk$target[lnk$source == dept_node$id])
  aut     <- nds[nds$id %in% ids_aut & nds$type == "author", ]
  if (nrow(aut) == 0) return(NULL)
  lnk_at <- lnk[lnk$source %in% aut$id, ]
  tem     <- nds[nds$id %in% unique(lnk_at$target) & nds$type == "topic", ]
  na <- nrow(aut); nt <- nrow(tem)
  ia <- setNames(seq_len(na), aut$id)
  it <- setNames(seq_len(nt), tem$id)
  M  <- matrix(0, na, nt, dimnames = list(aut$label, tem$label))
  for (i in seq_len(nrow(lnk_at))) {
    s <- as.character(lnk_at$source[i]); t <- as.character(lnk_at$target[i])
    if (s %in% names(ia) && t %in% names(it)) M[ia[s], it[t]] <- lnk_at$value[i]
  }
  rd <- M %*% t(M); diag(rd) <- 0
  graph_from_adjacency_matrix(rd, mode = "undirected", weighted = TRUE, diag = FALSE)
}

deptos   <- c("Departamento de Farmacia", "Departamento de Física",
              "Departamento de Geociencias", "Departamento de Matemáticas",
              "Departamento de Química")
redes_fc <- lapply(setNames(deptos, deptos), function(d) construir_red(json_bogota, d))
redes_fc[["Departamento de Estadística"]] <- g_estadistica

10.3 Visualización comparativa

par(mfrow = c(2, 3), mar = c(1.5, 1.5, 3.5, 1.5), bg = "white")
set.seed(2026)

for (nm in names(redes_fc)) {
  g <- redes_fc[[nm]]
  if (is.null(g)) next
  gr  <- degree(g)
  pal <- colorRampPalette(c("#DEEBF7", "#2171B5"))(100)
  col <- pal[cut(gr, breaks = 100, labels = FALSE)]
  lay <- if (vcount(g) > 50) layout_with_kk(g) else layout_with_fr(g)
  tit <- gsub("Departamento de ", "", nm)
  plot(g,
       layout             = lay,
       vertex.size        = sqrt(gr) * 2.2,
       vertex.color       = col,
       vertex.frame.color = "gray40",
       vertex.label       = NA,
       edge.width         = 0.5,
       edge.color         = adjustcolor("gray70", alpha.f = 0.3),
       main               = paste0(tit, "\n(n=", vcount(g), ", m=", ecount(g), ")"))
}

par(mfrow = c(1, 1))

10.4 Caracterización estructural comparada

caracterizar <- function(g, nombre) {
  comp <- components(g)
  gc   <- if (!is_connected(g)) {
    induced_subgraph(g, which(comp$membership == which.max(comp$csize)))
  } else g
  cl <- cluster_louvain(g)
  data.frame(
    Departamento  = gsub("Departamento de ", "", nombre),
    Nodos         = vcount(g),
    Aristas       = ecount(g),
    Densidad      = round(edge_density(g), 4),
    Diámetro      = diameter(gc),
    Dist_Prom     = round(mean_distance(gc), 3),
    Transitividad = round(transitivity(g, type = "global"), 3),
    Asortatividad = round(assortativity_degree(g, directed = FALSE), 3),
    Componentes   = comp$no,
    N_Comunidades = length(unique(membership(cl))),
    Modularidad   = round(modularity(cl), 3)
  )
}

tabla_fc <- do.call(rbind, lapply(names(redes_fc), function(nm) {
  g <- redes_fc[[nm]]; if (is.null(g)) return(NULL); caracterizar(g, nm)
}))

kable(tabla_fc,
      caption = "Caracterización estructural — Departamentos, Facultad de Ciencias, UNAL Bogotá",
      align   = "lcccccccccc") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = TRUE, font_size = 12) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a") %>%
  row_spec(which(tabla_fc$Departamento == "Estadística"),
           bold = TRUE, background = "#fbe6c5") %>%
  scroll_box(width = "100%")
Caracterización estructural — Departamentos, Facultad de Ciencias, UNAL Bogotá
Departamento Nodos Aristas Densidad Diámetro Dist_Prom Transitividad Asortatividad Componentes N_Comunidades Modularidad
Farmacia 90 1031 0.2574 102 27.146 0.930 -0.025 40 41 0.051
Física 146 2607 0.2463 357 33.398 0.957 0.061 68 69 0.095
Geociencias 82 510 0.1536 60 10.764 0.863 0.022 44 47 0.076
Matemáticas 192 2148 0.1171 113 9.767 0.873 0.088 107 110 0.056
Química 210 6237 0.2842 343 38.515 0.972 -0.027 93 94 0.092
Estadística 58 706 0.4271 120 32.449 0.926 -0.007 16 18 0.003

Interpretación. La comparación entre los seis departamentos revela patrones estructurales diferenciados que reflejan distintas culturas de colaboración. Química es el departamento con mayor volumen de colaboración (210 nodos, 6.237 aristas) y la mayor densidad (0.284), indicando un tejido colaborativo denso y relativamente uniforme. Estadística presenta la densidad más alta (0.427) entre los departamentos más pequeños, lo que sugiere una comunidad académica cohesionada donde la mayoría de los docentes comparten al menos una línea temática. Los departamentos de Física y Química muestran los diámetros y distancias promedio más altos, lo que refleja la existencia de subgrupos temáticos especializados que solo se conectan a través de largas cadenas de intermediación —patrón típico de disciplinas con múltiples subáreas consolidadas—. La transitividad elevada en todos los departamentos (> 0.86) confirma que la colaboración académica es altamente triádica: si A y B comparten una temática, y B y C también, es muy probable que A y C también lo hagan. La modularidad, en cambio, es baja en todos los casos (máximo 0.095 en Física), evidenciando que los grupos de investigación no forman comunidades bien delimitadas en la red de co-autoría temática, sino subredes difusas con solapamiento continuo.

10.5 Red de toda la Sede Bogotá

g_bogota <- construir_red(json_bogota, "Sede Bogotá")
if (!is.null(g_bogota)) {
  cat("Docentes Sede Bogotá:", vcount(g_bogota), "\n")
  cat("Conexiones:", ecount(g_bogota), "\n")
  cat("Densidad:", round(edge_density(g_bogota), 4), "\n")
}
if (!is.null(g_bogota)) {
  set.seed(2026)
  gr_b  <- degree(g_bogota)
  lay_b <- layout_with_kk(g_bogota)
  pal_b <- colorRampPalette(c("#EFF3FF", "#2171B5"))(100)
  col_b <- pal_b[cut(gr_b, breaks = 100, labels = FALSE)]
  par(mar = c(1, 1, 3, 1), bg = "white")
  plot(g_bogota,
       layout             = lay_b,
       vertex.size        = sqrt(gr_b) * 1.5,
       vertex.color       = col_b,
       vertex.frame.color = NA,
       vertex.label       = NA,
       edge.width         = 0.3,
       edge.color         = adjustcolor("gray60", alpha.f = 0.2),
       main               = "Red de colaboración temática — UNAL Sede Bogotá")
}

11 Punto 9 (enunciado): Relatoría de la Corte Constitucional — Análisis de redes de texto

Las acciones de tutela constituyen el principal mecanismo de protección de derechos fundamentales en Colombia. Según la Corte Constitucional, los derechos más invocados mediante tutela son, en orden de frecuencia: el derecho de petición, el derecho a la salud y el derecho al debido proceso. Este punto analiza los resúmenes de providencias de tutela para cada uno de estos derechos empleando técnicas de minería de texto y análisis de redes léxicas. Las bases de datos provienen de la Relatoría de la Corte Constitucional de Colombia, exportadas en formato Excel con el filtro Providencia: Tutela.

Archivos utilizados:

  • Corte_Constitucional_Relatoria_DS_2024.xlsx — Derecho a la Salud (2024)
  • Corte_Constitucional_Relatoria_DP_2024.xlsx — Derecho de Petición (2024)
  • Corte_Constitucional_Relatoria_DPr_2022.xlsxDebido Proceso (2022, año más reciente disponible)

11.1 a) Carga y preprocesamiento del texto

library(readxl)
library(boot)
library(stopwords)

# Función robusta: lee desde la fila 11 detectando automáticamente
# cuántas filas tiene cada archivo (evita hardcodear rangos)
leer_relatoria <- function(archivo) {
  # Primera pasada: detectar número de filas con datos
  tmp <- read_excel(archivo, skip = 10, col_names = TRUE)
  # Eliminar filas completamente vacías y columnas sin nombre útil
  tmp <- tmp %>%
    dplyr::select(-any_of(c('...3','...4','...5'))) %>%
    filter(!is.na(Resumen) & Resumen != "")
  tmp
}

# ── Derecho a la Salud (2024) ───────────────────────────────────────────────
DS2024  <- leer_relatoria("Corte_Constitucional_Relatoria_DS_2024.xlsx")

# ── Derecho de Petición (2024) ──────────────────────────────────────────────
DP2024  <- leer_relatoria("Corte_Constitucional_Relatoria_DP_2024.xlsx")

# ── Debido Proceso (2022 — año más reciente disponible) ─────────────────────
DPr2024 <- leer_relatoria("Corte_Constitucional_Relatoria_DPr_2022.xlsx")

cat("Providencias cargadas:\n")
## Providencias cargadas:
cat("  Salud (2024):          ", nrow(DS2024),  "\n")
##   Salud (2024):           166
cat("  Petición (2024):       ", nrow(DP2024),  "\n")
##   Petición (2024):        151
cat("  Debido Proceso (2022): ", nrow(DPr2024), "\n")
##   Debido Proceso (2022):  226
# Stopwords comunes a los tres derechos
sw_corte_base <- c(
  stopwords::stopwords("es"),
  "tutela","tutelas","sentencia","sentencias","corte","constitucional","asi",
  "derecho","derechos","fundamental","fundamentales","jurisprudencia",
  "amparo","proteccion","servicio","personas","tematica","invocado",
  "concedio","accionada","accion","analizo","accionante","anterior",
  "ordeno","caso","ordenes","hacer","manera","mismo","accionadas",
  "reitero","siguiente"
)

# Función de preprocesamiento unificada
limpiar_tokens <- function(df_raw, sw_extra = character(0)) {
  sw_total <- unique(c(sw_corte_base, sw_extra))
  tibble(line = seq_along(df_raw$Resumen), text = df_raw$Resumen) %>%
    mutate(text = str_replace_all(text, "\\s+", " "), text = str_trim(text)) %>%
    unnest_tokens(output = word, input = text) %>%
    filter(!is.na(word)) %>%
    mutate(
      word = str_to_lower(word),
      word = stri_trans_general(word, "Latin-ASCII"),
      word = str_remove_all(word, "[0-9]+"),
      word = str_remove_all(word, "[[:punct:]]"),
      word = str_remove_all(word, "[^[:alnum:] ]")
    ) %>%
    filter(str_detect(word, "^[a-z]+$"), word != "") %>%
    filter(!word %in% sw_total)
}

texto_DS2024  <- limpiar_tokens(DS2024)
texto_DP2024  <- limpiar_tokens(DP2024)
texto_DPr2024 <- limpiar_tokens(DPr2024)

cat("Tokens limpios — Salud:", nrow(texto_DS2024),
    "| Petición:", nrow(texto_DP2024),
    "| Debido Proceso:", nrow(texto_DPr2024), "\n")
## Tokens limpios — Salud: 17327 | Petición: 15010 | Debido Proceso: 24774

11.2 b) Análisis de unigramas

top10_salud <- texto_DS2024 %>% count(word, sort = TRUE) %>% head(10) %>%
  rename(Token = word, Frecuencia = n)
top10_peticion <- texto_DP2024 %>% count(word, sort = TRUE) %>% head(10) %>%
  rename(Token = word, Frecuencia = n)
top10_proceso  <- texto_DPr2024 %>% count(word, sort = TRUE) %>% head(10) %>%
  rename(Token = word, Frecuencia = n)

kable(
  bind_cols(
    top10_salud   %>% rename(`Token (Salud)` = Token, `Frec.` = Frecuencia),
    top10_peticion %>% rename(`Token (Petición)` = Token, `Frec. ` = Frecuencia),
    top10_proceso  %>% rename(`Token (Proceso)` = Token, `Frec.  ` = Frecuencia)
  ),
  caption = "Top 10 tokens más frecuentes por derecho (Salud 2024, Petición 2024, Debido Proceso 2022)",
  align   = "lclclc"
) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a")
Top 10 tokens más frecuentes por derecho (Salud 2024, Petición 2024, Debido Proceso 2022)
Token (Salud) Frec. Token (Petición) Frec. Token (Proceso) Frec.
salud 297 salud 106 proceso 213
hecho 92 proceso 91 decision 119
servicios 91 entidad 84 debido 115
situacion 90 debido 67 relacionada 115
especial 80 laboral 64 laboral 114
laboral 80 situacion 62 judicial 109
relacionada 75 hecho 59 salud 101
entidad 72 relacionada 58 entidad 99
proceso 66 solicitud 56 instancia 93
vulneracion 66 actora 54 sala 93
par(mfrow = c(1, 3), mar = c(0.5, 0.5, 2.5, 0.5), bg = "white")

pal_salud    <- colorRampPalette(c("#fbe6c5","#dc7176","#70284a"))(30)
pal_peticion <- colorRampPalette(c("#deeaf7","#5b9bd5","#1f4e79"))(30)
pal_proceso  <- colorRampPalette(c("#e2f0d9","#70ad47","#375623"))(30)

set.seed(1858)
texto_DS2024 %>% count(word, sort=TRUE) %>%
  with(wordcloud(word, n, max.words=25, colors=pal_salud, scale=c(3.5,0.6)))
title(main="Derecho a la Salud", col.main="#70284a", font.main=2)

texto_DP2024 %>% count(word, sort=TRUE) %>%
  with(wordcloud(word, n, max.words=25, colors=pal_peticion, scale=c(3.5,0.6)))
title(main="Derecho de Petición", col.main="#1f4e79", font.main=2)

texto_DPr2024 %>% count(word, sort=TRUE) %>%
  with(wordcloud(word, n, max.words=25, colors=pal_proceso, scale=c(3.5,0.6)))
title(main="Debido Proceso", col.main="#375623", font.main=2)

Análisis. Los tres wordclouds revelan un vocabulario jurídico compartido: términos como salud, laboral, entidad y vulneración aparecen en los tres derechos, confirmando que las tutelas analizadas comparten un núcleo semántico institucional. El wordcloud de salud está dominado por vocabulario médico-prestacional (EPS, tratamiento, capacidad laboral), mientras que el de petición incorpora más términos procedimentales (respuesta, entidad). El de debido proceso muestra mayor diversidad temática, con referencias al ámbito penal (providencia, proceso penal) y a actores judiciales (actor, actora).

tokens_totales <- bind_rows(
  list(salud = texto_DS2024, peticion = texto_DP2024, proceso = texto_DPr2024),
  .id = "tema"
)

frec_spread <- tokens_totales %>%
  count(tema, word) %>%
  group_by(tema) %>%
  mutate(proporcion = n / sum(n)) %>%
  select(-n) %>%
  spread(tema, proporcion, fill = 0) %>%
  select(word, salud, peticion, proceso)

frec_comun <- frec_spread %>%
  filter(salud > 0, peticion > 0, proceso > 0) %>%
  arrange(desc(salud))

kable(head(frec_comun, 12),
      caption = "Proporción relativa de las palabras compartidas entre los tres derechos",
      align   = "lccc", digits = 4) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a")
Proporción relativa de las palabras compartidas entre los tres derechos
word salud peticion proceso
salud 0.0171 0.0071 0.0041
hecho 0.0053 0.0039 0.0030
servicios 0.0053 0.0026 0.0013
situacion 0.0052 0.0041 0.0028
especial 0.0046 0.0031 0.0022
laboral 0.0046 0.0043 0.0046
relacionada 0.0043 0.0039 0.0046
entidad 0.0042 0.0056 0.0040
proceso 0.0038 0.0061 0.0086
vulneracion 0.0038 0.0028 0.0023
efectivo 0.0035 0.0028 0.0012
prestacion 0.0034 0.0027 0.0017
tbl_cor <- tibble(
  Comparación = c("Salud vs. Petición", "Salud vs. Proceso", "Proceso vs. Petición"),
  `r`         = round(c(
    cor.test(frec_spread$salud, frec_spread$peticion)$estimate,
    cor.test(frec_spread$salud, frec_spread$proceso)$estimate,
    cor.test(frec_spread$proceso, frec_spread$peticion)$estimate), 3),
  `p-valor`   = signif(c(
    cor.test(frec_spread$salud, frec_spread$peticion)$p.value,
    cor.test(frec_spread$salud, frec_spread$proceso)$p.value,
    cor.test(frec_spread$proceso, frec_spread$peticion)$p.value), 3)
)

kable(tbl_cor,
      caption = "Correlación de frecuencias léxicas entre los tres derechos",
      align   = "lcc") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a")
Correlación de frecuencias léxicas entre los tres derechos
Comparación r p-valor
Salud vs. Petición 0.866 0
Salud vs. Proceso 0.700 0
Proceso vs. Petición 0.828 0

Análisis. Las correlaciones de frecuencias léxicas son positivas y moderadamente altas en todos los pares, lo que confirma la existencia de un vocabulario jurídico-institucional compartido. La correlación más alta se presenta entre petición y proceso, reflejo de que ambos derechos se ventilan en contextos procedimentales similares. La correlación salud–proceso es la más baja, coherente con la mayor especificidad clínica del vocabulario de las tutelas de salud (EPS, tratamiento, diagnóstico) frente al lenguaje más general de las de proceso. Todos los p-valores son cercanos a cero, rechazando la hipótesis nula de ausencia de correlación.

11.3 c) Redes de bigramas

limpiar_bigramas <- function(df_raw, sw_extra = character(0)) {
  sw_total <- unique(c(sw_corte_base, sw_extra))
  df_raw$Resumen %>%
    str_replace_all("\\s+", " ") %>% str_trim() %>%
    tibble(text = .) %>%
    unnest_tokens(output = bigram, input = text, token = "ngrams", n = 2) %>%
    separate(bigram, into = c("word1","word2"), sep = " ",
             fill = "right", extra = "drop") %>%
    filter(!grepl("[0-9]", word1), !grepl("[0-9]", word2)) %>%
    mutate(
      word1 = stri_trans_general(word1, "Latin-ASCII"),
      word2 = stri_trans_general(word2, "Latin-ASCII")
    ) %>%
    filter(!word1 %in% sw_total, !word2 %in% sw_total) %>%
    drop_na(word1, word2) %>%
    count(word1, word2, sort = TRUE) %>%
    rename(weight = n)
}

bi_salud    <- limpiar_bigramas(DS2024)
bi_peticion <- limpiar_bigramas(DP2024)
bi_proceso  <- limpiar_bigramas(DPr2024)
kable(
  bind_cols(
    bi_salud    %>% head(8) %>% unite(Bigrama_Salud,    word1, word2, sep=" ") %>% rename(`Peso` = weight),
    bi_peticion %>% head(8) %>% unite(Bigrama_Peticion, word1, word2, sep=" ") %>% rename(`Peso ` = weight),
    bi_proceso  %>% head(8) %>% unite(Bigrama_Proceso,  word1, word2, sep=" ") %>% rename(`Peso  ` = weight)
  ),
  caption = "Top 8 bigramas más frecuentes por derecho",
  align   = "lclclc"
) %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a")
Top 8 bigramas más frecuentes por derecho
Bigrama_Salud Peso Bigrama_Peticion Peso Bigrama_Proceso Peso
carencia actual 48 debido proceso 47 debido proceso 94
hecho comun 36 carencia actual 38 providencias judiciales 46
seguridad social 35 capacidad laboral 27 seguridad social 40
ninos ninas 32 seguridad social 27 carencia actual 37
debido proceso 31 garantias constitucionales 24 capacidad laboral 29
estabilidad laboral 30 hecho superado 21 autoridad judicial 28
hecho superado 25 proceso administrativo 21 decision judicial 26
tratamiento integral 25 consulta previa 19 defecto factico 26
construir_gcc <- function(bi_df, umbral = 2) {
  g <- bi_df %>% filter(weight > umbral) %>%
    graph_from_data_frame(directed = FALSE)
  comp <- components(g)
  induced_subgraph(g, which(comp$membership == which.max(comp$csize)))
}

gcc_bi_salud    <- construir_gcc(bi_salud,    umbral = 2)
gcc_bi_peticion <- construir_gcc(bi_peticion, umbral = 2)
gcc_bi_proceso  <- construir_gcc(bi_proceso,  umbral = 2)

colores_gcc <- c("#70284a", "#1f4e79", "#375623")
titulos_gcc <- c("Salud — bigramas (umbral > 2)",
                 "Petición — bigramas (umbral > 2)",
                 "Proceso — bigramas (umbral > 2)")
gccs <- list(gcc_bi_salud, gcc_bi_peticion, gcc_bi_proceso)

par(mfrow = c(1, 3), mar = c(1, 1, 3, 1), bg = "white")
for (i in 1:3) {
  gcc_i <- gccs[[i]]
  set.seed(123)
  plot(gcc_i,
       layout             = layout_with_fr,
       vertex.color       = adjustcolor(colores_gcc[i], 0.25),
       vertex.frame.color = colores_gcc[i],
       vertex.size        = 2 + 5 * log1p(strength(gcc_i)) /
                              max(log1p(strength(gcc_i))),
       vertex.label.cex   = 0.62,
       vertex.label.color = "black",
       vertex.label.dist  = 0.8,
       edge.width         = 2 * E(gcc_i)$weight / max(E(gcc_i)$weight),
       edge.color         = adjustcolor("gray50", 0.4),
       main               = titulos_gcc[i])
}

Análisis. Las tres componentes gigantes de las redes de bigramas presentan estructuras estrella con un núcleo de términos altamente conectados. En la red de salud, los nodos centrales corresponden a bigramas médico-institucionales (seguridad social, tratamiento integral, médico tratante), que actúan como articuladores del discurso jurídico-clínico. En la red de petición, el núcleo es más compacto y orientado a expresiones procesales genéricas (hecho superado, carencia actual, debido proceso), lo que indica menor diversidad temática. La red de debido proceso es la más extensa de las tres, con subgrupos asociados a distintos ámbitos del sistema judicial (penal, laboral, administrativo), reflejando la naturaleza transversal de este derecho.

11.4 d) Redes de skipgramas

limpiar_skipgramas <- function(df_raw, sw_extra = character(0)) {
  sw_total <- unique(c(sw_corte_base, sw_extra))
  df_raw$Resumen %>%
    str_replace_all("\\s+", " ") %>% str_trim() %>%
    tibble(text = .) %>%
    unnest_tokens(output = skipgram, input = text,
                  token = "skip_ngrams", n = 2) %>%
    mutate(num_words = purrr::map_int(skipgram, wordcount)) %>%
    filter(num_words == 2) %>% select(-num_words) %>%
    separate(skipgram, into = c("word1","word2"), sep = " ",
             fill = "right", extra = "drop") %>%
    filter(!grepl("[0-9]", word1), !grepl("[0-9]", word2)) %>%
    mutate(
      word1 = stri_trans_general(word1, "Latin-ASCII"),
      word2 = stri_trans_general(word2, "Latin-ASCII")
    ) %>%
    filter(!word1 %in% sw_total, !word2 %in% sw_total) %>%
    drop_na(word1, word2) %>%
    count(word1, word2, sort = TRUE) %>%
    rename(weight = n)
}

skip_salud    <- limpiar_skipgramas(DS2024)
skip_peticion <- limpiar_skipgramas(DP2024)
skip_proceso  <- limpiar_skipgramas(DPr2024)
gcc_sk_salud    <- construir_gcc(skip_salud,    umbral = 4)
gcc_sk_peticion <- construir_gcc(skip_peticion, umbral = 4)
gcc_sk_proceso  <- construir_gcc(skip_proceso,  umbral = 4)

titulos_sk <- c("Salud — skipgramas (umbral > 4)",
                "Petición — skipgramas (umbral > 4)",
                "Proceso — skipgramas (umbral > 4)")
gccs_sk <- list(gcc_sk_salud, gcc_sk_peticion, gcc_sk_proceso)

par(mfrow = c(1, 3), mar = c(1, 1, 3, 1), bg = "white")
for (i in 1:3) {
  gcc_i <- gccs_sk[[i]]
  set.seed(123)
  plot(gcc_i,
       layout             = layout_with_fr,
       vertex.color       = adjustcolor(colores_gcc[i], 0.2),
       vertex.frame.color = colores_gcc[i],
       vertex.size        = 2 + 5 * log1p(strength(gcc_i)) /
                              max(log1p(strength(gcc_i))),
       vertex.label.cex   = 0.62,
       vertex.label.color = "black",
       vertex.label.dist  = 0.8,
       edge.width         = 2 * E(gcc_i)$weight / max(E(gcc_i)$weight),
       edge.color         = adjustcolor("gray50", 0.4),
       main               = titulos_sk[i])
}

11.5 e) Tabla comparativa de métricas estructurales

metricas_red <- function(g, nombre) {
  data.frame(
    Red           = nombre,
    Nodos         = vcount(g),
    Aristas       = ecount(g),
    Densidad      = round(edge_density(g), 4),
    `Dist. media` = round(mean_distance(g), 3),
    `Grado medio` = round(mean(degree(g)), 3),
    `Desv. grado` = round(sd(degree(g)), 3),
    Transitividad = round(transitivity(g, type = "global"), 3),
    Asortatividad = round(assortativity_degree(g, directed = FALSE), 3),
    `N.° cliques` = count_max_cliques(g),
    check.names   = FALSE
  )
}

# Bigramas
tbl_bi <- rbind(
  metricas_red(construir_gcc(bi_salud,    0), "Salud — bigramas"),
  metricas_red(construir_gcc(bi_peticion, 0), "Petición — bigramas"),
  metricas_red(construir_gcc(bi_proceso,  0), "Proceso — bigramas")
)
# Skipgramas
tbl_sk <- rbind(
  metricas_red(construir_gcc(skip_salud,    0), "Salud — skipgramas"),
  metricas_red(construir_gcc(skip_peticion, 0), "Petición — skipgramas"),
  metricas_red(construir_gcc(skip_proceso,  0), "Proceso — skipgramas")
)

kable(rbind(tbl_bi, tbl_sk),
      caption = "Métricas estructurales — Redes de bigramas y skipgramas (Salud/Petición 2024, Proceso 2022)",
      align   = "lccccccccc") %>%
  kable_styling(bootstrap_options = c("striped","hover","condensed"),
                full_width = TRUE, font_size = 12) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a") %>%
  row_spec(c(1,4), background = "#fdf6f9") %>%
  row_spec(c(2,5), background = "#f0f5fb") %>%
  row_spec(c(3,6), background = "#f0f5ee")
Métricas estructurales — Redes de bigramas y skipgramas (Salud/Petición 2024, Proceso 2022)
Red Nodos Aristas Densidad Dist. media Grado medio Desv. grado Transitividad Asortatividad N.° cliques
Salud — bigramas 2368 3270 0.0012 7.242 2.762 3.436 0.012 -0.103 3126
Petición — bigramas 2195 2917 0.0012 7.630 2.658 3.133 0.009 -0.123 2833
Proceso — bigramas 3256 4946 0.0009 6.630 3.038 4.175 0.013 -0.070 4700
Salud — skipgramas 3775 9256 0.0013 4.872 4.904 7.144 0.041 0.000 7085
Petición — skipgramas 3651 8371 0.0013 5.109 4.586 6.197 0.042 -0.007 6480
Proceso — skipgramas 5092 13780 0.0011 4.720 5.412 8.497 0.037 -0.001 10465

Análisis comparativo. Las redes de skipgramas son consistentemente más densas y con mayor grado medio que las de bigramas, lo que refleja que al capturar co-ocurrencias con salto se detectan más asociaciones léxicas entre términos no adyacentes —típicamente relaciones semánticas más abstractas o temáticas—. La mayor densidad de la red de salud en ambos tipos de n-grama sugiere un vocabulario más interconectado, coherente con la especificidad temática del discurso médico-jurídico. En contraste, la red de petición tiene el mayor número de cliques maximales en bigramas, indicando más subgrupos semánticos localmente cohesionados, posiblemente asociados a fórmulas jurídicas repetitivas propias del derecho de petición (hecho superado, carencia actual de objeto). La asortatividad negativa en todas las redes confirma el patrón hub-and-spoke propio de los léxicos jurídicos: unos pocos términos altamente conectados (proceso, entidad, laboral) articulan vocabulario más específico y periférico.


12 Punto 10 (enunciado): Populismo en Colombia — Redes de trabajo y alianzas

12.1 Carga de datos

alliance_edges <- read_csv("alliance-edges.csv")
work_edges     <- read_csv("work-edges.csv")
nodes          <- read_csv("nodes.csv")

limpiar_undirected <- function(df) {
  df2 <- as.data.frame(t(apply(df, 1, sort)))
  colnames(df2) <- colnames(df)
  unique(df2)
}

red_alianzas <- graph_from_data_frame(limpiar_undirected(alliance_edges),
                                       directed = FALSE)
red_trabajo  <- graph_from_data_frame(limpiar_undirected(work_edges),
                                       directed = FALSE)

12.2 a) Análisis local y estructural

12.2.1 Red de alianzas

deg  <- degree(red_alianzas)
clo  <- closeness(red_alianzas)
bet  <- betweenness(red_alianzas)
eig  <- eigen_centrality(red_alianzas)$vector
clus <- transitivity(red_alianzas, type = "local", isolates = "zero")

get_top <- function(v, n = 3) {
  nm <- names(sort(v, decreasing = TRUE)[1:n])
  paste(nm, collapse = ", ")
}

resumen_ali <- data.frame(
  Métrica    = c("Grado", "Cercanía", "Intermediación", "Eigenvector", "Clustering local"),
  `Top 3`    = c(get_top(deg), get_top(clo), get_top(bet), get_top(eig), get_top(clus)),
  `Valor máximo` = round(c(max(deg), max(clo), max(bet), max(eig), max(clus)), 3),
  check.names = FALSE
)

kable(resumen_ali,
      caption = "Métricas locales — Red de alianzas políticas",
      align   = "llr") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = TRUE, font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a")
Métricas locales — Red de alianzas políticas
Métrica Top 3 Valor máximo
Grado alvaro-uribe-velez, german-vargas-lleras, juan-manuel-santos-calderon 45.00
Cercanía cesar-pardo-villalba, jaime-araujo-renteria, juan-manuel-ospina-restrepo 1.00
Intermediación alvaro-uribe-velez, german-vargas-lleras, juan-manuel-santos-calderon 16493.21
Eigenvector alvaro-uribe-velez, sergio-fajardo, antanas-mockus 1.00
Clustering local alfredo-ramos-maya, alicia-arango-olmos, arturo-calderon-rivadeneira 1.00
global_ali <- data.frame(
  Medida = c("Densidad", "Distancia promedio", "Diámetro",
             "Transitividad", "N.° componentes", "Componente gigante (nodos)"),
  Valor  = round(c(edge_density(red_alianzas),
                   mean_distance(red_alianzas),
                   diameter(red_alianzas),
                   transitivity(red_alianzas, type = "global"),
                   components(red_alianzas)$no,
                   max(components(red_alianzas)$csize)), 3)
)

kable(global_ali,
      caption = "Métricas globales — Red de alianzas políticas",
      align   = "lr") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a")
Métricas globales — Red de alianzas políticas
Medida Valor
Densidad 0.011
Distancia promedio 4.108
Diámetro 10.000
Transitividad 0.140
N.° componentes 11.000
Componente gigante (nodos) 335.000

12.2.2 Red de trabajo

deg_t  <- degree(red_trabajo)
clo_t  <- closeness(red_trabajo)
bet_t  <- betweenness(red_trabajo)
eig_t  <- eigen_centrality(red_trabajo)$vector
clus_t <- transitivity(red_trabajo, type = "local", isolates = "zero")

resumen_trab <- data.frame(
  Métrica    = c("Grado", "Cercanía", "Intermediación", "Eigenvector", "Clustering local"),
  `Top 3`    = c(get_top(deg_t), get_top(clo_t), get_top(bet_t),
                 get_top(eig_t), get_top(clus_t)),
  `Valor máximo` = round(c(max(deg_t), max(clo_t), max(bet_t),
                            max(eig_t), max(clus_t)), 3),
  check.names = FALSE
)

kable(resumen_trab,
      caption = "Métricas locales — Red de trabajo institucional",
      align   = "llr") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = TRUE, font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a")
Métricas locales — Red de trabajo institucional
Métrica Top 3 Valor máximo
Grado juan-manuel-santos-calderon, alvaro-uribe-velez, andres-pastrana-arango 176.00
Cercanía edgar-gomez-roman, german-varon-cotrino, efrain-cepeda-sarabia 1.00
Intermediación juan-manuel-santos-calderon, alvaro-uribe-velez, andres-pastrana-arango 61813.33
Eigenvector juan-manuel-santos-calderon, alvaro-uribe-velez, juan-camilo-restrepo-salazar 1.00
Clustering local alejandro-eder-garces, alvaro-garcia-jimenez, bernardo-moreno 1.00
global_trab <- data.frame(
  Medida = c("Densidad", "Distancia promedio", "Diámetro",
             "Transitividad", "N.° componentes", "Componente gigante (nodos)"),
  Valor  = round(c(edge_density(red_trabajo),
                   mean_distance(red_trabajo),
                   diameter(red_trabajo),
                   transitivity(red_trabajo, type = "global"),
                   components(red_trabajo)$no,
                   max(components(red_trabajo)$csize)), 3)
)

kable(global_trab,
      caption = "Métricas globales — Red de trabajo institucional",
      align   = "lr") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a")
Métricas globales — Red de trabajo institucional
Medida Valor
Densidad 0.011
Distancia promedio 3.308
Diámetro 8.000
Transitividad 0.099
N.° componentes 14.000
Componente gigante (nodos) 485.000

12.3 b) Interpretación comparada

Red de alianzas. La estructura de poder está fuertemente concentrada en Álvaro Uribe Vélez, quien maximiza simultáneamente grado (45), intermediación (16.493) y centralidad eigenvector (1.0). Esta triple centralidad revela un actor cuya influencia no se limita al número de vínculos directos, sino que penetra estructuralmente la red como el principal nodo puente entre grupos políticos. La densidad es baja (0.011), pero el componente gigante integra 335 de los 346 actores —más del 96%—, lo que confirma que la élite política colombiana analizada constituye un campo relacional altamente conectado a pesar de la escasez de vínculos. La transitividad de 0.140 señala la existencia de algunos cierres triádicos, pero la estructura predominante es radial: muchas conexiones pasan por pocos intermediarios clave. La distancia geodésica promedio de 4.1 indica que cualquier par de actores está separado por poco más de cuatro intermediarios, confirmando el efecto de mundo pequeño en el campo político colombiano.

Red de trabajo. El liderazgo se desplaza hacia Juan Manuel Santos, quien encabeza grado (176), intermediación (61.813) y eigenvector en la red de vínculos laborales. Su participación en múltiples carteras ministeriales, la presidencia y organismos multilaterales genera un volumen de conexiones laborales sin precedente en el conjunto de datos. La red de trabajo es más extensa (485 actores en el componente gigante vs. 335 en alianzas) y sus actores están más cerca entre sí (distancia promedio 3.3 vs. 4.1), lo que sugiere que el espacio laboral-institucional produce una red más integrada que las alianzas políticas explícitas. La transitividad es menor (0.099 vs. 0.140), evidenciando que el trabajo institucional genera redes más lineales y menos cerradas en triángulos que las alianzas, posiblemente porque los cargos son asignados por jerarquía más que por afinidad grupal. La mayor fragmentación periférica (14 componentes vs. 11 en alianzas) refleja la existencia de equipos técnicos o sectoriales con escasa conexión al núcleo político principal.


13 Punto 11 (enunciado): Mercado de futuros de gas natural — NYMEX

13.1 a) Métricas semanales y prueba de Mann-Whitney

# Función para graficar serie temporal con medianas y test Mann-Whitney
graficar_serie <- function(valores, titulo, ylab) {
  df  <- data.frame(semana = 1:201, valor = valores)
  m_a <- median(df$valor[df$semana <= 83], na.rm = TRUE)
  m_d <- median(df$valor[df$semana >  83], na.rm = TRUE)
  wt  <- wilcox.test(df$valor[df$semana <= 83], df$valor[df$semana > 83])

  seg_a  <- data.frame(x = 1,   xend = 83,  y = m_a, yend = m_a)
  seg_d  <- data.frame(x = 84,  xend = 201, y = m_d, yend = m_d)
  lbl_df <- data.frame(semana = 195,
                        valor  = max(valores, na.rm = TRUE),
                        lbl    = paste0("p = ", signif(wt$p.value, 3)))

  ggplot(df, aes(x = semana, y = valor)) +
    geom_line(color = "gray50", linewidth = 0.45) +
    geom_vline(xintercept = 83, linetype = "dashed",
               color = "gray30", linewidth = 0.7) +
    geom_segment(data = seg_a,
                 aes(x = x, xend = xend, y = y, yend = yend),
                 color = "#8B668B", linewidth = 1.1, inherit.aes = FALSE) +
    geom_segment(data = seg_d,
                 aes(x = x, xend = xend, y = y, yend = yend),
                 color = "#4DAF4A", linewidth = 1.1, inherit.aes = FALSE) +
    geom_text(data = lbl_df,
              aes(x = semana, y = valor, label = lbl),
              size = 3, hjust = 1, inherit.aes = FALSE) +
    labs(title = titulo, x = "Semana", y = ylab) +
    theme_minimal(base_size = 10) +
    theme(plot.title = element_text(size = 9, face = "bold"))
}

p1 <- graficar_serie(metricas$densidad,           "Densidad",                "")
p2 <- graficar_serie(metricas$clustering,         "Coef. de agrupamiento",   "")
p3 <- graficar_serie(metricas$asortatividad,      "Asortatividad",           "")
p4 <- graficar_serie(metricas$reciprocidad,       "Reciprocidad",            "")
p5 <- graficar_serie(metricas$distancia_media,    "Distancia geodésica prom.","")
p6 <- graficar_serie(metricas$componente_gigante, "Componente gigante",       "")
p7 <- graficar_serie(metricas$g_out_prom,         "Grado salida promedio",   "")
p8 <- graficar_serie(metricas$g_in_prom,         "Grado entrada promedio",  "")

gridExtra::grid.arrange(p1, p2, p3, p4, p5, p6, p7, p8, ncol = 2)

vars_metr  <- c("densidad", "clustering", "asortatividad", "reciprocidad",
                "distancia_media", "componente_gigante", "g_out_prom", "g_in_prom")
labs_metr  <- c("Densidad", "Clustering", "Asortatividad", "Reciprocidad",
                "Distancia media", "Comp. gigante", "Grado salida", "Grado entrada")

res_mw <- lapply(seq_along(vars_metr), function(i) {
  v  <- metricas[[vars_metr[i]]]
  a  <- v[metricas$semana <= 83]; d <- v[metricas$semana > 83]
  wt <- wilcox.test(a, d)
  cambio <- ifelse(median(d, na.rm=TRUE) > median(a, na.rm=TRUE), "↑", "↓")
  data.frame(
    Métrica        = labs_metr[i],
    `Mediana ant.` = round(median(a, na.rm = TRUE), 4),
    `Mediana post.`= round(median(d, na.rm = TRUE), 4),
    Cambio         = cambio,
    `p-valor`      = signif(wt$p.value, 3),
    Significativa  = ifelse(wt$p.value < 0.05, "Sí", "No"),
    check.names    = FALSE
  )
})

res_mw_df <- do.call(rbind, res_mw)

kable(res_mw_df,
      caption = "Prueba de Mann–Whitney: comparación antes / después de la semana 83",
      align   = "lrrccc") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a") %>%
  row_spec(which(res_mw_df$Significativa == "Sí"), background = "#fbe6c5")
Prueba de Mann–Whitney: comparación antes / después de la semana 83
Métrica Mediana ant. Mediana post. Cambio p-valor Significativa
Densidad 0.1254 0.1975 0
Clustering 0.5238 0.4647 0
Asortatividad 0.0641 -0.2983 0
Reciprocidad 0.5284 0.5936 0
Distancia media 2.4618 1.8609 0
Comp. gigante 65.0000 69.0000 0
Grado salida 8.7746 13.8239 0
Grado entrada 8.7746 13.8239 0

Análisis. Las ocho métricas estructurales registran cambios estadísticamente significativos (p < 0.001 en todos los casos) tras la introducción de la plataforma electrónica de negociación en la semana 83. Los resultados muestran una transformación profunda del mercado: la densidad aumenta de 0.125 a 0.198 (+58%), indicando que más pares de participantes establecen relaciones de intercambio consistentes en el período post-electrónico. El coeficiente de agrupamiento disminuye de 0.524 a 0.465, señalando que la mayor densidad no se traduce en más triángulos sino en una conectividad más dispersa y menos cliquizada. La asortatividad pasa de 0.064 a −0.298, un cambio cualitativo relevante: el mercado transita de una mezcla prácticamente neutra a una estructura marcadamente disasortativa donde los grandes participantes se conectan preferentemente con participantes pequeños —patrón de estrella o hub—. La distancia geodésica media cae de 2.46 a 1.86, lo que implica que los participantes están estructuralmente más próximos entre sí. En conjunto, estos resultados son coherentes con la hipótesis de que la negociación electrónica centraliza y acelera el mercado: reduce fricciones de búsqueda, aumenta la conectividad global pero concentra el flujo en pocos actores dominantes.

13.2 b) Agrupamiento jerárquico semanal

pg1 <- graficar_serie(metricas_agru$n_clusters,  "Número de grupos",  "")
pg2 <- graficar_serie(metricas_agru$modularidad, "Modularidad",       "")
gridExtra::grid.arrange(pg1, pg2, ncol = 2)

Análisis. Antes de la semana 83, el mercado opera con una mediana de aproximadamente 9 grupos de negociación, con alta variabilidad semanal que refleja la inestabilidad característica de los mercados energéticos en el período pre-electrónico. Tras la introducción de la plataforma, el número de grupos se estabiliza alrededor de 5 (reducción del 44%) y la modularidad desciende de ~0.17 a ~0.12. La menor modularidad implica que los grupos identificados están más interconectados entre sí —las fronteras comunitarias se difuminan—, lo cual es directamente consistente con la mayor integración del mercado. La reducción del número de grupos sugiere que algunos clústeres de negociantes que operaban de forma relativamente aislada en el período de viva voz se integraron al pool central tras la adopción electrónica, posiblemente porque la plataforma eliminó las barreras geográficas e informacionales que sustentaban esa segmentación.

13.3 c) Redes de consenso Y₀ e Y₁

Y_0 <- apply(Y71[1:83, , ],   c(2, 3), mean)
Y_1 <- apply(Y71[84:201, , ], c(2, 3), mean)
Y_0 <- ifelse(Y_0 > 0.5, 1, 0)
Y_1 <- ifelse(Y_1 > 0.5, 1, 0)

g0 <- graph_from_adjacency_matrix(Y_0, mode = "directed")
g1 <- graph_from_adjacency_matrix(Y_1, mode = "directed")

calc_metr_g <- function(g) {
  data.frame(
    Densidad           = edge_density(g),
    Clustering         = transitivity(g, type = "global"),
    Asortatividad      = assortativity_degree(g, directed = TRUE),
    Reciprocidad       = reciprocity(g),
    `Dist. media`      = mean_distance(g, directed = TRUE, unconnected = TRUE),
    `Comp. gigante`    = max(components(as.undirected(g))$csize),
    `Grado out medio`  = mean(degree(g, mode = "out")),
    `Grado in medio`   = mean(degree(g, mode = "in")),
    check.names = FALSE
  )
}

tbl_comp <- rbind(Antes = calc_metr_g(g0), Después = calc_metr_g(g1)) %>%
  as.data.frame() %>% tibble::rownames_to_column("Período")

kable(tbl_comp,
      caption = "Métricas estructurales — Redes de consenso Y₀ (semanas 1–83) e Y₁ (semanas 84–201)",
      align   = "lrrrrrrrr",
      digits  = 3) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = TRUE, font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a") %>%
  row_spec(2, bold = TRUE, background = "#fbe6c5")
Métricas estructurales — Redes de consenso Y₀ (semanas 1–83) e Y₁ (semanas 84–201)
Período Densidad Clustering Asortatividad Reciprocidad Dist. media Comp. gigante Grado out medio Grado in medio
Antes 0.073 0.461 -0.154 0.878 2.090 44 5.099 5.099
Después 0.120 0.269 -0.618 0.933 1.933 64 8.366 8.366

Interpretación. La comparación entre Y₀ e Y₁ confirma con mayor nitidez los patrones detectados en las series semanales. La densidad aumenta de 0.073 a 0.120 (+64%), reflejando que en el período post-electrónico más del doble de pares de participantes mantienen vínculos suficientemente frecuentes para superar el umbral de consenso. La reciprocidad sube de 0.878 a 0.933, indicando relaciones de intercambio más simétricas y bilaterales —consistente con la naturaleza más igualitaria de la negociación electrónica respecto al open outcry—. La asortatividad se vuelve marcadamente más negativa (de −0.154 a −0.618), lo que confirma la emergencia de una estructura hub-and-spoke: un pequeño número de participantes de alto grado conecta a una periferia extensa de participantes con pocos vínculos. El componente gigante crece de 44 a 64 nodos (+45%), señal inequívoca de mayor integración estructural del mercado.

13.4 d) Visualización componentes gigantes Y₀ e Y₁

g0s <- as.undirected(g0, mode = "collapse")
g1s <- as.undirected(g1, mode = "collapse")

extraer_gigante <- function(g) {
  comp <- components(g)
  induced_subgraph(g, which(comp$membership == which.max(comp$csize)))
}

gig0 <- extraer_gigante(g0s)
gig1 <- extraer_gigante(g1s)

fg0 <- cluster_fast_greedy(gig0)
fg1 <- cluster_fast_greedy(gig1)

n_col <- max(length(fg0), length(fg1))
pal_c <- rainbow(n_col)

rescale_size <- function(x) 3 + 15 * (x - min(x)) / (max(x) - min(x) + 1e-9)

V(gig0)$color <- pal_c[membership(fg0)]
V(gig1)$color <- pal_c[membership(fg1)]
V(gig0)$size  <- rescale_size(strength(gig0))
V(gig1)$size  <- rescale_size(strength(gig1))

set.seed(123)
par(mfrow = c(1, 2), mar = c(1, 1, 3.5, 1), bg = "white")

plot(gig0,
     layout             = layout_with_fr(gig0, niter = 1000),
     vertex.label       = NA,
     vertex.frame.color = "white",
     edge.color         = rgb(0.6, 0.6, 0.6, 0.3),
     edge.width         = 0.7,
     main               = paste0("Y₀ — antes (semanas 1–83)\n",
                                  "n=", vcount(gig0), ", m=", ecount(gig0)))

plot(gig1,
     layout             = layout_with_fr(gig1, niter = 1000),
     vertex.label       = NA,
     vertex.frame.color = "white",
     edge.color         = rgb(0.6, 0.6, 0.6, 0.3),
     edge.width         = 0.7,
     main               = paste0("Y₁ — después (semanas 84–201)\n",
                                  "n=", vcount(gig1), ", m=", ecount(gig1)))

Interpretación. El contraste visual entre Y₀ e Y₁ captura de forma directa la transformación estructural del mercado. En Y₀, los colores (comunidades) están distribuidos de forma relativamente equilibrada y los tamaños de vértice son homogéneos, indicando que no existe un actor dominante y que el mercado opera de forma más descentralizada, con grupos de intercambio de tamaño comparable. En Y₁, emergen con claridad unos pocos vértices de gran tamaño (alta fuerza acumulada) que actúan como hubs centrales del mercado, rodeados de una periferia de nodos pequeños. Las comunidades tienen fronteras más difusas y el grafo es visualmente más denso y compacto. Este patrón es consistente con la literatura sobre efectos de la automatización en mercados financieros: la reducción de costos de transacción tiende a concentrar la actividad en los participantes más eficientes y a marginalizar a los pequeños operadores.


14 Punto 12 (enunciado): Análisis de Cien años de soledad

14.1 Importación del texto

texto_completo <- read_lines("gabriel_garcia_marquez_cien_annos_soledad.txt")
patron_meta    <- "Gabriel García Márquez|Cien años de soledad|EDITADO POR|ediciones la cueva|Para Jomi|María Luisa Elio"
texto_completo <- texto_completo[!grepl(patron_meta, texto_completo, ignore.case = TRUE)]
texto_completo <- texto_completo[nchar(trimws(texto_completo)) > 0]
cat("Líneas tras limpieza de metadatos:", length(texto_completo), "\n")
## Líneas tras limpieza de metadatos: 9158

14.2 División en 20 segmentos (capítulos aproximados)

texto_df <- tibble(line = seq_along(texto_completo), text = texto_completo)
lpc      <- nrow(texto_df) / 20
texto_df <- texto_df %>%
  mutate(capitulo = ceiling(line / lpc),
         capitulo = paste0("Cap_", sprintf("%02d", capitulo)))

corpus_caps <- texto_df %>%
  group_by(capitulo) %>%
  summarise(text = paste(text, collapse = " "), n_lineas = n(), .groups = "drop")

cat("Segmentos creados:", nrow(corpus_caps), "| Líneas por segmento (aprox.):", round(lpc), "\n")
## Segmentos creados: 20 | Líneas por segmento (aprox.): 458

14.3 Tokenización y stopwords personalizadas

tokens_caps <- corpus_caps %>%
  unnest_tokens(output = word, input = text) %>%
  filter(!is.na(word))

sw_es <- tibble(word = tm::stopwords("spanish"))

sw_custom <- tibble(word = c(
  "gabriel","garcia","marquez","cien","anos","soledad","editado",
  "ediciones","editorial","pagina","capitulo","cueva",
  "aureliano","arcadio","ursula","buendia","jose","amaranta","remedios",
  "fernanda","petra","pilar","rebeca","santa","sofia","renata","meme",
  "mauricio","babilonia","melquiades","gerineldo","crespi","pietro",
  "catalan","apolinar","moscote","prudencio",
  "coronel","don","dona","general","capitan","senor","senora",
  "macondo",
  "ser","era","fue","sido","sea","estar","estaba","estuvo","estado",
  "haber","habia","hubo","hay","han","hacer","hizo","hacia","hecho",
  "decir","dijo","decia","dice","ir","iba","tener","tuvo","tenia",
  "poner","puso","ver","vio","veia","dar","dio","saber","supo","poder",
  "pudo","querer","quiso","llegar","llego","volver","volvio","quedar",
  "quedo","seguir","siguio","encontrar","encontro","parecer","parecio",
  "creer","creyo","pensar","penso","sentir","sintio","mirar","miro",
  "pasar","paso","comenzar","comenzo","empezar","empezo","salir","salio",
  "entrar","entro","venir","vino","dejar","dejo","llevar","llevo",
  "morir","murio","vivir","vivio","abrir","abrio","cerrar","cerro",
  "escribir","escribio","conocer","conocio","perder","perdio",
  "despues","entonces","luego","ahora","pronto","tarde","temprano",
  "siempre","nunca","jamas","aun","todavia","ya","vez","veces",
  "dia","dias","noche","noches","manana","ano","anos","mes","meses",
  "tiempo","momento","hora","antes","durante","apenas","hasta",
  "mas","menos","mucho","poco","muy","tan","tanto","casi","solo",
  "tambien","ademas","incluso","mismo","misma","otro","otra","todo",
  "toda","cada","algun","alguna","algo","alguien","ningun","ninguna",
  "nada","nadie","primer","primero","ultimo","unico","varios","ambos",
  "dos","tres","cuatro","cinco",
  "yo","tu","el","ella","nosotros","ellos","me","te","se","nos","les",
  "le","lo","la","los","las","mi","si","este","esta","esto","ese","esa",
  "aquel","aquella","quien","cual","cuyo",
  "y","e","o","u","ni","pero","mas","sino","aunque","sin","embargo",
  "porque","pues","asi","como","segun","si","a","ante","bajo","con",
  "contra","de","desde","en","entre","hacia","para","por","sobre","tras",
  "casa","casas","puerta","habitacion","cuarto","sala","patio","calle",
  "lado","parte","lugar","cosa","cosas","manera","forma","modo",
  "hombre","hombres","mujer","mujeres","nino","ninos","hijo","hijos",
  "hija","hijas","padre","padres","madre","madres",
  "mano","cara","ojos","cabeza","cuerpo","pie","brazo","pierna",
  "palabra","palabras","voz","voces","mundo","tierra","aire","agua",
  "luz","sombra","color","vida","muerte","bien","mal","grande","gran",
  "pequeno","nuevo","viejo","bueno","malo","largo","corto","alto","bajo",
  "gente","personas","familia","familias","siendo","dado","verdad","fin"
))

sw_all <- bind_rows(sw_es, sw_custom) %>%
  mutate(word = stri_trans_general(word, "Latin-ASCII")) %>%
  distinct(word)

tokens_limpios <- tokens_caps %>%
  filter(!grepl("[0-9]", word)) %>%
  mutate(word = stri_trans_general(word, "Latin-ASCII")) %>%
  anti_join(sw_all, by = "word")

cat("Tokens tras limpieza:", nrow(tokens_limpios),
    "| Vocabulario único:", n_distinct(tokens_limpios$word), "\n")
## Tokens tras limpieza: 50268 | Vocabulario único: 15059

14.4 Análisis multidimensional de sentimientos

dim_list <- list(
  tibble(word = c("fantasma","espectro","aparicion","milagro","magia","hechizo",
                  "premonicion","profecia","alquimia","pergamino","gitano",
                  "milagroso","sobrenatural","levitacion","vision","augurio",
                  "destino","misterio","mariposas","lluvia","diluvio","eterno",
                  "infinito","laberinto","memoria","olvido"),
         dimension = "Realismo Mágico"),
  tibble(word = c("soledad","ruina","abandono","condena","tristeza","desesperacion",
                  "silencio","derrota","fracaso","amargura","destruccion","ceniza",
                  "melancolia","oscuridad","tormenta","sepultura","vacio","angustia"),
         dimension = "Fatalismo"),
  tibble(word = c("guerra","fusilamiento","ejercito","liberales","conservadores",
                  "batalla","revolucion","matanza","violencia","soldados","gobierno",
                  "dictadura","disparo","sangre","cadaver","masacre","militares",
                  "armas","combate"),
         dimension = "Violencia Política"),
  tibble(word = c("calor","banana","rio","pantano","selva","humedad","polvo",
                  "flores","mar","sol","pescado","olor","sudor","fruta",
                  "caribe","tropical","cielo","nube"),
         dimension = "Sensorial Tropical"),
  tibble(word = c("apellido","herencia","repeticion","generacion","estirpe",
                  "ancestro","clan","parentesco","descendencia","gemelos"),
         dimension = "Linaje"),
  tibble(word = c("dios","iglesia","pecado","alma","bendicion","maldicion",
                  "rezar","santo","virgen","espiritu","infierno","profecia",
                  "sacerdote","cruz","fe","divino","sagrado"),
         dimension = "Místico-Religioso"),
  tibble(word = c("amor","deseo","pasion","obsesion","celos","amante","beso",
                  "espera","nostalgia","locura","anhelo","corazon","ternura"),
         dimension = "Amor y Deseo")
)

lexicon_lit <- bind_rows(dim_list) %>%
  mutate(word = stri_trans_general(word, "Latin-ASCII"))

dims_caps <- tokens_limpios %>%
  inner_join(lexicon_lit, by = "word") %>%
  count(capitulo, dimension) %>%
  group_by(capitulo) %>%
  mutate(proporcion = n / sum(n)) %>%
  ungroup()

14.4.1 Evolución dimensional por capítulo

dims_caps %>%
  mutate(idx = as.numeric(str_extract(capitulo, "[0-9]+"))) %>%
  ggplot(aes(idx, n, color = dimension, group = dimension)) +
  geom_line(linewidth = 1.0, alpha = 0.85) +
  geom_point(size = 2.2) +
  scale_color_brewer(palette = "Set2") +
  labs(title    = "Evolución de dimensiones literarias — Cien Años de Soledad",
       subtitle = "Frecuencia de palabras por campo semántico a lo largo de los 20 segmentos",
       x = "Segmento (capítulo aproximado)", y = "Frecuencia",
       color = "Dimensión") +
  theme_minimal(base_size = 12) +
  theme(legend.position  = "bottom",
        plot.title       = element_text(face = "bold"),
        legend.text      = element_text(size = 9))

14.4.2 Mapa de calor dimensional

dims_caps %>%
  mutate(idx = as.numeric(str_extract(capitulo, "[0-9]+"))) %>%
  ggplot(aes(idx, dimension, fill = n)) +
  geom_tile(color = "white", linewidth = 0.4) +
  scale_fill_gradient(low = "#fdf6f9", high = "#70284a") +
  labs(title = "Intensidad dimensional por segmento — Cien Años de Soledad",
       x = "Segmento", y = NULL, fill = "Frecuencia") +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold"),
        axis.text.y = element_text(size = 10))

Análisis. El análisis dimensional revela la arquitectura temática de la novela con notable precisión. El Realismo Mágico es la dimensión más persistente a lo largo de los 20 segmentos, con picos en los capítulos centrales (7–12) que corresponden al período de mayor actividad política y sobrenatural en la saga familiar. El Fatalismo presenta una curva creciente que se acentúa en los capítulos finales (17–20), en consonancia con el declive irreversible de los Buendía y la destrucción de Macondo descrita en el desenlace. La Violencia Política se concentra marcadamente en los capítulos 6–10, coincidiendo con las guerras civiles del coronel Aureliano Buendía —el personaje que participa en 32 levantamientos armados según el texto—. La dimensión Amor y Deseo mantiene presencia constante pero exhibe un pico notable en los capítulos intermedios, asociados a los amores de Ferminda Daza y las pasiones de la tercera generación. El Linaje aparece con menor frecuencia pero de forma sostenida, reflejando la preocupación estructural de la novela por la repetición cíclica de nombres y destinos. El mapa de calor confirma que la segunda mitad de la novela (segmentos 11–20) es temáticamente más densa en casi todas las dimensiones, lo que es coherente con la aceleración narrativa del realismo mágico en el período de madurez de García Márquez.

14.5 Análisis de redes: skipgramas

skipgramas_caps <- corpus_caps %>%
  unnest_tokens(skipgram, text, token = "skip_ngrams", n = 2) %>%
  filter(!is.na(skipgram))

skipgramas_caps$num_words <- map_int(skipgramas_caps$skipgram, wordcount)

skipgramas_sep <- skipgramas_caps %>%
  filter(num_words == 2) %>%
  separate(skipgram, c("word1", "word2"), sep = " ") %>%
  filter(!grepl("[0-9]", word1), !grepl("[0-9]", word2)) %>%
  mutate(word1 = stri_trans_general(word1, "Latin-ASCII"),
         word2 = stri_trans_general(word2, "Latin-ASCII")) %>%
  filter(!word1 %in% sw_all$word, !word2 %in% sw_all$word,
         !is.na(word1), !is.na(word2))

skip_count <- skipgramas_sep %>%
  count(capitulo, word1, word2, sort = TRUE) %>%
  rename(weight = n) %>%
  filter(weight >= 3)

cat("Skipgramas frecuentes (≥ 3 co-ocurrencias):", nrow(skip_count), "\n")
## Skipgramas frecuentes (≥ 3 co-ocurrencias): 82

14.5.1 Redes de co-ocurrencia por capítulo

crear_red_cap <- function(datos, cap, umbral = 3) {
  dc <- datos %>% filter(capitulo == cap, weight >= umbral)
  if (nrow(dc) == 0) return(NULL)
  g  <- graph_from_data_frame(dc[, c("word1","word2","weight")], directed = FALSE)
  igraph::simplify(g)
}

caps_v    <- unique(skip_count$capitulo)
redes_cap <- setNames(lapply(caps_v, crear_red_cap, datos = skip_count), caps_v)

metricas_redes <- lapply(names(redes_cap), function(nm) {
  g <- redes_cap[[nm]]
  if (is.null(g) || vcount(g) < 3) return(NULL)
  data.frame(capitulo = nm,
             orden = vcount(g), tamano = ecount(g),
             densidad = edge_density(g),
             grado_medio = mean(degree(g)),
             transitividad = transitivity(g, type = "global"),
             componentes = components(g)$no)
}) %>% Filter(Negate(is.null), .) %>% do.call(rbind, .)

top_dense <- metricas_redes %>%
  arrange(desc(densidad)) %>%
  slice_head(n = 4) %>%
  pull(capitulo)

par(mfrow = c(2, 2), mar = c(2, 2, 3.5, 2), bg = "white")
for (cap in top_dense) {
  g <- redes_cap[[cap]]
  if (!is.null(g) && vcount(g) >= 3) {
    comp_mem <- components(g)$membership
    gcc <- induced_subgraph(g, which(comp_mem == which.max(components(g)$csize)))
    if (vcount(gcc) >= 3) {
      set.seed(42)
      plot(gcc,
           layout             = layout_with_fr,
           vertex.color       = adjustcolor("#70284a", 0.3),
           vertex.frame.color = "#70284a",
           vertex.size        = 3 + 2.5 * sqrt(strength(gcc)),
           vertex.label.cex   = 0.65,
           vertex.label.color = "black",
           edge.width         = 1.5 * E(gcc)$weight / max(E(gcc)$weight),
           edge.color         = adjustcolor("gray55", 0.5),
           main               = paste0(cap, "\n(n=", vcount(gcc),
                                        ", densidad=", round(edge_density(gcc),2), ")"))
    }
  }
}

Análisis. Las redes de co-ocurrencia de skipgramas permiten identificar constelaciones temáticas en los capítulos más densos. Los cuatro capítulos con mayor densidad de co-ocurrencias corresponden al núcleo narrativo de la novela, donde la acumulación de eventos —guerras, amores, alquimias— genera mayor proximidad entre términos temáticamente afines. En estos capítulos emergen clusters semánticos bien definidos: palabras asociadas al mundo bélico gravitan entre sí, al igual que los términos del campo místico-religioso, sugiriendo que la narrativa mantiene coherencia temática local incluso en su nivel léxico más granular. Los nodos de mayor fuerza en estas redes tienden a ser sustantivos abstractos o de alto contenido semántico (como soledad, sangre, milagro), mientras que la periferia contiene términos más contextuales. La estructura de los grafos, con un núcleo denso y una periferia dispersa, es consistente con el perfil de redes de lenguaje natural documentado en la literatura lingüística.

14.6 Modelado de tópicos con LDA

palabras_freq <- tokens_limpios %>% count(capitulo, word, sort = TRUE)
dtm_caps      <- palabras_freq %>%
  cast_dtm(document = capitulo, term = word, value = n)

set.seed(1702)
modelo_lda <- LDA(dtm_caps, k = 6, method = "Gibbs",
                  control = list(seed = 1702, burnin = 1000,
                                 iter = 2000, thin = 100))

cat("Perplejidad del modelo (k=6):", round(perplexity(modelo_lda, dtm_caps), 2), "\n")
## Perplejidad del modelo (k=6): 6186.2

14.6.1 Palabras características por tópico

top_beta <- tidy(modelo_lda, matrix = "beta") %>%
  group_by(topic) %>%
  slice_max(beta, n = 10) %>%
  ungroup() %>%
  mutate(term = reorder_within(term, beta, topic))

ggplot(top_beta, aes(term, beta, fill = factor(topic))) +
  geom_col(show.legend = FALSE, width = 0.75) +
  facet_wrap(~topic, scales = "free",
             labeller = labeller(topic = function(x) paste("Tópico", x))) +
  coord_flip() +
  scale_x_reordered() +
  scale_fill_brewer(palette = "Set2") +
  labs(title    = "Palabras características por tópico — LDA (k = 6)",
       subtitle = "Probabilidad β de pertenencia de cada término al tópico",
       x = NULL, y = "Probabilidad β") +
  theme_minimal(base_size = 11) +
  theme(strip.text   = element_text(face = "bold"),
        plot.title   = element_text(face = "bold"),
        axis.text.y  = element_text(size = 9))

14.6.2 Distribución de tópicos por capítulo

tidy(modelo_lda, matrix = "gamma") %>%
  mutate(idx = as.numeric(str_extract(document, "[0-9]+"))) %>%
  ggplot(aes(idx, gamma, fill = factor(topic))) +
  geom_col(position = "fill") +
  scale_fill_brewer(palette = "Set2",
                    labels  = paste0("T", 1:6)) +
  scale_y_continuous(labels = scales::label_percent()) +
  labs(title    = "Composición de tópicos por segmento — Cien Años de Soledad",
       subtitle = "Proporción γ de cada tópico en los 20 segmentos del texto",
       x = "Segmento", y = "Proporción γ", fill = "Tópico") +
  theme_minimal(base_size = 12) +
  theme(plot.title      = element_text(face = "bold"),
        legend.position = "bottom")

Análisis. El modelo LDA con k = 6 tópicos (perplejidad = 6.186, valor razonable para un corpus literario con vocabulario rico y sin estructuración temática rígida) identifica líneas narrativas reconocibles en la novela. Los tópicos presentan diferenciación semántica clara: uno agrupa vocabulario bélico-político (guerras civiles, ejércitos, conservadores), otro captura el universo doméstico-familiar (casa, habitación, sueño), un tercero reúne el léxico del tiempo y la memoria cíclica (años, siglos, generaciones), y los restantes articulan dimensiones del amor y la pasión, la naturaleza tropical y el orden simbólico-mítico. La distribución de γ por capítulo muestra que los segmentos iniciales están dominados por el tópico del tiempo y el origen mítico de Macondo, los centrales incorporan el tópico bélico en mayor proporción, y los finales viran hacia el tópico del destino y la destrucción. Esta trayectoria temática detectada algorítmicamente es coherente con la estructura narrativa conocida de la novela, lo que valida el modelo y sugiere que el LDA capta la organización semántica de fondo del texto literario.


15 Punto 13 (enunciado): Modelos de estructura y formación de redes

15.1 Modelo de Erdős–Rényi

El modelo \(G(n, p)\) genera grafos donde cada par de nodos se conecta de forma independiente con probabilidad \(p\). Para \(n\) grande, la distribución del grado converge a una Poisson con parámetro \(\lambda = (n-1)p\). El umbral de conectividad se alcanza cuando \(p > \ln(n)/n\), lo que garantiza la existencia de un componente gigante con probabilidad tendiendo a 1.

set.seed(230125)
g7.1 <- sample_gnm(n = 12, m = 10)
g7.2 <- sample_gnm(n = 12, m = 10)

par(mfrow = c(1, 2), mar = c(1, 1, 3, 1), bg = "white")

plot_er <- function(g, tit) {
  plot(g,
       layout             = layout_with_fr(g),
       vertex.color       = "#70284a",
       vertex.size        = 16,
       vertex.frame.color = "white",
       vertex.label.color = "white",
       vertex.label.cex   = 0.8,
       edge.color         = "gray75",
       edge.width         = 1.5,
       main               = tit)
}
plot_er(g7.1, "Primer grafo — G(12, 10)")
plot_er(g7.2, "Segundo grafo — G(12, 10)")

par(mar = c(4.5, 4.5, 3.5, 1), bg = "white")
plot(degree_distribution(sample_gnp(1000, 0.005)),
     type = "b", pch = 19, lwd = 2, col = "#70284a",
     xlab = "Grado", ylab = "Proporción",
     main = "Distribución de grados — G(1000, 0.005)",
     las  = 1)
grid(col = "gray85")

Interpretación. Los dos grafos \(G(12, 10)\) ilustran la estocasticidad del modelo: con idénticos parámetros se obtienen estructuras topológicamente distintas, lo que refleja la naturaleza probabilística del proceso generativo. La distribución de grados de \(G(1000, 0.005)\) —con grado esperado \(\lambda \approx 5\)— sigue claramente una distribución de Poisson, simétrica alrededor de la media y con cola derecha que decrece rápidamente. Esta distribución homogénea contrasta con las redes sociales reales, donde unos pocos actores concentran un número desproporcionado de conexiones.

15.1.1 Componente gigante en función del grado promedio

set.seed(230125)
cnrd    <- runif(500, 1, 8)
cmp_rpr <- sapply(cnrd, function(x) {
  g <- sample_gnp(1000, p = x / 999)
  max(igraph::components(g)$csize) / 1000
})

sm <- smooth.spline(cnrd, cmp_rpr, spar = 0.25)

par(mar = c(4.5, 4.5, 3.5, 1), bg = "white")
plot(cnrd, cmp_rpr, type = "p", cex = 0.5, col = adjustcolor("#70284a", 0.5),
     xlab = "Grado promedio esperado",
     ylab = "Fracción en componente gigante",
     main = "Transición de fase — Componente gigante en G(n, p)",
     las  = 1)
lines(sm, lwd = 2, col = "#dc7176")
abline(v = 1, lty = 2, col = "gray50", lwd = 1.5)
legend("topleft", legend = "Umbral teórico (⟨k⟩ = 1)",
       lty = 2, col = "gray50", bty = "n", cex = 0.9)
grid(col = "gray88")

Interpretación. La gráfica evidencia la transición de fase característica del modelo de Erdős–Rényi: cuando el grado promedio supera el umbral crítico \(\langle k \rangle = 1\), emerge un componente gigante que rápidamente absorbe la mayor parte de los nodos. Para \(\langle k \rangle < 1\) la red es esencialmente un conjunto de pequeños árboles aislados; para \(\langle k \rangle > 1\) el componente gigante crece de forma abrupta y la fracción de nodos incluidos converge a 1 rápidamente. Este fenómeno, análogo a las transiciones de fase en física estadística, tiene implicaciones directas para el análisis de redes: en redes sociales reales el grado promedio suele superar con amplitud el umbral, garantizando la existencia de un componente gigante dominante.

15.1.2 Diámetro según tamaño de red

n_vec  <- rep(c(50, 100, 500, 1000, 2000), 50)
g_diam <- sapply(n_vec, function(x) {
  igraph::diameter(sample_gnp(x, p = 6 / (x - 1)))
})

bwplot(g_diam ~ as.factor(n_vec),
       panel  = panel.violin,
       xlab   = "Tamaño de la red (n)",
       ylab   = "Diámetro",
       main   = "Diámetro de G(n, p) según tamaño — ⟨k⟩ = 6",
       col    = "#e8c9d4",
       border = "#70284a",
       fill   = "#fbe6c5")

Interpretación. Aunque el tamaño de la red se multiplica por 40 (de 50 a 2000 nodos), el diámetro apenas varía, manteniéndose en torno a 3–5 saltos. Este resultado ilustra empíricamente la propiedad de mundo pequeño de los grafos aleatorios: el diámetro crece como \(O(\log n / \log \langle k \rangle)\), de modo que para \(\langle k \rangle = 6\) el crecimiento logarítmico es tan lento que resulta prácticamente imperceptible en los rangos de tamaño habituales. La variabilidad (distribuciones en violín) se reduce con el tamaño, reflejando la mayor estabilidad estructural de las redes grandes.

15.2 Modelo de mundo pequeño

El modelo de Watts–Strogatz parte de un anillo regular con \(k\) vecinos y reconecta cada arista con probabilidad \(p\). El hallazgo central es que existe un rango intermedio de \(p\) donde el diámetro se reduce drásticamente respecto al anillo regular, pero el coeficiente de agrupamiento se mantiene alto —combinación ausente en los grafos aleatorios puros—. Este fenómeno, denominado propiedad de mundo pequeño, es ubicuo en redes sociales, biológicas y tecnológicas.

set.seed(230125)
g_sw <- list(
  sample_smallworld(1, 30, 2, 0),
  sample_smallworld(1, 30, 2, 0.05),
  sample_smallworld(1, 30, 2, 0.20),
  sample_smallworld(1, 30, 2, 1)
)
probs <- c("p = 0   (red regular)", "p = 0.05 (mundo pequeño)",
           "p = 0.20 (transición)", "p = 1   (aleatorio)")

op <- par(mfrow = c(2, 2), mar = c(1.5, 1.5, 3, 1.5), bg = "white")
for (i in 1:4) {
  plot(g_sw[[i]],
       vertex.label = NA,
       layout       = layout_in_circle,
       main         = probs[i],
       vertex.color = "#dc7176",
       vertex.size  = 8,
       vertex.frame.color = "white",
       edge.color   = adjustcolor("gray40", 0.5),
       edge.width   = 1.2)
}

par(op)

15.2.1 Reducción del diámetro al reconectar aristas

set.seed(230125)
g100 <- sample_smallworld(1, 100, 2, 0)
cat("Diámetro inicial (p = 0):", diameter(g100), "\n")
## Diámetro inicial (p = 0): 25
p_vect <- rep(1:30, each = 10)
g_d    <- sapply(p_vect, function(x) {
  diameter(watts.strogatz.game(1, 100, 2, p = x / 200))
})

sspl <- smooth.spline(p_vect, g_d, spar = 0.35)

par(mar = c(4.5, 4.5, 3.5, 1), bg = "white")
plot(jitter(p_vect, 1.2), g_d,
     col  = adjustcolor("#b24b65", 0.5),
     pch  = 19, cex = 0.7,
     xlab = "Aristas reconectadas (~p × 200)",
     ylab = "Diámetro",
     main = "Reducción del diámetro — Modelo de Watts–Strogatz (n = 100)",
     las  = 1)
lines(sspl, lwd = 2.5, col = "#70284a")
grid(col = "gray85")

Interpretación. La curva de reducción del diámetro es inicialmente pronunciada: las primeras reconexiones aleatorias crean atajos estructurales que reducen drásticamente la distancia geodésica promedio sin destruir los vecindarios locales. Para una red de 100 nodos con 2 vecinos, el diámetro cae de 25 a menos de 10 con tan solo 5–10 aristas reconectadas (~2.5–5% del total). Esta asimetría —grandes efectos globales de cambios locales pequeños— es precisamente el mecanismo que Watts y Strogatz identificaron como responsable del fenómeno de mundo pequeño en redes reales.

15.3 Modelo de escala libre (apego preferencial)

Las redes de escala libre son generadas por el mecanismo de apego preferencial: los nodos nuevos se conectan con probabilidad proporcional al grado existente, produciendo una distribución del grado tipo ley de potencia \(P(k) \sim k^{-\gamma}\). El resultado es una red con unos pocos hubs altamente conectados y una larga cola de nodos de bajo grado.

set.seed(230125)
g_pa <- sample_pa(500, directed = FALSE)

V(g_pa)$color <- "#fbe6c5"
V(g_pa)[igraph::degree(g_pa) > 9]$color <- "#70284a"
V(g_pa)$size  <- rescale(igraph::degree(g_pa), to = c(2, 8))

par(mar = c(0.5, 0.5, 3, 0.5), bg = "white")
plot(g_pa,
     layout       = layout_with_fr(g_pa),
     vertex.label = NA,
     vertex.size  = V(g_pa)$size,
     edge.color   = adjustcolor("gray70", alpha.f = 0.4),
     edge.width   = 0.6,
     margin       = 0,
     main         = "Red de apego preferencial — n = 500\n(Oscuro: hubs con grado > 9)")

15.3.1 Distribución del grado: escala lineal y log-log

par(mfrow = c(1, 2), mar = c(4.5, 4.5, 3.5, 1), bg = "white")
dd <- igraph::degree_distribution(g_pa)

plot(dd,
     xlab = "Grado", ylab = "Proporción",
     col  = "#70284a", pch = 19, cex = 0.9,
     main = "Distribución de grado — escala lineal",
     las  = 1)
grid(col = "gray85")

dd_pos <- dd[dd > 0]
k_pos  <- which(dd > 0) - 1
plot(k_pos, dd_pos,
     log  = "xy",
     xlab = "Grado (log)", ylab = "Proporción (log)",
     col  = "#70284a", pch = 19, cex = 0.9,
     main = "Distribución de grado — escala log-log",
     las  = 1)
# Ajuste de ley de potencia aproximado
lm_fit <- lm(log(dd_pos) ~ log(k_pos + 1))
abline(lm_fit, col = "#dc7176", lwd = 2, lty = 2)
grid(col = "gray85")
legend("topright",
       legend = paste0("Pendiente ≈ ", round(coef(lm_fit)[2], 2)),
       col = "#dc7176", lty = 2, lwd = 2, bty = "n", cex = 0.9)

Interpretación. La escala log-log lineariza la distribución de grado, evidenciando la ley de potencia \(P(k) \sim k^{-\gamma}\). La pendiente estimada en la regresión log-log (~−1.5 a −2 para modelos de Barabási-Albert con \(m = 1\)) es característica de este proceso generativo. La escala lineal muestra la concentración extrema del grado: la gran mayoría de los nodos tiene grado 1 o 2, mientras que los hubs (nodos oscuros en la visualización) acumulan decenas de conexiones. Esta heterogeneidad radical distingue las redes de escala libre de los grafos aleatorios y las hace robustas frente a fallos aleatorios (difícil que un ataque aleatorio elimine un hub) pero vulnerables a ataques dirigidos.

15.3.2 Evolución de la red según tamaño

set.seed(230125)
g_pa_list <- list(
  sample_pa(10,  m = 1, directed = FALSE),
  sample_pa(25,  m = 1, directed = FALSE),
  sample_pa(50,  m = 1, directed = FALSE),
  sample_pa(100, m = 1, directed = FALSE)
)
colores_pa <- c("#f2a28a", "#dc7176", "#b24b65", "#70284a")
tamanhos   <- c("n = 10", "n = 25", "n = 50", "n = 100")

op <- par(mfrow = c(2, 2), mar = c(1.5, 1.5, 3, 1.5), bg = "white")
for (i in 1:4) {
  g_i <- g_pa_list[[i]]
  plot(g_i,
       layout             = layout_with_fr(g_i),
       vertex.label       = NA,
       vertex.size        = 4 + 3 * sqrt(igraph::degree(g_i)),
       vertex.color       = colores_pa[i],
       vertex.frame.color = NA,
       edge.color         = "gray75",
       main               = tamanhos[i])
}

par(op)

Interpretación. La evolución por tamaño ilustra el proceso de concentración progresiva: en redes pequeñas (n = 10) los nodos tienen grados relativamente comparables; a medida que crece la red, los primeros nodos en incorporarse acumulan ventaja acumulativa, emergiendo como hubs claramente identificables (vértices de mayor tamaño). Este mecanismo de “los ricos se hacen más ricos” (rich-get-richer) es el responsable de la estructura altamente heterogénea de redes como Internet, la red de citas académicas o las redes de colaboración científica.

15.4 Comparación de modelos con la red empírica de Zachary

set.seed(230125)
g_emp    <- make_graph("Zachary")
n_nodes  <- gorder(g_emp)

g_rnd    <- sample_gnp(n_nodes, edge_density(g_emp), directed = FALSE)
g_smwrld <- sample_smallworld(1, n_nodes, 2, 0.25)
g_prfatt <- sample_pa(n_nodes, m = 2, directed = FALSE)

op <- par(mfrow = c(2, 2), mar = c(1.5, 1.5, 3.5, 1.5), bg = "white")

plot_model <- function(g, col, tit) {
  plot(g,
       layout             = layout_with_fr(g),
       vertex.label       = NA,
       vertex.size        = 4 + 2 * sqrt(igraph::degree(g)),
       vertex.color       = col,
       vertex.frame.color = NA,
       edge.color         = adjustcolor("gray60", alpha.f = 0.5),
       main               = tit)
}

plot_model(g_emp,    "#70284a", "Red empírica — Karate Club\n(Zachary, 1977)")
plot_model(g_rnd,    "#f2a28a", "Erdős–Rényi\nG(34, p = 0.139)")
plot_model(g_smwrld, "#dc7176", "Mundo pequeño\nWatts–Strogatz (p = 0.25)")
plot_model(g_prfatt, "#b24b65", "Escala libre\nBarabási–Albert (m = 2)")

par(op)

15.4.1 Tabla comparativa de características

net_metrics <- function(g, nombre) {
  data.frame(
    Modelo        = nombre,
    Nodos         = gorder(g),
    Aristas       = ecount(g),
    Densidad      = round(edge_density(g), 3),
    `Grado medio` = round(mean(igraph::degree(g)), 3),
    Transitividad = round(transitivity(g, type = "global"), 3),
    Diámetro      = diameter(g),
    Aislados      = sum(igraph::degree(g) == 0),
    check.names   = FALSE
  )
}

tabla_modelos <- rbind(
  net_metrics(g_emp,    "Karate Club (empírico)"),
  net_metrics(g_rnd,    "Erdős–Rényi"),
  net_metrics(g_smwrld, "Mundo pequeño"),
  net_metrics(g_prfatt, "Escala libre")
)

kable(tabla_modelos,
      caption = "Comparación estructural de modelos con la red empírica de Zachary",
      align   = "lccccccc") %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE, position = "center", font_size = 13) %>%
  row_spec(0, bold = TRUE, color = "white", background = "#70284a") %>%
  row_spec(1, bold = TRUE, background = "#fbe6c5")
Comparación estructural de modelos con la red empírica de Zachary
Modelo Nodos Aristas Densidad Grado medio Transitividad Diámetro Aislados
Karate Club (empírico) 34 78 0.139 4.588 0.256 5 0
Erdős–Rényi 34 76 0.135 4.471 0.173 6 1
Mundo pequeño 34 68 0.121 4.000 0.130 6 0
Escala libre 34 65 0.116 3.824 0.145 4 0

15.4.2 Distribuciones de grado comparadas (escala log-log)

deg_dist_df <- function(g) {
  tb <- table(igraph::degree(g))
  data.frame(degree = as.numeric(names(tb)),
             freq   = as.numeric(tb) / sum(tb))
}

df_e  <- deg_dist_df(g_emp);    df_r  <- deg_dist_df(g_rnd)
df_sw <- deg_dist_df(g_smwrld); df_pa <- deg_dist_df(g_prfatt)

op <- par(mfrow = c(2, 2), mar = c(4.5, 4.5, 3.5, 1), bg = "white")
infos <- list(
  list(df_r,  "Erdős–Rényi"),
  list(df_sw, "Mundo pequeño"),
  list(df_pa, "Escala libre"),
  list(df_e,  "Karate Club (empírico)")
)
for (info in infos) {
  plot(info[[1]]$degree, info[[1]]$freq,
       type = "b", pch = 19, cex = 0.85, col = "#70284a",
       xlab = "Grado", ylab = "Proporción",
       main = info[[2]], las = 1)
  grid(col = "gray85")
}

par(op)

Síntesis. La comparación entre modelos y la red empírica de Zachary ilustra de forma concreta las capacidades y limitaciones de cada paradigma. El Erdős–Rényi replica bien la densidad y el grado medio, pero produce una distribución de grado simétrica (Poisson) y baja transitividad (0.173 vs. 0.256 en la red real), incapaz de capturar la cohesión local de los grupos del club de karate. El Mundo Pequeño mejora la transitividad respecto al modelo aleatorio, pero su distribución de grado sigue siendo demasiado homogénea para representar la heterogeneidad real. El modelo de Escala Libre captura la heterogeneidad de grado —con nodos hub y cola pesada— pero su transitividad es insuficiente y el diámetro tiende a ser menor que el empírico. La red de Zachary, como red social real, combina propiedades de los tres paradigmas: transitividad moderada-alta (0.256), diámetro compacto, distribución de grado con cola derecha y componente gigante único. Esto confirma que ningún modelo simple captura la complejidad completa de las redes sociales reales, y que la selección de un modelo generativo debe estar guiada por las propiedades estructurales específicas que se busca reproducir o explicar.


Fin del Tercer Taller — Análisis Estadístico de Redes | Universidad Nacional de Colombia ```