Código
library(tidyverse)
library(kableExtra)
library(DiagrammeR)
::theme_set(ggplot2::theme_light())
ggplot2<- \(x) 1 / (1 + exp(-x)) inv_logit
library(tidyverse)
library(kableExtra)
library(DiagrammeR)
::theme_set(ggplot2::theme_light())
ggplot2<- \(x) 1 / (1 + exp(-x)) inv_logit
En esta sección exlicaremos brevemente cómo funcionan paquetes como Stan para producir simulaciones de una posteriores complicadas en dimensión alta.
En primer lugar, recordemos que si queremos calcular la posterior de un modelo (generalmente para calcular después resúmenes que involucran integrales de esta posterior) tenemos los siguiente enfoques:
Stan utiliza 3, y hay variedad de algoritmos MCMC. Ya discutimos que 1, la aproximación analítica, es en general imposible (a menos fuera de ciertos modelos restringidos). La aproximación 2 excesivamente intensiva, al grado que sólo para modelos muy chicos y con pocos parámetros es posible utilizarla. Existen otros métodos también como aproximaciones cuadráticas que en ciertos casos funcionan, pero son limitados en su aplicación.
La idea de simulación de variables aleatorias es ahora fundamental en muchas áreas científicas, incluyendo la estadística, y los métodos que la utilizan se llaman métodos de Monte Carlo. Por ejemplo, considera el método bootstrap, pruebas de permutaciones, validación cruzada, y en general simulación para cálculo de resúmenes que son difíciles de calcular directamente (por ejemplo, la mediana de una distribución Gamma, ver Median approximations and bounds aquí).
Uno de los primeros algoritmos MCMC fue el de Metropolis-Hastings, que veremos primero en algunos ejemplos. Veremos también por qué ahora tenemos mejores opciones que MH para estimar posteriores de nuestros modelos.
Supongamos que queremos simular de una variable aleatoria \(X\) con distribución discreta sobre los valores \(1,2\ldots, k\) con probabilidades \(p(1),p(2),\ldots,p(k)\). Este problema puede resolverse fácilmente de varias maneras, pero utilizaremos un método de Monte Carlo tipo Metropolis. Supongamos que podemos simular de una variable aleatoria \(U\) que es uniforme en \(1,2,\ldots, k\) (con probabilidades iguales a 1/k).
Lo que podemos hacer es lo que sigue, para \(i=1,\ldots, M\):
Para cada \(i\),
El resultado es una sucesión de valores \(x_1,x_2,\ldots, x_M\). Es posible demostrar que la distribución de estas \(x_i\) converge a la distribución \(p(1),\ldots, p(k)\) si \(M\) es suficientemente grande.
Este método se llama Metropolis-Hastings. Es un método de Monte Carlo, y como podemos ver, se trata de una cadena de Markov, pues la distribución cada siguiente lugar \(x_{i+1}\), condicionada al valor actual \(x_i\) no depende de valores anteriores de las \(x\).
set.seed(45123)
# definimos estas p
<- 40
k <- exp(-(1:k - k/3)^2 / 10)
p <- p /sum(p)
p <- tibble(x = 1:k, p = p)
dist_obj # simulamos
<- 200000
M <- numeric(M)
x 1] <- 20
x[for(i in 1:M){
<- sample(1:k, 1)
u <- p[u] / p[x[i]]
q if(runif(1) < q){
+1] <- u
x[ielse {
} +1] <- x[i]
x[i
} }
En rojo mostramos las probabilidades objetivo que queremos estimar, y en negro las estimadas con nuestro método de arriba.
<- tibble(x = x) |>
resultados_sim mutate(n_sim = row_number())
<- resultados_sim |> count(x) |>
resumen_sim right_join(tibble(x = 1:k, p = p)) |>
mutate(n = ifelse(is.na(n), 0, n)) |>
mutate(p_aprox = n / sum(n))
Joining with `by = join_by(x)`
ggplot(dist_obj, aes(x = x)) +
geom_point(aes(y = p)) +
geom_point(data = resumen_sim,
aes(y = p_aprox), color = "red", size = 3, alpha = 0.5)
Como vemos, los valores de \(x_1,\ldots, x_M\) se distribuyen aproximadamente como la distribución \(p\) objetivo. Esta es una manera de simular valores de esta distribución discreta \(p\). Podemos ver cómo se ven las simulaciones sucesivas:
ggplot(resultados_sim |> filter(n_sim < 400), aes(x = n_sim, y = x)) +
geom_line() + scale_y_continuous(breaks = 1:20)
El defecto que tiene este algoritmo es que puede ser relativamente lento, pues vemos que hay periodos largos donde se “atora” en valores de probabilidad relativamente alta. La razón es que en muchos pasos, estamos proponiendo “saltos al vacío” a lugares de probabilidad muy baja, que rara vez se aceptan.
Podemos hacer más eficiente nuestro algoritmo si le permitimos explorar con mayor facilidad los posibles valores de \(x\). Esto se logra proponiendo saltos locales: si estamos en \(x_i\), entonces proponemos los valores \(x_i - 1\) y \(x_i + 1\) con la misma probabilidad 1/2 (excepto en los extremos donde sólo proponemos \(x_i,x_i+1\) o \(x_i-1,x_i\)).
Proponemos entonces la suguiente modificación del paso 1 de propuesta:
Para cada \(i\),
Esto lo escribimos como sigue:
#set.seed(4511)
# simulamos
<- numeric(M)
x 1] <- 20
x[for(i in 1:M){
<- sample(c(x[i] - 1, x[i] + 1), 1)
u if(u == k+1) u <- k
if(u == 0) u <- 1
<- p[u] / p[x[i]]
q if(runif(1) < q){
+1] <- u
x[ielse {
} +1] <- x[i]
x[i
} }
Obtenemos:
<- tibble(x = x) |>
resultados_sim_2 mutate(n_sim = row_number())
<- resultados_sim_2 |> count(x) |>
resumen_sim_2 mutate(p_aprox = n / sum(n))
ggplot(dist_obj, aes(x = x)) +
geom_point(aes(y = p)) +
geom_point(data = resumen_sim_2,
aes(y = p_aprox), color = "red", size = 3, alpha = 0.5)
Y podemos ver cómo evoluciona nuestra cadena de Markov:
ggplot(resultados_sim_2 |> filter(n_sim < 400), aes(x = n_sim, y = x)) +
geom_line() + scale_y_continuous(breaks = 1:20)
¿Cómo se comparan estos dos métodos? Podemos ver por ejemplo cómo se comparan las distribuciones aproximadas hasta cierto número de iteraciones con la verdadera distribución objetivo:
<- map_df(seq(200, 30000, by = 200), function(n_sims){
approx_sim <- resultados_sim |> filter(n_sim <= n_sims) |>
resumen_1 count(x) |>
mutate(p_aprox = n / sum(n)) |>
select(-n) |>
right_join(dist_obj, by = "x") |>
mutate(metodo = "MH-1") |>
mutate(n_sims = n_sims)
<- resultados_sim_2 |> filter(n_sim <= n_sims) |>
resumen_2 count(x) |>
mutate(p_aprox = n / sum(n)) |>
select(-n) |>
right_join(dist_obj, by = "x") |>
mutate(metodo = "MH-2") |>
mutate(n_sims = n_sims)
bind_rows(resumen_1, resumen_2) |>
mutate(p_aprox = ifelse(is.na(p_aprox), 0, p_aprox))
})
|>
approx_sim mutate(dif_abs = abs (p_aprox-p)) |>
group_by(metodo, n_sims) |>
summarise(dif_abs = sum(dif_abs)) |>
ggplot(aes(n_sims, dif_abs, color = metodo)) +
geom_line()
`summarise()` has grouped output by 'metodo'. You can override using the
`.groups` argument.
En este caso, considera qué es lo que sucede en cada uno de estos casos:
Este es el primer trade-off que existe en este algoritmo: tomar pasos grandes y balancear las probabilidades quizá rechazando muy frecuentemente (no es eficiente), o tomar pasos chicos y vagar más tiempo para visitar regiones de alta probabilidad, aunque con menos tasa de rechazo. Dependiendo de la distribución que queremos aproximar podemos inclinarnos más por una o por otra opción. En dimensiones altas generalmente ninguna combinación es muy buena.
En MCMC, buscamos un cadena de Markov que, en el largo plazo, visite cada posible valor del parámetro proporcionalmente a la probabildad posterior del parámetro. En el caso multivariado es la misma idea: cada combinación de parámetros debe ser visitada por la cadena en proporción a su probabilidad posterior.
¿Por qué funcionan estos algoritmos? Supongamos que en cada paso, se cumple que (balance detallado): \[{q(x|y)}p(y) = {q(y|x)}{p(x)}\] donde \(q(x|y)\) son las probabilidades de transición de nuestra cadena de Markov propuesta. Esta ecuación dice que la proporción de transiciones de \(y\) a \(x\) en relación a las transiciones de \(x\) a \(y\) es la misma que la proporción de probabilidad que hay entre \(y\) y \(x\) en la distribución objetivo.
Esta ecuación implica que si la probabilidad se distribuye como \(p(x)\), entonces al transicionar con \(q\) la probabilidad fluje de manera que se mantiene estática en \(p\), es decir \(p\) es una distribución estacionaria para la cadena de Markov producida por las transiciones.
Esto es fácil de demostrar pues \[\sum_{y} q(x|y)p(y) = \sum_{y} q(y|x)p(x) = p(x) \sum_{y} q(y|x) = p(x).\]
Bajo otros supuestos adicionales de ergodicidad (aperiodicidad y tiempos de recurrencia finitos, es decir, las transiciones mezclan bien los estados), entonces podemos simular la cadena de Markov por un tiempo suficientemente largo y con esto obtener una muestra de la distribución objetivo \(p\), es decir, la distribución estacionaria \(p(x)\) también es la distribución de largo plazo para cualquier cadena que simulemos.
¿Cómo podemos diseñar entonces las \(q(x|y)\) correspondientes?
Comenzamos considerando distribuciones propuesta \(q_0(x|y)\) que no necesariamente satisfacen la ecuación de balance, y supondremos como en los ejemplos de arriba (verifícalo) que nuestras transiciones tienen probabilidades simétricas \(q_0(y|x) = q_0(x|y)\). Entonces, cuando \(p(y)/p(x) > 1\), queremos transicionar de \(x\) a \(y\) con más frecuencia que de \(y\) a \(x\). Comenzando en \(x\), si la propuesta de \(q_0(y|x)\) es \(y\), podríamos poner entonces que el sistema transicione con probabilidad 1 a \(y\). Sin embargo, si empezamos en \(y\) y la propuesta es \(x\), ponemos que el sistema sólo transiciona de \(y\) a \(x\) con probabilidad \(p(x)/p(y)\).
De esta manera, obtenemos que bajo \(q(y|x)\), \(x\) transiciona a \(y\) con probabilidad $q_0(y|x) multiplicada por \(\min\{1, p(y)/p(x)\}\). Entonces, el cociente \(\frac{q(y|x)}{q(x|y)}\) (usando también la simetría de \(q_0(y|x)\), es igual a \(\frac{p(y)}{p(x)}\) si \(p(y)<p(x)\). Si \(p(x)>p(y)\), entonces siguiendo el mismo argumento, y por simetría de \(q_0(y|x)\), se cumple que el cociente de probabilidades es tambien igual a \(\frac{p(y)}{p(x)}\). Esto demuestra que se cumple el balance detallado.
De esta forma, balanceamos las transiciones de \(q_0(y|x)\) modificando con el proceso de adaptación y rechazo, para que la cadena de Markov resultante tenga la distribución estacionario \(p(x)\), que es la distribución objetivo.
Supongamos ahora que quisiéramos simular de una normal multivariada con media en c(2,3) y matriz de covarianza \(\Sigma\), que supondremos es tal que la desviación estándar de cada variable es 1 y la correlación es 0.8. La matriz \(\Sigma\) tiene 1 en la diagonal y 0.8 fuera de la diagonal.
La distribución objetivo \(p\) está dada entonces (módulo una constante de proporcionalidad):
<- function(m, Sigma){
construir_log_p <- solve(Sigma)
Sigma_inv function(z){
- 0.5 * (t(z-m) %*% Sigma_inv %*% (z-m))
}
}<- matrix(c(1, 0.8, 0.8, 1), nrow = 2)
Sigma <- c(2, 3)
m <- construir_log_p(m, Sigma) log_p
Nótese que como Metropolis hace cocientes de probabilidades, sólo es necesario conocer la densidad objetivo módulo una constante de proporcionalidad.
Una algoritmo de Metropolis podría ser el siguiente:
# simulamos
<- 50000
M <- function(M, z_inicial = c(0,0), log_p, delta_x, delta_y){
metropolis_mc <- matrix(nrow = M, ncol = 2)
z 1, ] <-z_inicial
z[colnames(z) <- c("x", "y")
<- 0
rechazo for(i in 1:(M-1)){
<- rnorm(1, z[i, 1], delta_x)
x_prop <- rnorm(1, z[i, 2], delta_y)
y_prop <- c(x_prop, y_prop)
z_prop <- exp(log_p(z_prop) - log_p(z[i, ]))
q if(runif(1) < q){
+ 1, ] <- z_prop
z[i else {
} <- rechazo + 1
rechazo + 1, ] <- z[i, ]
z[i
}
}print(rechazo / M)
<- as_tibble(z) |>
z_tbl mutate(n_sim = row_number())
z_tbl
}<- metropolis_mc(M, c(2.5, 3.5), log_p, 1.0, 1.0) z_tbl
[1] 0.59752
Vemos que tenemos una tasa alta de rechazos. ¿Por qué? Veamos cómo se ven las simulaciones hasta 500 iteraciones:
# estas las usamos para graficar
<- mvtnorm::rmvnorm(100000, mean = m, sigma = Sigma)
sims_normal colnames(sims_normal) <- c("x", "y")
<- as_tibble(sims_normal)
sims_normal <-list(stat_ellipse(data = sims_normal, aes(x, y),
elipses_normal level = c(0.9), type = "norm", colour = "salmon"),
stat_ellipse(data = sims_normal, aes(x, y),
level = c(0.5), type = "norm", colour = "salmon"),
stat_ellipse(data = sims_normal, aes(x, y),
level = c(0.2), type = "norm", colour = "salmon"))
<- map_df(seq(10, 500, 20), function(i){
graf_tbl |> filter(n_sim <= i) |> mutate(num_sims = i)
z_tbl
})ggplot(graf_tbl, aes(x, y)) +
+
elipses_normal geom_point(alpha = 0.1) +
facet_wrap(~num_sims) + theme_minimal()
library(gganimate)
<- z_tbl |> filter(n_sim < 50) |>
anim_mh_1 ggplot() +
geom_point(aes(x, y, group = n_sim), size = 3) +
transition_reveal(n_sim) +
+
elipses_normal labs(title = 'Iteración: {frame_along}') +
theme_minimal()
anim_save(animation = anim_mh_1, filename = "figuras/mh-1-normal.gif",
renderer = gifski_renderer())
Observaciones:
Podemos entonces proponer saltos más chicos, por ejemplo:
<- metropolis_mc(M, c(2.5, 3.5), log_p, 0.2, 0.2) z_tbl
[1] 0.15584
<- map_df(seq(10, 500, 20), function(i){
graf_tbl |> filter(n_sim <= i) |> mutate(num_sims = i)
z_tbl
})ggplot(graf_tbl, aes(x, y)) +
stat_ellipse(data = sims_normal, aes(x, y),
level = c( 0.9), type = "norm", colour = "salmon") +
stat_ellipse(data = sims_normal, aes(x, y),
level = c( 0.5), type = "norm", colour = "salmon") +
geom_point(alpha = 0.1) +
facet_wrap(~num_sims) + theme_minimal()
<- z_tbl |> filter(n_sim < 150) |>
anim_mh_2 ggplot() +
geom_point(aes(x, y, group = n_sim), size = 3) +
transition_reveal(n_sim) +
+
elipses_normal labs(title = 'Iteración: {frame_along}') +
theme_minimal()
anim_save(animation = anim_mh_2, filename = "figuras/mh-2-normal.gif",
renderer = gifski_renderer())
Observaciones:
En el algoritmo de Metropolis Hastings hay una tensión natural entre el tamaño de salto y la tasa de aceptación. Si el tamaño de los saltos es muy grande, la tasa de aceptación puede ser baja y esto producen ineficiencias. Si el tamaño de los saltos es muy chico, la tasa de aceptación es más alta, pero esto también es ineficiente pues la cadena puede explorar muy lentamente el espacio de parámetros.
Existen métodos que pueden superar este problema, como son muestreo de Gibbs y Monte Carlo Hamiltoniano. El primero no lo discutiremos, pues requiere poder simular fácilmente de cada parámetro dados los otros, y esto no siempre es posible. Veremos más el segundo, donde usaremos información del gradiente de la distribución objetivo para proponer exploración más eficiente.
Una manera de mejorar la exploración de Metropolis es utilizar una distribución de propuestas más apropiada. La intuición en el caso anterior es:
La idea de HMC es considerar el problema de muestrear de una distribución como un problema físico, donde introducimos aleatoridad solamente en cuanto a la “energía” de la pelota que va a explorar la posterior. Inicialmente impartimos un momento tomado al azar a la pelota, seguimos su trayectoria por un tiempo y el lugar a donde llega es nuestra nueva simulación. Esto permite que podamos dar saltos más grandes, sin “despeñarnos” en regiones de probabilidad muy baja y así evitar rechazos.
Adicionalmente, veremos que si definimos el sistema físico apropiadamente, es posible obtener ecuaciones de balance detallado, lo cual en teoría nos garantiza una manera de transicionar que resultará a largo plazo en una muestra de la distribución objetivo.
Primero veremos cuál es la formulación Hamiltoniana (muy simple) de un sistema físico que nos sirve para encontrar la trayectoria de partículas del sistema. Consideremos una sola partícula cuya posición está dada por \(q\), que suponemos en una sola dimensión. La partícula rueda en una superficie cuya altura describimos como \(V(q)\), y tiene en cada instante tiene momento \(p = m\dot{q}\).
El Hamiltoniano es la energía total de este sistema, en el espacio fase que describe el estado de cada partícula dadas su posición y momento \((p,q)\), y es la suma de energía cinética más energía potencial:
\(H(p,q) = T(p) + V(q)\)
donde \(V(q) = q^2/2\) está dada y \(T(p) = \frac{p^2}{2m}\), de modo que
\[H(p, q) = \frac{p^2}{2m} + V(q) = \frac{p^2}{2m} + \frac{q^2}{2}\]
Ahora consideremos las curvas de nivel de \(H\) (energía total constante), que en este caso se conservan a lo largo del movimiento de la partícula. Como sabemos por cálculo, estas curvas son perpendiculares al gradiente del Hamiltoniano, que es \((\partial{H}/\partial{p}, \partial{H}/\partial{q})\). El movimiento de las partículas, sin embargo, es a lo largo de las curvas de nivel, de manera que el flujo instantáneo debe estar dado por el gradiente de \(H\) rotado 90 grados, es decir, por \((\partial{H}/\partial{q}, -\partial{H}/\partial{p})\).
Entonces tenemos que el movimiento de la partícula debe cumplir las ecuaciones de Hamilton:
\[\frac{dp}{dt} = \frac{\partial{H}}{\partial{q}}, \frac{dq}{dt} = -\frac{\partial{H}}{\partial{p}}\] Simplificando y usando la definición de \(H\), obtenemos que \[\frac{dq}{dt} = \frac{p}{m}, \frac{dp}{dt} = -\frac{\partial{V}}{\partial{q}} = -q\] Ilustramos este campo vectorial en la siguiente gráfica, donde escogemos \(V(q) = q^2/2\), \(m=1\), y dibujamos algunas curvas de nivel del Hamiltoniano:
<- tibble(p = seq(-3, 3, length.out = 1000), q = seq(-3, 3, length.out = 1000)) |>
espacio_fase_1 expand(p, q) |>
mutate(dq = p, dp = -q) |>
mutate(H = p^2/2 + q^2/2)
<- tibble(p = seq(-3, 3, length.out = 10), q = seq(-3, 3, length.out = 10)) |>
espacio_fase expand(p, q) |>
mutate(dq = p, dp = -q)
|>
espacio_fase ggplot(aes(p, q)) +
geom_contour(data = espacio_fase_1, aes(x = p, y = q, z = H)) +
geom_segment(aes(xend = p + dp/5, yend = q + dq/5),
arrow = arrow(length = unit(0.1, "inches"))) +
theme_minimal() +
labs(subtitle = "Movimiento en espacio fase: 1 dimensión")
Ojo: este no es le movimiento de una partícula en dimensión 2: es el movimiento de la partícula en el espacio fase \((p,q)\), y la variable de posición \(q\) es de dimensión 1. Los ciclos de la gráfica muestran como conforme la partícula se mueve, energía potencial y cinética se intercambian a lo largo de su trayectoria en un “hilo”.
Consideremos una partícula en el espacio de parámetros \(\theta\). En esta formulación, si \(\theta\) son los parámetros de interés, consideramos la energía potencial del sistema como \(V(p) = -\log p(\theta)\), donde \(p(\theta)\) es la distribución objetivo.
Buscamos simular del sistema con ecuaciones de movimiento para \(\theta\). Como hicimos antes, vamos a “levantar” al espacio fase incluyendo el momento, que denotaremos como \(\rho\). La energía cinética, en el caso más simple, podemos definirla (en la práctica existen reescalamientos) como como \(T(\rho) =\frac{1}{2}\sum_i \rho_i^2\) (la energía cinética es proporcional al momento cuadrado, pues el momento es masa por velocidad).
El Hamiltoniano por definición \(H(\rho, \theta) = T(\rho) + V(\theta)\), y las ecuaciones de Hamilton son las mismas que arriba, que en este caso nos dan
\[\frac{d\theta}{dt} = \rho, \frac{d\rho}{dt} = \nabla(\log(p(\theta)).\]
Si resolvemos estas ecuaciones, podemos entonces simular del sistema como sigue:
Este método produce simulaciones de la distribución objetivo bajo condiciones de regularidad. Podemos demostrar por ejemplo, que se cumple el balance detallado.
Supongamos que las transiciones que da este sistema son \(q(y|x)\). Nótese que dado el momento simulado, tenemos el estado \((\rho, x)\), y la transición \(x\to\y\) es determinista, gobernada por las ecuaciones de Hamilton. Escribimos la transición como \[(\rho, x) \to (\rho^*, y).\] Nótese que \(\rho\) y \(x\) determinan la transición, de modo que
\[p(x)q(y|x) = p(x)p(\rho) = \exp(-H(\rho, x)) = \exp(-H(\rho^*, y))\] Que es cierto por conservación de la energía total y la transición sigue exactamente trayectorias del Hamiltoniano. Esta última cantidad, usando un argumento similar, es igual a
\[p(y)p(\rho^*) = p(y)p(-\rho^*) = p(y) q(x|y)\] La segunda igualdad se da porque \(p(\rho)\) es Gaussiana (simétrica). Y finalmente, la última igualdad se da porque si necesitamos momento \(\rho\) para llegar de \(x\) a \((\rho^*, y)\), entonces necesitamos \(-\rho^*\) (volteamos la velocidad ifnal) para llegar de \(y\) a \((\rho, x)\), pues el sistema físico es reversible.
Nótese que este argumento se rompe si por ejemplo si es imposible transicionar de un punto a otro (por ejemplo, cuando la distribución objetivo \(p\) tiene dos regiones separadas de probabilidad positiva).
Para aproximar soluciones de estas ecuaciones diferenciales utilizamos el integrador leapfrog, en el que hacemos actualizaciones alternadas de posición y momento con un tamaño de paso \(\epsilon\) chico. Hacemos este paso un número \(L\) de veces, para no quedar muy cerca del valor inicial.
En nuestro ejemplo, actualizaríamos por ejemplo el momento a la mitad del paso:
\[\rho_{t+\epsilon/2} = \rho_t - \frac{\epsilon}{2}\nabla(\log(p(\theta_t)))\] Seguido de una actualización de la posición:
\[\theta_{t+\epsilon} = \theta_t + \epsilon \rho_{t+\epsilon/2}\] y finalmente otra actualización del momento:
\[\rho_{t+\epsilon} = \rho_{t+\epsilon/2} - \frac{\epsilon}{2}\nabla(\log(p(\theta_{t+\epsilon})))\] Al final de este proceso, encontraremos que por errores numéricos, quizá el Hamiltoniano varió un poco. Si esto sucede, podemos hacer un paso de aceptación y rechazo como en Metropolis Hastings, donde la probabilidad de aceptar es
\[\min\left(1, \exp(H(\rho,\theta) - H(\rho^{*},\theta^{*}))\right)\] donde \(\rho^{*}\) y \(\theta^{*}\) son los valores de momento y posición nuevos y \(H(\rho,\theta)\) es el Hamiltoniano en el paso anterior.
Observaciones:
Primero calculamos el gradiente que requerimos. En este caso, podemos hacerlo analíticamente:
<- function(m, Sigma){
construir_log_p <- solve(Sigma)
Sigma_inv function(z){
- 0.5 * (t(z-m) %*% Sigma_inv %*% (z-m))
}
}<- matrix(c(1, 0.8, 0.8, 1), nrow = 2)
Sigma <- c(2, 3)
m <- construir_log_p(m, Sigma)
log_p # en diferenciación automática, el siguiente constructor
# puede tomar como argumento log_p, pero aquí la escribimos
# explícitamente
<- function(m, Sigma){
construir_grad_log_p <- solve(Sigma)
Sigma_inv function(theta){
- Sigma_inv %*% (theta-m)
}
}<- construir_grad_log_p(m, Sigma)
grad_log_p <- function(m, Sigma){
construir_H <- solve(Sigma)
Sigma_inv function(theta, rho){
- log_p(theta) + 0.5 * sum(rho^2)
}
}<- construir_H(m, Sigma)
H log_p(c(1,3))
[,1]
[1,] -1.388889
grad_log_p(c(1,3))
[,1]
[1,] 2.777778
[2,] -2.222222
Ahora, implementamos el algoritmo de HMC. Primero, definimos una función
<- function(n, theta_0 = c(0,0), log_p, grad_log_p, epsilon, L){
hamilton_mc <- length(theta_0)
p <- matrix(0, n, p)
theta 1, ] <- theta_0
theta[<- matrix(0, n, p)
rho <- matrix(0, n*L, p)
theta_completa 1, 0] <- theta_0
theta_completa[<- matrix(0, n*L, p)
rho_completa <- 2
indice_completa <- 0
rechazo for(i in 2:n){
<- rnorm(p)
prop_rho -1, ] <- prop_rho
rho[i<- theta[i-1, ]
prop_theta for(t in 1:L){
<- prop_rho + 0.5 * epsilon * grad_log_p(prop_theta)
prop_rho <- prop_theta + epsilon * prop_rho
prop_theta <- prop_rho + 0.5 * epsilon * grad_log_p(prop_theta)
prop_rho <- prop_theta
theta_completa[indice_completa,] <- prop_rho
rho_completa[indice_completa,] <- indice_completa + 1
indice_completa
}
<- min(1, exp(H(theta[i-1, ], rho[i-1, ]) -
q H(prop_theta, prop_rho)))
if(runif(1) < q){
<- prop_theta
theta[i, ] else {
} <- rechazo + 1
rechazo <- theta[i-1, ]
theta[i, ] <- rho[i-1, ]
rho[i, ] - 1,] <- theta[i-1, ]
theta_completa[indice_completa - 1,] <- rho[i-1, ]
rho_completa[indice_completa
}
}print(rechazo / n)
list(sims = tibble(x = theta[,1], y = theta[,2]),
trayectorias = tibble(x = theta_completa[,1], y = theta_completa[,2]) |>
mutate(iteracion = rep(1:n, each = L), paso = rep(1:L, times = n)))
}
Revisamos que la muestra aproxima apropiadamente nuestra distribución
set.seed(10)
<- hamilton_mc(1000, c(0,0), log_p, grad_log_p, 0.2, 12) hmc_salida
[1] 0.016
ggplot(hmc_salida$sims, aes(x = x, y = y)) + geom_point() +
stat_ellipse(data = sims_normal, aes(x, y),
level = c(0.9), type = "norm", colour = "salmon") +
stat_ellipse(data = sims_normal, aes(x, y),
level = c( 0.5), type = "norm", colour = "salmon") +
stat_ellipse(level = c( 0.9), colour = "green", type = "norm") +
stat_ellipse(level = c( 0.5), colour = "green", type = "norm")
<- hmc_salida$trayectorias
tray_tbl head(tray_tbl)
# A tibble: 6 × 4
x y iteracion paso
<dbl> <dbl> <int> <int>
1 0 0 1 1
2 -0.0185 0.0409 1 2
3 -0.0757 0.231 1 3
4 -0.148 0.545 1 4
5 -0.201 0.940 1 5
6 -0.192 1.37 1 6
library(gganimate)
<- ggplot(tray_tbl |> mutate(iter = 4*as.numeric(paso == 1),
anim_hmc s = as.numeric(paso == 2)) |>
filter(iteracion < 30) |>
mutate(tiempo = row_number()) |>
mutate(tiempo = tiempo + cumsum(50 * s)),
aes(x = x, y = y)) +
geom_point(aes(colour = iter, alpha = iter, size = iter, group = tiempo)) +
geom_path(colour = "gray", alpha = 0.5) +
transition_reveal(tiempo) +
+
elipses_normal theme(legend.position = "none")
anim_save(animation = anim_hmc, filename = "figuras/hmc-normal.gif",
renderer = gifski_renderer())
Observaciones:
Observamos que hasta ahora no hemos aplicado estos algoritmos para simular de la posterior de un modelo: hemos tomado distribuciones fijas y usamos MCMC para simular de ellas. El proceso para una posterior es el mismo, pero usualmente más complicado pues generalmente involucra mucho más parámetros y una posterior que no tiene una forma analítica conocida.
Sin embargo, la aplicación para una posterior es la misma: siempre podemos calcular el logaritmo de la posterior (al menos hasta una constante de proporcionalidad), y siempre podemos usar diferenciación automática para calcular el gradiente de la log posterior. Podemos aplicar entonces HMC o Metropolis.
Finalmente, haremos una comparación entre el desempeño de HMC y Metropolis en el caso de la distribución normal. Utilizaremos otra normal bivariada con más correlación.
set.seed(737)
<- matrix(c(1, -0.9, -0.9, 1), nrow = 2)
Sigma <- c(1, 1)
m <- construir_log_p(m, Sigma)
log_p <- construir_grad_log_p(m, Sigma)
grad_log_p system.time(hmc_1 <- hamilton_mc(1000, c(1,2), log_p, grad_log_p, 0.2, 12))
[1] 0.042
user system elapsed
0.065 0.000 0.065
system.time(metropolis_1 <- metropolis_mc(1000, c(1,2), log_p, 0.2, 0.2))
[1] 0.204
user system elapsed
0.018 0.000 0.017
system.time(metropolis_2 <- metropolis_mc(1000, c(1,2), log_p, 1, 1))
[1] 0.692
user system elapsed
0.018 0.000 0.018
<- hmc_1$sims |> mutate(n_sim = row_number()) |>
sims_hmc mutate(algoritmo = "hmc")
<- metropolis_1 |>
sims_metropolis_1 mutate(algoritmo = "metropolis (corto)")
<- metropolis_2 |>
sims_metropolis_2 mutate(algoritmo = "metropolis (largo)")
<- bind_rows(sims_hmc, sims_metropolis_1, sims_metropolis_2)
sims_comp <- ggplot(sims_comp |> filter(n_sim < 200)) +
anim_comp transition_reveal(n_sim) +
theme(legend.position = "none") +
geom_path(aes(x, y), colour = "gray", alpha = 0.2) +
geom_point(aes(x, y, group = n_sim)) +
facet_wrap(~algoritmo)
anim_save(animation = anim_comp, filename = "figuras/comparacion-normal.gif", height = 250, width = 500,
units = "px",
renderer = gifski_renderer())
En Stan se incluyen tres componentes adicionales importantes para estimar posteriores de manera eficiente:
Aunque casi nunca es posible demostrar rigurosamente que las simulaciones de un algoritmo MCMC dan buena aproximación de la distribución posterior de interés, especialmente con HMC y NUTS, tenemos muchos diagnósticos que fallan cuando existen problemas serios.
En primer lugar, será útil correr distintas cadenas con valores iniciales aleatorios diferentes, analizamos cada una y las comparamos entre sí. Recordamos que cada una de estas cadenas tiene como distribución estacionaria límite la distribución posterior. Diagnósticos que indican que las cadenas se comportan de manera muy distinta, explorando distintas regiones del espacio de parámetros, o que no han convergido porque exploran lentamente el espacio de parámetros, son señales de problemas.
Los diagnósticos más comunes son:
Veremos el ejemplo de calificación de vinos de distintos países de McElreath (2020), sus diagnósticos, y aprovecharemos para introducir variables no observadas o latentes para enriquecer nuestras herramientas de modelación.
Nuestra pregunta general es si el país de origen de los vinos influye en su calidad. Los datos que tenemos son calificaciones de vinos de distintos países por distintos jueces. La calidad del vino no la observamos directamente, sino que es causa de las calificaciones que recibe. Para construir nuestro diagrama, las consideraciones básicas son:
library(DiagrammeR)
grViz("
digraph {
graph [ranksep = 0.2, rankdir = LR]
node [shape=plaintext]
Origen
Score
Origen_Juez
node [shape = circle]
Q
J
edge [minlen = 3]
Origen -> Q
Origen -> Score
Q -> Score
J -> Score
Origen_Juez -> J
}
")
Y vemos, por nuestro análisis del DAG, que podemos identificar el efecto de Origen sobre Calidad sin necesidad de estratificar por ninguna variable (no hay puertas traseras). Sin embaergo, podemos estratificar por Juez para obtener más precisión (ver sección anterior de buenos y malos controles).
Comenzamos con un modelo simple, y lo iremos construyendo para obtener la mejor estimación posible de la influencia del país de origen en la calidad del vino. Nuestro primer modelo consideramos que la calificación de cada vino depende de su calidad, y modelamos con una normal:
\[S_i \sim \text{Normal}(\mu_i, \sigma)\] donde \[\mu_i = Q_{vino(i)}\]. Nuestra medida de calidad tiene escala arbitaria. Como usaremos la calificación estandarizada, podemos poner \[Q_j \sim \text{Normal}(0, 1).\] finalmente, ponemos una inicial para \(\sigma\), por ejemplo \(\sigma \sim \text{Exponential}(1)\) (puedes experimentar con una normal truncada también)
library(cmdstanr)
This is cmdstanr version 0.7.1
- CmdStanR documentation and vignettes: mc-stan.org/cmdstanr
- CmdStan path: /home/runner/.cmdstan/cmdstan-2.34.0
- CmdStan version: 2.34.0
A newer version of CmdStan is available. See ?install_cmdstan() to install it.
To disable this check set option or environment variable CMDSTANR_NO_VER_CHECK=TRUE.
<- cmdstan_model("./src/vinos-1.stan")
mod_vinos_1 print(mod_vinos_1)
data {
int<lower=0> N; //número de calificaciones
int<lower=0> n_vinos; //número de vinos
int<lower=0> n_jueces; //número de jueces
vector[N] S;
array[N] int juez;
array[N] int vino;
}
parameters {
vector[n_vinos] Q;
real <lower=0> sigma;
}
transformed parameters {
vector[N] media_score;
// determinístico dado parámetros
for (i in 1:N){
media_score[i] = Q[vino[i]];
}
}
model {
// partes no determinísticas
S ~ normal(media_score, sigma);
Q ~ std_normal();
sigma ~ exponential(1);
}
# Wines 2022 de Statistical Rethinking
<- read_csv("../datos/wines_2012.csv") wines_2012
Rows: 180 Columns: 6
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (3): judge, flight, wine
dbl (3): score, wine.amer, judge.amer
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
glimpse(wines_2012)
Rows: 180
Columns: 6
$ judge <chr> "Jean-M Cardebat", "Jean-M Cardebat", "Jean-M Cardebat", "J…
$ flight <chr> "white", "white", "white", "white", "white", "white", "whit…
$ wine <chr> "A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1", "I1", "J1",…
$ score <dbl> 10.0, 13.0, 14.0, 15.0, 8.0, 13.0, 15.0, 11.0, 9.0, 12.0, 1…
$ wine.amer <dbl> 1, 1, 0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 0, 0, 1, 1, 1, 0, 1, 0,…
$ judge.amer <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,…
<- wines_2012 |>
wines_2012 mutate(juez_num = as.numeric(factor(judge)),
vino_num = as.numeric(factor(wine))) |>
mutate(score_est = (score - mean(score))/sd(score))
<- length(unique(wines_2012$juez_num))
n_jueces <- length(unique(wines_2012$vino_num))
n_vinos c("num_vinos" = n_jueces, "num_jueces" = n_vinos, "num_datos" = nrow(wines_2012))
num_vinos num_jueces num_datos
9 20 180
<- list(
datos_lst N = nrow(wines_2012),
n_vinos = n_vinos,
n_jueces = n_jueces,
S = wines_2012$score_est,
vino = wines_2012$vino_num,
juez = wines_2012$juez_num
)<- mod_vinos_1$sample(
ajuste_vinos_1 data = datos_lst,
chains = 4,
parallel_chains = 4,
iter_warmup = 1000,
iter_sampling = 2000,
refresh = 1000,
step_size = 0.1,
)
Running MCMC with 4 parallel chains...
Chain 1 Iteration: 1 / 3000 [ 0%] (Warmup)
Chain 1 Iteration: 1000 / 3000 [ 33%] (Warmup)
Chain 1 Iteration: 1001 / 3000 [ 33%] (Sampling)
Chain 1 Iteration: 2000 / 3000 [ 66%] (Sampling)
Chain 2 Iteration: 1 / 3000 [ 0%] (Warmup)
Chain 2 Iteration: 1000 / 3000 [ 33%] (Warmup)
Chain 2 Iteration: 1001 / 3000 [ 33%] (Sampling)
Chain 2 Iteration: 2000 / 3000 [ 66%] (Sampling)
Chain 3 Iteration: 1 / 3000 [ 0%] (Warmup)
Chain 3 Iteration: 1000 / 3000 [ 33%] (Warmup)
Chain 3 Iteration: 1001 / 3000 [ 33%] (Sampling)
Chain 3 Iteration: 2000 / 3000 [ 66%] (Sampling)
Chain 4 Iteration: 1 / 3000 [ 0%] (Warmup)
Chain 4 Iteration: 1000 / 3000 [ 33%] (Warmup)
Chain 4 Iteration: 1001 / 3000 [ 33%] (Sampling)
Chain 1 Iteration: 3000 / 3000 [100%] (Sampling)
Chain 2 Iteration: 3000 / 3000 [100%] (Sampling)
Chain 3 Iteration: 3000 / 3000 [100%] (Sampling)
Chain 4 Iteration: 2000 / 3000 [ 66%] (Sampling)
Chain 1 finished in 0.3 seconds.
Chain 2 finished in 0.4 seconds.
Chain 3 finished in 0.4 seconds.
Chain 4 Iteration: 3000 / 3000 [100%] (Sampling)
Chain 4 finished in 0.4 seconds.
All 4 chains finished successfully.
Mean chain execution time: 0.4 seconds.
Total execution time: 0.7 seconds.
Vemos que hay variabilidad en los vinos:
$summary(c("Q", "sigma")) |>
ajuste_vinos_1select(variable, mean, sd, q5, q95, rhat, ess_bulk, ess_tail) |>
filter(variable != "lp__") |>
mutate(across(c(mean, sd, q5, q95, rhat, ess_bulk, ess_tail), ~round(., 3))) |>
kable()
variable | mean | sd | q5 | q95 | rhat | ess_bulk | ess_tail |
---|---|---|---|---|---|---|---|
Q[1] | 0.137 | 0.317 | -0.396 | 0.672 | 1.001 | 20694.09 | 5373.128 |
Q[2] | 0.103 | 0.321 | -0.425 | 0.629 | 1.001 | 20994.93 | 5362.172 |
Q[3] | 0.271 | 0.318 | -0.247 | 0.798 | 1.000 | 20676.69 | 5913.964 |
Q[4] | 0.555 | 0.313 | 0.043 | 1.069 | 1.000 | 18730.09 | 5575.563 |
Q[5] | -0.120 | 0.315 | -0.636 | 0.390 | 1.001 | 19760.24 | 5848.199 |
Q[6] | -0.370 | 0.312 | -0.880 | 0.145 | 1.000 | 16654.60 | 5972.842 |
Q[7] | 0.286 | 0.314 | -0.231 | 0.795 | 1.001 | 17442.52 | 6084.769 |
Q[8] | 0.271 | 0.319 | -0.256 | 0.805 | 1.000 | 18199.26 | 5596.789 |
Q[9] | 0.080 | 0.314 | -0.438 | 0.584 | 1.000 | 19534.73 | 5983.451 |
Q[10] | 0.118 | 0.308 | -0.386 | 0.626 | 1.000 | 21143.80 | 5814.923 |
Q[11] | -0.010 | 0.321 | -0.546 | 0.510 | 1.000 | 20940.00 | 5531.936 |
Q[12] | -0.029 | 0.322 | -0.560 | 0.500 | 1.001 | 18276.46 | 5849.228 |
Q[13] | -0.105 | 0.312 | -0.609 | 0.414 | 1.003 | 20575.88 | 5957.570 |
Q[14] | 0.005 | 0.313 | -0.505 | 0.525 | 1.001 | 17541.60 | 5723.524 |
Q[15] | -0.215 | 0.318 | -0.740 | 0.312 | 1.001 | 17150.01 | 5412.980 |
Q[16] | -0.201 | 0.315 | -0.722 | 0.315 | 1.001 | 17596.06 | 5120.461 |
Q[17] | -0.142 | 0.316 | -0.665 | 0.379 | 1.000 | 20718.38 | 6172.910 |
Q[18] | -0.860 | 0.314 | -1.384 | -0.343 | 1.000 | 18745.31 | 6126.091 |
Q[19] | -0.161 | 0.311 | -0.666 | 0.343 | 1.000 | 18166.25 | 6352.717 |
Q[20] | 0.380 | 0.313 | -0.127 | 0.896 | 1.000 | 21072.45 | 5060.363 |
sigma | 0.997 | 0.054 | 0.912 | 1.092 | 1.000 | 13245.07 | 6385.483 |
Para hacer diagnósticos, podemos comenzar con las trazas de una cadena para todas las estimaciones de calidad de vino. Cada cadena se inicia con distintos valores aleatorios, pero cumplen en teoría que su distribución de equilibrio es la posterior de interés pues sus transiciones usan el mismo mecanismo.
library(bayesplot)
mcmc_trace(ajuste_vinos_1$draws("Q", format = "df") |> filter(.chain == 1))
La traza de una cadena es la gráfica de las simulaciones de cada parámetro. Generalmente buscamos que: no tenga tendencia, que no se quede “atorada” en algunos valores, y que no muestre oscilaciones de baja frecuencia (la cadena “vaga” por los valores que explora).
Si incluímos todas las cadenas, nos fijemos en que todas ellas exploren regiones similares del espacio de parámetros:
color_scheme_set("viridis")
mcmc_trace(ajuste_vinos_1$draws("Q", format = "df"))
Lo que no queremos ver es lo siguiente, por ejemplo:
<- mod_vinos_1$sample(
ajuste_vinos_malo data = datos_lst,
chains = 4,
parallel_chains = 4,
iter_warmup = 5,
iter_sampling = 100,
refresh = 1000,
step_size =1 ,
seed = 123
)
Running MCMC with 4 parallel chains...
Chain 1 WARNING: No variance estimation is
Chain 1 performed for num_warmup < 20
Chain 1 Iteration: 1 / 105 [ 0%] (Warmup)
Chain 1 Iteration: 6 / 105 [ 5%] (Sampling)
Chain 1 Iteration: 105 / 105 [100%] (Sampling)
Chain 2 WARNING: No variance estimation is
Chain 2 performed for num_warmup < 20
Chain 2 Iteration: 1 / 105 [ 0%] (Warmup)
Chain 2 Iteration: 6 / 105 [ 5%] (Sampling)
Chain 2 Iteration: 105 / 105 [100%] (Sampling)
Chain 3 WARNING: No variance estimation is
Chain 3 performed for num_warmup < 20
Chain 3 Iteration: 1 / 105 [ 0%] (Warmup)
Chain 3 Iteration: 6 / 105 [ 5%] (Sampling)
Chain 3 Iteration: 105 / 105 [100%] (Sampling)
Chain 3 Informational Message: The current Metropolis proposal is about to be rejected because of the following issue:
Chain 3 Exception: normal_lpdf: Scale parameter is 0, but must be positive! (in '/tmp/Rtmpor1cYv/model-282661f34b78.stan', line 25, column 2 to column 33)
Chain 3 If this warning occurs sporadically, such as for highly constrained variable types like covariance matrices, then the sampler is fine,
Chain 3 but if this warning occurs often then your model may be either severely ill-conditioned or misspecified.
Chain 3
Chain 4 WARNING: No variance estimation is
Chain 4 performed for num_warmup < 20
Chain 4 Iteration: 1 / 105 [ 0%] (Warmup)
Chain 4 Iteration: 6 / 105 [ 5%] (Sampling)
Chain 4 Iteration: 105 / 105 [100%] (Sampling)
Chain 1 finished in 0.0 seconds.
Chain 2 finished in 0.0 seconds.
Chain 3 finished in 0.0 seconds.
Chain 4 finished in 0.0 seconds.
All 4 chains finished successfully.
Mean chain execution time: 0.0 seconds.
Total execution time: 0.2 seconds.
Warning: 324 of 400 (81.0%) transitions ended with a divergence.
See https://mc-stan.org/misc/warnings for details.
Warning: 2 of 4 chains had an E-BFMI less than 0.3.
See https://mc-stan.org/misc/warnings for details.
color_scheme_set("viridisA")
mcmc_trace(ajuste_vinos_malo$draws("Q", format = "df"))
Hay varios problemas graves:
El diagnóstico de traza consiste en graficar las cadenas de los parámetros en el orden de la iteración. Buscamos ver que:
Cuando falla alguna de estas, en el mejor de los casos las cadenas son ineficientes (veremos que requerimos un número mucho mayor de iteraciones), y en el peor de los casos dan resultados sesgados que no son confiables.
Cuando nuestro método de simulación converge a la distribución posterior, esperamos que las cadenas, durante todo su proceso, exploran la misma región del espacio de parámetros.
Podemos entonces considerar, para cada parámetro:
Esperamos que la variación entre cadenas es chica, y la variación dentro de cada cadena es similar para todas las cadenas. Calculamos entonces un cociente de varianzas: la varianza total sobre todas las simulaciones de todas las cadenas, y el promedio de varianzas de las cadenas. Si las cadenas están explorando regiones similares, esperamos que este cociente de varianzas sea cercano a 1.
Escribiremos ahora esta idea para entender cómo se calculan estas cantidades. Supongamos que cada cadena se denota por \(\theta_m\), para \(M\) cadenas, y las iteraciones de cada cadena son \(\theta_m^{(i)}\) para \(i=1,\ldots, N\) iteraciones. Definimos (ver el manual de Stan o Brooks et al. (2011) por ejemplo) primero la varianza entre cadenas, que es (ojo: usaremos definiciones aproximadas para entender más fácilmente):
\[b=\frac{1}{M-1}\sum_{m=1}^M (\bar{\theta}_m - \bar{\theta})^2\] donde \(\bar{\theta}_m\) es el promedio de las iteraciones de la cadena \(m\), y \(\bar{\theta}\) es el promedio del las \(\bar{\theta}_m\).
Definimos también la varianza dentro de las cadenas, que es el promedio de la varianza de cada cadena:
\[w=\frac{1}{M}\sum_{m=1}^M \frac{1}{N}\sum_{i=1}^N (\theta_m^{(i)} - \bar{\theta}_m)^2\] Finalmente, la \(R\)-hat, o estadística de potencial de reducción de escala, es (para \(N\) grande),
\[\hat{R} = \sqrt{\frac{b+w}{w}}\]
Buscamos entonces que este valor sea cercano a 1. Si es mayor a 1.05, es señal de posibles problemas de convergencia (pocas iteraciones u otras fallas en la convergencia). Si es menor que 1.01, generalmente decimos que “pasamos” esta prueba. Esto no es garantía de que la convergencia se ha alcanzado: la primera razón es que este diagnóstico, por ejemplo, sólo considera media y varianza, de forma que en principio podríamos pasar esta prueba aún cuando las cadenas tengan comportamiento distinto en otras estadísticas de orden más alto (por ejemplo, una cadena que oscila poco y de vez en cuando salta a un atípico vs otra que tiene variación moderada pueden ser similares en medias y varianzas).
En Stan, adicionalmente, se divide cada cadena en dos mitades, y el análisis se hace sobre \(2M\) medias cadenas. Esto ayuda a detectar por ejemplo problemas donde una cadena sube y luego baja, por ejemplo, de modo que puede tener el mismo promedio que otras que exploran correctamente.
Nota: Estas fórmulas pretenden explicar de manera simple el concepto de \(R\)-hat, y son correctas para \(N\) grande, lo cual casi siempre es el caso (al menos \(N\geq 100\)). Puedes consultar una definición más estándar con correcciones por grados de libertad en el manual de Stan o cualquier libro de MCMC.
El diagnóstico de R-hat compara la varianza dentro de las cadenas y de cadena a cadena. Cuando este valor es relativamente grande (por ejemplo mayor a 1.05), es señal de que las cadenas no han explorado apropiadamente el espacio de parámetros (o decimos que no están “mezclando”). En general, buscamos que este valor sea menor a 1.02.
Se llama también potencial de reducción a escala porque busca indicar cuánto se podría reducir la varianza de la distribución actual si dejáramos correr las cadenas por más iteraciones (pues a largo plazo no debe haber varianza entre cadenas).
En nuestro ejemplo apropiado, observamos valores muy cercanos a 1 para todos los parámetros:
$summary(c("Q", "sigma")) |>
ajuste_vinos_1select(variable, mean, sd, q5, q95, rhat, ess_bulk, ess_tail) |>
mutate(across(c(mean, sd, q5, q95, rhat, ess_bulk, ess_tail), ~round(., 3))) |>
filter(variable != "lp__") |> kable()
variable | mean | sd | q5 | q95 | rhat | ess_bulk | ess_tail |
---|---|---|---|---|---|---|---|
Q[1] | 0.137 | 0.317 | -0.396 | 0.672 | 1.001 | 20694.09 | 5373.128 |
Q[2] | 0.103 | 0.321 | -0.425 | 0.629 | 1.001 | 20994.93 | 5362.172 |
Q[3] | 0.271 | 0.318 | -0.247 | 0.798 | 1.000 | 20676.69 | 5913.964 |
Q[4] | 0.555 | 0.313 | 0.043 | 1.069 | 1.000 | 18730.09 | 5575.563 |
Q[5] | -0.120 | 0.315 | -0.636 | 0.390 | 1.001 | 19760.24 | 5848.199 |
Q[6] | -0.370 | 0.312 | -0.880 | 0.145 | 1.000 | 16654.60 | 5972.842 |
Q[7] | 0.286 | 0.314 | -0.231 | 0.795 | 1.001 | 17442.52 | 6084.769 |
Q[8] | 0.271 | 0.319 | -0.256 | 0.805 | 1.000 | 18199.26 | 5596.789 |
Q[9] | 0.080 | 0.314 | -0.438 | 0.584 | 1.000 | 19534.73 | 5983.451 |
Q[10] | 0.118 | 0.308 | -0.386 | 0.626 | 1.000 | 21143.80 | 5814.923 |
Q[11] | -0.010 | 0.321 | -0.546 | 0.510 | 1.000 | 20940.00 | 5531.936 |
Q[12] | -0.029 | 0.322 | -0.560 | 0.500 | 1.001 | 18276.46 | 5849.228 |
Q[13] | -0.105 | 0.312 | -0.609 | 0.414 | 1.003 | 20575.88 | 5957.570 |
Q[14] | 0.005 | 0.313 | -0.505 | 0.525 | 1.001 | 17541.60 | 5723.524 |
Q[15] | -0.215 | 0.318 | -0.740 | 0.312 | 1.001 | 17150.01 | 5412.980 |
Q[16] | -0.201 | 0.315 | -0.722 | 0.315 | 1.001 | 17596.06 | 5120.461 |
Q[17] | -0.142 | 0.316 | -0.665 | 0.379 | 1.000 | 20718.38 | 6172.910 |
Q[18] | -0.860 | 0.314 | -1.384 | -0.343 | 1.000 | 18745.31 | 6126.091 |
Q[19] | -0.161 | 0.311 | -0.666 | 0.343 | 1.000 | 18166.25 | 6352.717 |
Q[20] | 0.380 | 0.313 | -0.127 | 0.896 | 1.000 | 21072.45 | 5060.363 |
sigma | 0.997 | 0.054 | 0.912 | 1.092 | 1.000 | 13245.07 | 6385.483 |
En nuestro ejemplo “malo”, obtenemos valores no aceptabels de R-hat para varios parámetros.
$summary(c("Q", "sigma")) |>
ajuste_vinos_maloselect(variable, mean, sd, q5, q95, rhat, ess_bulk, ess_tail) |>
mutate(across(c(mean, sd, q5, q95, rhat, ess_bulk, ess_tail), ~round(., 3))) |>
filter(variable != "lp__") |> kable()
variable | mean | sd | q5 | q95 | rhat | ess_bulk | ess_tail |
---|---|---|---|---|---|---|---|
Q[1] | -0.167 | 0.345 | -0.538 | 0.427 | 2.367 | 5.335 | 4.502 |
Q[2] | -0.012 | 0.289 | -0.306 | 0.556 | 1.633 | 7.010 | 4.670 |
Q[3] | 0.226 | 0.246 | -0.188 | 0.596 | 1.379 | 9.802 | 14.937 |
Q[4] | 0.651 | 0.385 | 0.032 | 1.088 | 1.596 | 7.207 | 12.047 |
Q[5] | -0.173 | 0.253 | -0.547 | 0.191 | 1.441 | 8.933 | 58.601 |
Q[6] | -0.387 | 0.626 | -1.277 | 0.704 | 2.367 | 5.372 | 4.348 |
Q[7] | 0.084 | 0.364 | -0.291 | 0.812 | 1.340 | 10.165 | 27.683 |
Q[8] | 0.272 | 0.254 | -0.180 | 0.558 | 1.372 | 9.580 | 18.466 |
Q[9] | -0.117 | 0.188 | -0.377 | 0.247 | 1.689 | 13.208 | 30.165 |
Q[10] | 0.004 | 0.347 | -0.460 | 0.496 | 1.557 | 7.600 | 25.190 |
Q[11] | 0.068 | 0.618 | -1.137 | 0.840 | 2.724 | 5.072 | 7.003 |
Q[12] | 0.119 | 0.270 | -0.245 | 0.625 | 1.237 | 14.834 | 42.161 |
Q[13] | 0.089 | 0.491 | -0.489 | 1.235 | 1.868 | 6.388 | 28.327 |
Q[14] | 0.172 | 0.736 | -0.780 | 1.299 | 2.064 | 5.741 | 18.739 |
Q[15] | 0.082 | 0.407 | -0.491 | 0.605 | 2.109 | 5.718 | 17.270 |
Q[16] | -0.055 | 0.339 | -0.548 | 0.394 | 1.711 | 6.843 | 25.826 |
Q[17] | 0.104 | 0.310 | -0.444 | 0.502 | 1.811 | 6.325 | 20.519 |
Q[18] | -0.562 | 0.429 | -1.076 | 0.479 | 1.959 | 6.022 | 28.327 |
Q[19] | -0.162 | 0.281 | -0.600 | 0.248 | 1.561 | 7.410 | 41.928 |
Q[20] | 0.122 | 0.737 | -1.036 | 0.922 | 1.949 | 6.030 | 4.348 |
sigma | 0.958 | 0.239 | 0.751 | 1.092 | 1.571 | 7.369 | 8.522 |
Nota: si algunos parámetros tienen valores R-hat cercanos a 1 pero otros no, en general no podemos confiar en los resultados de las simulaciones. Esto es señal de problemas de convergencia y deben ser diagnosticados.
Las simulaciones de MCMC típicamente están autocorrelacionadas (pues comenzamos en una región y muchas veces nos movemos poco). Esto significa que la cantidad de información de \(N\) simulaciones MCMC no es la misma que la que obtendríamos con \(N\) simulaciones independientes de la posterior.
Este concepto también se usa en muestreo: por ejemplo, existe menos información en una muestra de 100 personas que fueron muestreadas por conglomerados de 50 casas (por ejemplo, seleccionando al azar hogares y luego a dos adultos dentro de cada hogar) que seleccionar 100 hogares y escoger a un al azar un adulto de cada hogar. La segunda muestra tienen más información de la población, pues en la primera muestra parte de la información es “compartida” por el hecho de vivir en el mismo hogar. Para encontrar un número “efectivo” de muestra que haga comparables estos dos diseños, comparamos la varianza que obtendríamos del estimador de interes en cada caso. Si consideramos como base el segundo diseño (muestro aleatorio independiente), el primer diseño tendrá más varianza. Eso quiere decir que para que hubiera la misma varianza en los dos diseños, bastaría una muestra más chica (digamos 60 hogares) del segundo diseño independiente. Decimos que el tamaño efectivo de muestra del primer diseño es de 60 personas (el caso donde las varianzas de los dos diseños son iguales).
En el caso de series de tiempo, tenemos que considerar autocorrelación en la serie. Supongamos que quisiéramos estimar la media de una serie de tiempo (suponemos que a largo plazo el promedio de la serie de tiempo es una constante finita). Una muestra con autocorrelación alta produce malos estimadores de esta media incluso para tamaños de muestra relativamente grande:
set.seed(123)
<- 10
mu_verdadera <- function(T = 500, num_series = 100, ar = 0.9){
simular_series map_df(1:num_series, function(rep){
<- 10 + arima.sim(n = T, list(ar = ar), n.start = 200, sd = sqrt(1-ar^2))
serie tibble(t = 1:T, serie = serie, serie_id = rep, ar = ar)
})
}<- simular_series(T= 200, n = 4, ar = 0.80)
series_1_tbl <- simular_series(T= 200, n = 4, ar = 0.00001)
series_2_tbl <- bind_rows(series_1_tbl, series_2_tbl)
series_tbl |>
series_tbl ggplot(aes(t, serie, group = serie_id, colour = factor(serie_id))) +
geom_line(alpha = 0.9) +
geom_hline(yintercept = mu_verdadera, linetype = 2) +
facet_wrap(~ar, ncol = 1)
Calculamos las medias para un ejemplo con autocorrelación y otro sin ellas:
<- simular_series(T= 300, n = 500, ar = 0.80)
series_95_tbl <- simular_series(T= 300, n = 500, ar = 0.00001)
series_05_tbl <- bind_rows(series_95_tbl, series_05_tbl)
series_tbl |> group_by(serie_id, ar) |>
series_tbl summarise(media = mean(serie)) |>
ggplot(aes(media)) + geom_histogram() +
geom_vline(xintercept = mu_verdadera, linetype = 2) +
labs(title = "Distribución de medias de series de tiempo") +
facet_wrap(~ar)
`summarise()` has grouped output by 'serie_id'. You can override using the
`.groups` argument.
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Y vemos que la precisión de la estimación cuando la correlación es relativamente baja es mucho más alta que cuando la correlación es alta. ¿Cuál es el tamaño efectivo de muestra para series con autocorrelación ar= 0.8? Vemos que es aproximadamente 35, o dicho de otra manera, la serie sin correlación nos da casi 10 veces más información por observación que la correlacionada:
<- simular_series(T= 300, n = 1000, ar = 0.8)
series_95_tbl <- simular_series(T= 35, n = 1000, ar = 0.00001)
series_05_tbl <- bind_rows(series_95_tbl, series_05_tbl)
series_tbl |> group_by(serie_id, ar) |>
series_tbl summarise(media = mean(serie)) |>
ggplot(aes(media)) + geom_histogram() +
geom_vline(xintercept = mu_verdadera, linetype = 2) +
labs(title = "Distribución de medias de series de tiempo") +
facet_wrap(~ar)
`summarise()` has grouped output by 'serie_id'. You can override using the
`.groups` argument.
`stat_bin()` using `bins = 30`. Pick better value with `binwidth`.
Es posible hacer una estimación teórica del tamaño efectivo de muestra. Para esto, podemos notar que la varianza del promedio de una serie de tiempo depende de la estructura de autocorrelación. Supondremos que la serie de tiempo es estacionaria y cada \(y_t\) tiene varianza \(\sigma^2\) y correlación \(\rho\) con \(y_{t-1}\). Entonces:
\[Var(\bar{y})=\frac{1}{n^2}\text{Var} \left(\sum_{t=1}^n y_t \right) = \frac{n\sigma^2}{n^2}\sum_{t=1}^n \text{Var}(y_t) + \frac{2\sigma^2}{n^2}\sum_{t=1}^{n-1}\sum_{s=t+1}^n\text{Corr}(y_t, y_s)\]
Que se simplifica a (para \(n\) grande):
\[\text{Var}(\bar{y}) = \frac{\sigma^2}{n} + \frac{2\sigma^2}{n}\sum_{h=1}^{n-1}(1-h/n)\rho_{h} \approx \frac{\sigma^2}{n}\left (1+2\sum_{h=1}^{n-1}\rho_t\right )\] Si suponemos \(\rho_h = \text{corr}(y_t, y_{t+h})\) para cualquier \(t\). En nuestro caso anterior, el factor de corrección es aproximadamente:
1 + 2*(0.8)^(1:1000) |> sum()
[1] 9
Si hacemos \(N\) iteraciones en una cadena estacionaria con función de autocorrelación \(\rho_h\), el tamaño efectivo de muestra teórico se define como
\[N_{eff} = \frac{N}{1 + 2\sum_{h=1}^{\infty}\rho_h}\]
Si pudiéramos hacer simulaciones independientes de la posterior, \(N_{eff}\) es el tamaño de muestra requerido para obtener la misma información que la cadena autocorrelacionada de tamaño \(N\). Usualmente, aunque no siempre, \(N_{eff}<N\) para cadenas de MCMC.
Observaciones:
En nuestro ejemplo, tenemos tamaños de muestra efectivos satisfactorios:
$summary(c("Q", "sigma")) |>
ajuste_vinos_1select(variable, mean, sd, q5, q95, rhat, contains("ess")) |>
mutate(across(c(mean, sd, q5, q95, rhat, ess_bulk, ess_tail), ~round(., 3))) |>
filter(variable != "lp__") |> kable()
variable | mean | sd | q5 | q95 | rhat | ess_bulk | ess_tail |
---|---|---|---|---|---|---|---|
Q[1] | 0.137 | 0.317 | -0.396 | 0.672 | 1.001 | 20694.09 | 5373.128 |
Q[2] | 0.103 | 0.321 | -0.425 | 0.629 | 1.001 | 20994.93 | 5362.172 |
Q[3] | 0.271 | 0.318 | -0.247 | 0.798 | 1.000 | 20676.69 | 5913.964 |
Q[4] | 0.555 | 0.313 | 0.043 | 1.069 | 1.000 | 18730.09 | 5575.563 |
Q[5] | -0.120 | 0.315 | -0.636 | 0.390 | 1.001 | 19760.24 | 5848.199 |
Q[6] | -0.370 | 0.312 | -0.880 | 0.145 | 1.000 | 16654.60 | 5972.842 |
Q[7] | 0.286 | 0.314 | -0.231 | 0.795 | 1.001 | 17442.52 | 6084.769 |
Q[8] | 0.271 | 0.319 | -0.256 | 0.805 | 1.000 | 18199.26 | 5596.789 |
Q[9] | 0.080 | 0.314 | -0.438 | 0.584 | 1.000 | 19534.73 | 5983.451 |
Q[10] | 0.118 | 0.308 | -0.386 | 0.626 | 1.000 | 21143.80 | 5814.923 |
Q[11] | -0.010 | 0.321 | -0.546 | 0.510 | 1.000 | 20940.00 | 5531.936 |
Q[12] | -0.029 | 0.322 | -0.560 | 0.500 | 1.001 | 18276.46 | 5849.228 |
Q[13] | -0.105 | 0.312 | -0.609 | 0.414 | 1.003 | 20575.88 | 5957.570 |
Q[14] | 0.005 | 0.313 | -0.505 | 0.525 | 1.001 | 17541.60 | 5723.524 |
Q[15] | -0.215 | 0.318 | -0.740 | 0.312 | 1.001 | 17150.01 | 5412.980 |
Q[16] | -0.201 | 0.315 | -0.722 | 0.315 | 1.001 | 17596.06 | 5120.461 |
Q[17] | -0.142 | 0.316 | -0.665 | 0.379 | 1.000 | 20718.38 | 6172.910 |
Q[18] | -0.860 | 0.314 | -1.384 | -0.343 | 1.000 | 18745.31 | 6126.091 |
Q[19] | -0.161 | 0.311 | -0.666 | 0.343 | 1.000 | 18166.25 | 6352.717 |
Q[20] | 0.380 | 0.313 | -0.127 | 0.896 | 1.000 | 21072.45 | 5060.363 |
sigma | 0.997 | 0.054 | 0.912 | 1.092 | 1.000 | 13245.07 | 6385.483 |
Finalmente, podemos checar el error montecarlo, que es error de estimación usual
$summary(c("Q", "sigma")) |>
ajuste_vinos_1select(variable, mean, sd, q5, q95, rhat, contains("ess")) |>
mutate(across(c(mean, sd, q5, q95, rhat, ess_bulk, ess_tail), ~round(., 3))) |>
filter(variable != "lp__") |> kable()
variable | mean | sd | q5 | q95 | rhat | ess_bulk | ess_tail |
---|---|---|---|---|---|---|---|
Q[1] | 0.137 | 0.317 | -0.396 | 0.672 | 1.001 | 20694.09 | 5373.128 |
Q[2] | 0.103 | 0.321 | -0.425 | 0.629 | 1.001 | 20994.93 | 5362.172 |
Q[3] | 0.271 | 0.318 | -0.247 | 0.798 | 1.000 | 20676.69 | 5913.964 |
Q[4] | 0.555 | 0.313 | 0.043 | 1.069 | 1.000 | 18730.09 | 5575.563 |
Q[5] | -0.120 | 0.315 | -0.636 | 0.390 | 1.001 | 19760.24 | 5848.199 |
Q[6] | -0.370 | 0.312 | -0.880 | 0.145 | 1.000 | 16654.60 | 5972.842 |
Q[7] | 0.286 | 0.314 | -0.231 | 0.795 | 1.001 | 17442.52 | 6084.769 |
Q[8] | 0.271 | 0.319 | -0.256 | 0.805 | 1.000 | 18199.26 | 5596.789 |
Q[9] | 0.080 | 0.314 | -0.438 | 0.584 | 1.000 | 19534.73 | 5983.451 |
Q[10] | 0.118 | 0.308 | -0.386 | 0.626 | 1.000 | 21143.80 | 5814.923 |
Q[11] | -0.010 | 0.321 | -0.546 | 0.510 | 1.000 | 20940.00 | 5531.936 |
Q[12] | -0.029 | 0.322 | -0.560 | 0.500 | 1.001 | 18276.46 | 5849.228 |
Q[13] | -0.105 | 0.312 | -0.609 | 0.414 | 1.003 | 20575.88 | 5957.570 |
Q[14] | 0.005 | 0.313 | -0.505 | 0.525 | 1.001 | 17541.60 | 5723.524 |
Q[15] | -0.215 | 0.318 | -0.740 | 0.312 | 1.001 | 17150.01 | 5412.980 |
Q[16] | -0.201 | 0.315 | -0.722 | 0.315 | 1.001 | 17596.06 | 5120.461 |
Q[17] | -0.142 | 0.316 | -0.665 | 0.379 | 1.000 | 20718.38 | 6172.910 |
Q[18] | -0.860 | 0.314 | -1.384 | -0.343 | 1.000 | 18745.31 | 6126.091 |
Q[19] | -0.161 | 0.311 | -0.666 | 0.343 | 1.000 | 18166.25 | 6352.717 |
Q[20] | 0.380 | 0.313 | -0.127 | 0.896 | 1.000 | 21072.45 | 5060.363 |
sigma | 0.997 | 0.054 | 0.912 | 1.092 | 1.000 | 13245.07 | 6385.483 |
Ahora continuamos con nuestro modelo de calidad de vinos. Incluímos el origen del vino (que tiene dos niveles). La idea es que el origen, si vemos en el diagrama original, puede ser una variable de confusión entre calidad y score (pues afecta a calidad y también potencialmente al score). Adicionalmente, el origen no tiene puerta trasera, así que podemos examinar su efecto total sobre el score de los vinos. Estratificamos de la manera más simple, incluyendo origen en nuestra regresión:
<- wines_2012 |> mutate(origen_num = ifelse(wine.amer == 1, 1, 2))
wines_2012 |> select(wine.amer, origen_num) |> unique() wines_2012
# A tibble: 2 × 2
wine.amer origen_num
<dbl> <dbl>
1 1 1
2 0 2
<- length(unique(wines_2012$juez_num))
n_jueces <- length(unique(wines_2012$vino_num))
n_vinos <- length(unique(wines_2012$origen_num))
n_origen c("num_vinos" = n_vinos, "num_jueces" = n_jueces, "num_datos" = nrow(wines_2012))
num_vinos num_jueces num_datos
20 9 180
<-cmdstan_model("./src/vinos-2.stan")
mod_vinos_2 print(mod_vinos_2)
data {
int<lower=0> N; //número de calificaciones
int<lower=0> n_vinos; //número de vinos
int<lower=0> n_jueces; //número de jueces
int<lower=0> n_origen; //número de jueces
vector[N] S;
array[N] int juez;
array[N] int vino;
array[N] int origen;
}
parameters {
vector[n_vinos] Q;
vector[n_origen] O;
real <lower=0> sigma;
}
transformed parameters {
vector[N] media_score;
// determinístico dado parámetros
for (i in 1:N){
media_score[i] = Q[vino[i]] + O[origen[i]];
}
}
model {
// partes no determinísticas
S ~ normal(media_score, sigma);
Q ~ std_normal();
O ~ std_normal();
sigma ~ exponential(1);
}
generated quantities {
real dif_origen;
dif_origen = O[1] - O[2];
}
<- list(
datos_lst N = nrow(wines_2012),
n_vinos = n_vinos,
n_jueces = n_jueces,
n_origen = n_origen,
S = wines_2012$score_est,
vino = wines_2012$vino_num,
juez = wines_2012$juez_num,
origen = wines_2012$origen_num
)<- mod_vinos_2$sample(
ajuste_vinos_2 data = datos_lst,
chains = 4,
parallel_chains = 4,
iter_warmup = 1000,
iter_sampling = 2000,
refresh = 1000,
step_size = 0.1,
)
Running MCMC with 4 parallel chains...
Chain 1 Iteration: 1 / 3000 [ 0%] (Warmup)
Chain 1 Iteration: 1000 / 3000 [ 33%] (Warmup)
Chain 1 Iteration: 1001 / 3000 [ 33%] (Sampling)
Chain 2 Iteration: 1 / 3000 [ 0%] (Warmup)
Chain 2 Iteration: 1000 / 3000 [ 33%] (Warmup)
Chain 2 Iteration: 1001 / 3000 [ 33%] (Sampling)
Chain 3 Iteration: 1 / 3000 [ 0%] (Warmup)
Chain 3 Iteration: 1000 / 3000 [ 33%] (Warmup)
Chain 3 Iteration: 1001 / 3000 [ 33%] (Sampling)
Chain 4 Iteration: 1 / 3000 [ 0%] (Warmup)
Chain 4 Iteration: 1000 / 3000 [ 33%] (Warmup)
Chain 4 Iteration: 1001 / 3000 [ 33%] (Sampling)
Chain 1 Iteration: 2000 / 3000 [ 66%] (Sampling)
Chain 2 Iteration: 2000 / 3000 [ 66%] (Sampling)
Chain 3 Iteration: 2000 / 3000 [ 66%] (Sampling)
Chain 4 Iteration: 2000 / 3000 [ 66%] (Sampling)
Chain 1 Iteration: 3000 / 3000 [100%] (Sampling)
Chain 3 Iteration: 3000 / 3000 [100%] (Sampling)
Chain 1 finished in 0.6 seconds.
Chain 3 finished in 0.6 seconds.
Chain 2 Iteration: 3000 / 3000 [100%] (Sampling)
Chain 4 Iteration: 3000 / 3000 [100%] (Sampling)
Chain 2 finished in 0.6 seconds.
Chain 4 finished in 0.6 seconds.
All 4 chains finished successfully.
Mean chain execution time: 0.6 seconds.
Total execution time: 0.8 seconds.
$summary(c("O", "Q", "sigma")) |>
ajuste_vinos_2select(variable, mean, sd, q5, q95, rhat, contains("ess")) |>
mutate(across(c(mean, sd, q5, q95, rhat, ess_bulk, ess_tail), ~round(., 3))) |>
filter(variable != "lp__") |> kable()
variable | mean | sd | q5 | q95 | rhat | ess_bulk | ess_tail |
---|---|---|---|---|---|---|---|
O[1] | -0.066 | 0.290 | -0.543 | 0.421 | 1.002 | 2287.306 | 3321.151 |
O[2] | 0.106 | 0.351 | -0.465 | 0.679 | 1.001 | 2484.077 | 3858.430 |
Q[1] | 0.197 | 0.409 | -0.471 | 0.878 | 1.001 | 3942.153 | 5029.018 |
Q[2] | 0.007 | 0.447 | -0.729 | 0.725 | 1.001 | 3855.171 | 5170.915 |
Q[3] | 0.332 | 0.415 | -0.364 | 1.006 | 1.001 | 4170.358 | 5201.923 |
Q[4] | 0.458 | 0.447 | -0.276 | 1.180 | 1.001 | 3678.397 | 5085.397 |
Q[5] | -0.223 | 0.448 | -0.952 | 0.514 | 1.001 | 3854.360 | 5259.621 |
Q[6] | -0.307 | 0.412 | -0.976 | 0.366 | 1.001 | 4109.770 | 5083.645 |
Q[7] | 0.198 | 0.446 | -0.534 | 0.928 | 1.001 | 3981.338 | 5487.974 |
Q[8] | 0.330 | 0.410 | -0.344 | 1.015 | 1.000 | 4160.583 | 5196.431 |
Q[9] | 0.140 | 0.411 | -0.536 | 0.821 | 1.001 | 3925.106 | 5383.750 |
Q[10] | 0.182 | 0.409 | -0.489 | 0.871 | 1.000 | 4198.558 | 4953.803 |
Q[11] | 0.051 | 0.409 | -0.626 | 0.711 | 1.001 | 3835.440 | 5195.758 |
Q[12] | 0.028 | 0.414 | -0.648 | 0.707 | 1.001 | 4066.563 | 4891.343 |
Q[13] | -0.046 | 0.414 | -0.733 | 0.619 | 1.000 | 3731.818 | 4772.284 |
Q[14] | -0.083 | 0.448 | -0.814 | 0.656 | 1.001 | 3660.288 | 4800.175 |
Q[15] | -0.315 | 0.448 | -1.052 | 0.423 | 1.000 | 4042.565 | 5126.763 |
Q[16] | -0.139 | 0.416 | -0.818 | 0.540 | 1.000 | 3953.433 | 5064.421 |
Q[17] | -0.083 | 0.414 | -0.770 | 0.595 | 1.000 | 4124.527 | 5544.165 |
Q[18] | -0.799 | 0.409 | -1.464 | -0.126 | 1.001 | 4042.865 | 4892.500 |
Q[19] | -0.259 | 0.447 | -0.996 | 0.480 | 1.001 | 3741.120 | 5044.906 |
Q[20] | 0.287 | 0.445 | -0.439 | 1.027 | 1.001 | 3701.595 | 5366.897 |
sigma | 0.998 | 0.056 | 0.911 | 1.094 | 1.000 | 9688.425 | 6351.040 |
Todo parece bien con los diagnósticos. Podemos graficar las estimaciones (nota: aquí estan intervalos de 50% y 90%):
library(bayesplot)
color_scheme_set("red")
mcmc_intervals(ajuste_vinos_2$draws(c("Q", "O", "sigma")))
Todo parece ir bien, así que podemos expandir el modelo para incluir la forma de calificar de los jueces. Esto no es necesario (los jueces son una causa adicional que afecta el score), pero puede mejorar nuestras estimaciones.
Para estratificar por estas variables, tenemos que separarnos un poco de efectos adivitivos. Una razón importante por la que varían las calificaciones es que hay jueces que son más duros que otros, o que discriminan más qué otros. Esto es usual también cuando pensamos que los jueces son reactivos que las personas contestan: existen reactivos más difíciles que otros, y también discriminan de diferente manera.
En primer lugar, definimos un nivel general \(H\) que indica qué tan alto o bajo califica un juez en general. Adicionalmente, incluímos un parámetro de discriminación \(D\) de los jueces, que indica qué tanto del rango de la escala usa cada juez El modelo para el valor esperado del Score de un vino \(i\) calificado por el juez \(j\) es:
\[\mu_{i} = Q_{vino(i)} + O_{origen(i)} - H_{juez(i)}\] Podemos pensar que el valor \(H\) de cada juez es qué tan duro es en sus calificaciones. Para cada vino, un juez con valor alto de \(H\) tendrá a calificar más bajo un vino de misma calidad y origen que otro juez con un valor más bajo de \(H\). Podemos incluír un parámetro de discriminación \(D\) para cada juez, que indica qué tanto del rango de la escala usa cada juez de la siguiente forma:
\[\mu_{i} = (Q_{vino(i)} + O_{origen(i)} - H_{juez(i)})D_{juez(i)}\] Un juez con valor alto de \(D\) es más extremo en sus calificaciones: un vino por arriba de su promedio lo califica más alto en la escala, y un vino por debajo de su promedio lo califica más bajo. El extremo es que \(D=0\), que quiere decir que el juez tiende a calificar a todos los vinos con un score.
<-cmdstan_model("./src/vinos-3.stan")
mod_vinos_3 print(mod_vinos_3)
data {
int<lower=0> N; //número de calificaciones
int<lower=0> n_vinos; //número de vinos
int<lower=0> n_jueces; //número de jueces
int<lower=0> n_origen; //número de jueces
vector[N] S;
array[N] int juez;
array[N] int vino;
array[N] int origen;
}
parameters {
vector[n_vinos] Q;
vector[n_origen] O;
vector[n_jueces] H;
vector<lower=0>[n_jueces] D;
real <lower=0> sigma;
}
transformed parameters {
vector[N] media_score;
// determinístico dado parámetros
for (i in 1:N){
media_score[i] = (Q[vino[i]] + O[origen[i]] - H[juez[i]]) * D[juez[i]];
}
}
model {
// partes no determinísticas
S ~ normal(media_score, sigma);
Q ~ std_normal();
O ~ std_normal();
H ~ std_normal();
D ~ std_normal();
sigma ~ exponential(1);
}
generated quantities {
real dif_origen;
dif_origen = O[1] - O[2];
}
<- list(
datos_lst N = nrow(wines_2012),
n_vinos = n_vinos,
n_jueces = n_jueces,
n_origen = n_origen,
S = wines_2012$score_est,
vino = wines_2012$vino_num,
juez = wines_2012$juez_num,
origen = wines_2012$origen_num
)<- mod_vinos_3$sample(
ajuste_vinos_3 data = datos_lst,
chains = 4,
parallel_chains = 4,
iter_warmup = 1000,
iter_sampling = 2000,
refresh = 1000,
step_size = 0.1,
)
Running MCMC with 4 parallel chains...
Chain 1 Iteration: 1 / 3000 [ 0%] (Warmup)
Chain 2 Iteration: 1 / 3000 [ 0%] (Warmup)
Chain 3 Iteration: 1 / 3000 [ 0%] (Warmup)
Chain 4 Iteration: 1 / 3000 [ 0%] (Warmup)
Chain 1 Iteration: 1000 / 3000 [ 33%] (Warmup)
Chain 1 Iteration: 1001 / 3000 [ 33%] (Sampling)
Chain 2 Iteration: 1000 / 3000 [ 33%] (Warmup)
Chain 2 Iteration: 1001 / 3000 [ 33%] (Sampling)
Chain 3 Iteration: 1000 / 3000 [ 33%] (Warmup)
Chain 3 Iteration: 1001 / 3000 [ 33%] (Sampling)
Chain 4 Iteration: 1000 / 3000 [ 33%] (Warmup)
Chain 4 Iteration: 1001 / 3000 [ 33%] (Sampling)
Chain 1 Iteration: 2000 / 3000 [ 66%] (Sampling)
Chain 2 Iteration: 2000 / 3000 [ 66%] (Sampling)
Chain 3 Iteration: 2000 / 3000 [ 66%] (Sampling)
Chain 4 Iteration: 2000 / 3000 [ 66%] (Sampling)
Chain 1 Iteration: 3000 / 3000 [100%] (Sampling)
Chain 2 Iteration: 3000 / 3000 [100%] (Sampling)
Chain 4 Iteration: 3000 / 3000 [100%] (Sampling)
Chain 1 finished in 1.5 seconds.
Chain 2 finished in 1.5 seconds.
Chain 4 finished in 1.5 seconds.
Chain 3 Iteration: 3000 / 3000 [100%] (Sampling)
Chain 3 finished in 1.5 seconds.
All 4 chains finished successfully.
Mean chain execution time: 1.5 seconds.
Total execution time: 1.7 seconds.
Checamos diagnósticos:
$summary(c("O", "Q", "H", "D", "sigma")) |>
ajuste_vinos_3select(variable, mean, sd, q5, q95, rhat, contains("ess")) |>
mutate(across(c(mean, sd, q5, q95, rhat, ess_bulk, ess_tail), ~round(., 3))) |>
filter(variable != "lp__") |> kable()
variable | mean | sd | q5 | q95 | rhat | ess_bulk | ess_tail |
---|---|---|---|---|---|---|---|
O[1] | -0.115 | 0.447 | -0.859 | 0.629 | 1.002 | 3518.843 | 4790.784 |
O[2] | 0.198 | 0.476 | -0.572 | 0.986 | 1.001 | 3963.128 | 5271.349 |
Q[1] | 0.366 | 0.562 | -0.543 | 1.315 | 1.000 | 5657.887 | 5719.280 |
Q[2] | 0.168 | 0.586 | -0.779 | 1.132 | 1.000 | 5514.261 | 5765.562 |
Q[3] | 0.508 | 0.537 | -0.369 | 1.377 | 1.000 | 6854.468 | 5604.398 |
Q[4] | 0.825 | 0.579 | -0.111 | 1.800 | 1.000 | 6452.743 | 5446.307 |
Q[5] | -0.483 | 0.558 | -1.385 | 0.451 | 1.001 | 6299.704 | 5695.944 |
Q[6] | -0.830 | 0.593 | -1.783 | 0.171 | 1.000 | 4716.862 | 5114.054 |
Q[7] | 0.229 | 0.602 | -0.753 | 1.229 | 1.000 | 4749.244 | 6033.222 |
Q[8] | 0.618 | 0.553 | -0.274 | 1.552 | 1.001 | 6631.817 | 5578.857 |
Q[9] | 0.278 | 0.556 | -0.642 | 1.179 | 1.000 | 5940.039 | 5748.859 |
Q[10] | 0.313 | 0.531 | -0.568 | 1.172 | 1.000 | 6559.060 | 5848.707 |
Q[11] | 0.183 | 0.535 | -0.708 | 1.054 | 1.000 | 6569.389 | 6443.012 |
Q[12] | -0.079 | 0.537 | -0.941 | 0.807 | 1.000 | 6281.310 | 5903.469 |
Q[13] | 0.052 | 0.549 | -0.860 | 0.940 | 1.001 | 6563.927 | 5507.827 |
Q[14] | -0.158 | 0.558 | -1.077 | 0.753 | 1.001 | 6874.838 | 5893.450 |
Q[15] | -0.521 | 0.584 | -1.498 | 0.422 | 1.001 | 5853.924 | 5734.059 |
Q[16] | -0.121 | 0.565 | -1.036 | 0.815 | 1.001 | 5233.317 | 5555.469 |
Q[17] | 0.103 | 0.562 | -0.845 | 1.007 | 1.001 | 5025.607 | 5096.996 |
Q[18] | -1.507 | 0.552 | -2.424 | -0.608 | 1.000 | 5907.052 | 5459.271 |
Q[19] | -0.367 | 0.561 | -1.292 | 0.545 | 1.001 | 6738.238 | 6061.758 |
Q[20] | 0.482 | 0.569 | -0.445 | 1.408 | 1.001 | 5687.180 | 5521.888 |
H[1] | 0.619 | 0.586 | -0.269 | 1.601 | 1.000 | 6123.319 | 4979.950 |
H[2] | -0.274 | 0.437 | -0.998 | 0.424 | 1.001 | 4060.809 | 4583.991 |
H[3] | -0.469 | 0.734 | -1.656 | 0.745 | 1.000 | 6648.205 | 5243.692 |
H[4] | 1.233 | 0.601 | 0.322 | 2.271 | 1.000 | 5632.281 | 5695.773 |
H[5] | -1.779 | 0.618 | -2.853 | -0.815 | 1.000 | 6082.938 | 5816.825 |
H[6] | -1.169 | 0.649 | -2.268 | -0.170 | 1.000 | 6714.764 | 5139.543 |
H[7] | -0.234 | 0.588 | -1.195 | 0.672 | 1.002 | 5076.004 | 5060.476 |
H[8] | 1.213 | 0.570 | 0.355 | 2.218 | 1.001 | 4389.700 | 5537.353 |
H[9] | 0.823 | 0.824 | -0.577 | 2.108 | 1.001 | 5338.110 | 4003.320 |
D[1] | 0.461 | 0.253 | 0.097 | 0.932 | 1.000 | 2983.543 | 2565.263 |
D[2] | 0.925 | 0.350 | 0.359 | 1.529 | 1.001 | 2810.568 | 3022.820 |
D[3] | 0.251 | 0.186 | 0.022 | 0.613 | 1.001 | 3483.762 | 2882.873 |
D[4] | 0.446 | 0.198 | 0.169 | 0.804 | 1.000 | 3765.231 | 3179.683 |
D[5] | 0.450 | 0.151 | 0.243 | 0.725 | 1.000 | 5896.061 | 5536.056 |
D[6] | 0.342 | 0.167 | 0.104 | 0.641 | 1.001 | 3743.613 | 2423.599 |
D[7] | 0.573 | 0.411 | 0.044 | 1.324 | 1.000 | 1909.202 | 3066.287 |
D[8] | 0.621 | 0.245 | 0.285 | 1.069 | 1.001 | 3828.092 | 4701.569 |
D[9] | 0.196 | 0.143 | 0.016 | 0.463 | 1.000 | 3181.170 | 2733.067 |
sigma | 0.824 | 0.050 | 0.745 | 0.909 | 1.001 | 5443.715 | 5411.834 |
Y vemos efectivamente que el uso de la escala de los jueces es considerablemente diferente, y que hemos absorbido parte de la variación con los parámetros \(H\) y \(D\) (\(sigma\) es más baja que en los modelos anteriores):
mcmc_intervals(ajuste_vinos_3$draws(c("H", "D", "sigma")))
mcmc_intervals(ajuste_vinos_3$draws(c("Q")))
mcmc_intervals(ajuste_vinos_1$draws(c("Q")))
Con el modelo completo, examinamos ahora el contraste de interés: ¿hay diferencias en las calificaciones de vinos de diferentes orígenes? La respuesta es que no hay mucha evidencia de que haya una diferencia, aunque hay variación considerable en este contraste:
$summary(c("dif_origen")) |>
ajuste_vinos_3select(variable, mean, sd, q5, q95, rhat, contains("ess")) |>
mutate(across(c(mean, sd, q5, q95, rhat, ess_bulk, ess_tail), ~round(., 3)))
# A tibble: 1 × 8
variable mean sd q5 q95 rhat ess_bulk ess_tail
<chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
1 dif_origen -0.312 0.497 -1.12 0.495 1.00 4087. 4952.
mcmc_hist(ajuste_vinos_3$draws("dif_origen"), binwidth = 0.07)
Finalmente, discutiremos otro tipo de diagnósticos de Stan. Cuando una trayectoria tuvo un cambio grande en energía \(H\) desde su valor actual a la propuesta final, usualmente del orden de 10^3 por ejemplo, esto implica un rechazo “fuerte” en el nuevo punto de la trayectoria, e implica que la el integrador numérico falló de manera grave.
Cuando en Stan obtenemos un número considerable de transiciones divergentes, generalmente esto indica que el integrador numérico de Stan no está funcionando bien, y por lo tanto la exploración puede ser deficiente y/o puede estar sesgada al espacio de parámetros donde no ocurren estos rechazos.
Esto puede pasar cuando encontramos zonas de alta curvatura en el espacio de parámetros. Que una posterior esté altamente concentrada o más dispersa generalmente no es un problema, pero si la concentración varía fuertemente (curvatura) entonces puede ser difícil encontrar la escala correcta para que el integrador funcione apropiadamente.
Para ver un ejemplo, consideremos un ejemplo de una distribución cuya forma aparecerá más tarde en modelos jerárquicos. Primero, la marginal de \(y\) es normal con media 0 y desviación estándar 3. La distribución condicional \(p(x|y)\) de \(x = c(x_1,\ldots, x_9)\) dado \(y\) es normal multivariada, todas con media cero y desviación estándar \(e^{y/2}\). Veamos qué pasa si intentamos simular de esta distribución en Stan:
<- cmdstan_model("./src/embudo-neal.stan")
mod_embudo <- mod_embudo$sample(
ajuste_embudo chains = 1,
iter_warmup = 1000,
iter_sampling = 3000,
refresh = 1000)
Running MCMC with 1 chain...
Chain 1 Iteration: 1 / 4000 [ 0%] (Warmup)
Chain 1 Iteration: 1000 / 4000 [ 25%] (Warmup)
Chain 1 Iteration: 1001 / 4000 [ 25%] (Sampling)
Chain 1 Iteration: 2000 / 4000 [ 50%] (Sampling)
Chain 1 Iteration: 3000 / 4000 [ 75%] (Sampling)
Chain 1 Iteration: 4000 / 4000 [100%] (Sampling)
Chain 1 finished in 0.1 seconds.
Warning: 86 of 3000 (3.0%) transitions ended with a divergence.
See https://mc-stan.org/misc/warnings for details.
Warning: 1 of 1 chains had an E-BFMI less than 0.3.
See https://mc-stan.org/misc/warnings for details.
Y vemos que aparecen algunos problemas.
<- ajuste_embudo$draws(format = "df")
simulaciones <- ajuste_embudo$sampler_diagnostics(format = "df")
diagnosticos <- simulaciones |> inner_join(diagnosticos, by = c(".draw", ".iteration", ".chain"))
sims_diag $summary() |>
ajuste_embudoselect(variable, mean, rhat, contains("ess"))
# A tibble: 11 × 5
variable mean rhat ess_bulk ess_tail
<chr> <dbl> <dbl> <dbl> <dbl>
1 lp__ -5.55 1.35 2.45 NA
2 y 0.189 1.34 2.46 2.23
3 x[1] 0.208 1.28 2494. 417.
4 x[2] -0.0835 1.25 1208. 346.
5 x[3] -0.0653 1.18 294. 300.
6 x[4] -0.0787 1.28 1015. 288.
7 x[5] -0.0731 1.28 1912. 251.
8 x[6] 0.0298 1.34 2737. 266.
9 x[7] 0.0539 1.17 452. 248.
10 x[8] -0.0301 1.30 2469. 313.
11 x[9] 0.0600 1.23 1814. 348.
ggplot(sims_diag, aes(y = y, x = `x[1]`)) +
geom_point(alpha = 0.1) +
geom_point(data = sims_diag |> filter(divergent__ == 1), color = "red", size = 2) +
geom_hline(yintercept = -2.5, linetype = 2)
Y vemos que hay transiciones divergentes. Cuando el muestreador entra en el cuello del embudo, es muy fácil que se “despeñe” en probabilidad y que no pueda explorar correctamente la forma del cuello. Esto lo podemos ver, por ejemplo, si hacemos más simulaciones:
<- cmdstan_model("./src/embudo-neal.stan")
mod_embudo print(mod_embudo)
parameters {
real y;
vector[9] x;
}
model {
y ~ normal(0, 3);
x ~ normal(0, exp(y/2));
}
<- mod_embudo$sample(
ajuste_embudo chains = 1,
iter_warmup = 1000,
iter_sampling = 30000,
refresh = 10000)
Running MCMC with 1 chain...
Chain 1 Iteration: 1 / 31000 [ 0%] (Warmup)
Chain 1 Iteration: 1001 / 31000 [ 3%] (Sampling)
Chain 1 Iteration: 11000 / 31000 [ 35%] (Sampling)
Chain 1 Iteration: 21000 / 31000 [ 67%] (Sampling)
Chain 1 Iteration: 31000 / 31000 [100%] (Sampling)
Chain 1 finished in 0.8 seconds.
Warning: 1200 of 30000 (4.0%) transitions ended with a divergence.
See https://mc-stan.org/misc/warnings for details.
Warning: 16 of 30000 (0.0%) transitions hit the maximum treedepth limit of 10.
See https://mc-stan.org/misc/warnings for details.
Warning: 1 of 1 chains had an E-BFMI less than 0.3.
See https://mc-stan.org/misc/warnings for details.
$summary() |>
ajuste_embudoselect(variable, mean, rhat, contains("ess"))
# A tibble: 11 × 5
variable mean rhat ess_bulk ess_tail
<chr> <dbl> <dbl> <dbl> <dbl>
1 lp__ -14.2 1.01 55.4 13.7
2 y 2.08 1.01 57.6 27.2
3 x[1] 0.227 1.00 13728. 2727.
4 x[2] 0.175 1.00 10467. 2770.
5 x[3] -0.0281 1.01 6702. 2551.
6 x[4] -0.112 1.01 7941. 2514.
7 x[5] -0.0890 1.01 6287. 2346.
8 x[6] 0.0658 1.00 10519. 2765.
9 x[7] -0.121 1.00 12467. 2538.
10 x[8] -0.107 1.00 7124. 2443.
11 x[9] 0.00905 1.00 2954. 2351.
<- ajuste_embudo$draws(format = "df")
simulaciones <- ajuste_embudo$sampler_diagnostics(format = "df")
diagnosticos <- simulaciones |> inner_join(diagnosticos, by = c(".draw", ".iteration", ".chain"))
sims_diag ggplot(sims_diag, aes(y = y, x = `x[1]`)) +
geom_point(alpha = 0.1) +
geom_point(data = sims_diag |> filter(divergent__ == 1), color = "red", size = 2) +
geom_hline(yintercept = -2.5, linetype = 2)
Y vemos que ahora que en el primer ejemplo estábamos probablemente sobreestimando la media de \(y\). Las divergencias indican que esto puede estar ocurriendo. En este ejemplo particular, también vemos que las R-hat y los tamaños efectivos de muestra son bajos.
Este es un ejemplo extremo. Sin embargo, podemos reparametrizar para hacer las cosas más fáciles para el muestreador. Podemos simular \(y\), y después, simular \(x\) como \(x \sim e^{y/2} z\) donde \(z\) es normal estándar.
<- cmdstan_model("./src/embudo-neal-reparam.stan")
mod_embudo_reparam print(mod_embudo_reparam)
parameters {
real y;
vector[9] z;
}
transformed parameters {
vector[9] x;
x = exp(y/2) * z;
}
model {
y ~ normal(0, 3);
z ~ std_normal();
}
<- mod_embudo_reparam$sample(
ajuste_embudo chains = 4,
iter_warmup = 1000,
iter_sampling = 10000,
refresh = 1000)
Running MCMC with 4 sequential chains...
Chain 1 Iteration: 1 / 11000 [ 0%] (Warmup)
Chain 1 Iteration: 1000 / 11000 [ 9%] (Warmup)
Chain 1 Iteration: 1001 / 11000 [ 9%] (Sampling)
Chain 1 Iteration: 2000 / 11000 [ 18%] (Sampling)
Chain 1 Iteration: 3000 / 11000 [ 27%] (Sampling)
Chain 1 Iteration: 4000 / 11000 [ 36%] (Sampling)
Chain 1 Iteration: 5000 / 11000 [ 45%] (Sampling)
Chain 1 Iteration: 6000 / 11000 [ 54%] (Sampling)
Chain 1 Iteration: 7000 / 11000 [ 63%] (Sampling)
Chain 1 Iteration: 8000 / 11000 [ 72%] (Sampling)
Chain 1 Iteration: 9000 / 11000 [ 81%] (Sampling)
Chain 1 Iteration: 10000 / 11000 [ 90%] (Sampling)
Chain 1 Iteration: 11000 / 11000 [100%] (Sampling)
Chain 1 finished in 0.2 seconds.
Chain 2 Iteration: 1 / 11000 [ 0%] (Warmup)
Chain 2 Iteration: 1000 / 11000 [ 9%] (Warmup)
Chain 2 Iteration: 1001 / 11000 [ 9%] (Sampling)
Chain 2 Iteration: 2000 / 11000 [ 18%] (Sampling)
Chain 2 Iteration: 3000 / 11000 [ 27%] (Sampling)
Chain 2 Iteration: 4000 / 11000 [ 36%] (Sampling)
Chain 2 Iteration: 5000 / 11000 [ 45%] (Sampling)
Chain 2 Iteration: 6000 / 11000 [ 54%] (Sampling)
Chain 2 Iteration: 7000 / 11000 [ 63%] (Sampling)
Chain 2 Iteration: 8000 / 11000 [ 72%] (Sampling)
Chain 2 Iteration: 9000 / 11000 [ 81%] (Sampling)
Chain 2 Iteration: 10000 / 11000 [ 90%] (Sampling)
Chain 2 Iteration: 11000 / 11000 [100%] (Sampling)
Chain 2 finished in 0.2 seconds.
Chain 3 Iteration: 1 / 11000 [ 0%] (Warmup)
Chain 3 Iteration: 1000 / 11000 [ 9%] (Warmup)
Chain 3 Iteration: 1001 / 11000 [ 9%] (Sampling)
Chain 3 Iteration: 2000 / 11000 [ 18%] (Sampling)
Chain 3 Iteration: 3000 / 11000 [ 27%] (Sampling)
Chain 3 Iteration: 4000 / 11000 [ 36%] (Sampling)
Chain 3 Iteration: 5000 / 11000 [ 45%] (Sampling)
Chain 3 Iteration: 6000 / 11000 [ 54%] (Sampling)
Chain 3 Iteration: 7000 / 11000 [ 63%] (Sampling)
Chain 3 Iteration: 8000 / 11000 [ 72%] (Sampling)
Chain 3 Iteration: 9000 / 11000 [ 81%] (Sampling)
Chain 3 Iteration: 10000 / 11000 [ 90%] (Sampling)
Chain 3 Iteration: 11000 / 11000 [100%] (Sampling)
Chain 3 finished in 0.2 seconds.
Chain 4 Iteration: 1 / 11000 [ 0%] (Warmup)
Chain 4 Iteration: 1000 / 11000 [ 9%] (Warmup)
Chain 4 Iteration: 1001 / 11000 [ 9%] (Sampling)
Chain 4 Iteration: 2000 / 11000 [ 18%] (Sampling)
Chain 4 Iteration: 3000 / 11000 [ 27%] (Sampling)
Chain 4 Iteration: 4000 / 11000 [ 36%] (Sampling)
Chain 4 Iteration: 5000 / 11000 [ 45%] (Sampling)
Chain 4 Iteration: 6000 / 11000 [ 54%] (Sampling)
Chain 4 Iteration: 7000 / 11000 [ 63%] (Sampling)
Chain 4 Iteration: 8000 / 11000 [ 72%] (Sampling)
Chain 4 Iteration: 9000 / 11000 [ 81%] (Sampling)
Chain 4 Iteration: 10000 / 11000 [ 90%] (Sampling)
Chain 4 Iteration: 11000 / 11000 [100%] (Sampling)
Chain 4 finished in 0.2 seconds.
All 4 chains finished successfully.
Mean chain execution time: 0.2 seconds.
Total execution time: 1.0 seconds.
Y con este truco de reparametrización el muestreador funciona correctamente (observa que la media de \(y\) está estimada correctamente, y no hay divergencias).
$summary() |>
ajuste_embudoselect(variable, mean, rhat, contains("ess")) |>
mutate(across(c(mean, rhat, ess_bulk, ess_tail), ~round(., 3)))
# A tibble: 20 × 5
variable mean rhat ess_bulk ess_tail
<chr> <dbl> <dbl> <dbl> <dbl>
1 lp__ -5.00 1 16285. 24865.
2 y 0.002 1 74705. 28688.
3 z[1] -0.001 1 85160. 29570.
4 z[2] -0.002 1 84329. 30159.
5 z[3] -0.006 1 83548. 29967.
6 z[4] 0.002 1 81672. 29573.
7 z[5] 0 1 81966. 31008.
8 z[6] -0.004 1 85319. 29486.
9 z[7] 0.001 1 85417. 28666.
10 z[8] 0 1 83917. 28876.
11 z[9] 0.004 1 83128. 30034.
12 x[1] -0.022 1 41969. 31013.
13 x[2] -0.036 1 39866. 29902.
14 x[3] -0.133 1 40322. 30635.
15 x[4] 0.023 1 41022. 29315.
16 x[5] -0.008 1 41923. 31293.
17 x[6] -0.116 1 41851. 29370.
18 x[7] 0.005 1 42307. 30895.
19 x[8] -0.043 1 40895. 29853.
20 x[9] -0.021 1 39208. 28808.