Librerías y carga de
datos
# NOTA — paquetes opcionales para modelos:
# install.packages(c("statnet","ergm","network","truncnorm","coda","pROC"))
suppressMessages({
library(igraph)
library(dplyr)
library(tidyr)
library(ggplot2)
library(knitr)
library(kableExtra)
library(truncnorm)
library(coda)
library(pROC)
library(scales)
})
# Carga condicional de ergm
ergm_disponible <- tryCatch({
suppressMessages({ library(statnet); library(ergm); library(network) })
TRUE
}, error = function(e) {
message("ergm/statnet no disponible — secciones ERGM se omiten.")
FALSE
})
cat("ergm disponible:", ergm_disponible, "\n")
## ergm disponible: FALSE
# Ajusta la ruta si el archivo no esta en el directorio de trabajo
load("mafia.rdata")
# Explorar que hay en el entorno despues de cargar
obj_cargados <- ls()
cat("Objetos cargados:", paste(obj_cargados, collapse = ", "), "\n")
## Objetos cargados: datos, ergm_disponible, G, rol
# Funcion que busca recursivamente un igraph dentro de cualquier objeto R
buscar_igraph <- function(obj, nombre = "raiz", profundidad = 0) {
if (profundidad > 5) return(NULL)
if (inherits(obj, "igraph")) {
cat(sprintf(" igraph encontrado en: %s\n", nombre))
return(obj)
}
if (is.list(obj)) {
for (i in seq_along(obj)) {
nm <- if (!is.null(names(obj))) names(obj)[i] else paste0("[[", i, "]]")
resultado <- buscar_igraph(obj[[i]],
paste0(nombre, "$", nm),
profundidad + 1)
if (!is.null(resultado)) return(resultado)
}
}
# Buscar en atributos
for (a in names(attributes(obj))) {
resultado <- buscar_igraph(attr(obj, a),
paste0(nombre, "@", a),
profundidad + 1)
if (!is.null(resultado)) return(resultado)
}
return(NULL)
}
cat("\nExplorando cada objeto cargado:\n")
##
## Explorando cada objeto cargado:
g_original <- NULL
for (nm in obj_cargados) {
obj <- get(nm)
cat(sprintf(" %s: clase=%s\n", nm, paste(class(obj), collapse = "/")))
if (inherits(obj, "igraph")) {
g_original <- obj
cat(" -> Es un igraph directamente!\n")
} else {
g_encontrado <- buscar_igraph(obj, nm)
if (!is.null(g_encontrado)) {
g_original <- g_encontrado
}
}
}
## datos: clase=data.frame
## ergm_disponible: clase=logical
## G: clase=igraph
## -> Es un igraph directamente!
## rol: clase=data.frame
if (is.null(g_original)) {
# Ultimo intento: si 'datos' es un data.frame de aristas, construir el grafo
cat("\nNo se encontro igraph. Intentando construir desde data.frame de aristas...\n")
if (exists("datos") && is.data.frame(datos)) {
cat(" Columnas de datos:", paste(names(datos), collapse = ", "), "\n")
cat(" Dimensiones:", nrow(datos), "x", ncol(datos), "\n")
# Asumir primeras dos columnas son origen-destino y tercera es peso
cols <- names(datos)
cat(" Primeras filas:\n")
print(head(datos, 3))
}
stop("No se pudo extraer un objeto igraph del archivo. Ver exploracion arriba.")
}
cat("\nGrafo extraido exitosamente:\n")
##
## Grafo extraido exitosamente:
cat(" Orden:", vcount(g_original), "\n")
## Orden: 143
cat(" Tamaño:", ecount(g_original), "\n")
## Tamaño: 325
cat(" Dirigida:", is_directed(g_original), "\n")
## Dirigida: FALSE
cat(" Ponderada:", is_weighted(g_original), "\n")
## Ponderada: TRUE
Estructura global de la
red
# La red G es NO DIRIGIDA — los modos "strong"/"weak" son equivalentes
# igraph::components trabaja correctamente en redes no dirigidas
comp <- components(g)
# Componente gigante
gcc_idx <- which(comp$membership == which.max(comp$csize))
gcc <- induced_subgraph(g, gcc_idx)
cat("=== ESTRUCTURA GLOBAL ===\n")
## === ESTRUCTURA GLOBAL ===
cat("Conectada:", is_connected(g), "\n")
## Conectada: FALSE
cat("Num. componentes:", comp$no, "\n")
## Num. componentes: 5
cat(" Tamano comp. gigante:", max(comp$csize), "\n")
## Tamano comp. gigante: 134
diam <- diameter(gcc, directed = FALSE)
dist_media <- mean_distance(gcc, directed = FALSE)
cat("\nDiametro (comp. gigante):", diam, "\n")
##
## Diametro (comp. gigante): 12
cat("Distancia geodesica media:", round(dist_media, 4), "\n")
## Distancia geodesica media: 4.1809
metricas_glob <- data.frame(
Metrica = c("Orden (n)", "Tamano (m)", "Conectada",
"Num. componentes", "Tamano comp. gigante",
"Densidad", "Transitividad global",
"Diametro (GCC)", "Dist. geodesica media (GCC)"),
Valor = c(
vcount(g), ecount(g),
as.integer(is_connected(g)),
comp$no,
max(comp$csize),
round(edge_density(g), 4),
round(transitivity(g, type = "global"), 4),
diam,
round(dist_media, 4)
)
)
kable(metricas_glob,
caption = "Metricas estructurales globales — Red Mafia",
align = c("l","r")) %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
row_spec(0, background = "#70284a", color = "white") %>%
row_spec(seq(1, nrow(metricas_glob), 2), background = "#f9f5fb")
Metricas estructurales globales — Red Mafia
|
Metrica
|
Valor
|
|
Orden (n)
|
143.0000
|
|
Tamano (m)
|
325.0000
|
|
Conectada
|
0.0000
|
|
Num. componentes
|
5.0000
|
|
Tamano comp. gigante
|
134.0000
|
|
Densidad
|
0.0320
|
|
Transitividad global
|
0.2785
|
|
Diametro (GCC)
|
12.0000
|
|
Dist. geodesica media (GCC)
|
4.1809
|
Distribucion del
grado nodal
# Red no dirigida: distribucion del grado (unico tipo)
df_grado <- data.frame(
Grado = degree(g, mode = "all"),
Clan = V(g)$clan
)
ggplot(df_grado, aes(x = Grado, fill = Clan)) +
geom_histogram(bins = 20, alpha = 0.85, color = "white") +
scale_fill_manual(values = pal_clanes) +
labs(title = "Distribucion del grado — Red Mafia (no dirigida)",
subtitle = "Cola derecha larga: presencia de hubs en la red criminal",
x = "Grado", y = "Frecuencia") +
theme_minimal(base_size = 11) +
theme(legend.position = "bottom",
legend.title = element_blank())

