7  Estimación de Modelos Bayesianos (Parte 1)

7.1 Comparación de medias

Cushny, A. R. and Peebles, A. R. (1905) “The action of optical isomers: II hyoscines.” The Journal of Physiology 32, 501–510.

7.1.1 Datos

Cargamos y exploramos los datos. (extra = horas adicionales de sueño con el medicamento; group se renombra a drug porque se refiere al tipo de medicación).

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.1     ✔ tibble    3.2.1
✔ lubridate 1.9.4     ✔ tidyr     1.3.1
✔ purrr     1.0.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(ggformula)
Loading required package: scales

Attaching package: 'scales'

The following object is masked from 'package:purrr':

    discard

The following object is masked from 'package:readr':

    col_factor

Loading required package: ggridges

New to ggformula?  Try the tutorials: 
    learnr::run_tutorial("introduction", package = "ggformula")
    learnr::run_tutorial("refining", package = "ggformula")
library(dplyr)

sleep <- 
  datasets::sleep %>% 
  rename(drug = group)

gf_boxplot(extra ~ drug, data = sleep)

df_stats(extra ~ drug, data = sleep)
  response drug  min     Q1 median   Q3 max mean       sd  n missing
1    extra    1 -1.6 -0.175   0.35 1.70 3.7 0.75 1.789010 10       0
2    extra    2 -0.1  0.875   1.75 4.15 5.5 2.33 2.002249 10       0

Pregunta de investigación: ¿Cómo afectan dos medicamentos distintos al tiempo de sueño, comparando a los mismos sujetos sin medicación y luego con el medicamento?

7.1.2 Modelo

La forma más simple para comparar medias consiste en suponer que cada grupo proviene de una distribución normal con media desconocida y desviación estándar común. Más adelante veremos cómo permitir desviaciones estándar diferentes por grupo.

Para dos grupos, definimos tres parámetros: las medias \(\mu_1\) y \(\mu_2\), y la desviación estándar compartida \(\sigma\). Además, necesitamos distribuciones previas para esos parámetros. Una elección habitual es asignar a cada media una normal previa y a \(\sigma\) una distribución uniforme (aunque luego discutiremos alternativas más informativas).

Así, nuestro modelo jerárquico queda:

\[ \begin{aligned} Y_{i \mid g} & \sim \text{Normal}(\mu_g,\,\sigma), \\ \mu_g & \sim \text{Normal}(0,\,3^2), \\ \sigma & \sim \text{Uniform}(0.002,\,2000). \end{aligned} \]

  • La normal previa para \(\mu_g\) con media 0 y desviación estándar 3 significa que, a priori, estamos 95% seguros de que el efecto del medicamento está entre –6 y +6 horas de sueño adicional, y resulta muy improbable un cambio mayor a 9 horas.
  • Para \(\sigma\) usamos Uniform\((0.002, 2000)\), una previa no restrictiva que cubre desde varianzas muy pequeñas hasta extremadamente grandes.

A continuación, se muestra la implementación en JAGS usando la librería R2jags.

library(R2jags)
Loading required package: rjags
Loading required package: coda
Linked to JAGS 4.3.2
Loaded modules: basemod,bugs

Attaching package: 'R2jags'
The following object is masked from 'package:coda':

    traceplot
sleep_model <- function() {
  for (i in 1:Nobs) {
    extra[i] ~ dnorm(mu[drug[i]], 1 / sigma^2)
  }
  for (d in 1:Ndrugs) {
    mu[d] ~ dnorm(0, 1 / 3^2)    # desviación estándar = 3
  }
  sigma ~ dunif(0.002, 2000)    # desviación estándar uniforme
  delta_mu <- mu[2] - mu[1]
  tau      <- 1 / sigma^2
}

sleep_jags <- jags(
  model             = sleep_model,
  parameters.to.save = c("mu", "sigma", "delta_mu"),
  data = list(
    extra  = sleep$extra,
    drug   = sleep$drug,
    Nobs   = nrow(sleep),
    Ndrugs = 2
  ),
  DIC = FALSE  # no calculamos DIC por ahora
)
module glm loaded
module dic loaded
Compiling model graph
   Resolving undeclared variables
   Allocating nodes
Graph information:
   Observed stochastic nodes: 20
   Unobserved stochastic nodes: 3
   Total graph size: 56

Initializing model
library(CalvinBayes)
Loading required package: bayesplot
This is bayesplot version 1.11.1
- Online documentation and vignettes at mc-stan.org/bayesplot
- bayesplot theme set to bayesplot::theme_default()
   * Does _not_ affect other ggplot2 plots
   * See ?bayesplot_theme_set for details on theme setting

Attaching package: 'CalvinBayes'
The following object is masked from 'package:bayesplot':

    rhat
The following object is masked from 'package:datasets':

    HairEyeColor
library(bayesplot)

summary(sleep_jags)
fit using jags
 3 chains, each with 2000 iterations (first 1000 discarded)
 n.sims = 3000 iterations saved
         mu.vect sd.vect   2.5%   25%   50%   75% 97.5%  Rhat n.eff
delta_mu   1.524   0.912 -0.265 0.926 1.534 2.113 3.316 1.001  2000
mu[1]      0.713   0.659 -0.551 0.291 0.722 1.143 1.972 1.001  2300
mu[2]      2.237   0.632  0.964 1.837 2.231 2.647 3.510 1.001  3000
sigma      2.051   0.385  1.471 1.773 2.002 2.266 2.917 1.003  1200
sleep_mcmc <- as.mcmc(sleep_jags)

# Distribuciones marginales para mu y sigma
mcmc_areas(sleep_mcmc, prob = 0.95, regex_pars = "mu")

