Considere una red binaria no dirigida representada mediante la matriz de adyacencia \(\mathbf{Y} = [y_{i,j}]\), donde cada elemento \(y_{i,j} \in \{0,1\}\) indica la presencia (\(y_{i,j} = 1\)) o ausencia (\(y_{i,j} = 0\)) de una conexión entre los \(n\) individuos que conforman la red.
El modelo para la probabilidad de conexión entre dos nodos se define
como:
\[
y_{i,j} \mid \theta_{i,j} \overset{\text{ind}}{\sim}
\textsf{Ber}(\theta_{i,j}), \quad \text{para } i < j,
\]
con:
\[
\theta_{i,j} = \Phi(\mu + \delta_i + \delta_j),
\]
donde \(\Phi(\cdot)\) denota la función
de distribución acumulada de una Normal estándar. Esta
función se utilizada como función de enlace para
garantizar que las probabilidades \(\theta_{i,j,k}\) permanezcan dentro del
intervalo \((0, 1)\).
Para los parámetros, se asumen las siguientes
distribuciones previas:
\[
\mu \sim \textsf{N}(0, \sigma^2), \quad \delta_i
\overset{\text{iid}}{\sim} \textsf{N}(0, \tau^2), \quad \sigma^2 \sim
\textsf{GI}(a_\sigma, b_\sigma), \quad \tau^2 \sim \textsf{GI}(a_\tau,
b_\tau).
\]
El conjunto de parámetros del modelo es:
\[
\Theta = \{\mu, \delta_1, \delta_2, \dots, \delta_n, \sigma^2, \tau^2\},
\] donde:
Los \(\delta_i\) capturan diferencias individuales en conectividad, permitiendo modelar patrones de centralidad y aislamiento en la red.
En total, el modelo tiene \(n + 3\) parámetros: \(n\) efectos de sociabilidad, más \(\mu\), \(\sigma^2\), y \(\tau^2\).
Los hiperparámetros del modelo son:
\[
\{a_\sigma, b_\sigma, a_\tau, b_\tau\},
\]
donde \(a_\sigma, b_\sigma\) controlan
la varianza de \(\mu\), y \(a_\tau, b_\tau\) regulan la variabilidad de
\(\delta_i\).
En el modelo propuesto, las distribuciones condicionales completas no tienen forma cerrada debido a que la función de enlace introduce una relación no lineal entre los parámetros y las observaciones.
Para abordar esta limitación, se puede introducir variables
auxiliares latentes \(z_{i,j}\), tales que:
\[
y_{i,j} =
\begin{cases}
1 & \text{si } z_{i,j} > 0, \\
0 & \text{si } z_{i,j} \leq 0,
\end{cases}
\] donde: \[
z_{i,j} \mid \mu, \delta_i, \delta_j \sim \textsf{N}(\mu + \delta_i +
\delta_j, 1).
\]
La introducción de las variables latentes \(z_{i,j}\) mantiene el modelo original porque estas variables son consistentes con la probabilidad subyacente del modelo inicial. Al integrar \(z_{i,j}\) sobre su distribución condicional, recuperamos la probabilidad original de \(y_{i,j}\):
Dado que \(z_{i,j} \mid \mu, \delta_i,
\delta_j \sim \textsf{N}(\mu + \delta_i + \delta_j, 1)\), la
probabilidad de que \(y_{i,j} = 1\) es
equivalente a la probabilidad de que \(z_{i,j}
> 0\):
\[
\textsf{P}(y_{i,j} = 1 \mid \mu, \delta_i, \delta_j) =
\textsf{P}(z_{i,j} > 0) = \int_0^\infty \textsf{N}(z_{i,j} \mid \mu +
\delta_i + \delta_j, 1) \, \textsf{d}z_{i,j}.
\]
Evaluando esta integral, obtenemos: \[ \textsf{P}(y_{i,j} = 1 \mid \mu, \delta_i, \delta_j) = \Phi(\mu + \delta_i + \delta_j), \] donde \(\Phi(\cdot)\) es la función de distribución acumulada de una normal estándar.
De manera similar: \[ \textsf{P}(y_{i,j} = 0 \mid \mu, \delta_i, \delta_j) = 1 - \Phi(\mu + \delta_i + \delta_j). \]
Este enfoque linealiza el modelo al sustituir la función de enlace \(\Phi(\cdot)\) por variables auxiliares \(z_{i,j,k}\) con distribución Normal estándar. Esta reformulación simplifica el cálculo de las distribuciones condicionales completas, permitiendo que adopten formas estándar.
\(z_{i,j} \mid y_{i,j}, \mu, \delta_i, \delta_j\) es Normal truncada: \[ z_{i,j} \mid y_{i,j}, \mu, \delta_i, \delta_j \sim \begin{cases} \textsf{N}_{(0,\infty)}\,\,\, (\mu + \delta_i + \delta_j, 1), & \text{ si } y_{i,j} = 1, \\ \textsf{N}_{(-\infty,0]}(\mu + \delta_i + \delta_j, 1), & \text{ si } y_{i,j} = 0. \end{cases} \]
\(\mu \mid \mathbf{z}, \boldsymbol{\delta}, \sigma^2 \sim \textsf{N}(m, v^2)\), con: \[ v^2 = \left(\frac{1}{\sigma^2} + \sum_{i<j} 1\right)^{-1}, \quad m = v^2 \sum_{i<j} (z_{i,j} - \delta_i - \delta_j). \]
\(\delta_i \mid \mathbf{z}, \mu, \tau^2
\sim \textsf{N}(m_i, v_i^2)\), con:
\[
v_i^2 = \left(\frac{1}{\tau^2} + \sum_{j \neq i} 1\right)^{-1}, \quad
m_i = v_i^2 \sum_{j \neq i} (z_{i,j} - \mu - \delta_j).
\]
\(\sigma^2 \mid \mu \sim \textsf{GI}(a,
b)\), con:
\[
a = a_\sigma + \frac{1}{2}, \quad b = b_\sigma + \frac{\mu^2}{2}.
\]
\(\tau^2 \mid \boldsymbol{\delta} \sim
\textsf{GI}(a, b)\), con:
\[
a = a_\tau + \frac{n}{2}, \quad b_\tau^* = b_\tau + \frac{\sum_{i}
\delta_i^2}{2}.
\]
# Distribuciones condicionales completas
# DCC 1: Muestreo de z
sample_z <- function(y, mu, delta, z) {
for (i in 1:(n - 1)) {
for (j in (i + 1):n) {
mean_z <- mu + delta[i] + delta[j]
if (y[i, j] == 1) {
z[i, j] <- truncnorm::rtruncnorm(n = 1, a = 0, b = Inf, mean = mean_z, sd = 1)
} else {
z[i, j] <- truncnorm::rtruncnorm(n = 1, a = -Inf, b = 0, mean = mean_z, sd = 1)
}
z[j, i] <- z[i, j] # Simetría
}
}
return(z)
}
# DCC 2: Muestreo de mu
sample_mu <- function(z, delta, sigma2) {
v2_mu <- 1 / (1 / sigma2 + sum(upper.tri(z)))
m_mu <- v2_mu * sum(z[upper.tri(z)] - delta[row(z)[upper.tri(z)]] - delta[col(z)[upper.tri(z)]])
return(rnorm(1, mean = m_mu, sd = sqrt(v2_mu)))
}
# DCC 3: Muestreo de delta
sample_delta <- function(z, mu, tau2, delta) {
for (i in 1:n) {
neighbors <- setdiff(1:n, i)
v2_delta <- 1 / (1 / tau2 + length(neighbors))
m_delta <- v2_delta * sum(z[i, neighbors] - mu - delta[neighbors])
delta[i] <- rnorm(1, mean = m_delta, sd = sqrt(v2_delta))
}
return(delta)
}
# DCC 4: Muestreo de sigma^2
sample_sigma2 <- function(mu) {
a_sigma_post <- a_sigma + 0.5
b_sigma_post <- b_sigma + 0.5 * mu^2
return(1 / rgamma(1, shape = a_sigma_post, rate = b_sigma_post))
}
# DCC 5: Muestreo de tau^2
sample_tau2 <- function(delta) {
a_tau_post <- a_tau + n / 2
b_tau_post <- b_tau + 0.5 * sum(delta^2)
return(1 / rgamma(1, shape = a_tau_post, rate = b_tau_post))
}
# Muestreador de Gibbs
gibbs_sampler <- function(y, n_iter, n_burn, n_thin) {
# Inicialización
mu <- 0
delta <- rnorm(n, 0, 1)
sigma2 <- 1
tau2 <- 1
z <- matrix(0, n, n) # Variables auxiliares
# Almacenamiento
n_samples <- (n_iter - n_burn) / n_thin
samples <- list(mu = numeric(n_samples),
delta = matrix(0, nrow = n_samples, ncol = n),
sigma2 = numeric(n_samples),
tau2 = numeric(n_samples))
# Muestreo
cat("Iniciando muestreador de Gibbs...\n")
for (t in 1:n_iter) {
# Llamar a las funciones de muestreo
z <- sample_z(y, mu, delta, z)
mu <- sample_mu(z, delta, sigma2)
delta <- sample_delta(z, mu, tau2, delta)
sigma2 <- sample_sigma2(mu)
tau2 <- sample_tau2(delta)
# Almacenar muestras según n_thin
if (t > n_burn && (t - n_burn) %% n_thin == 0) {
idx <- (t - n_burn) / n_thin
samples$mu[idx] <- mu
samples$delta[idx, ] <- delta
samples$sigma2[idx] <- sigma2
samples$tau2[idx] <- tau2
}
# Mostrar progreso
if (t %% (n_iter / 10) == 0) {
cat(sprintf("Progreso: %d%% completado\n", (t / n_iter) * 100))
}
}
cat("Muestreador completado.\n")
return(samples)
}
Se recomienda \(\mathbb{E}[\sigma^2] \leq \mathbb{E}[\tau^2]\) para que la heterogeneidad individual \(\delta_i\) sea mayor que la incertidumbre global \(\mu\).
\(a_\sigma = 2, b_\sigma = 1\).
\(a_\sigma = 1, b_\sigma = 1\).
\(a_\tau = 3, b_\tau = 2\).
\(a_\tau = 2, b_\tau = 1\).
# Simulación de datos según el modelo
simulate_data <- function(n, mu_true, tau2_true) {
# Generar parámetros verdaderos
delta_true <- rnorm(n, mean = 0, sd = sqrt(tau2_true)) # Efectos nodales
z_true <- matrix(0, n, n) # Variables latentes
Y <- matrix(0, n, n) # Matriz de adyacencia
# Generar la matriz de adyacencia
for (i in 1:(n - 1)) {
for (j in (i + 1):n) {
z_true[i, j] <- rnorm(1, mean = mu_true + delta_true[i] + delta_true[j], sd = 1)
z_true[j, i] <- z_true[i, j] # Simetría
Y[i, j] <- ifelse(z_true[i, j] > 0, 1, 0)
Y[j, i] <- Y[i, j] # Simetría
}
}
# Retornar los datos simulados
list(Y = Y, z_true = z_true, delta_true = delta_true, mu_true = mu_true, tau2_true = tau2_true)
}
# Parámetros verdaderos
n <- 50
mu_true <- -0.5
tau2_true <- 0.5
# Simulación
set.seed(123)
sim_data <- simulate_data(n, mu_true, tau2_true)
# Matriz de adyacencia simulada
Y <- sim_data$Y
# Hiperparámetros
a_sigma <- 2
b_sigma <- 1
a_tau <- 2
b_tau <- 1
# Ajustar el modelo usando Gibbs
n_iter <- 100000 + 10000
n_burn <- 10000
n_thin <- 5
set.seed(123)
samples <- gibbs_sampler(Y, n_iter, n_burn, n_thin)
save(samples, file = "samples_regresion_binaria_sinteticos.RData")
Para cada muestra de \(\mu\) y \(\boldsymbol{\delta}\), la log-verosimilitud
del modelo se calcula como:
\[
\log p(\mu, \boldsymbol{\delta} \mid \mathbf{y}) = \sum_{i < j}
\left[ y_{i,j} \log(\theta_{i,j}) + (1 - y_{i,j}) \log(1 - \theta_{i,j})
\right],
\] donde \(\theta_{i,j} = \Phi(\mu +
\delta_i + \delta_j)\).
# Función para calcular la log-verosimilitud
log_likelihood <- function(y, samples) {
n <- nrow(y) # Número de nodos
log_lik_samples <- numeric(length(samples$mu)) # Almacenar la log-verosimilitud para cada muestra
for (s in seq_along(samples$mu)) {
mu <- samples$mu[s]
delta <- samples$delta[s, ]
log_lik <- 0
for (i in 1:(n - 1)) {
for (j in (i + 1):n) {
eta_ij <- mu + delta[i] + delta[j] # Predictor lineal
p_ij <- pnorm(eta_ij) # Probabilidad del modelo (probit)
# Sumar la contribución del par (i, j) a la log-verosimilitud
log_lik <- log_lik + y[i, j] * log(p_ij + 1e-10) + (1 - y[i, j]) * log(1 - p_ij + 1e-10)
}
}
log_lik_samples[s] <- log_lik
}
return(log_lik_samples)
}
# Calcular la log-verosimilitud para las muestras del muestreador
log_lik <- log_likelihood(Y, samples)
# Cargar librerías necesarias
suppressMessages(suppressWarnings(library(coda)))
# Función para calcular MCSE
mc_ee <- function(samples) {
n_eff <- effectiveSize(samples) # Tamaño efectivo
sd_est <- sd(as.vector(samples)) # Desviación estándar
mcse <- abs(sd_est / sqrt(n_eff)) # Error estándar de Monte Carlo (MCSE)
return(mcse)
}
round(mc_ee(samples$mu), 4)
## var1
## 0.0044
round(mc_ee(samples$sigma2), 4)
## var1
## 0.0069
round(summary(mc_ee(samples$delta)), 4)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.0068 0.0072 0.0075 0.0075 0.0077 0.0081
Comparación Relativa con la Desviación Estándar:
El MCSE se considera pequeño si es
menor o igual al 5% de la desviación estándar posterior
(\(\hat{\sigma}\)):
\[
\frac{\text{MCSE}}{\hat{\sigma}} \leq 0.05 \quad (5\%).
\]
Comparación con la Media Estimada:
El MCSE se considera pequeño si es
menor o igual al 1% de la media estimada (\(\hat{\theta}\)):
\[
\frac{\text{MCSE}}{|\hat{\theta}|} \leq 0.15 \quad (15\%).
\]
Evaluación Basada en el Tamaño Efectivo de Muestras (\(n_{\text{eff}}\)):
** El MCSE es inversamente proporcional al
tamaño efectivo de muestra (\(n_{\text{eff}}\)):
\[
\text{MCSE} \propto \frac{1}{\sqrt{n_{\text{eff}}}}.
\]
Criterio: - \(n_{\text{eff}} > 1000\): MCSE
pequeño (alta precisión).
- \(n_{\text{eff}} <
200\): MCSE grande (baja precisión).
# inferencia sobre mu, sigma2 y tau2
# mu_true = -0.5
# tau2_true = 0.5
round(quantile(samples$mu, probs = c(0.025,0.5,0.975)), 3)
## 2.5% 50% 97.5%
## -0.742 -0.378 -0.028
round(quantile(samples$tau2, probs = c(0.025,0.5,0.975)), 3)
## 2.5% 50% 97.5%
## 0.270 0.413 0.651
# Valores reales
delta_true <- sim_data$delta_true
# Crear un DataFrame con las estimaciones y los valores reales
delta_mean <- colMeans(samples$delta) # Media posterior de delta
delta_ci95 <- apply(samples$delta, 2, quantile, probs = c(0.025, 0.975)) # IC al 95%
delta_ci99 <- apply(samples$delta, 2, quantile, probs = c(0.005, 0.995)) # IC al 99%
delta_df <- data.frame(
Node = 1:n,
Delta_Est = delta_mean,
Delta_True = delta_true,
CI95_Lower = delta_ci95[1, ],
CI95_Upper = delta_ci95[2, ],
CI99_Lower = delta_ci99[1, ],
CI99_Upper = delta_ci99[2, ]
)
# Graficar usando ggplot2
ggplot(delta_df, aes(x = Node)) +
# IC al 99%
geom_linerange(aes(ymin = CI99_Lower, ymax = CI99_Upper), color = "gray70", linewidth = 1) +
# IC al 95%
geom_linerange(aes(ymin = CI95_Lower, ymax = CI95_Upper), color = "lightblue", linewidth = 1.5) +
# Estimaciones puntuales
geom_point(aes(y = Delta_Est), color = "blue", size = 3) +
# Valores reales
geom_point(aes(y = Delta_True), color = "red", shape = 4, size = 3) +
# Personalización
labs(
title = "Estimaciones de Delta con Intervalos de Credibilidad",
x = "Vértice",
y = expression(delta)
) +
theme_minimal(base_size = 14) +
theme(
plot.title = element_text(hjust = 0.5, face = "bold")
)
Los datos de Lazega corresponden a una red de relaciones de trabajo colaborativo entre miembros de una firma de abogados.
Estos datos se recopilaron para estudiar la cooperación entre actores sociales dentro de una organización, analizando el intercambio de diversos tipos de recursos entre ellos.
# librerías
suppressMessages(suppressWarnings(library(igraph)))
suppressMessages(suppressWarnings(library(ggraph)))
suppressMessages(suppressWarnings(library(sand)))
# grafo
g <- graph_from_data_frame(d = elist.lazega, directed = "F")
# clase de objeto
class(g)
## [1] "igraph"
# dirigida?
is_directed(g)
## [1] FALSE
# ponderada?
is_weighted(g)
## [1] FALSE
# orden
(n <- vcount(g))
## [1] 34
# tamaño
(s <- ecount(g))
## [1] 115
# matriz de adyacencia
Y <- as.matrix(as_adjacency_matrix(graph = g, names = F))
# clase de objeto
class(Y)
## [1] "matrix" "array"
# dimensión
dim(Y)
## [1] 34 34
# simétrica?
isSymmetric(Y)
## [1] TRUE
# Hiperparámetros
a_sigma <- 2
b_sigma <- 1
a_tau <- 2
b_tau <- 1
# Ajustar el modelo usando Gibbs
n_iter <- 100000 + 10000
n_burn <- 10000
n_thin <- 5
samples <- gibbs_sampler(Y, n_iter, n_burn, n_thin)
save(samples, file = "samples_regresion_binaria_lazega.RData")
# Cargar librerías necesarias
suppressMessages(suppressWarnings(library(coda)))
# Función para calcular MCSE
mc_ee <- function(samples) {
n_eff <- effectiveSize(samples) # Tamaño efectivo
sd_est <- sd(as.vector(samples)) # Desviación estándar
mcse <- abs(sd_est / sqrt(n_eff)) # Error estándar de Monte Carlo (MCSE)
return(mcse)
}
round(mc_ee(samples$mu), 4)
## var1
## 0.0032
round(mc_ee(samples$sigma2), 4)
## var1
## 0.0098
round(summary(mc_ee(samples$delta)), 4)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 0.0040 0.0042 0.0043 0.0043 0.0044 0.0045
Los parámetros \(\delta_i\) representan la socialidad individual de cada individuo, es decir, su propensión a formar conexiones en la red.
La varianza \(\tau^2\) mide la
heterogeneidad entre los \(\delta_i\):
- Alta varianza \(\tau^2\) implica mayor dispersión
en la socialidad (nodos centrales y periféricos).
- Baja varianza \(\tau^2\) sugiere una red
socialmente homogénea.
La matriz estimada de probabilidades de interacción se calcula iterando sobre las muestras posteriores del modelo y evaluando \(\theta_{ij} = \Phi(\mu + \delta_i + \delta_j)\) para cada par de nodos. Finalmente, se promedian estas matrices a lo largo de todas las muestras para obtener la probabilidad esperada de conexión entre los nodos.
# Función para calcular la matriz de probabilidades theta_ij
compute_theta <- function(samples) {
n_samples <- length(samples$mu)
n <- ncol(samples$delta)
theta_avg <- matrix(0, n, n) # Inicializar matriz promedio
# Iterar sobre cada muestra
for (s in 1:n_samples) {
mu <- samples$mu[s]
delta <- samples$delta[s, ]
theta <- matrix(0, n, n)
# Calcular theta_ij para cada par (i, j)
for (i in 1:(n - 1)) {
for (j in (i + 1):n) {
theta[i, j] <- pnorm(mu + delta[i] + delta[j]) # Probit link
theta[j, i] <- theta[i, j] # Simetría
}
}
# Promediar sobre las muestras
theta_avg <- theta_avg + theta/n_samples
}
return(theta_avg)
}
# Calcular la matriz promedio de theta_ij
theta_avg <- compute_theta(samples)
Las probabilidades de coagrupamiento (Co-clustering probabilities) se estiman a partir del agrupamiento de los parámetros de sociabilidad \(\delta_i\) en cada iteración del muestreador de Gibbs. El procedimiento utiliza el algoritmo k-means para asignar grupos en cada iteración y calcula las probabilidades de pertenencia conjunta al mismo grupo.
# Librerías
suppressMessages(suppressWarnings(library(cluster))) # Para la silueta
suppressMessages(suppressWarnings(library(factoextra))) # Para validación de clusters
# Función para determinar automáticamente el número óptimo de clusters
find_optimal_k <- function(delta) {
max_k <- min(10, length(delta)) # Máximo número de clusters a evaluar
wss <- numeric(max_k) # Suma de cuadrados dentro de los clusters
# Calcular WSS para cada k
for (k in 2:max_k) {
model <- kmeans(delta, centers = k, nstart = 10)
wss[k] <- model$tot.withinss # Suma de cuadrados dentro del cluster
}
# Método de codo para encontrar el punto óptimo
optimal_k <- which(diff(diff(wss)) > 0)[1] + 1
if (is.na(optimal_k)) optimal_k <- 2 # Por defecto al menos 2 clusters
return(optimal_k)
}
# Función para calcular probabilidades de coagrupamiento
compute_coclustering <- function(samples) {
n_samples <- length(samples$mu) # Número de muestras
n <- ncol(samples$delta) # Número de nodos
coclustering <- matrix(0, n, n) # Inicializar matriz de coagrupamiento
# Iterar sobre cada muestra
for (s in 1:n_samples) {
# Obtener los delta para la muestra actual
delta <- samples$delta[s, ]
# Determinar automáticamente el número óptimo de clusters
k <- find_optimal_k(delta)
# Aplicar k-means con el k óptimo
clusters <- kmeans(delta, centers = k, nstart = 10)$cluster
# Actualizar matriz de co-clustering
for (i in 1:(n - 1)) {
for (j in (i + 1):n) {
if (clusters[i] == clusters[j]) {
coclustering[i, j] <- coclustering[i, j] + 1
coclustering[j, i] <- coclustering[i, j] # Simetría
}
}
}
}
# Promediar sobre el número total de muestras
coclustering <- coclustering / n_samples
return(coclustering)
}
# Calcular probabilidades de coagrupamiento
coclustering_probs <- compute_coclustering(samples)
## Warning: Using `size` aesthetic for lines was deprecated in ggplot2 3.4.0.
## ℹ Please use `linewidth` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
La validación del modelo mediante la distribución predictiva posterior evalúa su capacidad para reproducir las características estructurales de la red real.
Se generan redes simuladas en cada iteración del muestreador Gibbs y se calculan estadísticas de prueba como densidad, transitividad, asortatividad, distancia geodésica promedio, grado promedio y desviación estándar del grado.
Estas se comparan con las distribuciones posteriores simuladas, reportando la media posterior y el intervalo de credibilidad al 95%, proporcionando evidencia de ajuste y adecuación del modelo propuesto.
# Calcular las estadísticas observadas en la red real
obs_density <- edge_density(g) # Densidad
obs_transitivity <- transitivity(g, type = "global") # Transitividad
obs_assortativity <- assortativity_degree(g, directed = FALSE) # Asortatividad
obs_avg_path <- mean_distance(g, directed = FALSE) # Distancia geodésica promedio
obs_avg_degree <- mean(degree(g)) # Grado promedio
obs_sd_degree <- sd(degree(g)) # Desviación estándar del grado
# Guardar en un vector
obs_stats <- c(
Density = obs_density,
Transitivity = obs_transitivity,
Assortativity = obs_assortativity,
AvgPathLength = obs_avg_path,
AvgDegree = obs_avg_degree,
SD_Degree = obs_sd_degree
)
# Inicializar listas para almacenar estadísticas simuladas
sim_density <- c()
sim_transitivity <- c()
sim_assortativity <- c()
sim_avg_path <- c()
sim_avg_degree <- c()
sim_sd_degree <- c()
# Iterar sobre las muestras del muestreador
for (iter in 1:length(samples$mu)) {
# Extraer parámetros para esta iteración
mu <- samples$mu[iter]
delta <- samples$delta[iter, ]
# Construir la matriz de probabilidad
P <- pnorm(mu + outer(delta, delta, "+"))
# Simular la red binaria
Y_sim <- matrix(rbinom(length(P), size = 1, prob = P), n, n)
Y_sim[lower.tri(Y_sim)] <- t(Y_sim)[lower.tri(Y_sim)] # Simetrizar
diag(Y_sim) <- 0 # Sin bucles
# Crear grafo a partir de la matriz simulada
g_sim <- graph_from_adjacency_matrix(Y_sim, mode = "undirected")
# Calcular estadísticas para la red simulada
sim_density <- c(sim_density, edge_density(g_sim))
sim_transitivity <- c(sim_transitivity, transitivity(g_sim, type = "global"))
sim_assortativity <- c(sim_assortativity, assortativity_degree(g_sim, directed = FALSE))
sim_avg_path <- c(sim_avg_path, mean_distance(g_sim, directed = FALSE))
sim_avg_degree <- c(sim_avg_degree, mean(degree(g_sim)))
sim_sd_degree <- c(sim_sd_degree, sd(degree(g_sim)))
}
## Warning: The dot-dot notation (`..density..`) was deprecated in ggplot2 3.4.0.
## ℹ Please use `after_stat(density)` instead.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
La validación cruzada implementada divide los elementos de la matriz de adyacencia \(Y\) (triangular superior) en dos subconjuntos: entrenamiento (80%) y validación (20%). Los valores faltantes en el conjunto de entrenamiento se tratan como datos latentes y se actualizan iterativamente mediante un muestreador de Gibbs, donde en cada iteración se muestrean según la distribución muestral para mantener la coherencia del modelo.
Las observaciones faltantes muestreadas se almacenan para calcular la probabilidad posterior de conexión promediando los valores generados a lo largo del algoritmo. Finalmente, estas probabilidades predictivas se comparan con los datos reales de validación, evaluando el rendimiento del modelo mediante la curva ROC y el AUC para medir su capacidad discriminativa.
# Librerías necesarias
suppressWarnings(
suppressMessages({
library(igraph)
library(ggraph)
library(tidygraph)
library(pROC)
library(ggplot2)
})
)
# Funciones auxiliares para muestreo Gibbs
gibbs_sampler_missing <- function(Y, n_iter, n_burn, n_thin, valid_indices, indices) {
n <- nrow(Y)
# Inicialización de parámetros
mu <- 0
delta <- rnorm(n, 0, 1)
sigma2 <- 1
tau2 <- 1
z <- matrix(0, n, n) # Variables auxiliares
# Almacenamiento solo para Y_missing
n_samples <- (n_iter - n_burn) / n_thin
Y_missing_samples <- matrix(0, nrow = n_samples, ncol = length(valid_indices))
cat("Iniciando muestreador de Gibbs...
")
for (t in 1:n_iter) {
# Paso 1: Muestrear los Y faltantes
for (idx in valid_indices) {
i <- indices[idx, 1]
j <- indices[idx, 2]
eta_ij <- mu + delta[i] + delta[j]
prob_ij <- pnorm(eta_ij)
Y[i, j] <- rbinom(1, 1, prob_ij)
Y[j, i] <- Y[i, j] # Simetría
}
# Paso 2: Actualizar parámetros condicionales completas
z <- sample_z(Y, mu, delta, z)
mu <- sample_mu(z, delta, sigma2)
delta <- sample_delta(z, mu, tau2, delta)
sigma2 <- sample_sigma2(mu)
tau2 <- sample_tau2(delta)
# Paso 3: Almacenar solo muestras de Y_missing según n_thin
if (t > n_burn && (t - n_burn) %% n_thin == 0) {
idx_sample <- (t - n_burn) / n_thin
Y_missing_samples[idx_sample, ] <- sapply(valid_indices, function(k) {
i <- indices[k, 1]
j <- indices[k, 2]
Y[i, j]
})
}
# Mostrar progreso
if (t %% (n_iter / 10) == 0) {
cat(sprintf("Progreso: %d%% completado\n", (t / n_iter) * 100))
}
}
cat("Muestreador completado.\n")
return(Y_missing_samples)
}
# Hiperparámetros
a_sigma <- 2
b_sigma <- 1
a_tau <- 2
b_tau <- 1
# Crear folds excluyentes y exhaustivos
set.seed(123)
n <- nrow(Y)
indices <- which(upper.tri(Y, diag = FALSE), arr.ind = TRUE)
n_edges <- nrow(indices)
L <- 5
folds <- split(sample(1:n_edges), rep(1:L, length.out = n_edges))
# Validación cruzada con 5 folds
auc_values <- numeric(L)
roc_list <- list()
for (l in 1:L) {
cat("Procesando fold", l, "de", L, "\n")
valid_indices <- folds[[l]]
Y_train <- Y
for (idx in valid_indices) {
i <- indices[idx, 1]
j <- indices[idx, 2]
Y_train[i, j] <- NA
Y_train[j, i] <- NA
}
# Muestreador Gibbs
n_iter <- 100000 + 10000
n_burn <- 10000
n_thin <- 5
Y_missing_samples <- gibbs_sampler_missing(Y_train, n_iter, n_burn, n_thin, valid_indices, indices)
# Evaluación de resultados
Y_valid_values <- Y[indices[valid_indices, ]]
prob_missing <- colMeans(Y_missing_samples)
roc_curve <- roc(Y_valid_values, prob_missing)
auc_values[l] <- auc(roc_curve)
roc_data <- data.frame(
FPR = 1 - roc_curve$specificities,
TPR = roc_curve$sensitivities
)
roc_list[[l]] <- roc_data
}
# Guardar resultados
save(auc_values, file = "auc_values.RData")
save(roc_list, file = "roc_list.RData")
# Promedio y desviación estándar del AUC
auc_mean <- mean(auc_values)
auc_sd <- sd(auc_values)
cat("AUC promedio:", round(auc_mean, 3), "\n")
## AUC promedio: 0.714
cat("AUC CV:", round(auc_sd/auc_mean, 3), "\n")
## AUC CV: 0.09
# Graficar todas las curvas ROC en un solo panel
ggplot() +
geom_line(data = do.call(rbind, lapply(1:L, function(l) cbind(roc_list[[l]], fold = l))),
aes(x = FPR, y = TPR, color = as.factor(fold)), size = 1, alpha = 0.6) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed", color = "gray") +
labs(title = "Curvas ROC para los 5 folds", x = "FPR", y = "TPR", color = "Fold") +
theme_minimal(base_size = 14)
El modelo para la probabilidad de conexión entre dos nodos se define
como:
\[
y_{i,j} \mid \theta_{i,j} \overset{\text{iid}}{\sim}
\textsf{Ber}(\theta_{i,j}), \quad \text{para } i < j,
\] donde la probabilidad \(\theta_{i,j}\) está dada por:
\[
\theta_{i,j} = \Phi(\mu + \delta_i + \delta_j +
\boldsymbol{x}_{i,j}^{\top} \boldsymbol{\beta}),
\] donde:
Las covariables \(\boldsymbol{x}_{i,j}\) deben ser escaladas para garantizar estabilidad numérica y evitar problemas de convergencia durante la estimación. El modelo puede incorporar covariables específicas para cada nodo (\(\boldsymbol{x}_i\) y \(\boldsymbol{x}_j\)) y modelarlas mediante combinaciones lineales, como: \[ \theta_{i,j} = \Phi\left( \mu + \delta_i + \delta_j + \boldsymbol{x}_i^{\top} \boldsymbol{\beta}_1 + \boldsymbol{x}_j^{\top} \boldsymbol{\beta}_2 \right). \] Esto permite diferenciar los efectos de las covariables de los nodos origen y destino en la probabilidad de conexión.