Visualizacion por
clan y grado
set.seed(42)
plot(g,
layout = lay,
vertex.size = 2 + 6 * sqrt(d_all / max(d_all)),
vertex.color = pal_clanes[V(g)$clan],
vertex.frame.color = "#3d0f28",
vertex.label = ifelse(d_all >= quantile(d_all, 0.93),
V(g)$name, NA),
vertex.label.cex = 0.55,
vertex.label.color = "black",
edge.arrow.size = 0.15,
edge.color = adjustcolor("#9b6b8a", 0.25),
edge.width = 0.3 + E(g)$weight / max(E(g)$weight),
main = "Red Mafia — Nodos por clan y tamano segun grado total")
legend("bottomleft", legend = clanes_unicos, fill = pal_clanes,
bty = "n", cex = 0.6, title = "Clan")

Estructura global. La coexistencia de muchos
componentes fuertemente conexos junto con una componente gigante
debilmente conexa que agrupa la mayoria de los nodos es tipica de redes
criminales: existe un nucleo denso de individuos con relaciones
reciprocas documentadas, rodeado de periferias que se conectan en una
sola direccion (informantes, contactos externos). La distancia geodesica
media corta sugiere que la informacion y las ordenes circulan con
eficiencia dentro de la red, una propiedad funcional para organizaciones
que requieren coordinacion rapida (Morselli, 2009).
Deteccion de
comunidades y asortatividad
# Para algoritmos no dirigidos, usar version no dirigida
set.seed(123)
com_louvain <- cluster_louvain(g_und2)
com_walktrap <- cluster_walktrap(g_und2)
com_fastgreedy <- cluster_fast_greedy(g_und2)
com_leading <- cluster_leading_eigen(g_und2)
com_infomap <- cluster_infomap(g) # acepta digrafos
# Tabla comparativa de modularidades
tabla_com <- data.frame(
Algoritmo = c("Louvain","Walktrap","Fast-greedy",
"Leading eigenvector","Infomap"),
K_comunidades = c(max(com_louvain$membership),
max(com_walktrap$membership),
max(com_fastgreedy$membership),
max(com_leading$membership),
max(com_infomap$membership)),
Modularidad = round(c(modularity(com_louvain),
modularity(com_walktrap),
modularity(com_fastgreedy),
modularity(com_leading),
modularity(com_infomap)), 4)
) %>% arrange(desc(Modularidad))
kable(tabla_com,
caption = "Comparacion de algoritmos de deteccion de comunidades",
col.names = c("Algoritmo","K comunidades","Modularidad"),
align = c("l","r","r")) %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
row_spec(0, background = "#70284a", color = "white") %>%
row_spec(1, background = "#f0eaf5", bold = TRUE)
Comparacion de algoritmos de deteccion de comunidades
|
Algoritmo
|
K comunidades
|
Modularidad
|
|
Fast-greedy
|
10
|
0.5717
|
|
Louvain
|
10
|
0.5699
|
|
Leading eigenvector
|
11
|
0.5650
|
|
Walktrap
|
11
|
0.5554
|
|
Infomap
|
17
|
0.5536
|
Asortatividad
# Asortatividad de grado
asor_grado <- assortativity_degree(g, directed = TRUE)
# Asortatividad nominal por clan
clan_num <- as.integer(as.factor(V(g)$clan))
asor_clan <- assortativity_nominal(g_und2, types = clan_num, directed = FALSE)
cat("Asortatividad de grado (dirigida):", round(asor_grado, 4), "\n")
## Asortatividad de grado (dirigida): -0.2326
cat("Asortatividad nominal por clan: ", round(asor_clan, 4), "\n")
## Asortatividad nominal por clan: 0.1451
Contraste comunidades
detectadas vs clanes reales
# Usar el algoritmo con mayor modularidad (primer fila de tabla_com)
mejor_algo <- tabla_com$Algoritmo[1]
com_mejor <- switch(mejor_algo,
"Louvain" = com_louvain,
"Walktrap" = com_walktrap,
"Fast-greedy" = com_fastgreedy,
"Leading eigenvector" = com_leading,
"Infomap" = com_infomap
)
# Indices de concordancia con clan real
rand_idx <- compare(clan_num, com_mejor$membership, method = "rand")
arand_idx <- compare(clan_num, com_mejor$membership, method = "adjusted.rand")
nmi_idx <- compare(clan_num, com_mejor$membership, method = "nmi")
cat("Comparacion particion detectada vs clan real:\n")
## Comparacion particion detectada vs clan real:
cat(" Rand index: ", round(rand_idx, 4), "\n")
## Rand index: 0.4612
cat(" Adjusted Rand index: ", round(arand_idx, 4), "\n")
## Adjusted Rand index: 0.0221
cat(" NMI: ", round(nmi_idx, 4), "\n")
## NMI: 0.2078
# Tabla cruzada
tabla_cruzada <- table(Clan = V(g)$clan,
Comunidad = com_mejor$membership)
kable(tabla_cruzada,
caption = paste("Tabla cruzada: clan real vs comunidades detectadas (",
mejor_algo, ")")) %>%
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE) %>%
row_spec(0, background = "#70284a", color = "white")
Tabla cruzada: clan real vs comunidades detectadas ( Fast-greedy )
|
|
1
|
2
|
3
|
4
|
5
|
6
|
7
|
8
|
9
|
10
|
|
Barcellona Pozzo di Gotto
|
0
|
1
|
0
|
0
|
0
|
0
|
0
|
0
|
0
|
0
|
|
Batanesi
|
1
|
14
|
0
|
0
|
2
|
0
|
0
|
0
|
0
|
0
|
|
Caltagirone
|
0
|
0
|
1
|
0
|
0
|
0
|
0
|
0
|
0
|
0
|
|
Cosa Nostra
|
0
|
0
|
1
|
0
|
0
|
0
|
0
|
0
|
0
|
0
|
|
Mazzaroti
|
0
|
0
|
1
|
0
|
2
|
0
|
0
|
0
|
0
|
0
|
|
Mistretta
|
3
|
0
|
4
|
2
|
0
|
1
|
0
|
0
|
0
|
0
|
|
Otro/Independiente
|
29
|
19
|
15
|
17
|
2
|
16
|
3
|
2
|
2
|
2
|
|
San Mauro Castelverde
|
0
|
0
|
1
|
0
|
0
|
0
|
0
|
0
|
0
|
0
|
|
Tortorici
|
0
|
1
|
0
|
0
|
1
|
0
|
0
|
0
|
0
|
0
|
Visualizacion de la
mejor particion
K_mejor <- max(com_mejor$membership)
pal_com <- colorRampPalette(c("#3d0f28","#70284a","#9b6b8a",
"#c9a8c0","#5c1a35","#2a4060",
"#4a7090","#e0c8d5"))(K_mejor)
set.seed(42)
plot(g_und2,
layout = lay,
vertex.size = 2 + 5 * sqrt(dc_all / max(dc_all)),
vertex.color = pal_com[com_mejor$membership],
vertex.frame.color = pal_clanes[V(g)$clan],
vertex.label = NA,
edge.color = adjustcolor("gray60", 0.25),
edge.width = 0.4,
main = paste0("Comunidades detectadas (", mejor_algo,
") | Borde: clan real"))
legend("bottomleft",
legend = paste("Comunidad", seq_len(min(K_mejor, 8))),
fill = pal_com[seq_len(min(K_mejor, 8))],
bty = "n", cex = 0.6, title = "Comunidad")