mcmc_areas(sleep_mcmc, prob = 0.95, regex_pars = "sigma")

# Gráficos de pares para mu[1], mu[2], sigma
mcmc_pairs(sleep_mcmc)

Para cuantificar la probabilidad posterior de que el tiempo medio de sueño con la segunda droga supere al de la primera, calculamos:

library(mosaic)
Registered S3 method overwritten by 'mosaic':
  method                           from   
  fortify.SpatialPolygonsDataFrame ggplot2

The 'mosaic' package masks several functions from core packages in order to add 
additional features.  The original behavior of these functions should not be affected by this.

Attaching package: 'mosaic'
The following object is masked from 'package:Matrix':

    mean
The following object is masked from 'package:scales':

    rescale
The following objects are masked from 'package:dplyr':

    count, do, tally
The following object is masked from 'package:purrr':

    cross
The following object is masked from 'package:ggplot2':

    stat
The following objects are masked from 'package:stats':

    binom.test, cor, cor.test, cov, fivenum, IQR, median, prop.test,
    quantile, sd, t.test, var
The following objects are masked from 'package:base':

    max, mean, min, prod, range, sample, sum
prop( ~(delta_mu > 0), data = posterior(sleep_jags) )
prop_TRUE 
    0.952 

7.1.3 Desviaciones estándar diferentes por grupo

Si en lugar de una \(\sigma\) común queremos una \(\sigma\) distinta para cada medicamento, el modelo cambia ligeramente:

sleep_model2 <- function() {
  for (i in 1:Nobs) {
    extra[i] ~ dnorm(mu[drug[i]], 1 / sigma[drug[i]]^2)
  }
  for (d in 1:Ndrugs) {
    mu[d]    ~ dnorm(0, 1 / 3^2)
    sigma[d] ~ dunif(0.002, 2000)
    tau[d]   <- 1 / sigma[d]^2
  }
  delta_mu    <- mu[2] - mu[1]
  delta_sigma <- sigma[2] - sigma[1]
}

sleep_jags2 <- jags(
  model             = sleep_model2,
  parameters.to.save = c("mu", "sigma", "delta_mu", "delta_sigma", "tau"),
  data = list(
    extra  = sleep$extra,
    drug   = sleep$drug,
    Nobs   = nrow(sleep),
    Ndrugs = 2
  ),
  DIC = FALSE
)
Compiling model graph
   Resolving undeclared variables
   Allocating nodes
Graph information:
   Observed stochastic nodes: 20
   Unobserved stochastic nodes: 4
   Total graph size: 60

Initializing model

Resultados:

summary(sleep_jags2)
fit using jags
 3 chains, each with 2000 iterations (first 1000 discarded)
 n.sims = 3000 iterations saved
            mu.vect sd.vect   2.5%    25%   50%   75% 97.5%  Rhat n.eff
delta_mu      1.499   0.970 -0.438  0.883 1.492 2.140 3.377 1.001  3000
delta_sigma   0.255   0.877 -1.511 -0.244 0.222 0.724 2.158 1.002  3000
mu[1]         0.692   0.646 -0.621  0.286 0.699 1.088 2.001 1.002  1400
mu[2]         2.191   0.720  0.713  1.750 2.205 2.637 3.597 1.001  3000
sigma[1]      2.072   0.586  1.275  1.652 1.961 2.359 3.494 1.001  3000
sigma[2]      2.327   0.657  1.435  1.862 2.196 2.652 4.008 1.001  3000
tau[1]        0.284   0.141  0.082  0.180 0.260 0.366 0.615 1.001  3000
tau[2]        0.224   0.108  0.062  0.142 0.207 0.289 0.485 1.001  3000
sleep_mcmc2 <- as.mcmc(sleep_jags2)

mcmc_areas(sleep_mcmc2, prob = 0.95, regex_pars = "mu")

mcmc_areas(sleep_mcmc2, prob = 0.95, regex_pars = "sigma")

Posteriores para las diferencias:

prop( ~(delta_mu > 0), data = posterior(sleep_jags2) )
prop_TRUE 
0.9346667 
prop( ~(delta_sigma > 0), data = posterior(sleep_jags2) )
prop_TRUE 
    0.627 
hdi(sleep_jags2, pars = c("delta_mu", "delta_sigma"))
          par         lo       hi prob chain
1    delta_mu -0.4664344 3.434118 0.95     1
2    delta_mu -0.4789051 3.298065 0.95     2
3    delta_mu -0.4232610 3.253078 0.95     3
4 delta_sigma -1.6369472 1.953089 0.95     1
5 delta_sigma -1.5444861 2.299134 0.95     2
6 delta_sigma -1.1307738 2.267777 0.95     3
hdi(sleep_jags2)
           par          lo        hi prob chain
1     delta_mu -0.46643439 3.4341181 0.95     1
2     delta_mu -0.47890510 3.2980651 0.95     2
3     delta_mu -0.42326103 3.2530779 0.95     3
4  delta_sigma -1.63694721 1.9530885 0.95     1
5  delta_sigma -1.54448615 2.2991336 0.95     2
6  delta_sigma -1.13077378 2.2677769 0.95     3
7        mu[1] -0.57857771 1.9129402 0.95     1
8        mu[1] -0.44231332 2.1518514 0.95     2
9        mu[1] -0.72385789 1.9029980 0.95     3
10       mu[2]  0.67645041 3.5966548 0.95     1
11       mu[2]  0.73277311 3.5351246 0.95     2
12       mu[2]  0.82185541 3.7170548 0.95     3
13    sigma[1]  1.13639954 3.2315454 0.95     1
14    sigma[1]  1.21810205 3.3089583 0.95     2
15    sigma[1]  1.16014207 3.1437155 0.95     3
16    sigma[2]  1.36961504 3.6640926 0.95     1
17    sigma[2]  1.33611088 3.7986736 0.95     2
18    sigma[2]  1.29994364 3.6564382 0.95     3
19      tau[1]  0.05631105 0.5525126 0.95     1
20      tau[1]  0.06739347 0.5631331 0.95     2
21      tau[1]  0.07112880 0.5738644 0.95     3
22      tau[2]  0.05971804 0.4475736 0.95     1
23      tau[2]  0.03448941 0.4177954 0.95     2
24      tau[2]  0.05576736 0.4694446 0.95     3

