A continuación se presenta el desarrollo del taller correspondiente a la asignatura Análisis Estadístico de Redes, perteneciente a la Maestría en Ciencias – Estadística.
Presentado por: Estudiante: Helen Granados Rodríguez | CC: 1000835249 | Correo: hgranados@unal.edu.co
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(ggrepel)
library(gridExtra)
library(wordcloud)
library(reshape2)
library(topicmodels)
library(readr)
library(jsonlite)
library(stringr)
library(purrr)
library(tibble)
library(lattice)# 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
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
## Tamaño (aristas): 11
## ¿Es dirigido?: TRUE
## Densidad: 0.0262
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"))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.
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.
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")| 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.
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)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")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")| 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. El actor 11 —nivel gerencial— presenta el mayor sesgo absoluto (0.60), reportando un grado de salida propio de 0.60 frente a un consenso de 0.00; los actores 7, 10 y 15 le siguen con un sesgo de 0.40, todos con grado de consenso nulo. Esto significa que estos actores perciben que tienen múltiples amistades que el resto del grupo no valida en absoluto. El grado de entrada exhibe un patrón diferente: para varios actores, el consenso supera la autopercepción, indicando que son percibidos como nodos populares por los demás pero no se reconocen como tales. Cabe destacar que el actor 1 (presidente), aunque no encabeza la tabla de sesgos de salida, sí muestra en el grado de entrada una de las mayores discrepancias a favor del consenso, coherente con el reconocimiento colectivo de su posición central.
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)$vectorpar(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)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)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")| 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 notablemente 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 arroja un resultado contraintuitivo respecto a la jerarquía formal: los actores 5, 9 y 19 —todos de nivel gerencial (nivel 3)— obtienen el valor máximo de centralidad eigenvector (1.0) en el consenso, mientras que el actor 1 (presidente) registra un valor de 0. Este hallazgo sugiere que, en la red de amistad de consenso, la influencia relacional no coincide con la autoridad formal: los actores 5, 9 y 19 están conectados a nodos que a su vez son percibidos como centrales por el colectivo, mientras que el presidente queda estructuralmente aislado en la red de consenso a pesar de su posición jerárquica.
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í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.
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)## Transitividad promedio percepciones: 0.354
## 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.
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)## Asortatividad promedio percepciones: -0.13
## 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.
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")| 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.
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.
Enunciado. El Sistema HORUS de la Universidad Nacional de Colombia integra y visualiza la productividad científica institucional. A partir de los datos disponibles para la Sede Bogotá, Facultad de Ciencias, construya la red binaria no dirigida de docentes para cada departamento (Estadística, Farmacia, Física, Geociencias, Matemáticas, Química) mediante el producto matricial de la red bipartita. Cree visualizaciones detalladas, caracterice cada red a nivel local y estructural con todas las métricas disponibles, y presente un análisis comparativo. Repita el análisis para toda la Sede Bogotá.
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
## Conexiones: 706
## Densidad: 0.4271
# Visualización con ggraph usando layout de Fruchterman–Reingold
# Se filtran aristas de bajo peso para reducir ruido visual
umbral_est <- quantile(E(g_estadistica)$weight, 0.30)
g_est_filt <- delete_edges(g_estadistica,
E(g_estadistica)[E(g_estadistica)$weight < umbral_est])
g_est_filt <- delete_vertices(g_est_filt, which(degree(g_est_filt) == 0))
# Calcular métricas de vértices
V(g_est_filt)$grado <- degree(g_est_filt)
V(g_est_filt)$betweenness <- betweenness(g_est_filt, normalized = TRUE)
V(g_est_filt)$comunidad <- as.factor(membership(cluster_louvain(g_est_filt)))
set.seed(2026)
ggraph(g_est_filt, layout = "fr") +
geom_edge_link(aes(width = weight, alpha = weight),
color = "#9b6b8a",
show.legend = FALSE) +
geom_node_point(aes(size = sqrt(grado) * 3,
color = betweenness),
show.legend = TRUE) +
geom_node_text(aes(label = ifelse(grado >= quantile(grado, 0.70),
name, NA)),
repel = TRUE, size = 2.8,
color = "gray20", max.overlaps = 15) +
scale_edge_width(range = c(0.3, 2.5)) +
scale_edge_alpha(range = c(0.15, 0.6)) +
scale_size_identity() +
scale_color_gradient(low = "#fdf0f5", high = "#70284a",
name = "Intermediación\n(normalizada)") +
labs(title = "Red de colaboración temática — Departamento de Estadística",
subtitle = "UNAL Bogotá · Nodos: tamaño ∝ √grado · Color: intermediación\nEtiquetas: 30% de docentes con mayor grado",
caption = "Aristas con peso ≥ percentil 30; layout Fruchterman–Reingold") +
theme_graph(base_family = "sans") +
theme(plot.title = element_text(face = "bold", size = 13, color = "#70284a"),
plot.subtitle = element_text(size = 9, color = "gray40"),
plot.caption = element_text(size = 8, color = "gray50"),
legend.position = "right",
plot.background = element_rect(fill = "white", color = NA))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_estadisticaLas redes se visualizan de forma individual para preservar la
legibilidad estructural de cada departamento. Para cada red se emplea
ggraph con layout Fruchterman–Reingold, el tamaño de los
nodos proporcional a \(\sqrt{\text{grado}}\) y el color mapeado a
la intermediación normalizada (Luke, 2015, cap. 4).
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%")| 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.043 |
| 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 | 19 | 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.
El análisis de intermediación (betweenness centrality) permite identificar a los docentes que actúan como puentes entre distintas líneas temáticas dentro de cada departamento. Siguiendo a Newman (2002), un nodo con alta intermediación controla el flujo de información entre subredes que de otro modo estarían desconectadas.
options(knitr.table.format = "html")
# Función: top k docentes por intermediación normalizada en una red
top_betw <- function(g, nombre, k = 5) {
if (is.null(g) || vcount(g) < 3) return(NULL)
bt <- betweenness(g, normalized = TRUE)
df <- data.frame(
Departamento = gsub("Departamento de ", "", nombre),
Docente = names(sort(bt, decreasing = TRUE)[seq_len(min(k, length(bt)))]),
Intermediación = round(sort(bt, decreasing = TRUE)[seq_len(min(k, length(bt)))], 4),
Grado = degree(g)[names(sort(bt, decreasing = TRUE)[seq_len(min(k, length(bt)))])],
row.names = NULL
)
df
}
tbl_betw_all <- do.call(rbind, lapply(names(redes_fc), function(nm) {
top_betw(redes_fc[[nm]], nm, k = 5)
}))
kable(tbl_betw_all,
caption = "Top 5 docentes por intermediación normalizada — Facultad de Ciencias, UNAL Bogotá",
align = "llrr") %>%
kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
full_width = TRUE, font_size = 12) %>%
row_spec(0, bold = TRUE, color = "white", background = "#70284a") %>%
column_spec(1, bold = TRUE) %>%
scroll_box(width = "100%")| Departamento | Docente | Intermediación | Grado |
|---|---|---|---|
| Farmacia | Plazas Bonilla Clara Eugenia | 0.1182 | 41 |
| Farmacia | Crosby Granados Milton Josue | 0.0916 | 20 |
| Farmacia | Maldonado Muete Carlos Enrique | 0.0710 | 30 |
| Farmacia | Perico Franco Litta Samari | 0.0628 | 25 |
| Farmacia | Becerra Camargo Jesus | 0.0565 | 45 |
| Física | Torres Salcedo Nestor Jaime | 0.1702 | 67 |
| Física | Rey Gonzalez Rafael Ramon | 0.0788 | 44 |
| Física | Leal Contreras Hildebrando | 0.0618 | 71 |
| Física | Perilla Perilla Carlos Joel | 0.0437 | 12 |
| Física | Franco Peñaloza Roberto Emilio | 0.0383 | 17 |
| Geociencias | Moreno Murillo Juan Manuel | 0.0916 | 31 |
| Geociencias | Leon Aristizabal Gloria Esperanza | 0.0813 | 15 |
| Geociencias | Diaz Almanza Eliecer David | 0.0650 | 30 |
| Geociencias | Cadena Sanchez Ariel Oswaldo | 0.0525 | 30 |
| Geociencias | Montoya Gaviria Gerardo De Jesus | 0.0284 | 7 |
| Matemáticas | Moreno Penagos Martha Cecilia | 0.0742 | 50 |
| Matemáticas | Berenstein Opscholtens Alexander Jonathan | 0.0487 | 41 |
| Matemáticas | Gaitan Orjuela Hernando | 0.0311 | 36 |
| Matemáticas | Castro Chadid Ivan | 0.0309 | 23 |
| Matemáticas | Sanchez Vasquez Alejandra | 0.0226 | 42 |
| Química | Osorno Reyes Oscar Eduardo | 0.1276 | 62 |
| Química | Trujillo Carlos Alexander | 0.1129 | 90 |
| Química | Castellanos Marquez Nelson Jair | 0.0820 | 82 |
| Química | Cubillos Gonzalez Gloria Ivonne | 0.0716 | 112 |
| Química | Diaz Velasquez Jose De Jesus | 0.0540 | 110 |
| Estadística | Ortiz Pinilla Jorge Eduardo | 0.2287 | 35 |
| Estadística | Lopez Perez Luis Alberto | 0.2261 | 16 |
| Estadística | Martínez Martínez Sergio Daniel | 0.1868 | 15 |
| Estadística | Trujillo Oyola Leonardo | 0.1789 | 39 |
| Estadística | Arunachalam Viswanathan | 0.0503 | 39 |
Análisis. Los docentes con mayor intermediación en cada departamento funcionan como articuladores temáticos: conectan grupos de investigación que de otro modo operarían de forma aislada. En departamentos como Química y Física, donde la red es grande y diversa, la intermediación alta suele corresponder a investigadores con producción publicada en múltiples áreas (por ejemplo, físico-química o geofísica-aplicada). En Estadística, dado el tamaño reducido y la alta densidad, los valores de intermediación son más bajos en términos absolutos, pero su rol articulador es igualmente relevante pues conectan subáreas como estadística computacional, estadística bayesiana y bioestadística. Nótese que intermediación alta y grado alto no siempre coinciden: un docente puede tener pocos colaboradores pero ser el único puente entre dos grupos temáticos, generando una intermediación desproporcionalmente elevada.
# Construir data frame de grados para todos los departamentos
df_grados_fc <- do.call(rbind, lapply(names(redes_fc), function(nm) {
g <- redes_fc[[nm]]
if (is.null(g)) return(NULL)
data.frame(
Departamento = gsub("Departamento de ", "", nm),
grado = degree(g)
)
}))
ggplot(df_grados_fc, aes(x = grado, fill = Departamento)) +
geom_histogram(binwidth = 5, color = "white", alpha = 0.85,
show.legend = FALSE) +
facet_wrap(~Departamento, scales = "free", ncol = 3) +
scale_fill_manual(values = c(
"Estadística" = "#70284a",
"Farmacia" = "#9b6b8a",
"Física" = "#3d0f28",
"Geociencias" = "#c9a8c0",
"Matemáticas" = "#5c1a35",
"Química" = "#b24b65"
)) +
labs(title = "Distribución del grado por departamento — Facultad de Ciencias",
subtitle = "Cada panel muestra la distribución de colaboraciones temáticas entre docentes",
x = "Grado (número de co-colaboradores temáticos)", y = "Frecuencia") +
theme_minimal(base_size = 11) +
theme(
plot.title = element_text(face = "bold", color = "#70284a"),
plot.subtitle = element_text(size = 9, color = "gray40"),
strip.text = element_text(face = "bold"),
panel.grid.minor = element_blank()
)Análisis. Las distribuciones de grado revelan heterogeneidad estructural entre departamentos. En Estadística y Geociencias, con redes pequeñas y densas, la distribución es relativamente concentrada alrededor del grado medio —patrón más cercano a Erdős–Rényi—. En Química y Física, los histogramas muestran colas más pesadas hacia la derecha, con algunos docentes que acumulan decenas o centenares de colaboraciones temáticas, sugiriendo un mecanismo de apego preferencial en la formación de vínculos académicos (Barabási & Albert, 1999). Esta heterogeneidad implica que la resiliencia de la red de colaboración varía entre departamentos: redes más homogéneas son más robustas ante eliminaciones aleatorias de nodos, mientras que las redes con hubs son vulnerables a la pérdida de sus articuladores centrales pero muy robustas frente a perturbaciones aleatorias (Luke, 2015).
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á")
}Enunciado. Considere las acciones de tutela disponibles en la página de la Relatoría de la Corte Constitucional de Colombia. Según la Corte, los derechos más protegidos mediante tutelas son, en primer lugar, el derecho de petición, seguido del derecho a la salud y, finalmente, el derecho al debido proceso. Una vez en la página, busque cada derecho filtrando por Tipo de providencia: Tutela y exporte los resultados. Realice un análisis exhaustivo del texto relacionado con estos derechos empleando técnicas de redes sociales. Compare los resultados obtenidos y preséntelos de manera clara y estructurada, destacando las similitudes, diferencias y patrones relevantes.
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.xlsx —
Debido Proceso (2022, año más reciente
disponible)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:
## Salud (2024): 166
## Petición (2024): 151
## 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
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")| 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")| 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")| 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 altas en todos los pares (r entre 0.70 y 0.87), lo que confirma la existencia de un vocabulario jurídico-institucional compartido. La correlación más alta corresponde al par salud–petición (r = 0.866), lo que indica que el discurso de las tutelas de salud y de petición comparte más vocabulario del esperado —términos como laboral, situacion, entidad y vulneracion son dominantes en ambos, reflejando que muchas tutelas de salud tienen componentes de petición implícitos—. La correlación salud–proceso es la más baja (r = 0.700), 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 debido proceso. Todos los p-valores son indistinguibles de cero, rechazando con total claridad la hipótesis nula de ausencia de correlación.
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")| 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 |
# Red de bigramas — Derecho a la Salud
set.seed(123)
plot(gcc_bi_salud,
layout = layout_with_fr,
vertex.color = adjustcolor(colores_gcc[1], 0.25),
vertex.frame.color = colores_gcc[1],
vertex.size = 2 + 5 * log1p(strength(gcc_bi_salud)) /
max(log1p(strength(gcc_bi_salud))),
vertex.label.cex = 0.65,
vertex.label.color = "black",
vertex.label.dist = 0.8,
edge.width = 2 * E(gcc_bi_salud)$weight / max(E(gcc_bi_salud)$weight),
edge.color = adjustcolor("gray50", 0.4),
main = "Bigramas — Derecho a la Salud (umbral > 2)\nComponente gigante")# Red de bigramas — Derecho de Petición
set.seed(123)
plot(gcc_bi_peticion,
layout = layout_with_fr,
vertex.color = adjustcolor(colores_gcc[2], 0.25),
vertex.frame.color = colores_gcc[2],
vertex.size = 2 + 5 * log1p(strength(gcc_bi_peticion)) /
max(log1p(strength(gcc_bi_peticion))),
vertex.label.cex = 0.65,
vertex.label.color = "black",
vertex.label.dist = 0.8,
edge.width = 2 * E(gcc_bi_peticion)$weight / max(E(gcc_bi_peticion)$weight),
edge.color = adjustcolor("gray50", 0.4),
main = "Bigramas — Derecho de Petición (umbral > 2)\nComponente gigante")# Red de bigramas — Debido Proceso
set.seed(123)
plot(gcc_bi_proceso,
layout = layout_with_fr,
vertex.color = adjustcolor(colores_gcc[3], 0.25),
vertex.frame.color = colores_gcc[3],
vertex.size = 2 + 5 * log1p(strength(gcc_bi_proceso)) /
max(log1p(strength(gcc_bi_proceso))),
vertex.label.cex = 0.65,
vertex.label.color = "black",
vertex.label.dist = 0.8,
edge.width = 2 * E(gcc_bi_proceso)$weight / max(E(gcc_bi_proceso)$weight),
edge.color = adjustcolor("gray50", 0.4),
main = "Bigramas — Debido Proceso (umbral > 2)\nComponente gigante")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.
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)# Red de skipgramas — Derecho a la Salud
set.seed(123)
plot(gcc_sk_salud,
layout = layout_with_fr,
vertex.color = adjustcolor(colores_gcc[1], 0.20),
vertex.frame.color = colores_gcc[1],
vertex.size = 2 + 5 * log1p(strength(gcc_sk_salud)) /
max(log1p(strength(gcc_sk_salud))),
vertex.label.cex = 0.62,
vertex.label.color = "black",
vertex.label.dist = 0.8,
edge.width = 2 * E(gcc_sk_salud)$weight / max(E(gcc_sk_salud)$weight),
edge.color = adjustcolor("gray50", 0.4),
main = "Skipgramas — Derecho a la Salud (umbral > 4)\nComponente gigante")# Red de skipgramas — Derecho de Petición
set.seed(123)
plot(gcc_sk_peticion,
layout = layout_with_fr,
vertex.color = adjustcolor(colores_gcc[2], 0.20),
vertex.frame.color = colores_gcc[2],
vertex.size = 2 + 5 * log1p(strength(gcc_sk_peticion)) /
max(log1p(strength(gcc_sk_peticion))),
vertex.label.cex = 0.62,
vertex.label.color = "black",
vertex.label.dist = 0.8,
edge.width = 2 * E(gcc_sk_peticion)$weight / max(E(gcc_sk_peticion)$weight),
edge.color = adjustcolor("gray50", 0.4),
main = "Skipgramas — Derecho de Petición (umbral > 4)\nComponente gigante")# Red de skipgramas — Debido Proceso
set.seed(123)
plot(gcc_sk_proceso,
layout = layout_with_fr,
vertex.color = adjustcolor(colores_gcc[3], 0.20),
vertex.frame.color = colores_gcc[3],
vertex.size = 2 + 5 * log1p(strength(gcc_sk_proceso)) /
max(log1p(strength(gcc_sk_proceso))),
vertex.label.cex = 0.62,
vertex.label.color = "black",
vertex.label.dist = 0.8,
edge.width = 2 * E(gcc_sk_proceso)$weight / max(E(gcc_sk_proceso)$weight),
edge.color = adjustcolor("gray50", 0.4),
main = "Skipgramas — Debido Proceso (umbral > 4)\nComponente gigante")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")| 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 grandes y con mayor grado medio que las de bigramas en los tres derechos, lo que refleja que al capturar co-ocurrencias con salto se detectan más asociaciones léxicas entre términos no adyacentes. La red de debido proceso es la más extensa en ambos tipos de n-grama (3.256 nodos y 4.946 aristas en bigramas; 5.092 nodos y 13.780 aristas en skipgramas), coherente con el mayor volumen de texto disponible (226 providencias de 2022) y la naturaleza transversal de este derecho. La red de petición presenta la asortatividad más negativa en bigramas (−0.123), indicando la estructura hub-and-spoke más marcada: unas pocas fórmulas jurídicas repetitivas (carencia actual, hecho superado) articulan un amplio vocabulario periférico. La transitividad es uniformemente baja en todas las redes (< 0.05), confirmando que los léxicos jurídicos no forman triángulos cerrados sino cadenas lineales de términos co-ocurrentes. En los skipgramas, la asortatividad se aproxima a cero en todos los casos, señal de que al ampliar la ventana de co-ocurrencia las diferencias entre derechos se homogeneizan y la estructura hub-and-spoke se diluye.
Enunciado. Con base en García-Arteaga y Pellegrino (2021), analice las redes de trabajo (
work-edges.csv) y alianzas (alliance-edges.csv) de figuras del panorama político colombiano recolectadas de La Silla Vacía. Analice cada red a nivel local y estructural —distancia, centralidad, cohesión, conectividad y agrupamiento— e interprete los hallazgos.
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)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é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")| 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 |
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é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")| 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 |
# Extraer componente gigante para la visualización
comp_ali <- components(red_alianzas)
gig_ali <- induced_subgraph(red_alianzas,
which(comp_ali$membership == which.max(comp_ali$csize)))
# Métricas de los vértices en el componente gigante
V(gig_ali)$grado <- degree(gig_ali)
V(gig_ali)$betw <- betweenness(gig_ali, normalized = TRUE)
# Comunidades con Louvain
set.seed(42)
cl_ali <- cluster_louvain(gig_ali)
V(gig_ali)$comunidad <- as.factor(membership(cl_ali))
# Paleta de comunidades en tonos morados y contrastantes
n_com_ali <- length(unique(membership(cl_ali)))
pal_com <- colorRampPalette(c("#c9a8c0","#70284a","#3d0f28","#9b6b8a"))(n_com_ali)
# Layout: solo etiquetar los 15 actores con mayor intermediación
top_labels <- names(sort(V(gig_ali)$betw, decreasing = TRUE)[1:15])
set.seed(2026)
par(mar = c(1, 1, 3.5, 1), bg = "white")
plot(gig_ali,
layout = layout_with_fr(gig_ali, niter = 1000),
vertex.color = pal_com[membership(cl_ali)],
vertex.size = 2 + 4 * sqrt(V(gig_ali)$grado),
vertex.frame.color = adjustcolor("gray20", 0.5),
vertex.label = ifelse(V(gig_ali)$name %in% top_labels,
V(gig_ali)$name, NA),
vertex.label.cex = 0.6,
vertex.label.color = "black",
vertex.label.dist = 0.5,
edge.color = adjustcolor("gray60", 0.25),
edge.width = 0.5,
main = "Red de alianzas políticas — Colombia\n(Componente gigante · Etiquetas: top 15 por intermediación · Color: comunidad Louvain)")comp_trab <- components(red_trabajo)
gig_trab <- induced_subgraph(red_trabajo,
which(comp_trab$membership == which.max(comp_trab$csize)))
V(gig_trab)$grado <- degree(gig_trab)
V(gig_trab)$betw <- betweenness(gig_trab, normalized = TRUE)
set.seed(42)
cl_trab <- cluster_louvain(gig_trab)
V(gig_trab)$comunidad <- as.factor(membership(cl_trab))
n_com_trab <- length(unique(membership(cl_trab)))
pal_com_t <- colorRampPalette(c("#c9a8c0","#70284a","#3d0f28","#9b6b8a","#5c1a35"))(n_com_trab)
top_labels_t <- names(sort(V(gig_trab)$betw, decreasing = TRUE)[1:15])
set.seed(2026)
par(mar = c(1, 1, 3.5, 1), bg = "white")
plot(gig_trab,
layout = layout_with_fr(gig_trab, niter = 1000),
vertex.color = pal_com_t[membership(cl_trab)],
vertex.size = 2 + 4 * sqrt(V(gig_trab)$grado),
vertex.frame.color = adjustcolor("gray20", 0.5),
vertex.label = ifelse(V(gig_trab)$name %in% top_labels_t,
V(gig_trab)$name, NA),
vertex.label.cex = 0.6,
vertex.label.color = "black",
vertex.label.dist = 0.5,
edge.color = adjustcolor("gray60", 0.25),
edge.width = 0.5,
main = "Red de trabajo institucional — Colombia\n(Componente gigante · Etiquetas: top 15 por intermediación · Color: comunidad Louvain)")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.
Enunciado. El archivo
traders.RDatacontiene un arreglo \(T \times I \times I\) con \(T = 201\) redes semanales de \(I = 71\) comerciantes del mercado de futuros de gas natural en la NYMEX (enero 2005 – diciembre 2008). Hasta la semana 83 las operaciones fueron exclusivamente open outcry; desde la semana 84 se introdujo una plataforma electrónica. (a) Calcule métricas semanales (densidad, clustering, asortatividad, reciprocidad, distancia media, componente gigante, grados promedio), grafique las series con líneas de mediana antes/después de la semana 83 y aplique la prueba de Mann–Whitney. (b) Segmente semanalmente con agrupamiento jerárquico sobre la red simetrizada y añada número de grupos y modularidad como series adicionales. (c) Calcule las redes de consenso \(Y_0\) e \(Y_1\) y compare sus métricas estructurales. (d) Visualice los componentes gigantes de \(Y_0\) e \(Y_1\) con tamaño proporcional a la fuerza del vértice y color según comunidad. Interprete los resultados.
# 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")| Métrica | Mediana ant. | Mediana post. | Cambio | p-valor | Significativa |
|---|---|---|---|---|---|
| Densidad | 0.1254 | 0.1975 | ↑ | 0 | Sí |
| Clustering | 0.5238 | 0.4647 | ↓ | 0 | Sí |
| Asortatividad | 0.0641 | -0.2983 | ↓ | 0 | Sí |
| Reciprocidad | 0.5284 | 0.5936 | ↑ | 0 | Sí |
| Distancia media | 2.4618 | 1.8609 | ↓ | 0 | Sí |
| Comp. gigante | 65.0000 | 69.0000 | ↑ | 0 | Sí |
| Grado salida | 8.7746 | 13.8239 | ↑ | 0 | Sí |
| Grado entrada | 8.7746 | 13.8239 | ↑ | 0 | Sí |
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.
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.
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")| 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.
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.
Enunciado. Considere la novela Cien años de soledad de Gabriel García Márquez. Realice un análisis exhaustivo de la obra por capítulos empleando técnicas de análisis de sentimientos y análisis de redes sociales para realizar comparaciones entre capítulos. Además, lleve a cabo un análisis de tópicos utilizando el modelo LDA estructurando el corpus a partir de los capítulos de la novela.
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
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
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
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()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))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.
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
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.
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
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))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.
Enunciado. Sintetice y replique el Capítulo 10 de Luke, D. A. (2015), que aborda los principales modelos generativos de redes: Erdős–Rényi, mundo pequeño (Watts–Strogatz) y escala libre (Barabási–Albert). Para cada modelo, ilustre su mecanismo generativo, analice la distribución de grados y las propiedades estructurales, y compare los resultados con una red empírica (red de Zachary).
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.
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.
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.
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 = "#9b6b8a",
vertex.size = 8,
vertex.frame.color = "white",
edge.color = adjustcolor("gray40", 0.5),
edge.width = 1.2)
}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("#9b6b8a", 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.
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)")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.
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("#c9a8c0", "#9b6b8a", "#70284a", "#3d0f28")
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])
}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.
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, "#c9a8c0", "Erdős–Rényi\nG(34, p = 0.139)")
plot_model(g_smwrld, "#9b6b8a", "Mundo pequeño\nWatts–Strogatz (p = 0.25)")
plot_model(g_prfatt, "#3d0f28", "Escala libre\nBarabási–Albert (m = 2)")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")| 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 |
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")
}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 ```