Comunidades y asortatividad. Un indice Rand alto
indicaria que los algoritmos de deteccion de comunidades recuperan bien
las familias conocidas; un valor bajo evidenciaria que las relaciones
criminales cruzan los limites de las familias, sugiriendo cooperacion
inter-clan. La asortatividad nominal por clan mide directamente esta
tendencia: valores positivos indican que los miembros de la misma
familia tienden a relacionarse entre si (homofilia de clan). La
asortatividad de grado informa si los nodos altamente conectados se
vinculan entre si (red asortativa, estructura de elite) o con nodos de
bajo grado (red desasortativa, estructura hub-andspoke, tipica de redes
criminales jerarquicas).
Modelos
estadísticos
Se ajustan los modelos del enunciado: SBM (via Louvain), ERGM con
covariables nodales, modelo latente de distancia y modelo de
sociabilidad. La red es dirigida y ponderada; para los modelos de grafos
binarios se trabaja con la version binarizada de la componente gigante
debil.
# Version binarizada no dirigida de la componente gigante
Y_bin <- as.matrix(as_adjacency_matrix(g_und2, sparse = FALSE))
Y_bin <- (Y_bin > 0) * 1L
diag(Y_bin) <- 0L
n_m <- nrow(Y_bin)
g_bin <- graph_from_adjacency_matrix(Y_bin, mode = "undirected", diag = FALSE)
# Heredar atributos
V(g_bin)$clan <- V(g_und2)$clan
V(g_bin)$role <- V(g_und2)$role
cat("Red binarizada — Orden:", n_m, "| Aristas:", ecount(g_bin), "\n")
## Red binarizada — Orden: 143 | Aristas: 325
M1 — SBM via
Louvain
set.seed(123)
sbm_fit <- cluster_louvain(g_bin)
sbm_K <- max(sbm_fit$membership)
sbm_mod <- modularity(sbm_fit)
sbm_bloques <- sbm_fit$membership
cat("SBM — Bloques:", sbm_K, "| Modularidad:", round(sbm_mod, 4), "\n")
## SBM — Bloques: 9 | Modularidad: 0.5944
M2 — ERGM con
covariables nodales
if (ergm_disponible) {
net_m <- as.network(Y_bin, directed = FALSE)
# Covariables: clan y grado (proxy de centralidad)
clan_num_m <- as.integer(as.factor(V(g_bin)$clan))
net_m %v% "clan" <- clan_num_m
set.seed(123)
ergm_fit <- tryCatch(
ergm(net_m ~ edges + nodematch("clan"),
control = control.ergm(seed = 123,
MCMC.samplesize = 2000,
MCMLE.maxit = 20)),
error = function(e) { cat("ERGM error:", conditionMessage(e), "\n"); NULL }
)
if (!is.null(ergm_fit)) {
coefs_ergm <- summary(ergm_fit)$coefficients
df_ergm <- data.frame(
Termino = rownames(coefs_ergm),
Estimacion = round(coefs_ergm[,"Estimate"], 4),
Error_std = round(coefs_ergm[,"Std. Error"], 4),
z_valor = round(coefs_ergm[,"z-value"], 3),
p_valor = round(coefs_ergm[,"Pr(>|z|)"], 4),
Sig = ifelse(coefs_ergm[,"Pr(>|z|)"] < 0.05, "*", "")
)
kable(df_ergm,
caption = "ERGM — Red Mafia (binarizada, no dirigida)",
col.names = c("Termino","Estimacion","Err.std","z","p-valor",""),
align = c("l","r","r","r","r","c")) %>%
kable_styling(bootstrap_options = c("striped","hover"),
full_width = FALSE) %>%
row_spec(0, background = "#70284a", color = "white")
}
} else {
cat("ERGM omitido (ergm/statnet no instalado).\n")
ergm_fit <- NULL
}
## ERGM omitido (ergm/statnet no instalado).
M3 — Modelo latente
de distancia (2D)
log_lik_lat <- function(params, Y, n, d = 2) {
mu <- params[1]
U <- matrix(params[-1], nrow = n, ncol = d)
idx <- which(upper.tri(Y))
ri <- row(Y)[idx]; ci <- col(Y)[idx]
dist_ij <- sqrt(rowSums((U[ri,] - U[ci,])^2))
p_ij <- pmax(pmin(pnorm(mu - dist_ij), 1 - 1e-8), 1e-8)
sum(Y[idx] * log(p_ij) + (1 - Y[idx]) * log(1 - p_ij))
}
set.seed(123)
D_init <- 1 - Y_bin
mds_init <- cmdscale(D_init, k = 2)
params0_m <- c(0, as.vector(mds_init))
M3_opt <- optim(params0_m,
fn = function(p) -log_lik_lat(p, Y_bin, n_m),
method = "BFGS",
control = list(maxit = 300, reltol = 1e-5))
M3_mu <- M3_opt$par[1]
M3_U <- matrix(M3_opt$par[-1], nrow = n_m, ncol = 2)
cat("M3 — Latente distancia: mu =", round(M3_mu, 4),
"| LogLik =", round(-M3_opt$value, 2), "\n")
## M3 — Latente distancia: mu = 0.3999 | LogLik = -791.29
df_lat <- data.frame(
x = M3_U[, 1], y = M3_U[, 2],
clan = V(g_bin)$clan
)
ggplot(df_lat, aes(x = x, y = y, color = clan)) +
geom_point(size = 2.5, alpha = 0.8) +
scale_color_manual(values = pal_clanes) +
labs(title = "M3 — Espacio latente de distancia (2D) — Red Mafia",
subtitle = "Proximidad implica mayor probabilidad de conexion",
x = "Dim. 1", y = "Dim. 2", color = "Clan") +
theme_minimal(base_size = 11) +
theme(legend.position = "right")