Comparación con la prueba t de dos muestras:

t.test(extra ~ drug, data = sleep)

    Welch Two Sample t-test

data:  extra by drug
t = -1.8608, df = 17.776, p-value = 0.07939
alternative hypothesis: true difference in means between group 1 and group 2 is not equal to 0
95 percent confidence interval:
 -3.3654832  0.2054832
sample estimates:
mean in group 1 mean in group 2 
           0.75            2.33 
prop( ~(delta_mu < 0), data = posterior(sleep_jags2) )
 prop_TRUE 
0.06533333 

7.1.4 ROPE (Región de Equivalencia Práctica)

Para saber si una diferencia pequeña carece de relevancia práctica, definimos una región de equivalencia práctica (ROPE). Por ejemplo, si no nos importan diferencias menores a 10 minutos (1/6 horas), la ROPE para \(\delta_\mu\) sería \((-1/6,1/6)\). Podemos verificar si el 95% HDI queda completamente fuera de esa ROPE:

plot_post(
  posterior(sleep_jags2)$delta_mu,
  ROPE     = c(-1/6, 1/6),
  hdi_prob = 0.95
)

$posterior
      ESS     mean   median     mode
var1 3000 1.498507 1.491914 1.436025

$hdi
  prob         lo      hi
1 0.95 -0.5027985 3.29843

$ROPE
          lo        hi  P(< ROPE) P(in ROPE) P(> ROPE)
1 -0.1666667 0.1666667 0.05066667 0.03533333     0.914

7.1.5 Comparaciones pareadas

Los datos originales incluyen otra variable: ID, porque las mismas diez personas fueron evaluadas con cada medicamento. Si queremos comparar directamente ambos medicamentos por sujeto, podemos usar las diferencias individuales:

sleep_wide <-
  datasets::sleep %>% 
  rename(drug = group) %>%
  mutate(drug = paste0("drug", drug)) %>%
  spread(key = drug, value = extra)

sleep_wide <-
  sleep_wide %>%
  mutate(delta = drug2 - drug1)

gf_boxplot(~ delta, data = sleep_wide)

Entonces, para modelar la diferencia \(\delta_i\) de cada sujeto \(i\), podemos usar una distribución \(t\) robusta:

sleep_model4 <- function() {
  for (i in 1:Nsubj) {
    delta[i] ~ dt(mu, 1 / sigma^2, nu)
  }
  mu         ~ dnorm(0, 2)            
  sigma      ~ dunif(0.002, 2000)     
  nuMinusOne ~ dexp(1/29)              
  nu         <- nuMinusOne + 1        
  tau        <- 1 / sigma^2          
}

sleep_jags4 <- jags(
  model             = sleep_model4,
  parameters.to.save = c("mu", "sigma", "nu"),
  data = list(
    delta = sleep_wide$delta,
    Nsubj = nrow(sleep_wide)
  ),
  n.iter = 5000,
  DIC    = FALSE
)
Compiling model graph
   Resolving undeclared variables
   Allocating nodes
Graph information:
   Observed stochastic nodes: 10
   Unobserved stochastic nodes: 3
   Total graph size: 24

Initializing model
summary(sleep_jags4)
fit using jags
 3 chains, each with 5000 iterations (first 2500 discarded), n.thin = 2
 n.sims = 3750 iterations saved
      mu.vect sd.vect  2.5%   25%    50%    75%  97.5%  Rhat n.eff
mu      1.108   0.371 0.312 0.896  1.133  1.353  1.790 1.001  3500
nu     21.579  25.362 1.299 4.406 12.008 29.780 91.843 1.006   400
sigma   1.183   0.493 0.407 0.851  1.129  1.446  2.363 1.002  1200
sleep_mcmc4 <- as.mcmc(sleep_jags4)

mcmc_areas(sleep_mcmc4, prob = 0.95, pars = "mu")

mcmc_areas(sleep_mcmc4, prob = 0.95, pars = "nu")

mcmc_pairs(sleep_mcmc4)

prop( ~(mu > 0), data = posterior(sleep_jags4) )
prop_TRUE 
0.9933333 
hdi(sleep_jags4, pars = c("mu"))
  par        lo       hi prob chain
1  mu 0.4359805 1.832829 0.95     1
2  mu 0.3154208 1.777718 0.95     2
3  mu 0.3856669 1.912459 0.95     3
hdi(sleep_jags4)
    par        lo        hi prob chain
1    mu 0.4359805  1.832829 0.95     1
2    mu 0.3154208  1.777718 0.95     2
3    mu 0.3856669  1.912459 0.95     3
4    nu 1.0043826 69.264565 0.95     1
5    nu 1.0013740 65.134106 0.95     2
6    nu 1.0200925 86.187685 0.95     3
7 sigma 0.2894731  2.089948 0.95     1
8 sigma 0.3436961  2.109135 0.95     2
9 sigma 0.3120321  2.245814 0.95     3

7.2 Regresión Lineal Simple

Cargamos Stan:

library(rstan)
Loading required package: StanHeaders

