Los modelos de bloques estocásticos (SBM, stochastic block models) representan una red mediante clases de vértices estocásticamente equivalentes.
La probabilidad de vínculo entre dos vértices depende únicamente de las clases a las que pertenecen, y no de sus identidades individuales.
El SBM puede representar patrones assortativos, disassortativos o núcleo-periferia.
El término comunidad se reserva aquí para grupos con conectividad interna relativamente alta.
# Matrices de interacción representativas
Pi_comunidad <- matrix(c(0.75, 0.08, 0.08, 0.70), 2, 2)
Pi_disassort <- matrix(c(0.08, 0.75, 0.75, 0.08), 2, 2)
Pi_nucleo <- matrix(c(0.75, 0.45, 0.45, 0.05), 2, 2)
# Mostrar las tres estructuras
matrices <- list(Pi_comunidad, Pi_disassort, Pi_nucleo)
titulos <- c("Comunidades", "Disassortativa", "Núcleo-periferia")
par(mfrow = c(1, 3), mar = c(2, 2, 3, 1))
for (k in seq_along(matrices)) {
image(
x = 1:2, y = 1:2, z = matrices[[k]],
col = hcl.colors(100, "Blues 3"), zlim = c(0, 1),
axes = FALSE, xlab = "", ylab = "", main = titulos[k]
)
axis(1, at = 1:2, labels = 1:2, tick = FALSE)
axis(2, at = 1:2, labels = 1:2, tick = FALSE)
box()
}El caso particular de un solo bloque (\(Q=1\)) corresponde al modelo Bernoulli homogéneo:
\[ y_{i,j}\overset{\text{iid}}{\sim}\textsf{Bernoulli}(\theta), \qquad i<j. \]
Consideramos un grafo simple no dirigido \(G=(V,E)\), con \(V=\{1,\ldots,n\}\) y matriz de adyacencia \(\mathbf{Y}=[y_{i,j}]\).
Cada vértice pertenece a una de \(Q\) clases latentes.
Sea \(\xi_i\in\{1,\ldots,Q\}\) su etiqueta y \(z_{i,q}=\mathbb{1}(\xi_i=q)\) su indicador de pertenencia.
Se supone que \[ \boldsymbol{z}_i\mid\boldsymbol{\alpha} \overset{\text{iid}}{\sim}\textsf{Multinomial}(1,\boldsymbol{\alpha}), \qquad i=1,\ldots,n, \] donde \(\boldsymbol{\alpha}=(\alpha_1,\ldots,\alpha_Q)\), \(\alpha_q\geq 0\) y \(\sum_{q=1}^Q\alpha_q=1\).
Por tanto, \(\textsf{Pr}(\xi_i=q\mid\boldsymbol\alpha)=\alpha_q\).
Sea \(\mathbf{\Pi}=[\pi_{q,r}]\) una matriz simétrica de probabilidades, con \(0\leq\pi_{q,r}\leq 1\).
Condicionalmente a las etiquetas, las díadas distintas son independientes: \[ y_{i,j}\mid\boldsymbol\xi,\mathbf\Pi \overset{\text{ind}}{\sim} \textsf{Bernoulli}(\pi_{\xi_i,\xi_j}),\qquad i<j. \]
los vínculos son condicionalmente independientes dadas las asignaciones de clase, y sus probabilidades dependen únicamente de las clases de los vértices involucrados.
La distribución condicional de la red se expresa como \[ p(\mathbf y\mid\boldsymbol\xi,\mathbf\Pi) = \prod_{i<j} p(y_{i,j}\mid\xi_i,\xi_j,\mathbf\Pi) = \prod_{i<j} \pi_{\xi_i,\xi_j}^{y_{i,j}} (1-\pi_{\xi_i,\xi_j})^{1-y_{i,j}}. \]
Aunque los vínculos son condicionalmente independientes dadas las asignaciones de clase, esta propiedad no necesariamente se conserva al marginalizar las clases latentes. En efecto, la distribución marginal de la red se obtiene sumando sobre todas las posibles configuraciones de las clases: \[ p(\mathbf y\mid\boldsymbol\alpha,\mathbf\Pi) = \sum_{\boldsymbol\xi} p(\mathbf y\mid\boldsymbol\xi,\mathbf\Pi)\, p(\boldsymbol\xi\mid\boldsymbol\alpha), \] donde la suma considera las \(Q^n\) posibles configuraciones de \(\boldsymbol\xi\).
Por lo tanto, las aristas pueden presentar dependencia marginal cuando comparten un vértice. Por ejemplo, \(y_{i,j}\) y \(y_{i,k}\) dependen de la misma asignación latente \(\xi_i\), lo que puede inducir una asociación entre ambas.
Sean \(n_q\) el número de vértices de la clase \(q\), \(m_{q,r}\) el número de aristas entre las clases \(q\) y \(r\), y \(N_{q,r}\) el número de díadas posibles: \[ N_{q,r}=\begin{cases} \binom{n_q}{2},&q=r,\\ n_q\,n_r,&q<r. \end{cases} \]
Condicionalmente a una partición conocida, el estimador de máxima verosimilitud es \[ \hat\pi_{q,r}=\frac{m_{q,r}}{N_{q,r}},\qquad N_{q,r}>0, \] y el estimador de las proporciones de clases es \(\hat\alpha_q=n_q/n\).
Para un vértice \(i\) de la clase \(q\), el grado esperado condicional es \[ \textsf{E}(D_i\mid\boldsymbol\xi,\mathbf\Pi) =(n_q-1)\pi_{q,q}+\sum_{r\neq q}n_r\pi_{q,r}. \]
Los nombres de las clases son arbitrarios, pues permutar simultáneamente las etiquetas, las entradas de \(\boldsymbol\alpha\) y las filas y columnas de \(\mathbf\Pi\) no modifica el modelo.
Este fenómeno se conoce como permutación de etiquetas (label switching).
Simulamos una red con tres clases conocidas para estudiar la generación y recuperación de los parámetros.
# Paquetes
suppressMessages(suppressWarnings(library(igraph)))
suppressMessages(suppressWarnings(library(blockmodels)))
suppressMessages(suppressWarnings(library(sand)))
suppressMessages(suppressWarnings(library(corrplot)))# Tamaños y clases verdaderas
block_sizes_true <- c(40, 50, 45)
Q_true <- length(block_sizes_true)
n_true <- sum(block_sizes_true)
labels_true <- rep(seq_len(Q_true), times = block_sizes_true)
# Probabilidades verdaderas de interacción
Pi_true <- matrix(
c(0.55, 0.04, 0.03,
0.04, 0.45, 0.06,
0.03, 0.06, 0.50),
nrow = Q_true, byrow = TRUE
)
# Simular una red simple no dirigida
set.seed(42)
g_true <- sample_sbm(
n = n_true,
pref.matrix = Pi_true,
block.sizes = block_sizes_true,
directed = FALSE,
loops = FALSE
)
# Visualizar los bloques verdaderos
cols_true <- grDevices::hcl.colors(Q_true, "Dark 3")
set.seed(42)
plot(
g_true,
layout = igraph::layout_with_fr(g_true),
vertex.label = NA,
vertex.size = 5,
vertex.color = cols_true[labels_true],
vertex.frame.color = cols_true[labels_true],
edge.color = grDevices::adjustcolor("black", alpha.f = 0.12)
)Estimamos la matriz \(\mathbf\Pi\) contando aristas observadas y díadas posibles en cada par de bloques.
# Matriz de adyacencia de la red simulada
A_true <- as.matrix(igraph::as_adjacency_matrix(g_true, sparse = FALSE))
# Número de vértices por clase
nq_true <- tabulate(labels_true, nbins = Q_true)
# Matrices de aristas observadas y díadas posibles
M <- matrix(0, nrow = Q_true, ncol = Q_true)
N <- matrix(0, nrow = Q_true, ncol = Q_true)
for (q in seq_len(Q_true)) {
for (r in q:Q_true) {
# Identificar los vértices de los bloques
iq <- which(labels_true == q)
ir <- which(labels_true == r)
if (q == r) {
# Contar aristas internas una sola vez
A_qq <- A_true[iq, iq, drop = FALSE]
M[q, r] <- sum(A_qq[upper.tri(A_qq)])
N[q, r] <- choose(length(iq), 2)
} else {
# Contar aristas entre bloques
M[q, r] <- sum(A_true[iq, ir, drop = FALSE])
N[q, r] <- length(iq) * length(ir)
}
# Completar las matrices simétricas
M[r, q] <- M[q, r]
N[r, q] <- N[q, r]
}
}
# Estimadores de máxima verosimilitud
Pi_mle <- M / N
alpha_mle <- nq_true / n_true
# Comparar valores verdaderos y estimados
round(Pi_true, 3)## [,1] [,2] [,3]
## [1,] 0.55 0.04 0.03
## [2,] 0.04 0.45 0.06
## [3,] 0.03 0.06 0.50
## [,1] [,2] [,3]
## [1,] 0.569 0.039 0.027
## [2,] 0.039 0.446 0.059
## [3,] 0.027 0.059 0.522
## [1] 0.296 0.370 0.333
Cuando las etiquetas de clase son desconocidas,
blockmodels utiliza un algoritmo EM
variacional para estimar las probabilidades de
pertenencia \(\tau_{i,q}=\textsf{Pr}(\xi_i=q\mid\mathbf
y)\), los parámetros del modelo \((\boldsymbol\alpha,\mathbf\Pi)\) y explorar
diferentes números de bloques \(Q\).
Con clases desconocidas, la verosimilitud marginal involucra una suma sobre las \(Q^n\) asignaciones posibles.
El algoritmo maximiza iterativamente una cota inferior de la log-verosimilitud (ELBO), definida como \[ \mathcal{L}(q,\boldsymbol\theta) = \textsf{E}_{q(\boldsymbol\xi)} \left[\log p(\mathbf y,\boldsymbol\xi\mid\boldsymbol\theta)\right] - \textsf{E}_{q(\boldsymbol\xi)} \left[\log q(\boldsymbol\xi)\right], \] donde \(\boldsymbol\theta=(\boldsymbol\alpha,\mathbf\Pi)\).
En el paso E, se actualizan las probabilidades \(\tau_{i,q}\); en el paso M, se estiman los parámetros \(\boldsymbol\alpha\) y \(\mathbf\Pi\).
Este procedimiento realiza máxima verosimilitud aproximada, no inferencia plenamente bayesiana.
La verosimilitud integrada de clasificación (Integrated Classification Likelihood, ICL) combina la bondad de ajuste del modelo con una penalización por complejidad.
Para el SBM Bernoulli simétrico, el criterio
ICL implementado en blockmodels se expresa como
\[
\mathrm{ICL}(Q)
=
\mathrm{PL}_Q
-\frac12\left\{
\frac{Q(Q+1)}{2}\log\binom{n}{2}
+(Q-1)\log n
\right\},
\] donde \[
\mathrm{PL}_Q
=
\textsf{E}_{\hat q_Q(\boldsymbol\xi)}
\left[
\log p(\mathbf y,\boldsymbol\xi\mid\hat{\boldsymbol\theta}_Q)
\right]
\] es el valor esperado variacional de la
log-verosimilitud completa, evaluado en las estimaciones del
modelo con \(Q\) bloques. La
penalización por complejidad distingue entre los \(Q(Q+1)/2\) parámetros de
interacción (díadas) y los \(Q-1\) parámetros de proporción de
clases (vértices).
Se selecciona el número de bloques que maximiza el ICL.
El resultado puede depender de inicializaciones, óptimos locales y del intervalo de valores de \(Q\) considerado.
Sea \(\tau_{i,q}\) la probabilidad variacional de que el vértice \(i\) pertenezca al bloque \(q\).
Una clasificación puntual se obtiene mediante \[ \hat\xi_i=\underset{q}{\arg\max}\;\tau_{i,q}. \]
Esta regla no requiere un umbral y asigna todos los vértices. La incertidumbre de clasificación puede resumirse mediante \(u_i=1-\max_q\tau_{i,q}\) o la entropía \(H_i=-\sum_q\tau_{i,q}\log\tau_{i,q}\), con \(0\log0=0\).
# Ajustar SBM Bernoulli no dirigido
set.seed(42)
fit_true <- blockmodels::BM_bernoulli(
membership_type = "SBM_sym",
adj = A_true,
verbosity = 0,
plotting = "",
explore_min = 4, # Explorar al menos hasta 4 bloques
explore_max = 6, # Explorar como máximo 6 bloques
ncores = 1
)
fit_true$estimate()## [1] 3
# Probabilidades estimadas de pertenencia a los bloques
Z_sim <- fit_true$memberships[[Q_sim]]$Z
head(Z_sim)## [,1] [,2] [,3]
## [1,] 0.0007401925 0.9985196 0.0007401925
## [2,] 0.0007401925 0.9985196 0.0007401925
## [3,] 0.0007401925 0.9985196 0.0007401925
## [4,] 0.0007401925 0.9985196 0.0007401925
## [5,] 0.0007401925 0.9985196 0.0007401925
## [6,] 0.0007401925 0.9985196 0.0007401925
## [1] 2 2 2 2 2 2
# Comparar las clases verdaderas con las estimadas
igraph::compare(labels_true, labels_sim, method = "adjusted.rand")## [1] 1
El Adjusted Rand Index (ARI) compara particiones y es invariante a las permutaciones de etiquetas. Un valor alto indica mayor concordancia, pero no demuestra por sí solo que el modelo recupere exactamente todos los parámetros.
Debido a la invariancia ante permutaciones de etiquetas (label switching) en los SBM, las clases estimadas pueden tener etiquetas diferentes de las verdaderas.
Para comparar las matrices de probabilidades, se identifica la permutación óptima que maximiza el acuerdo entre ambas clasificaciones: \[ p^*=\underset{p\in\mathcal S_Q}{\arg\max} \sum_{i=1}^n\mathbb{1}\left(p(\xi_i)=\hat\xi_i\right), \] donde \(\mathcal S_Q\) contiene las \(Q!\) permutaciones posibles.
Finalmente, se reordenan las filas y columnas de \(\widehat{\mathbf\Pi}\) según \(p^*\), permitiendo compararla con \(\mathbf\Pi\) sin alterar las probabilidades estimadas.
Este procedimiento supone que el número de bloques estimados coincide con el verdadero.
# Matriz estimada, con etiquetas potencialmente permutadas
Pi_sim <- fit_true$model_parameters[[Q_sim]]$pi
round(Pi_sim, 3)## [,1] [,2] [,3]
## [1,] 0.445 0.040 0.059
## [2,] 0.040 0.567 0.028
## [3,] 0.059 0.028 0.521
# Comparar entradas después de alinear etiquetas si Q coincide
if (Q_sim == Q_true) {
# Generar las permutaciones de las etiquetas
opciones <- rep(list(seq_len(Q_true)), Q_true)
permutaciones <- as.matrix(expand.grid(opciones))
distintas <- apply(permutaciones, 1, function(x) length(unique(x)) == Q_true)
permutaciones <- permutaciones[distintas, , drop = FALSE]
# Elegir la correspondencia con mayor acuerdo
acuerdos <- apply(permutaciones, 1, function(p) {
sum(p[labels_true] == labels_sim)
})
perm <- as.integer(permutaciones[which.max(acuerdos), ])
# Reordenar la matriz estimada según las clases verdaderas
Pi_sim_alineada <- Pi_sim[perm, perm, drop = FALSE]
round(Pi_sim_alineada, 3)
round(Pi_true, 3)
} else {
# Las matrices no son comparables entrada a entrada
cat("Se seleccionó un número distinto de bloques; comparar por ARI y estructura.\n")
}## [,1] [,2] [,3]
## [1,] 0.55 0.04 0.03
## [2,] 0.04 0.45 0.06
## [3,] 0.03 0.06 0.50
La red fblog del paquete sand representa
referencias entre blogs políticos franceses,
recopiladas en octubre de 2006.
Una arista no dirigida indica que al menos uno de los blogs enlaza al otro.
Contiene 192 vértices, 1431 aristas
y una categoría observada de afiliación política
(PolParty). Estas categorías son información externa y
no necesariamente coinciden con los bloques
latentes.
Fuente: documentación
de fblog.
# Cargar y actualizar el grafo almacenado
suppressMessages(suppressWarnings(data("fblog", package = "sand")))
fblog <- igraph::upgrade_graph(fblog)
# Afiliaciones políticas observadas
partidos <- factor(igraph::vertex_attr(fblog, "PolParty"))
stopifnot(!anyNA(partidos))
cols_partidos <- grDevices::hcl.colors(nlevels(partidos), "Dark 3")
# Calcular una disposición reproducible
set.seed(42)
layout_blog <- igraph::layout_with_fr(fblog)
# Graficar según las afiliaciones políticas
plot(fblog, layout = layout_blog, vertex.label = NA,
vertex.size = 4, vertex.color = cols_partidos[as.integer(partidos)],
vertex.frame.color = cols_partidos[as.integer(partidos)],
edge.color = grDevices::adjustcolor("black", alpha.f = 0.10))# Matriz binaria de adyacencia
A <- as.matrix(igraph::as_adjacency_matrix(fblog, sparse = FALSE))
# Resumen de la red
resumen_red <- data.frame(
Vertices = igraph::vcount(fblog),
Aristas = igraph::ecount(fblog),
Densidad = igraph::edge_density(fblog),
Grado_medio = mean(igraph::degree(fblog)),
Transitividad = igraph::transitivity(fblog, type = "global", isolates = "zero"),
Componentes = igraph::count_components(fblog)
)
knitr::kable(resumen_red, digits = 3)| Vertices | Aristas | Densidad | Grado_medio | Transitividad | Componentes |
|---|---|---|---|---|---|
| 192 | 1431 | 0.078 | 14.906 | 0.386 | 1 |
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 2.00 8.00 13.00 14.91 18.00 56.00
# Ajustar un SBM Bernoulli para la red no dirigida
set.seed(42)
fblog.sbm <- blockmodels::BM_bernoulli(
membership_type = "SBM_sym",
adj = A,
verbosity = 0,
plotting = "",
explore_min = 4,
explore_max = 15,
ncores = 1
)
fblog.sbm$estimate()## [1] 10
# Informar si la búsqueda terminó en el límite superior
if (Q == length(ICLs)) {
message("El mejor ICL está en el extremo explorado: conviene ampliar explore_max.")
}# Comparar los números de bloques explorados
plot(seq_along(ICLs), ICLs, type = "b", pch = 16,
xlab = "Número de bloques (Q)", ylab = "ICL")
abline(v = Q, col = "red", lty = 2)Como diagnóstico de estabilidad, se recomienda ajustar el modelo con diferentes inicializaciones y comparar los resultados, conservando la solución con mayor ELBO para cada número de bloques \(Q\). Esto permite reducir el riesgo de convergencia a óptimos locales, aunque incrementa el costo computacional.
## [,1] [,2] [,3] [,4] [,5] [,6]
## [1,] 0.995332 0.0005186722 0.0005186722 0.0005186722 0.0005186722 0.0005186722
## [2,] 0.995332 0.0005186722 0.0005186722 0.0005186722 0.0005186722 0.0005186722
## [3,] 0.995332 0.0005186722 0.0005186722 0.0005186722 0.0005186722 0.0005186722
## [4,] 0.995332 0.0005186722 0.0005186722 0.0005186722 0.0005186722 0.0005186722
## [5,] 0.995332 0.0005186722 0.0005186722 0.0005186722 0.0005186722 0.0005186722
## [6,] 0.995332 0.0005186722 0.0005186722 0.0005186722 0.0005186722 0.0005186722
## [,7] [,8] [,9] [,10]
## [1,] 0.0005186722 0.0005186722 0.0005186722 0.0005186722
## [2,] 0.0005186722 0.0005186722 0.0005186722 0.0005186722
## [3,] 0.0005186722 0.0005186722 0.0005186722 0.0005186722
## [4,] 0.0005186722 0.0005186722 0.0005186722 0.0005186722
## [5,] 0.0005186722 0.0005186722 0.0005186722 0.0005186722
## [6,] 0.0005186722 0.0005186722 0.0005186722 0.0005186722
## [1] 1 1 1 1 1 1
# Incertidumbre de pertenencia
prob_max <- apply(Z, 1, max)
incertidumbre <- 1 - prob_max
entropia <- -rowSums(ifelse(Z > 0, Z * log(pmax(Z, .Machine$double.eps)), 0))
# Resumir la incertidumbre
summary(prob_max)## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.8586 0.9953 0.9953 0.9938 0.9953 0.9953
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.004668 0.004668 0.004668 0.006206 0.004668 0.141441
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.03997 0.03997 0.03997 0.04498 0.03997 0.43493
## [1] 35 27 11 21 24 25 6 7 13 23
# Proporciones basadas en asignaciones puntuales
alpha_dura <- nq / igraph::vcount(fblog)
# Proporciones a partir de las probabilidades variacionales
alpha_variacional <- colMeans(Z)
round(rbind(Dura = alpha_dura, Variacional = alpha_variacional), 3)## [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
## Dura 0.182 0.141 0.057 0.109 0.125 0.130 0.031 0.036 0.068 0.12
## Variacional 0.182 0.140 0.058 0.109 0.125 0.131 0.032 0.037 0.067 0.12
La matriz \(\widehat{\mathbf\Pi}\) describe la conectividad entre bloques, no necesariamente comunidades.
Valores altos en la diagonal caracterizan una estructura assortativa; valores altos fuera de ella pueden indicar otras organizaciones.
# Matriz de probabilidades estimadas
Pi <- fblog.sbm$model_parameters[[Q]]$pi
# Eliminar pequeñas discrepancias numéricas de simetría
Pi <- (Pi + t(Pi)) / 2
# Visualizar una matriz de probabilidades, no de correlaciones
corrplot::corrplot(
corr = Pi,
is.corr = FALSE,
method = "color",
col.lim = c(0, 1),
type = "full",
addgrid.col = "gray90",
tl.col = "black"
)## [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
## [1,] 0.084 0.003 0.003 0.015 0.004 0.006 0.105 0.062 0.087 0.031
## [2,] 0.003 0.212 0.007 0.001 0.002 0.005 0.014 0.615 0.034 0.001
## [3,] 0.003 0.007 0.910 0.001 0.001 0.036 0.018 0.002 0.043 0.001
## [4,] 0.015 0.001 0.001 0.349 0.001 0.002 0.429 0.069 0.081 0.091
## [5,] 0.004 0.002 0.001 0.001 0.543 0.009 0.029 0.008 0.093 0.004
## [6,] 0.006 0.005 0.036 0.002 0.009 0.341 0.041 0.085 0.108 0.004
## [7,] 0.105 0.014 0.018 0.429 0.029 0.041 0.906 0.191 0.380 0.776
## [8,] 0.062 0.615 0.002 0.069 0.008 0.085 0.191 0.974 0.382 0.063
## [9,] 0.087 0.034 0.043 0.081 0.093 0.108 0.380 0.382 0.383 0.071
## [10,] 0.031 0.001 0.001 0.091 0.004 0.004 0.776 0.063 0.071 0.545
La matriz de probabilidades predichas bajo la aproximación variacional es \(\widehat{\mathbf P}=\mathbf Z\widehat{\mathbf\Pi}\mathbf Z^\top\) para \(i\neq j\). Sus entradas no diagonales representan aproximaciones a las probabilidades marginales de enlace bajo la distribución variacional de clases. La diagonal se fija en cero porque se excluyen los bucles.
# Ordenar los vértices según los bloques estimados
orden <- order(labs)
A_ordenada <- A[orden, orden]
# Calcular las probabilidades estimadas de enlace
P_hat <- Z %*% Pi %*% t(Z)
diag(P_hat) <- 0
P_ordenada <- P_hat[orden, orden]
# Identificar las divisiones entre bloques
tam_bloques <- tabulate(labs, nbins = Q)
limites <- cumsum(tam_bloques)
limites <- unique(limites[limites > 0 & limites < nrow(A)] + 0.5)
# Definir los colores: blanco (0) y azul (1)
colores <- colorRampPalette(c("white", "#08519C"))(100)
# Función para visualizar matrices
graficar_matriz <- function(matriz, titulo, limites = numeric(0)) {
n <- nrow(matriz)
# Representar los valores mediante colores
image(
x = seq_len(n),
y = seq_len(n),
z = matriz,
col = colores,
zlim = c(0, 1),
axes = FALSE,
xlab = "",
ylab = "",
main = titulo,
useRaster = TRUE
)
# Marcar las divisiones entre bloques
if (length(limites) > 0) {
abline(v = limites, h = limites, col = "red", lwd = 1)
}
box()
}
# Guardar la configuración gráfica
par_anterior <- par(no.readonly = TRUE)
# Mostrar los tres gráficos
par(mfrow = c(1, 3), mar = c(2, 2, 3, 1))
graficar_matriz(A, "Adyacencia original")
graficar_matriz(A_ordenada, "Adyacencia ordenada", limites)
graficar_matriz(P_ordenada, "Probabilidad estimada", limites)# Colores de los bloques estimados
cols_bloques <- grDevices::hcl.colors(Q, "Dark 3")
# Comparar afiliaciones políticas y bloques en la misma disposición
par(mfrow = c(1, 2), mar = c(2, 1, 3, 1))
plot(fblog, layout = layout_blog, vertex.label = NA,
vertex.size = 4, vertex.color = cols_partidos[as.integer(partidos)],
vertex.frame.color = cols_partidos[as.integer(partidos)],
edge.color = grDevices::adjustcolor("black", alpha.f = 0.10),
main = "Afiliaciones políticas")
plot(fblog, layout = layout_blog, vertex.label = NA,
vertex.size = 4, vertex.color = cols_bloques[labs],
vertex.frame.color = cols_bloques[labs],
edge.color = grDevices::adjustcolor("black", alpha.f = 0.10),
main = "Bloques estimados")Las afiliaciones políticas constituyen una clasificación externa, no clases latentes verdaderas.
La concordancia puede medirse con Rand, ARI e información mutua normalizada (NMI). Valores altos indican similitud, pero una discrepancia no implica necesariamente un mal ajuste del SBM.
# Etiquetas de las afiliaciones observadas
party_nums <- as.integer(partidos)
# Índices invariantes a la numeración de los grupos
concordancia <- c(
Rand = igraph::compare(party_nums, labs, method = "rand"),
ARI = igraph::compare(party_nums, labs, method = "adjusted.rand"),
NMI = igraph::compare(party_nums, labs, method = "nmi")
)
round(concordancia, 4)## Rand ARI NMI
## 0.8619 0.4608 0.6565
| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | |
|---|---|---|---|---|---|---|---|---|---|---|
| Cap21 | 2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| Commentateurs Analystes | 2 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 7 | 0 |
| Les Verts | 7 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| liberaux | 1 | 0 | 0 | 0 | 24 | 0 | 0 | 0 | 0 | 0 |
| Parti Radical de Gauche | 9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
| PCF - LCR | 7 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| PS | 6 | 0 | 0 | 21 | 0 | 0 | 6 | 0 | 2 | 22 |
| UDF | 0 | 24 | 0 | 0 | 0 | 1 | 0 | 6 | 1 | 0 |
| UMP | 1 | 2 | 11 | 0 | 0 | 24 | 0 | 0 | 2 | 0 |
La bondad de ajuste compara estadísticas observadas con las obtenidas de redes generadas por el modelo estimado.
Comenzamos con simulaciones condicionales a las clases puntuales estimadas; después mostramos la alternativa de simular también las pertenencias.
Se consideran densidad, varianza del grado, grado máximo, transitividad, triángulos, assortatividad de grados, número de componentes y tamaño relativo del mayor componente. Un ajuste adecuado de la densidad no garantiza que el modelo reproduzca la estructura local o la heterogeneidad de grados.
# Extraer estadísticas de una red simple no dirigida
estadisticas_red <- function(g) {
grados <- igraph::degree(g)
componentes <- igraph::components(g)
assort <- igraph::assortativity_degree(g)
# Evitar valores no definidos en grafos degenerados
if (!is.finite(assort)) assort <- NA_real_
c(
Densidad = igraph::edge_density(g),
Varianza_grado = stats::var(grados),
Grado_maximo = max(grados),
Transitividad = igraph::transitivity(g, type = "global", isolates = "zero"),
Triangulos = sum(igraph::count_triangles(g)) / 3,
Assortatividad = assort,
Componentes = componentes$no,
Mayor_componente = max(componentes$csize) / igraph::vcount(g)
)
}
# Calcular las estadísticas observadas
estad_obs <- estadisticas_red(fblog)
round(estad_obs, 4)## Densidad Varianza_grado Grado_maximo Transitividad
## 0.0780 102.7765 56.0000 0.3858
## Triangulos Assortatividad Componentes Mayor_componente
## 3821.0000 0.0124 1.0000 1.0000
# Número de redes simuladas
n_sim <- 500
n_blog <- igraph::vcount(fblog)
# Guardar estadísticas y grados ordenados
estad_sim <- matrix(NA_real_, nrow = length(estad_obs), ncol = n_sim)
rownames(estad_sim) <- names(estad_obs)
grados_sim <- matrix(NA_real_, nrow = n_blog, ncol = n_sim)
# Tamaños de bloques y probabilidades estimados
block_sizes <- tabulate(labs, nbins = Q)
# Simular con la partición y parámetros estimados
set.seed(42)
for (s in seq_len(n_sim)) {
g_sim <- igraph::sample_sbm(
n = n_blog,
pref.matrix = Pi,
block.sizes = block_sizes,
directed = FALSE,
loops = FALSE
)
# Almacenar estadísticas y distribución de grados
estad_sim[, s] <- estadisticas_red(g_sim)
grados_sim[, s] <- sort(igraph::degree(g_sim))
}
# Calcular medias y cuantiles simulados
media_sim <- apply(estad_sim, 1, mean, na.rm = TRUE)
desv_sim <- apply(estad_sim, 1, sd, na.rm = TRUE)
lim_inf <- apply(estad_sim, 1, quantile, probs = 0.025, na.rm = TRUE)
lim_sup <- apply(estad_sim, 1, quantile, probs = 0.975, na.rm = TRUE)
# Resumir el diagnóstico
resultados <- data.frame(
Estadistico = names(estad_obs),
Observado = as.numeric(estad_obs),
Media = media_sim,
Desviacion = desv_sim,
Limite_2.5 = lim_inf,
Limite_97.5 = lim_sup,
row.names = NULL
)
knitr::kable(resultados, digits = 4,
caption = "Estadísticos observados e intervalos de referencia simulados (95 %)")| Estadistico | Observado | Media | Desviacion | Limite_2.5 | Limite_97.5 |
|---|---|---|---|---|---|
| Densidad | 0.0780 | 0.0779 | 0.0016 | 0.0747 | 0.0810 |
| Varianza_grado | 102.7765 | 78.0112 | 4.8985 | 68.2039 | 87.1225 |
| Grado_maximo | 56.0000 | 49.2360 | 2.8256 | 44.0000 | 55.0000 |
| Transitividad | 0.3858 | 0.3053 | 0.0097 | 0.2848 | 0.3240 |
| Triangulos | 3821.0000 | 2776.0160 | 159.1182 | 2471.3250 | 3102.1500 |
| Assortatividad | 0.0124 | 0.0718 | 0.0253 | 0.0227 | 0.1202 |
| Componentes | 1.0000 | 1.0460 | 0.2190 | 1.0000 | 2.0000 |
| Mayor_componente | 1.0000 | 0.9998 | 0.0011 | 0.9948 | 1.0000 |
# Graficar distribuciones de las estadísticas simuladas
par(mfrow = c(2, 4), mar = c(3.5, 3.5, 2.5, 0.5), mgp = c(2, 0.7, 0))
for (j in seq_along(estad_obs)) {
valores <- estad_sim[j, ]
valores <- valores[is.finite(valores)]
# Comparar simulaciones con el valor observado
hist(valores, breaks = "Sturges", main = names(estad_obs)[j],
xlab = "Valor simulado", col = "gray85", border = "white")
abline(v = estad_obs[j], col = "red", lwd = 2)
}# Construir bandas por posición en los grados ordenados
posicion <- seq_len(n_blog)
grado_obs <- sort(igraph::degree(fblog))
grado_inf <- apply(grados_sim, 1, quantile, probs = 0.025)
grado_sup <- apply(grados_sim, 1, quantile, probs = 0.975)
# Dibujar el intervalo simulado y los grados observados
plot(posicion, grado_obs, type = "n",
ylim = range(c(grado_obs, grado_inf, grado_sup)),
xlab = "Posición del vértice (grados ordenados)",
ylab = "Grado", main = "Distribución de grados")
polygon(c(posicion, rev(posicion)), c(grado_inf, rev(grado_sup)),
col = grDevices::adjustcolor("steelblue", alpha.f = 0.25), border = NA)
lines(posicion, grado_obs, col = "red", lwd = 2)
lines(posicion, apply(grados_sim, 1, median), col = "steelblue", lwd = 2)
legend("topleft", legend = c("Observado", "Mediana simulada", "Banda 95 %"),
col = c("red", "steelblue", "steelblue"),
lty = c(1, 1, 1), lwd = c(2, 2, 8), bty = "n")Las bandas de grados ordenados comparan distribuciones empíricas, no intervalos para vértices individualmente identificados. Una discrepancia sistemática sugiere limitaciones del SBM para reproducir la heterogeneidad de grados.
También es posible generar primero los tamaños de bloques con una distribución multinomial, usando las proporciones variacionales estimadas. Esta estrategia evalúa otro aspecto de la distribución generativa, pues permite que cambien los tamaños de las clases entre simulaciones.
# Generar tamaños de clases según las probabilidades estimadas
set.seed(202)
bs_nuevo <- as.integer(stats::rmultinom(
n = 1, size = n_blog, prob = alpha_variacional
))
# Conservar únicamente bloques no vacíos
activos <- which(bs_nuevo > 0)
# Simular una red con nuevas asignaciones de tamaños
red_nueva <- igraph::sample_sbm(
n = n_blog,
pref.matrix = Pi[activos, activos, drop = FALSE],
block.sizes = bs_nuevo[activos],
directed = FALSE,
loops = FALSE
)
# Comparar estadísticas básicas con las observadas
round(rbind(Observado = estad_obs,
Simulado = estadisticas_red(red_nueva)), 3)## Densidad Varianza_grado Grado_maximo Transitividad Triangulos
## Observado 0.078 102.777 56 0.386 3821
## Simulado 0.080 71.754 49 0.300 2778
## Assortatividad Componentes Mayor_componente
## Observado 0.012 1 1
## Simulado 0.051 1 1
La comprobación predictiva paramétrica genera redes utilizando los parámetros estimados \(\hat{\boldsymbol\theta}=(\hat{\boldsymbol\alpha},\widehat{\mathbf\Pi})\) como valores fijos, sin incorporar su incertidumbre de estimación. Para considerarla, existen dos alternativas:
Bootstrap paramétrico: generar \(B\) redes a partir del modelo ajustado y reestimar sus parámetros, obteniendo \(\hat{\boldsymbol\theta}^{(1)},\ldots,\hat{\boldsymbol\theta}^{(B)}\). Estas estimaciones permiten aproximar la variabilidad muestral de los estimadores.
Inferencia bayesiana: asignar distribuciones previas a los parámetros e incorporar su incertidumbre mediante la distribución predictiva posterior: \[ p(\mathbf y^{\mathrm{rep}}\mid\mathbf y) = \int p(\mathbf y^{\mathrm{rep}}\mid\boldsymbol\theta)\, p(\boldsymbol\theta\mid\mathbf y) \,\textsf{d}\boldsymbol\theta, \] donde \(p(\boldsymbol\theta\mid\mathbf y)\) es la distribución posterior de los parámetros y las clases latentes se encuentran marginalizadas.
blockmodels,
igraph
y notas
de inferencia en SBM.