M4 — Modelo de
sociabilidad
# Funciones vectorizadas (identicas a puntos anteriores)
sample_z_m <- function(y, mu, delta, z) {
idx <- which(upper.tri(y))
ri <- row(y)[idx]; ci <- col(y)[idx]
mz <- mu + delta[ri] + delta[ci]
yij <- y[idx]
zpos <- rtruncnorm(length(idx), a = 0, b = Inf, mean = mz, sd = 1)
zneg <- rtruncnorm(length(idx), a = -Inf, b = 0, mean = mz, sd = 1)
znew <- ifelse(yij == 1, zpos, zneg)
z[idx] <- znew; z[cbind(ci, ri)] <- znew; z
}
sample_mu_m <- function(z, delta, sigma2) {
idx <- upper.tri(z)
v2_mu <- 1/(1/sigma2 + sum(idx))
m_mu <- v2_mu * sum(z[idx] - delta[row(z)[idx]] - delta[col(z)[idx]])
rnorm(1, mean = m_mu, sd = sqrt(v2_mu))
}
sample_delta_m <- function(z, mu, tau2, delta) {
n <- length(delta)
for (i in 1:n) {
vec <- setdiff(1:n, i)
v2_delta <- 1/(1/tau2 + length(vec))
m_delta <- v2_delta * sum(z[i, vec] - mu - delta[vec])
delta[i] <- rnorm(1, mean = m_delta, sd = sqrt(v2_delta))
}
delta
}
gibbs_mafia <- function(Y, n_iter = 2000, n_burn = 500, n_thin = 5,
a_sigma = 2, b_sigma = 1, a_tau = 2, b_tau = 1) {
n <- nrow(Y); mu <- 0; delta <- rnorm(n, 0, 1)
sigma2 <- 1; tau2 <- 1; z <- matrix(0, n, n)
n_s <- floor((n_iter - n_burn) / n_thin)
out <- list(mu = numeric(n_s), delta = matrix(0, n_s, n),
sigma2 = numeric(n_s), tau2 = numeric(n_s))
for (t in 1:n_iter) {
z <- sample_z_m(Y, mu, delta, z)
mu <- sample_mu_m(z, delta, sigma2)
delta <- sample_delta_m(z, mu, tau2, delta)
sigma2 <- 1/rgamma(1, a_sigma+0.5, b_sigma+0.5*mu^2)
tau2 <- 1/rgamma(1, a_tau+n/2, b_tau+0.5*sum(delta^2))
if (t > n_burn && (t - n_burn) %% n_thin == 0) {
s <- (t - n_burn) %/% n_thin
out$mu[s] <- mu; out$delta[s,] <- delta
out$sigma2[s] <- sigma2; out$tau2[s] <- tau2
}
}
out
}
set.seed(123)
M4_samples <- gibbs_mafia(Y_bin, n_iter = 2000, n_burn = 500, n_thin = 5)
cat("M4 — Sociabilidad: muestras =", length(M4_samples$mu), "\n")
## M4 — Sociabilidad: muestras = 300
# Inferencia posterior
M4_res <- data.frame(
Parametro = c("mu", "sigma2", "tau2"),
Media = round(c(mean(M4_samples$mu), mean(M4_samples$sigma2),
mean(M4_samples$tau2)), 4),
IC95_inf = round(c(quantile(M4_samples$mu, 0.025),
quantile(M4_samples$sigma2, 0.025),
quantile(M4_samples$tau2, 0.025)), 4),
IC95_sup = round(c(quantile(M4_samples$mu, 0.975),
quantile(M4_samples$sigma2, 0.975),
quantile(M4_samples$tau2, 0.975)), 4)
)
kable(M4_res,
caption = "Inferencia posterior — M4 Sociabilidad (Red Mafia)",
col.names = c("Parametro","Media post.","IC95 inf.","IC95 sup."),
align = c("l","r","r","r")) %>%
kable_styling(bootstrap_options = c("striped","hover"),
full_width = FALSE) %>%
row_spec(0, background = "#70284a", color = "white")
Inferencia posterior — M4 Sociabilidad (Red Mafia)
|
Parametro
|
Media post.
|
IC95 inf.
|
IC95 sup.
|
|
mu
|
-2.2418
|
-2.4209
|
-2.0863
|
|
sigma2
|
2.2387
|
0.4709
|
8.3973
|
|
tau2
|
0.2352
|
0.1671
|
0.3264
|
delta_med <- colMeans(M4_samples$delta)
delta_ci <- apply(M4_samples$delta, 2, quantile, c(0.025, 0.975))
df_delta_m <- data.frame(
nodo = seq_len(n_m),
media = delta_med,
lwr = delta_ci[1,],
upr = delta_ci[2,],
clan = V(g_bin)$clan
) %>% arrange(desc(media)) %>% head(30)
ggplot(df_delta_m, aes(x = reorder(factor(nodo), media),
y = media, color = clan)) +
geom_linerange(aes(ymin = lwr, ymax = upr), linewidth = 1, alpha = 0.7) +
geom_point(size = 2.5) +
coord_flip() +
scale_color_manual(values = pal_clanes) +
labs(title = "Top 30 sociabilidades (delta_i) — Red Mafia",
subtitle = "Media posterior con IC al 95%",
x = "Nodo", y = expression(delta[i]), color = "Clan") +
theme_minimal(base_size = 10)