rstan version 2.32.7 (Stan version 2.32.2)
For execution on a local, multicore CPU with excess RAM we recommend calling
options(mc.cores = parallel::detectCores()).
To avoid recompilation of unchanged Stan programs, we recommend calling
rstan_options(auto_write = TRUE)
For within-chain threading using `reduce_sum()` or `map_rect()` Stan functions,
change `threads_per_chain` option:
rstan_options(threads_per_chain = 1)

Attaching package: 'rstan'
The following object is masked from 'package:R2jags':

    traceplot
The following object is masked from 'package:coda':

    traceplot
The following object is masked from 'package:tidyr':

    extract

7.2.1 Ejemplo: Datos de Galton

Para ilustrar regresión, utilizaremos un conjunto de datos histórico: las alturas recopiladas por Francis Galton, que incluyeron la altura de adultos y la de sus padres.

library(mosaicData)
head(Galton)

Para simplificar, nos quedamos solo con las mujeres y, de cada familia, tomamos un único registro:

set.seed(54321)
GaltonW <-
  Galton %>% 
  filter(sex == "F") %>%
  group_by(family) %>%
  sample_n(1)

Pregunta de estudio: ¿Cómo se relaciona la altura de una persona con la “altura media de los padres”, definida como promedio de padre y madre?

GaltonW <- 
  GaltonW %>%
  mutate(midparent = (father + mother) / 2)

gf_point(height ~ midparent, data = GaltonW, alpha = 0.5)

7.2.2 Modelo de regresión

7.2.2.1 Verosimilitud

El modelo lineal simple asume que cada observación \(y_i\) se distribuye según una normal (o t-Student) cuya media depende linealmente de \(x_i\). Es decir:

\[\begin{align*} y_i &\sim \text{Normal}(\mu_i,\,\sigma),\\ \mu_i &= \beta_0 + \beta_1\,x_i. \end{align*}\]

Algunas variaciones posibles:

  1. Regresión robusta (con t-Student en lugar de normal):

    \[\begin{align*} y_i &\sim t(\mu_i,\,\sigma,\,\nu),\\ \mu_i &= \beta_0 + \beta_1\,x_i. \end{align*}\]

  2. Permitir que la desviación estándar dependa de \(x_i\).

  3. Usar una relación funcional distinta (regresión no lineal).

7.2.2.2 Previas

Debemos asignar previas a los cuatro parámetros: \(\beta_0\), \(\beta_1\), \(\sigma\) y \(\nu\).

  • \(\nu\) (grados de libertad): Una Gamma trasladada centrada alrededor de 30 es una elección genérica que permite flexibilidad para alejarse de la normalidad si hace falta.

  • \(\beta_1\) (pendiente): El estimador MLE de \(\beta_1\) es

    \[ \hat\beta_1 \;=\; r\,\frac{\mathrm{SD}_y}{\mathrm{SD}_x}\,, \]

    donde \(r\) es la correlación muestral entre \(x\) e \(y\). Por tanto, podemos usar una normal previa amplia que cubra al menos el intervalo

    \[ \Bigl(-\,\tfrac{\mathrm{SD}_y}{\mathrm{SD}_x},\;\tfrac{\mathrm{SD}_y}{\mathrm{SD}_x}\Bigr). \]

  • \(\beta_0\) (intersección): El MLE es

    \[ \hat\beta_0 \;=\;\overline{y} \;-\;\hat\beta_1\,\overline{x} \;=\;\overline{y} \;-\;r\,\frac{\mathrm{SD}_y}{\mathrm{SD}_x}\,\overline{x}. \]

    Una normal previa amplia para \(\beta_0\) cubrirá el rango

    \[ \Bigl(\overline{y} - \tfrac{\mathrm{SD}_y}{\mathrm{SD}_x}\,\overline{x},\; \overline{y} + \tfrac{\mathrm{SD}_y}{\mathrm{SD}_x}\,\overline{x}\Bigr). \]

  • \(\sigma\) mide la variabilidad de \(y_i\) alrededor de \(\mu_i\), para un \(x_i\) fijo. Una previa uniforme (muy poco informativa) sobre un rango amplio de \(\sigma\) garantiza cobertura sin forzar demasiado.

7.2.2.2.1 Implementación en JAGS
galton_model <- function() {
  for (i in 1:length(y)) {
    y[i]   ~ dt(mu[i], 1/sigma^2, nu)
    mu[i] <- beta0 + beta1 * x[i]
  }
  sigma ~ dunif(6/100, 6*100)
  nuMinusOne ~ dexp(1/29)
  nu <- nuMinusOne + 1

  # Previas para beta0 y beta1:
  beta0 ~ dnorm(0, 1/100^2)  # "100" es orden de magnitud de las alturas
  beta1 ~ dnorm(0, 1/4^2)    # esperamos pendiente en torno a 1
}
galton_jags <-
  jags(
    model             = galton_model,
    data              = list(y = GaltonW$height, x = GaltonW$midparent),
    parameters.to.save = c("beta0", "beta1", "sigma", "nu"),
    n.iter            = 5000,
    n.burnin          = 2000,
    n.chains          = 4,
    n.thin            = 1
  )

galton_jags
mcmc_combo(as.mcmc(galton_jags))

Observamos que la convergencia no es óptima, debido a la fuerte correlación entre \(\beta_0\) y \(\beta_1\), que genera una “cresta” estrecha en la distribución conjunta y ralentiza al muestreador de Gibbs.

7.2.2.3 Posibles soluciones

  1. Reparametrizar el modelo, para reducir la correlación posterior entre parámetros.
  2. Usar un algoritmo más eficiente (por ejemplo, Hamiltonian Monte Carlo en Stan).

7.3 Reparametrización “centrada”

Definimos

\[ \mu_i \;=\; \alpha_0 \;+\;\alpha_1\,\bigl(x_i - \overline{x}\bigr). \]