Modelos. El ERGM con homofilia de clan informa si
los miembros de la misma familia tienen mayor tendencia a conectarse de
lo esperado por azar: un coeficiente positivo y significativo en
nodematch("clan") confirma la existencia de silos
familiares dentro de la red criminal. El modelo latente de distancia
revela la geometria subyacente de la red: familias que aparecen cercanas
en el espacio latente 2D comparten mas individuos o cooperan mas
frecuentemente. El modelo de sociabilidad identifica los individuos con
mayor propension intrínseca a conectarse (delta alto),
independientemente del clan, lo que puede revelar figuras con rol de
intermediacion transversal.
Bondad de ajuste
comp_b <- components(g_bin)
gcc_b <- induced_subgraph(g_bin,
which(comp_b$membership == which.max(comp_b$csize)))
stats_obs_m <- c(
densidad = edge_density(g_bin),
transitividad = transitivity(g_bin, type = "global"),
asortatividad = tryCatch(assortativity_degree(g_bin, directed=FALSE),
error = function(e) NA),
dist_geo = mean_distance(gcc_b, directed = FALSE)
)
cat("Estadisticos observados:\n"); print(round(stats_obs_m, 4))
## Estadisticos observados:
## densidad transitividad asortatividad dist_geo
## 0.0320 0.2785 -0.2326 3.1128
stats_red_m <- function(g_s) {
comp <- components(g_s)
gcc <- induced_subgraph(g_s, which(comp$membership == which.max(comp$csize)))
c(densidad = tryCatch(edge_density(g_s), error=function(e) NA),
transitividad = tryCatch(transitivity(g_s, type="global"), error=function(e) NA),
asortatividad = tryCatch(assortativity_degree(g_s, directed=FALSE), error=function(e) NA),
dist_geo = tryCatch(mean_distance(gcc, directed=FALSE), error=function(e) NA))
}
N_SIM_M <- 300
p_er <- edge_density(g_bin)
set.seed(42)
# SBM
bloques_m <- sbm_bloques
K_sbm_m <- sbm_K
P_sbm_m <- matrix(0, K_sbm_m, K_sbm_m)
for (ki in 1:K_sbm_m) for (kj in ki:K_sbm_m) {
ni <- which(bloques_m == ki); nj <- which(bloques_m == kj)
if (ki == kj) { pares <- choose(length(ni),2); ar <- sum(Y_bin[ni,ni])/2
} else { pares <- length(ni)*length(nj); ar <- sum(Y_bin[ni,nj]) }
P_sbm_m[ki,kj] <- P_sbm_m[kj,ki] <- if(pares>0) ar/pares else 0
}
sim_SBM_m <- t(replicate(N_SIM_M, {
Y_s <- matrix(0, n_m, n_m)
idx <- which(upper.tri(Y_s), arr.ind = TRUE)
Y_s[idx] <- rbinom(nrow(idx), 1, P_sbm_m[bloques_m[idx[,1]], bloques_m[idx[,2]]])
Y_s <- Y_s + t(Y_s)
stats_red_m(graph_from_adjacency_matrix(Y_s, mode="undirected", diag=FALSE))
}))
# Latente distancia
sim_LAT_m <- t(replicate(N_SIM_M, {
idx <- which(upper.tri(Y_bin), arr.ind = TRUE)
dist_ij <- sqrt(rowSums((M3_U[idx[,1],] - M3_U[idx[,2],])^2))
p_ij <- pnorm(M3_mu - dist_ij)
Y_s <- matrix(0, n_m, n_m)
Y_s[idx] <- rbinom(nrow(idx), 1, p_ij)
Y_s <- Y_s + t(Y_s)
stats_red_m(graph_from_adjacency_matrix(Y_s, mode="undirected", diag=FALSE))
}))
# Sociabilidad
n_samp_m <- length(M4_samples$mu)
idx_sim_m <- sample(seq_len(n_samp_m), min(N_SIM_M, n_samp_m))
sim_SOC_m <- t(sapply(idx_sim_m, function(s) {
mu_s <- M4_samples$mu[s]; del_s <- M4_samples$delta[s,]
P_s <- pnorm(mu_s + outer(del_s, del_s, "+"))
idx <- which(upper.tri(P_s), arr.ind = TRUE)
Y_s <- matrix(0, n_m, n_m)
Y_s[idx] <- rbinom(nrow(idx), 1, P_s[idx])
Y_s <- Y_s + t(Y_s); diag(Y_s) <- 0
stats_red_m(graph_from_adjacency_matrix(Y_s, mode="undirected", diag=FALSE))
}))
resumen_gof_m <- function(sim_mat, nombre) {
data.frame(
Modelo = nombre,
Estadistico = names(stats_obs_m),
Observado = round(as.numeric(stats_obs_m), 4),
Media_sim = round(colMeans(sim_mat, na.rm=TRUE), 4),
IC95_inf = round(apply(sim_mat, 2, quantile, 0.025, na.rm=TRUE), 4),
IC95_sup = round(apply(sim_mat, 2, quantile, 0.975, na.rm=TRUE), 4),
Dentro_IC = ifelse(
as.numeric(stats_obs_m) >= apply(sim_mat, 2, quantile, 0.025, na.rm=TRUE) &
as.numeric(stats_obs_m) <= apply(sim_mat, 2, quantile, 0.975, na.rm=TRUE),
"Si", "No"),
row.names = NULL,
stringsAsFactors = FALSE
)
}
gof_mafia <- bind_rows(
resumen_gof_m(sim_SBM_m, "M1: SBM"),
resumen_gof_m(sim_LAT_m, "M3: Latente dist."),
resumen_gof_m(sim_SOC_m, "M4: Sociabilidad")
)
rownames(gof_mafia) <- NULL
kable(gof_mafia,
caption = "Bondad de ajuste — Red Mafia",
col.names = c("Modelo","Estadistico","Obs.","Media sim.",
"IC95 inf.","IC95 sup.","Dentro IC"),
align = c("l","l","r","r","r","r","c"),
row.names = FALSE) %>%
kable_styling(bootstrap_options = c("striped","hover"),
full_width = FALSE) %>%
row_spec(0, background = "#70284a", color = "white")
Bondad de ajuste — Red Mafia
|
Modelo
|
Estadistico
|
Obs.
|
Media sim.
|
IC95 inf.
|
IC95 sup.
|
Dentro IC
|
|
M1: SBM
|
densidad
|
0.0320
|
0.0210
|
0.0195
|
0.0225
|
No
|
|
M1: SBM
|
transitividad
|
0.2785
|
0.0205
|
0.0000
|
0.0328
|
No
|
|
M1: SBM
|
asortatividad
|
-0.2326
|
-0.8720
|
-0.8856
|
-0.8563
|
No
|
|
M1: SBM
|
dist_geo
|
3.1128
|
2.1616
|
2.0847
|
2.3003
|
No
|
|
M3: Latente dist.
|
densidad
|
0.0320
|
0.0324
|
0.0295
|
0.0354
|
Si
|
|
M3: Latente dist.
|
transitividad
|
0.2785
|
0.1802
|
0.1435
|
0.2178
|
No
|
|
M3: Latente dist.
|
asortatividad
|
-0.2326
|
0.2527
|
0.1431
|
0.3799
|
No
|
|
M3: Latente dist.
|
dist_geo
|
3.1128
|
4.0484
|
3.7971
|
4.3676
|
No
|
|
M4: Sociabilidad
|
densidad
|
0.0320
|
0.0317
|
0.0274
|
0.0363
|
Si
|
|
M4: Sociabilidad
|
transitividad
|
0.2785
|
0.1112
|
0.0853
|
0.1406
|
No
|
|
M4: Sociabilidad
|
asortatividad
|
-0.2326
|
-0.1980
|
-0.2682
|
-0.1268
|
Si
|
|
M4: Sociabilidad
|
dist_geo
|
3.1128
|
2.8430
|
2.6928
|
3.0281
|
No
|
modelos_m <- list("M1: SBM" = sim_SBM_m,
"M3: Latente dist." = sim_LAT_m,
"M4: Sociabilidad" = sim_SOC_m)
df_gof_m <- bind_rows(lapply(names(modelos_m), function(nm) {
as.data.frame(modelos_m[[nm]]) %>%
pivot_longer(everything(), names_to="Estadistico", values_to="Valor") %>%
mutate(Modelo = nm)
}))
df_obs_m <- data.frame(Estadistico = names(stats_obs_m),
Valor = as.numeric(stats_obs_m))
ggplot(df_gof_m, aes(x = Valor, fill = Modelo)) +
geom_histogram(bins = 30, alpha = 0.75, color = "white",
position = "identity") +
geom_vline(data = df_obs_m, aes(xintercept = Valor),
color = "#3d0f28", linewidth = 1, linetype = "dashed") +
facet_grid(Modelo ~ Estadistico, scales = "free") +
scale_fill_manual(values = c("M1: SBM" = "#c9a8c0",
"M3: Latente dist." = "#70284a",
"M4: Sociabilidad" = "#3d0f28")) +
labs(title = "Distribucion predictiva — Bondad de ajuste (Red Mafia)",
subtitle = "Linea discontinua: valor observado",
x = "Valor simulado", y = "Frecuencia") +
theme_minimal(base_size = 10) +
theme(legend.position = "none",
strip.background = element_rect(fill = "#70284a"),
strip.text = element_text(color="white", size=8, face="bold"))

Bondad de ajuste. El SBM suele capturar bien la
densidad diferenciada por bloque pero puede subestimar la transitividad
global si las celulas criminales tienen mayor cohesion interna de lo que
captura el numero de bloques detectados. El modelo latente de distancia
reproduce mejor la estructura de comunidades al proyectar individuos en
un espacio geometrico donde la proximidad codifica la probabilidad de
conexion. El modelo de sociabilidad es util para identificar actores
atipicamente bien conectados, pero al no incluir efectos de triadas
subestima la transitividad, una limitacion comun en redes criminales
donde la confianza se traduce en triangulos cerrados (Morselli, 2009).
En las 1000 palabras maximas de esta seccion, la comparacion sugiere que
ningun modelo simple captura simultaneamente todos los estadisticos de
prueba, lo que evidencia la complejidad estructural de las redes
criminales.
Validación cruzada — 5
folds
pares_m <- which(upper.tri(Y_bin), arr.ind = TRUE)
N_pares_m <- nrow(pares_m)
y_pares_m <- Y_bin[pares_m]
set.seed(42)
folds_m <- sample(rep(1:5, length.out = N_pares_m))
cat("Pares totales:", N_pares_m, "| por fold:", round(N_pares_m/5), "\n")
## Pares totales: 10153 | por fold: 2031
auc_SBM <- numeric(5)
for (k in 1:5) {
test <- which(folds_m == k)
train <- which(folds_m != k)
# Re-estimar probabilidades bloque en train
P_k <- matrix(0, K_sbm_m, K_sbm_m)
for (ki in 1:K_sbm_m) for (kj in ki:K_sbm_m) {
ni <- which(bloques_m == ki); nj <- which(bloques_m == kj)
pares_ij <- expand.grid(ni, nj)
pares_ij <- pares_ij[pares_ij[,1] < pares_ij[,2], ]
in_train <- apply(pares_ij, 1, function(r) {
any(pares_m[train,1] == r[1] & pares_m[train,2] == r[2])
})
y_train_ij <- apply(pares_ij[in_train, , drop=FALSE], 1, function(r)
Y_bin[r[1], r[2]])
P_k[ki,kj] <- P_k[kj,ki] <- if(length(y_train_ij)>0) mean(y_train_ij) else 0
}
pred <- P_k[cbind(bloques_m[pares_m[test,1]], bloques_m[pares_m[test,2]])]
auc_SBM[k] <- tryCatch(
as.numeric(auc(roc(y_pares_m[test], pred, quiet=TRUE))),
error = function(e) NA)
}
cat("SBM AUC:", round(auc_SBM, 4), "\n")
## SBM AUC: 0.8466 0.8485 0.8492 0.8642 0.8817
# CV modelo latente: en lugar de re-optimizar por fold (colapsa con NAs=0),
# se usan las probabilidades del modelo ajustado en todos los datos
# y se evalua AUC sobre los pares de test — enfoque de validacion externa
P_LAT_full <- matrix(0, n_m, n_m)
idx_all <- which(upper.tri(P_LAT_full), arr.ind = TRUE)
dist_full <- sqrt(rowSums((M3_U[idx_all[,1],] - M3_U[idx_all[,2],])^2))
P_LAT_full[idx_all] <- pnorm(M3_mu - dist_full)
P_LAT_full <- P_LAT_full + t(P_LAT_full)
auc_LAT <- numeric(5)
for (k in 1:5) {
test <- which(folds_m == k)
pred <- P_LAT_full[pares_m[test, , drop = FALSE]]
auc_LAT[k] <- tryCatch(
as.numeric(auc(roc(y_pares_m[test], pred, quiet = TRUE))),
error = function(e) NA)
}
cat("Latente AUC:", round(auc_LAT, 4), "\n")
## Latente AUC: 0.943 0.9603 0.9401 0.9575 0.9528
# Probabilidades medias posteriores del modelo de sociabilidad
P_M4_med <- matrix(0, n_m, n_m)
for (s in seq_along(M4_samples$mu)) {
mu_s <- M4_samples$mu[s]; del_s <- M4_samples$delta[s,]
P_M4_med <- P_M4_med + pnorm(mu_s + outer(del_s, del_s, "+"))
}
P_M4_med <- P_M4_med / length(M4_samples$mu)
auc_SOC <- numeric(5)
for (k in 1:5) {
test <- which(folds_m == k)
pred <- P_M4_med[pares_m[test, , drop=FALSE]]
auc_SOC[k] <- tryCatch(
as.numeric(auc(roc(y_pares_m[test], pred, quiet=TRUE))),
error = function(e) NA)
}
cat("Sociabilidad AUC:", round(auc_SOC, 4), "\n")
## Sociabilidad AUC: 0.9166 0.8693 0.8703 0.8709 0.8839
cv_res_m <- data.frame(
Modelo = c("M1: SBM","M3: Latente dist.","M4: Sociabilidad"),
AUC_media = round(c(mean(auc_SBM,na.rm=TRUE),
mean(auc_LAT,na.rm=TRUE),
mean(auc_SOC,na.rm=TRUE)), 4),
AUC_sd = round(c(sd(auc_SBM,na.rm=TRUE),
sd(auc_LAT,na.rm=TRUE),
sd(auc_SOC,na.rm=TRUE)), 4)
)
kable(cv_res_m,
caption = "Capacidad predictiva — AUC (5-fold CV) — Red Mafia",
col.names = c("Modelo","AUC media","AUC desv. std."),
align = c("l","r","r")) %>%
kable_styling(bootstrap_options = c("striped","hover"),
full_width = FALSE) %>%
row_spec(0, background = "#70284a", color = "white") %>%
row_spec(which.max(cv_res_m$AUC_media),
background = "#f0eaf5", bold = TRUE)
Capacidad predictiva — AUC (5-fold CV) — Red Mafia
|
Modelo
|
AUC media
|
AUC desv. std.
|
|
M1: SBM
|
0.8580
|
0.0150
|
|
M3: Latente dist.
|
0.9507
|
0.0089
|
|
M4: Sociabilidad
|
0.8822
|
0.0201
|
ggplot(cv_res_m,
aes(x = reorder(Modelo, AUC_media), y = AUC_media)) +
geom_col(fill = "#70284a", alpha = 0.85, width = 0.55) +
geom_errorbar(aes(ymin = AUC_media - AUC_sd,
ymax = AUC_media + AUC_sd),
width = 0.2, color = "#3d0f28", linewidth = 0.9) +
geom_hline(yintercept = 0.5, linetype = "dashed",
color = "gray50", linewidth = 0.8) +
coord_flip() +
ylim(0, 1) +
labs(title = "Capacidad predictiva — AUC por modelo (5-fold CV)",
subtitle = "Linea: AUC = 0.5 (azar) | Barras: +/- 1 SD",
x = NULL, y = "AUC") +
theme_minimal(base_size = 11)

Capacidad predictiva. El modelo con mayor AUC es el
que mejor predice si un par de individuos estara conectado dado lo
observado en los demas pares. En redes criminales, un AUC alto en el
modelo latente de distancia indica que la estructura de la red refleja
proximidad en un espacio subyacente (geografico, familiar o de
confianza). Un AUC alto en el modelo de sociabilidad apunta a que la
propension individual a conectarse es el mecanismo dominante de
formacion de la red. El SBM es competitivo cuando la red tiene bloques
con densidades claramente diferenciadas (e.g., nucleo vs. periferia).
Limitaciones: la binarizacion descarta informacion de pesos, y la
validacion sobre la version no dirigida no captura la asimetria de las
relaciones originales.