Entonces

\[ \alpha_0 + \alpha_1\,(x_i - \overline{x}) \;=\; (\alpha_0 - \alpha_1\,\overline{x}) + \alpha_1\,x_i, \]

de modo que \(\beta_1 = \alpha_1\) y \(\beta_0 = \alpha_0 - \alpha_1,\overline{x}\). Con esta parametrización, \(\alpha_0\) es la media predicha cuando \(x=\overline{x}\), y \(\alpha_1\) sigue siendo la pendiente. La ventaja es que \(\alpha_0\) y \(\alpha_1\) suelen aparecer menos correlacionados en la posterior.

galtonC_model <- function() {
  xbar <- mean(x)
  for (i in 1:length(y)) {
    y[i]   ~ dt(mu[i], 1/sigma^2, nu)
    mu[i] <- alpha0 + alpha1 * (x[i] - xbar)
  }
  sigma ~ dunif(6/100, 6*100)
  nuMinusOne ~ dexp(1/29)
  nu <- nuMinusOne + 1

  alpha0 ~ dnorm(0, 1/100^2)
  alpha1 ~ dnorm(0, 1/4^2)

  # Reconstrucción de beta0 y beta1
  beta0 <- alpha0 - alpha1 * xbar
  beta1 <- alpha1
}
galtonC_jags <-
  jags(
    model             = galtonC_model,
    data              = list(y = GaltonW$height, x = GaltonW$midparent),
    parameters.to.save = c("beta0", "beta1", "alpha0", "alpha1", "sigma", "nu"),
    n.iter            = 5000,
    n.burnin          = 2000,
    n.chains          = 4,
    n.thin            = 1
  )

galtonC_jags
mcmc_combo(as.mcmc(galtonC_jags))

gf_point(beta1 ~ beta0, data = posterior(galtonC_jags), alpha = 0.1)
gf_point(alpha1 ~ alpha0, data = posterior(galtonC_jags), alpha = 0.1)

7.3.1 Análisis posterior

7.3.1.1 Estimación de parámetros

Si nuestro interés principal es la pendiente (generalmente más relevante que el intercepto), podemos resumir \(\beta_1\) con su HDI:

hdi(posterior(galtonC_jags), pars = "beta1")
mcmc_areas(as.mcmc(galtonC_jags), pars = "beta1", prob = 0.95)

Galton observó que \(\beta_1 < 1\), lo que refleja el fenómeno de “regresión hacia la media”: hijos de padres muy altos tienden a ser algo más bajos, y niños de padres bajos, algo más altos.

7.3.1.2 Predicción

Supongamos que la “altura media parental” es \(x=70\). Cada muestra posterior define una t-Student( \(\beta_0 + \beta_1x\), \(\sigma\), \(\nu\) ). La distribución predictiva de la altura de una hija para \(x=70\) se obtiene así:

PosteriorPred <- 
  posterior(galtonC_jags) %>% 
  mutate(mean_daughter = beta0 + beta1 * 70)

gf_dens(~mean_daughter, data = PosteriorPred)

Galton_hdi <-
  PosteriorPred %>% 
  hdi(pars = "mean_daughter")
Galton_hdi

En promedio, esperamos alturas de alrededor de 66–67 pulgadas.

Podemos trazar todas las líneas de regresión de cada muestra posterior y superponer el 95% HDI en \(x=70\):

PosteriorSample <- 
  posterior(galtonC_jags) %>% 
  sample_n(2000)

gf_abline(intercept = ~beta0, slope = ~beta1,
          data = PosteriorSample, alpha = 0.01, color = "steelblue") %>%
  gf_point(height ~ midparent, data = GaltonW, inherit = FALSE, alpha = 0.5) %>%
  gf_errorbar(lo + hi ~ 70, data = Galton_hdi,
              color = "skyblue", width = 0.2, size = 1.2, inherit = FALSE)

Sin embargo, ese HDI solo describe la media (\(\beta_0 + \beta_1\cdot 70\)), no la dispersión individual alrededor de dicha media. Para simular alturas individuales agregamos ruido t-Student con \(\sigma\) y \(\nu\):

PosteriorIndividuals <-
  posterior(galtonC_jags) %>%  
  mutate(new_ht = beta0 + beta1 * 70 + rt(nrow(GaltonW), df = nu) * sigma)

gf_point(new_ht ~ 70, data = PosteriorIndividuals,
         alpha = 0.01, size = 0.7, color = "steelblue") %>%
  gf_point(height ~ midparent, data = GaltonW, inherit = FALSE, alpha = 0.5)

Galton_hdi2 <-
  PosteriorIndividuals %>% 
  hdi(regex_pars = "new_ht")
Galton_hdi2

7.3.1.3 Verificación predictiva posterior con bayesplot

Para evaluar si el modelo reproduce bien los datos, necesitamos:

  • \(y_{\text{obs}}\): vector de observaciones reales (GaltonW$height).
  • \(y_{\text{rep}}\): matriz con simulaciones de \(y\) para cada muestra posterior. Cada fila corresponde a una muestra posterior y cada columna a un individuo.

Podemos generar esas simulaciones con CalvinBayes::posterior_calc():

# Predicción de medias (sin variabilidad individual)
y_avg <- posterior_calc(
  galtonC_jags, 
  height ~ beta0 + beta1 * midparent, 
  data = GaltonW
)

# Predicción individual (con ruido t-Student)
y_ind <- posterior_calc(
  galtonC_jags, 
  height ~ beta0 + beta1 * midparent + rt(nrow(GaltonW), df = nu) * sigma, 
  data = GaltonW
)

Con bayesplot trazamos intervalos predictivos en función de \(x\):

ppc_intervals(GaltonW$height, y_avg, x = GaltonW$midparent)
ppc_intervals(GaltonW$height, y_ind, x = GaltonW$midparent)

Si queremos personalizar, extraemos los datos con ppc_ribbon_data() y luego usamos ggformula:

plot_data <- 
  ppc_ribbon_data(GaltonW$height, y_ind, x = GaltonW$midparent)

# Banda central y cintas de predicción:
plot_data %>%
  gf_ribbon(ll + hh ~ x, fill = "steelblue") %>%
  gf_ribbon(l + h ~ x, fill = "steelblue") %>%
  gf_line(m ~ x, color = "steelblue") %>%
  gf_point(y_obs ~ x, alpha = 0.5)

7.4 Ajuste de modelos con Stan

Al centrar la variable explicativa, JAGS suele converger aceptablemente. No obstante, podemos usar Stan, que maneja la correlación posterior de manera más eficiente y no requiere reparametrización.

El modelo Stan se define en el archivo galton.stan. Para compilarlo:

modelo_galton <- stan_model(file = "galton.stan", verbose = TRUE)

Luego ajustamos MCMC:

galton_stanfit <-
  sampling(
    modelo_galton,
    data = list(
      N = nrow(GaltonW),
      x = GaltonW$midparent,
      y = GaltonW$height
    ),
    chains = 4,
    iter   = 2000,
    warmup = 1000
  )

galton_stanfit
gf_point(beta1 ~ beta0, data = posterior(galton_stanfit), alpha = 0.5)
mcmc_combo(as.mcmc.list(galton_stanfit), pars = c("beta0", "beta1", "sigma", "nu"))

Aunque \(\beta_0\) y \(\beta_1\) sigan correlacionados en la posterior, Stan explora ese espacio con Hamiltonian Monte Carlo sin estancarse como JAGS.


7.4.1 Modelo con dos interceptos (hombres vs. mujeres)

Podemos extender el modelo para incluir a ambos sexos, con pendiente común pero interceptos distintos para hombres y mujeres.

set.seed(12345)
modelo_galton2 <- stan_model(file = "galton2.stan", verbose = TRUE)

GaltonBoth <- 
  mosaicData::Galton %>% 
  mutate(
    midparent = (father + mother) / 2,
    group     = as.numeric(factor(sex)) - 1  # codifica: 0 = F, 1 = M
  ) %>%
  group_by(family) %>%
  sample_n(1)

galton2_stanfit <-
  sampling(
    modelo_galton2,
    data = list(
      N = nrow(GaltonBoth),
      x = GaltonBoth$midparent,
      y = GaltonBoth$height,
      g = GaltonBoth$group
    ),
    chains = 4,
    iter   = 2000,
    warmup = 1000
  )

galton2_stanfit
galton2_mcmc <- as.mcmc.list(galton2_stanfit)
Post_galton2 <- posterior(galton2_stanfit)

mcmc_combo(galton2_mcmc, regex_pars = c("beta", "sigma", "log10nu"))
plot_post(Post_galton2$beta2, 
          xlab = "beta2", 
          hdi_prob = 0.95)
mcmc_areas(galton2_mcmc, regex_pars = "beta2", prob = 0.95)
mcmc_areas(galton2_mcmc, regex_pars = "log10nu", prob = 0.95)

Para verificar predictivamente agrupando por sexo:

yind <-
  posterior_calc(
    galton2_stanfit, 
    yind ~ beta0 + beta1 * midparent + beta2 * group + 
           rt(nrow(GaltonBoth), df = nu) * sigma,
    data = GaltonBoth
  )

ppc_intervals_grouped(
  GaltonBoth$height, yind, 
  group = GaltonBoth$sex, 
  x     = GaltonBoth$midparent
)

ppc_data <-
  ppc_intervals_data(
    GaltonBoth$height, yind, 
    group = GaltonBoth$sex, 
    x     = GaltonBoth$midparent
  )

plot_data <- ppc_data

plot_data %>%
  gf_ribbon(ll + hh ~ x, fill = ~group) %>%
  gf_ribbon(l + h   ~ x, fill = ~group) %>%
  gf_point(y_obs    ~ x, color = ~group) %>%
  gf_facet_grid(group ~ .)

Conclusiones finales:

  • Los diagnósticos muestran que el modelo converge adecuadamente.
  • Las gráficas de verificación posterior no exhiben discrepancias importantes, de modo que la pendiente igual para ambos sexos parece razonable.
  • La distribución “ruido” se aproxima bien a una normal (la mayor parte de la posterior de \(\log_{10}(\nu)\) está por encima de 1.5).
hdi(posterior(galton2_stanfit), regex_pars = "nu", prob = 0.90)
  • El parámetro \(\beta_2\) cuantifica la diferencia en alturas promedio entre hombres y mujeres cuyos padres tienen la misma altura media. El 95% HDI de \(\beta_2\) se obtiene con:
hdi(posterior(galton2_stanfit), regex_pars = "beta")

7.5 Regresión Lineal Múltiple

7.5.1 Ejemplo: SAT

¿Gastar más en educación resulta en puntajes SAT más altos? La prueba SAT es un examen estandarizado utilizado para la admisión a universidades en los Estados Unidos, que evalúa habilidades en lectura crítica, matemáticas y escritura. Datos de 1999 pueden usarse para explorar esta pregunta. Entre otras cosas, los datos incluyen el puntaje SAT promedio total (en una escala de 400 a 1600) y la cantidad de dinero gastado en educación (en miles de dólares por estudiante) en cada estado de Estados Unidos.

Como primer intento, podríamos ajustar un modelo lineal (sat vs expend). Usando centrado, el núcleo del modelo se ve así:

  for (i in 1:length(y)) {
    y[i]   ~ dt(mu[i], 1/sigma^2, nu)
    mu[i] <- alpha0 + alpha1 * (x[i] - mean(x))
  }

alpha1 mide cuánto mejora el rendimiento en el SAT por cada $1000 gastados en educación en un estado. Para ajustar el modelo, necesitamos priors para nuestros cuatro parámetros:

  • nu: Podemos usar nuestro exponencial desplazado habitual.
  • sigma: {Unif}(?, ?)
  • alpha0: {Norm}(?, ?)
  • alpha1: {Norm}(0, ?)

Las interrogaciones dependen de la escala de nuestras covariables.

library(brms)
sat_model <- function() {
  for (i in 1:length(y)) {
    y[i]   ~ dt(mu[i], 1/sigma^2, nu)
    mu[i] <- alpha0 + alpha1 * (x[i] - mean(x))
  }
  nuMinusOne ~ dexp(1/29.0)
  nu        <- nuMinusOne + 1
  alpha0     ~ dnorm(alpha0mean, 1 / alpha0sd^2) 
  alpha1     ~ dnorm(0, 1 / alpha1sd^2)
  sigma      ~ dunif(sigma_lo, sigma_hi * 1000)
  log10nu   <- log(nu) / log(10)    # log10(nu)
  beta0     <- alpha0 - mean(x) * alpha1          # intercepto verdadero
}

Entonces, ¿cómo llenamos los signos de interrogación para este conjunto de datos?

  • sigma: {Unif}(?,?)

    Esto cuantifica la cantidad de variación de un estado a otro entre estados que tienen el mismo gasto por estudiante. La escala del SAT varía de 400 a 1600. Los promedios estatales no estarán cerca de los extremos de esta escala. Una ventana de 6 órdenes de magnitud alrededor de 1 da {Unif}(0.001, 1000), ambos extremos de los cuales están bastante lejos de lo que creemos razonable.

  • alpha0: {Norm}(?, ?)

    alpha0 mide el puntaje SAT promedio para los estados que gastan una cantidad promedio. Dado que los SAT promedio están alrededor de 1000, algo como {Norm}(1000, 100) parece razonable.

  • alpha1: {Norm}(0, ?)

    Este es el más complicado. La pendiente de una línea de regresión no puede ser mucho más que \(\frac{SD_y}{SD_x}\), por lo que podemos estimar esa relación o calcularla a partir de nuestros datos para guiar nuestra elección de prior.

sat_jags <- 
  jags(
    model = sat_model,
    data = list(
      y = SAT$sat,
      x = SAT$expend,
      alpha0mean = 1000,    # SAT scores are roughly 500 + 500
      alpha0sd   = 100,     # broad prior on scale of 400 - 1600
      alpha1sd   = 4 * sd(SAT$sat) / sd(SAT$expend),
      sigma_lo = 0.001,     # 3 o.m. less than 1
      sigma_hi = 1000       # 3 o.m. greater than 1
    ),
    parameters.to.save = c("nu", "log10nu", "alpha0", "beta0", "alpha1", "sigma"),
    n.iter   = 4000,
    n.burnin = 1000,
    n.chains = 3
  ) 
sat_jags
diag_mcmc(as.mcmc(sat_jags))
mcmc_combo(as.mcmc(sat_jags))

Nuestro interés principal es alpha1.

summary_df(sat_jags) %>% filter(param == "alpha1")
plot_post(posterior(sat_jags)$alpha1, xlab = "alpha1", ROPE = c(-5, 5))
hdi(posterior(sat_jags), pars = "alpha1", prob = 0.95)

Esto parece extraño: ¿Realmente podemos aumentar los puntajes SAT reduciendo la financiación a las escuelas? Quizás deberíamos observar los datos en bruto con nuestro modelo superpuesto.

gf_point(sat ~ expend, data = SAT) %>%
  gf_abline(slope = ~ alpha1, intercept = ~ beta0, 
            data = posterior(sat_jags) %>% sample_n(2000),
            alpha = 0.01, color = "steelblue")

Hay mucha dispersión, y la tendencia negativa está fuertemente influenciada por los 4 estados que gastan más (y tienen puntajes SAT relativamente bajos). Para solventar este problema vamos a incluir más covariables.

Tenemos algunos datos adicionales sobre cada estado. Vamos a ajustar un modelo con dos predictores: expend y frac.

SAT %>% head(4)
sat_model2 <- function() {
  for (i in 1:length(y)) {
    y[i]   ~ dt(mu[i], 1/sigma^2, nu)
    mu[i] <- alpha0 + alpha1 * (x1[i] - mean(x1)) + alpha2 * (x2[i] - mean(x2))
  }
  nuMinusOne ~ dexp(1/29.0)
  nu        <- nuMinusOne + 1
  alpha0     ~ dnorm(alpha0mean, 1 / alpha0sd^2) 
  alpha1     ~ dnorm(0, 1 / alpha1sd^2)
  alpha2     ~ dnorm(0, 1 / alpha2sd^2)
  sigma      ~ dunif(sigma_lo, sigma_hi * 1000)
  beta0     <- alpha0 - mean(x1) * alpha1 - mean(x2) * alpha2
  log10nu   <- log(nu) / log(10)
}
sat2_jags <- 
  jags(
    model = sat_model2,
    data = list(
      y = SAT$sat,
      x1 = SAT$expend,
      x2 = SAT$frac,
      alpha0mean = 1000,    # Los puntajes SAT son aproximadamente 500 + 500
      alpha0sd   = 100,     # Prior amplio en la escala de 400 - 1600
      alpha1sd   = 4 * sd(SAT$sat) / sd(SAT$expend),
      alpha2sd   = 4 * sd(SAT$sat) / sd(SAT$frac),
      sigma_lo = 0.001,
      sigma_hi = 1000
    ),
    parameters.to.save = c("log10nu", "alpha0", "alpha1", "alpha2", "beta0","sigma"),
    n.iter   = 4000,
    n.burnin = 1000,
    n.chains = 3
  ) 
sat2_jags
diag_mcmc(as.mcmc(sat2_jags))
mcmc_combo(as.mcmc(sat2_jags))
summary_df(sat2_jags) %>% filter(param == "alpha1")
plot_post(posterior(sat2_jags)$alpha1, xlab = "alpha1", ROPE = c(-5, 5))
hdi(posterior(sat2_jags), pars = "alpha1", prob = 0.95)
summary_df(sat2_jags) %>% filter(param == "alpha2")
plot_post(posterior(sat2_jags)$alpha2, xlab = "alpha2")
hdi(posterior(sat2_jags), pars = "alpha2", prob = 0.95)

Las covariables parecen estar correlacionadas:

gf_point(expend ~ frac, data = SAT) 

por lo que un modelo con interacción puede ser más apropiado:

sat_model3 <- function() {
  for (i in 1:length(y)) {
    y[i]   ~ dt(mu[i], 1/sigma^2, nu)
    mu[i] <- alpha0 + alpha1 * (x1[i] - mean(x1)) + alpha2 * (x2[i] - mean(x2)) +  alpha3 * (x3[i] - mean(x3))
  }
  nuMinusOne ~ dexp(1/29.0)
  nu        <- nuMinusOne + 1
  alpha0     ~ dnorm(alpha0mean, 1 / alpha0sd^2) 
  alpha1     ~ dnorm(0, 1 / alpha1sd^2)
  alpha2     ~ dnorm(0, 1 / alpha2sd^2)
  alpha3     ~ dnorm(0, 1 / alpha3sd^2)
  sigma      ~ dunif(sigma_lo, sigma_hi)
  beta0     <- alpha0 - mean(x1) * alpha1 - mean(x2) * alpha2
  log10nu   <- log(nu) / log(10)
}
sat3_jags <- 
  jags(
    model = sat_model3,
    data = list(
      y = SAT$sat,
      x1 = SAT$expend,
      x2 = SAT$frac,
      x3 = SAT$frac * SAT$expend,
      alpha0mean = 1000,    # SAT scores are roughly 500 + 500
      alpha0sd   = 100,     # broad prior on scale of 400 - 1600
      alpha1sd   = 4 * sd(SAT$sat) / sd(SAT$expend),
      alpha2sd   = 4 * sd(SAT$sat) / sd(SAT$frac),
      alpha3sd   = 4 * sd(SAT$sat) / sd(SAT$frac * SAT$expend),
      sigma_lo = 0.001,
      sigma_hi = 1000
    ),
    parameters.to.save = c("log10nu", "alpha0", "alpha1", "alpha2", "alpha3", "beta0","sigma"),
    n.iter   = 20000,
    n.burnin = 1000,
    n.chains = 3
  ) 
sat3_jags
diag_mcmc(as.mcmc(sat3_jags))
mcmc_combo(as.mcmc(sat3_jags))
mcmc_pairs(as.mcmc(sat3_jags), regex_pars = "alpha")

7.6 Ajuste de un modelo lineal con brms

El paquete brms proporciona una forma simplificada de describir modelos lineales generalizados y ajustarlos con Stan. La función brm() convierte una descripción concisa del modelo en código Stan, lo compila y lo ejecuta. Aquí hay un modelo lineal con sat como respuesta, y expend, frac, y una interacción como predictores.

library(brms)  
sat3_brm <- brm(sat ~ expend * frac, data = SAT)
sat3_stan <- stanfit(sat3_brm)

Stan maneja mejor los parámetros correlacionados que JAGS.

sat3_stan
mcmc_combo(as.mcmc.list(sat3_stan))

Podemos usar stancode() para extraer el código Stan utilizado para ajustar el modelo.

stancode(sat3_brm)

Podemos usar standata() para mostrar los datos que brm() pasa a Stan.

standata(sat3_brm) %>% 
  lapply(head)  # trunca la salida para ahorrar espacio

Supongamos que queremos construir un modelo que tenga el mismo prior y verosimilitud que nuestro modelo JAGS. Aquí están algunos valores que necesitaremos.

4 * sd(SAT$sat) / sd(SAT$expend)
4 * sd(SAT$sat) / sd(SAT$frac)
4 * sd(SAT$sat) / sd(SAT$frac * SAT$expend)

Para usar una distribución t para la respuesta, usamos family = student(). Para establecer los priors, es útil saber cuáles serán los nombres de los parámetros y cuáles serían los priors predeterminados si no hacemos nada. (Si no se lista ningún prior, se usará un prior plano impropio.)

get_prior(
  sat ~ expend * frac, data = SAT,
  family = student()   # distribución para la variable de respuesta
)

Podemos comunicar los priors a brm() de la siguiente manera:

sat3a_brm <- 
  brm(
    sat ~ expend * frac, data = SAT,
    family = student(),
    prior = c(
        set_prior("normal(0,220)", coef = "expend"),
        set_prior("normal(0,11)", coef = "frac"),
        set_prior("normal(0,1.5)", coef = "expend:frac"),
        set_prior("normal(1000, 100)", class = "Intercept"),
        set_prior("exponential(1/30.0)", class = "nu"),
        set_prior("uniform(0.001,1000)", class = "sigma")
    )
  )
sat3a_stan <- stanfit(sat3a_brm)
sat3a_stan
mcmc_combo(as.mcmc.list(sat3a_stan))