The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

Working with shrinkr in the Tidy Bayesian Ecosystem

Jacob M. Maronge

2026-06-29

knitr::opts_chunk$set(
  collapse = TRUE,
  comment = "#>",
  fig.width = 8,
  fig.height = 6,
  warning = FALSE,
  message = FALSE
)

Overview

shrinkr is designed to work seamlessly with modern R workflows. This vignette shows practical examples of using shrinkr with:

library(shrinkr)
library(distributional)
library(MASS)

# Bayesian ecosystem
library(posterior)
library(bayesplot)
library(tidybayes)
library(ggdist)

# Tidyverse
library(dplyr)
library(tidyr)
library(ggplot2)

theme_set(theme_minimal(base_size = 12))

Example: Multi-Region Clinical Trial

Imagine a clinical trial run across 5 regions testing a new treatment. We have Stage 1 posterior samples from region-specific analyses.

Simulate Stage 1 Results

set.seed(1104)

# True effects (unknown in practice)
true_effects <- c(0.45, 0.60, 0.38, -0.10, 0.65)
region_names <- c("North", "South", "East", "West", "Central")

# Simulate posterior samples from Stage 1
samples_list <- lapply(1:5, function(i) {
  matrix(rnorm(2000, true_effects[i], 0.20), ncol = 1)
})
names(samples_list) <- region_names

Fit shrinkr Model

# Fit mixture approximation
mix <- fit_mixture(samples_list, K_max = 3, verbose = FALSE)

# Specify hierarchical priors
priors <- list(
  mu = dist_normal(0, 5),
  tau = dist_truncated(dist_student_t(3, 0, 1), lower = 0)
)

# Run hierarchical shrinkage
fit <- shrink(
  mixture = mix,
  hierarchical_priors = priors,
  chains = 4,
  iter = 2000,
  warmup = 1000,
  cores = 1,
  seed = 2024,
  refresh = 0
)
#> 
#> SAMPLING FOR MODEL 'stage2_shrinkage' NOW (CHAIN 1).
#> Chain 1: 
#> Chain 1: Gradient evaluation took 9e-06 seconds
#> Chain 1: 1000 transitions using 10 leapfrog steps per transition would take 0.09 seconds.
#> Chain 1: Adjust your expectations accordingly!
#> Chain 1: 
#> Chain 1: 
#> Chain 1: Iteration:    1 / 2000 [  0%]  (Warmup)
#> Chain 1: Iteration:  100 / 2000 [  5%]  (Warmup)
#> Chain 1: Iteration:  200 / 2000 [ 10%]  (Warmup)
#> Chain 1: Iteration:  300 / 2000 [ 15%]  (Warmup)
#> Chain 1: Iteration:  400 / 2000 [ 20%]  (Warmup)
#> Chain 1: Iteration:  500 / 2000 [ 25%]  (Warmup)
#> Chain 1: Iteration:  600 / 2000 [ 30%]  (Warmup)
#> Chain 1: Iteration:  700 / 2000 [ 35%]  (Warmup)
#> Chain 1: Iteration:  800 / 2000 [ 40%]  (Warmup)
#> Chain 1: Iteration:  900 / 2000 [ 45%]  (Warmup)
#> Chain 1: Iteration: 1000 / 2000 [ 50%]  (Warmup)
#> Chain 1: Iteration: 1001 / 2000 [ 50%]  (Sampling)
#> Chain 1: Iteration: 1100 / 2000 [ 55%]  (Sampling)
#> Chain 1: Iteration: 1200 / 2000 [ 60%]  (Sampling)
#> Chain 1: Iteration: 1300 / 2000 [ 65%]  (Sampling)
#> Chain 1: Iteration: 1400 / 2000 [ 70%]  (Sampling)
#> Chain 1: Iteration: 1500 / 2000 [ 75%]  (Sampling)
#> Chain 1: Iteration: 1600 / 2000 [ 80%]  (Sampling)
#> Chain 1: Iteration: 1700 / 2000 [ 85%]  (Sampling)
#> Chain 1: Iteration: 1800 / 2000 [ 90%]  (Sampling)
#> Chain 1: Iteration: 1900 / 2000 [ 95%]  (Sampling)
#> Chain 1: Iteration: 2000 / 2000 [100%]  (Sampling)
#> Chain 1: 
#> Chain 1:  Elapsed Time: 0.144 seconds (Warm-up)
#> Chain 1:                0.078 seconds (Sampling)
#> Chain 1:                0.222 seconds (Total)
#> Chain 1: 
#> 
#> SAMPLING FOR MODEL 'stage2_shrinkage' NOW (CHAIN 2).
#> Chain 2: 
#> Chain 2: Gradient evaluation took 6e-06 seconds
#> Chain 2: 1000 transitions using 10 leapfrog steps per transition would take 0.06 seconds.
#> Chain 2: Adjust your expectations accordingly!
#> Chain 2: 
#> Chain 2: 
#> Chain 2: Iteration:    1 / 2000 [  0%]  (Warmup)
#> Chain 2: Iteration:  100 / 2000 [  5%]  (Warmup)
#> Chain 2: Iteration:  200 / 2000 [ 10%]  (Warmup)
#> Chain 2: Iteration:  300 / 2000 [ 15%]  (Warmup)
#> Chain 2: Iteration:  400 / 2000 [ 20%]  (Warmup)
#> Chain 2: Iteration:  500 / 2000 [ 25%]  (Warmup)
#> Chain 2: Iteration:  600 / 2000 [ 30%]  (Warmup)
#> Chain 2: Iteration:  700 / 2000 [ 35%]  (Warmup)
#> Chain 2: Iteration:  800 / 2000 [ 40%]  (Warmup)
#> Chain 2: Iteration:  900 / 2000 [ 45%]  (Warmup)
#> Chain 2: Iteration: 1000 / 2000 [ 50%]  (Warmup)
#> Chain 2: Iteration: 1001 / 2000 [ 50%]  (Sampling)
#> Chain 2: Iteration: 1100 / 2000 [ 55%]  (Sampling)
#> Chain 2: Iteration: 1200 / 2000 [ 60%]  (Sampling)
#> Chain 2: Iteration: 1300 / 2000 [ 65%]  (Sampling)
#> Chain 2: Iteration: 1400 / 2000 [ 70%]  (Sampling)
#> Chain 2: Iteration: 1500 / 2000 [ 75%]  (Sampling)
#> Chain 2: Iteration: 1600 / 2000 [ 80%]  (Sampling)
#> Chain 2: Iteration: 1700 / 2000 [ 85%]  (Sampling)
#> Chain 2: Iteration: 1800 / 2000 [ 90%]  (Sampling)
#> Chain 2: Iteration: 1900 / 2000 [ 95%]  (Sampling)
#> Chain 2: Iteration: 2000 / 2000 [100%]  (Sampling)
#> Chain 2: 
#> Chain 2:  Elapsed Time: 0.109 seconds (Warm-up)
#> Chain 2:                0.085 seconds (Sampling)
#> Chain 2:                0.194 seconds (Total)
#> Chain 2: 
#> 
#> SAMPLING FOR MODEL 'stage2_shrinkage' NOW (CHAIN 3).
#> Chain 3: 
#> Chain 3: Gradient evaluation took 6e-06 seconds
#> Chain 3: 1000 transitions using 10 leapfrog steps per transition would take 0.06 seconds.
#> Chain 3: Adjust your expectations accordingly!
#> Chain 3: 
#> Chain 3: 
#> Chain 3: Iteration:    1 / 2000 [  0%]  (Warmup)
#> Chain 3: Iteration:  100 / 2000 [  5%]  (Warmup)
#> Chain 3: Iteration:  200 / 2000 [ 10%]  (Warmup)
#> Chain 3: Iteration:  300 / 2000 [ 15%]  (Warmup)
#> Chain 3: Iteration:  400 / 2000 [ 20%]  (Warmup)
#> Chain 3: Iteration:  500 / 2000 [ 25%]  (Warmup)
#> Chain 3: Iteration:  600 / 2000 [ 30%]  (Warmup)
#> Chain 3: Iteration:  700 / 2000 [ 35%]  (Warmup)
#> Chain 3: Iteration:  800 / 2000 [ 40%]  (Warmup)
#> Chain 3: Iteration:  900 / 2000 [ 45%]  (Warmup)
#> Chain 3: Iteration: 1000 / 2000 [ 50%]  (Warmup)
#> Chain 3: Iteration: 1001 / 2000 [ 50%]  (Sampling)
#> Chain 3: Iteration: 1100 / 2000 [ 55%]  (Sampling)
#> Chain 3: Iteration: 1200 / 2000 [ 60%]  (Sampling)
#> Chain 3: Iteration: 1300 / 2000 [ 65%]  (Sampling)
#> Chain 3: Iteration: 1400 / 2000 [ 70%]  (Sampling)
#> Chain 3: Iteration: 1500 / 2000 [ 75%]  (Sampling)
#> Chain 3: Iteration: 1600 / 2000 [ 80%]  (Sampling)
#> Chain 3: Iteration: 1700 / 2000 [ 85%]  (Sampling)
#> Chain 3: Iteration: 1800 / 2000 [ 90%]  (Sampling)
#> Chain 3: Iteration: 1900 / 2000 [ 95%]  (Sampling)
#> Chain 3: Iteration: 2000 / 2000 [100%]  (Sampling)
#> Chain 3: 
#> Chain 3:  Elapsed Time: 0.103 seconds (Warm-up)
#> Chain 3:                0.082 seconds (Sampling)
#> Chain 3:                0.185 seconds (Total)
#> Chain 3: 
#> 
#> SAMPLING FOR MODEL 'stage2_shrinkage' NOW (CHAIN 4).
#> Chain 4: 
#> Chain 4: Gradient evaluation took 6e-06 seconds
#> Chain 4: 1000 transitions using 10 leapfrog steps per transition would take 0.06 seconds.
#> Chain 4: Adjust your expectations accordingly!
#> Chain 4: 
#> Chain 4: 
#> Chain 4: Iteration:    1 / 2000 [  0%]  (Warmup)
#> Chain 4: Iteration:  100 / 2000 [  5%]  (Warmup)
#> Chain 4: Iteration:  200 / 2000 [ 10%]  (Warmup)
#> Chain 4: Iteration:  300 / 2000 [ 15%]  (Warmup)
#> Chain 4: Iteration:  400 / 2000 [ 20%]  (Warmup)
#> Chain 4: Iteration:  500 / 2000 [ 25%]  (Warmup)
#> Chain 4: Iteration:  600 / 2000 [ 30%]  (Warmup)
#> Chain 4: Iteration:  700 / 2000 [ 35%]  (Warmup)
#> Chain 4: Iteration:  800 / 2000 [ 40%]  (Warmup)
#> Chain 4: Iteration:  900 / 2000 [ 45%]  (Warmup)
#> Chain 4: Iteration: 1000 / 2000 [ 50%]  (Warmup)
#> Chain 4: Iteration: 1001 / 2000 [ 50%]  (Sampling)
#> Chain 4: Iteration: 1100 / 2000 [ 55%]  (Sampling)
#> Chain 4: Iteration: 1200 / 2000 [ 60%]  (Sampling)
#> Chain 4: Iteration: 1300 / 2000 [ 65%]  (Sampling)
#> Chain 4: Iteration: 1400 / 2000 [ 70%]  (Sampling)
#> Chain 4: Iteration: 1500 / 2000 [ 75%]  (Sampling)
#> Chain 4: Iteration: 1600 / 2000 [ 80%]  (Sampling)
#> Chain 4: Iteration: 1700 / 2000 [ 85%]  (Sampling)
#> Chain 4: Iteration: 1800 / 2000 [ 90%]  (Sampling)
#> Chain 4: Iteration: 1900 / 2000 [ 95%]  (Sampling)
#> Chain 4: Iteration: 2000 / 2000 [100%]  (Sampling)
#> Chain 4: 
#> Chain 4:  Elapsed Time: 0.101 seconds (Warm-up)
#> Chain 4:                0.081 seconds (Sampling)
#> Chain 4:                0.182 seconds (Total)
#> Chain 4:

Working with posterior Package

The posterior package provides the foundation for working with MCMC draws.

Extract Draws

# Extract all parameters as draws_df
draws <- as_draws_df(fit)

# See what's available
variables(draws)
#> [1] "mu"          "tau"         "theta[1]"    "theta[2]"    "theta[3]"   
#> [6] "theta[4]"    "theta[5]"    "tau_squared" "lp__"

# Extract specific parameters
mu_tau_draws <- extract_mu_tau(fit)
theta_draws <- extract_theta(fit)

Basic Summaries

# Quick summary of all parameters
summarize_draws(draws)
#> # A tibble: 9 × 10
#>   variable     mean  median    sd    mad       q5    q95  rhat ess_bulk ess_tail
#>   <chr>       <dbl>   <dbl> <dbl>  <dbl>    <dbl>  <dbl> <dbl>    <dbl>    <dbl>
#> 1 mu         0.398   0.395  0.182 0.155   1.11e-1  0.700  1.00    1597.    1394.
#> 2 tau        0.306   0.269  0.203 0.173   4.73e-2  0.687  1.00    1175.    1258.
#> 3 theta[1]   0.427   0.424  0.162 0.154   1.68e-1  0.704  1.00    5061.    3283.
#> 4 theta[2]   0.517   0.509  0.171 0.168   2.50e-1  0.811  1.00    4120.    3799.
#> 5 theta[3]   0.378   0.383  0.162 0.154   1.09e-1  0.639  1.00    5106.    3403.
#> 6 theta[4]   0.0997  0.108  0.211 0.219  -2.63e-1  0.430  1.00    2176.    2757.
#> 7 theta[5]   0.549   0.539  0.179 0.183   2.80e-1  0.856  1.00    3853.    3141.
#> 8 tau_squa…  0.135   0.0723 0.196 0.0816  2.23e-3  0.472  1.00    1175.    1258.
#> 9 lp__      -6.44   -6.11   3.01  2.95   -1.17e+1 -2.05   1.00    1240.    2011.

# Focus on theta parameters
summarize_draws(theta_draws, mean, sd, median, mad, ~quantile(.x, c(0.025, 0.975)))
#> # A tibble: 19 × 7
#>    variable         mean    sd   median    mad     `2.5%` `97.5%`
#>    <chr>           <dbl> <dbl>    <dbl>  <dbl>      <dbl>   <dbl>
#>  1 mu           0.398    0.182  0.395   0.155    0.0220     0.800
#>  2 tau          0.306    0.203  0.269   0.173    0.0255     0.806
#>  3 theta_c[1]   0.0282   0.987  0.0260  0.993   -1.89       1.99 
#>  4 theta_c[2]   0.000676 1.02  -0.00862 1.02    -1.93       2.03 
#>  5 theta_c[3]  -0.0272   0.990 -0.0175  0.998   -1.96       1.90 
#>  6 theta_c[4]  -0.00466  0.980 -0.0140  0.971   -1.93       1.88 
#>  7 theta_c[5]   0.0108   1.04  -0.00245 1.06    -1.97       2.09 
#>  8 z[1]         0.108    0.748  0.103   0.711   -1.37       1.63 
#>  9 z[2]         0.421    0.727  0.404   0.701   -1.01       1.88 
#> 10 z[3]        -0.0531   0.711 -0.0742  0.673   -1.46       1.41 
#> 11 z[4]        -1.02     0.783 -1.02    0.770   -2.55       0.518
#> 12 z[5]         0.524    0.753  0.508   0.730   -0.962      2.02 
#> 13 theta[1]     0.427    0.162  0.424   0.154    0.118      0.771
#> 14 theta[2]     0.517    0.171  0.509   0.168    0.204      0.872
#> 15 theta[3]     0.378    0.162  0.383   0.154    0.0491     0.696
#> 16 theta[4]     0.0997   0.211  0.108   0.219   -0.334      0.473
#> 17 theta[5]     0.549    0.179  0.539   0.183    0.230      0.918
#> 18 tau_squared  0.135    0.196  0.0723  0.0816   0.000649   0.649
#> 19 lp__        -6.44     3.01  -6.11    2.95   -13.1       -1.45

# Convergence diagnostics
summarize_draws(draws, default_convergence_measures())
#> # A tibble: 9 × 4
#>   variable     rhat ess_bulk ess_tail
#>   <chr>       <dbl>    <dbl>    <dbl>
#> 1 mu           1.00    1597.    1394.
#> 2 tau          1.00    1175.    1258.
#> 3 theta[1]     1.00    5061.    3283.
#> 4 theta[2]     1.00    4120.    3799.
#> 5 theta[3]     1.00    5106.    3403.
#> 6 theta[4]     1.00    2176.    2757.
#> 7 theta[5]     1.00    3853.    3141.
#> 8 tau_squared  1.00    1175.    1258.
#> 9 lp__         1.00    1240.    2011.

# Custom summaries
summarise_draws(
  theta_draws,
  mean,
  sd,
  prob_positive = ~mean(.x > 0),
  prob_large = ~mean(.x > 0.5)
)
#> # A tibble: 19 × 5
#>    variable         mean    sd prob_positive prob_large
#>    <chr>           <dbl> <dbl>         <dbl>      <dbl>
#>  1 mu           0.398    0.182        0.980     0.247  
#>  2 tau          0.306    0.203        1         0.145  
#>  3 theta_c[1]   0.0282   0.987        0.513     0.313  
#>  4 theta_c[2]   0.000676 1.02         0.494     0.309  
#>  5 theta_c[3]  -0.0272   0.990        0.491     0.298  
#>  6 theta_c[4]  -0.00466  0.980        0.494     0.298  
#>  7 theta_c[5]   0.0108   1.04         0.498     0.323  
#>  8 z[1]         0.108    0.748        0.560     0.286  
#>  9 z[2]         0.421    0.727        0.724     0.448  
#> 10 z[3]        -0.0531   0.711        0.457     0.208  
#> 11 z[4]        -1.02     0.783        0.0855    0.026  
#> 12 z[5]         0.524    0.753        0.764     0.503  
#> 13 theta[1]     0.427    0.162        0.995     0.306  
#> 14 theta[2]     0.517    0.171        1.000     0.518  
#> 15 theta[3]     0.378    0.162        0.988     0.212  
#> 16 theta[4]     0.0997   0.211        0.688     0.0138 
#> 17 theta[5]     0.549    0.179        1.000     0.580  
#> 18 tau_squared  0.135    0.196        1         0.0435 
#> 19 lp__        -6.44     3.01         0.001     0.00025

Check Convergence

# Check Rhat for all parameters
all_rhats <- summarise_draws(draws, "rhat")
max(all_rhats$rhat, na.rm = TRUE)
#> [1] 1.001981

# Check effective sample size
summarise_draws(draws, "ess_bulk", "ess_tail") %>%
  filter(ess_bulk < 400 | ess_tail < 400)
#> # A tibble: 0 × 3
#> # ℹ 3 variables: variable <chr>, ess_bulk <dbl>, ess_tail <dbl>

# Detailed diagnostics for specific parameters
summarise_draws(
  subset_draws(draws, variable = c("mu", "tau")),
  default_convergence_measures()
)
#> # A tibble: 2 × 4
#>   variable  rhat ess_bulk ess_tail
#>   <chr>    <dbl>    <dbl>    <dbl>
#> 1 mu        1.00    1597.    1394.
#> 2 tau       1.00    1175.    1258.

Diagnostic Plots with bayesplot

bayesplot provides essential MCMC diagnostic visualizations.

Trace Plots

Check for mixing and stationarity:

# Check hyperparameters
mcmc_trace(draws, pars = c("mu", "tau", "tau_squared"))


# Check first few thetas
mcmc_trace(draws, regex_pars = "theta\\[[1-3]\\]")


# All thetas at once (if not too many)
mcmc_trace(draws, regex_pars = "theta")

Density Plots

Compare chains and check for multimodality:

# Overlay densities from different chains
mcmc_dens_overlay(draws, pars = c("mu", "tau"))


# Individual densities
mcmc_dens(draws, pars = c("mu", "tau", "tau_squared"))


# Compare all thetas
mcmc_dens_overlay(draws, regex_pars = "theta")

Interval Plots

Visualize posterior uncertainties:

# All thetas with 50% and 95% intervals
mcmc_intervals(draws, regex_pars = "theta", prob = 0.5, prob_outer = 0.95)


# With point estimates
mcmc_intervals_data(draws, regex_pars = "theta") %>%
  ggplot(aes(y = parameter)) +
  geom_pointrange(aes(x = m, xmin = ll, xmax = hh)) +
  geom_point(aes(x = m), size = 3) +
  labs(title = "Posterior Intervals for Regional Effects", x = "Effect Size", y = NULL)

Area Plots

Density plots with shaded intervals:

# Hyperparameters
mcmc_areas(draws, pars = c("mu", "tau"), prob = 0.95, prob_outer = 0.99)


# All thetas
mcmc_areas(draws, regex_pars = "theta", prob = 0.8)

Tidy Analysis with tidybayes

tidybayes makes it easy to manipulate and visualize posteriors using tidy principles.

Spread and Gather Draws

# Gather theta parameters into long format
theta_tidy <- draws %>%
  gather_draws(theta[region]) %>%
  mutate(region = region_names[region])

head(theta_tidy)
#> # A tibble: 6 × 6
#> # Groups:   region, .variable [1]
#>   region .chain .iteration .draw .variable .value
#>   <chr>   <int>      <int> <int> <chr>      <dbl>
#> 1 North       1          1     1 theta      0.596
#> 2 North       1          2     2 theta      0.527
#> 3 North       1          3     3 theta      0.526
#> 4 North       1          4     4 theta      0.355
#> 5 North       1          5     5 theta      0.467
#> 6 North       1          6     6 theta      0.386

# Spread into wide format
theta_wide <- draws %>%
  spread_draws(theta[region]) %>%
  mutate(region = region_names[region])

head(theta_wide)
#> # A tibble: 6 × 5
#> # Groups:   region [1]
#>   region theta .chain .iteration .draw
#>   <chr>  <dbl>  <int>      <int> <int>
#> 1 North  0.596      1          1     1
#> 2 North  0.527      1          2     2
#> 3 North  0.526      1          3     3
#> 4 North  0.355      1          4     4
#> 5 North  0.467      1          5     5
#> 6 North  0.386      1          6     6

Point and Interval Summaries

# Median and 95% quantile intervals
theta_tidy %>%
  group_by(region) %>%
  median_qi(.value, .width = 0.95)
#> # A tibble: 5 × 7
#>   region  .value  .lower .upper .width .point .interval
#>   <chr>    <dbl>   <dbl>  <dbl>  <dbl> <chr>  <chr>    
#> 1 Central  0.539  0.230   0.918   0.95 median qi       
#> 2 East     0.383  0.0491  0.696   0.95 median qi       
#> 3 North    0.424  0.118   0.771   0.95 median qi       
#> 4 South    0.509  0.204   0.872   0.95 median qi       
#> 5 West     0.108 -0.334   0.473   0.95 median qi

# Multiple interval widths
theta_tidy %>%
  group_by(region) %>%
  median_qi(.value, .width = c(0.5, 0.8, 0.95))
#> # A tibble: 15 × 7
#>    region  .value  .lower .upper .width .point .interval
#>    <chr>    <dbl>   <dbl>  <dbl>  <dbl> <chr>  <chr>    
#>  1 Central  0.539  0.419   0.668   0.5  median qi       
#>  2 East     0.383  0.273   0.482   0.5  median qi       
#>  3 North    0.424  0.320   0.527   0.5  median qi       
#>  4 South    0.509  0.399   0.626   0.5  median qi       
#>  5 West     0.108 -0.0406  0.254   0.5  median qi       
#>  6 Central  0.539  0.332   0.788   0.8  median qi       
#>  7 East     0.383  0.174   0.574   0.8  median qi       
#>  8 North    0.424  0.227   0.630   0.8  median qi       
#>  9 South    0.509  0.304   0.744   0.8  median qi       
#> 10 West     0.108 -0.179   0.374   0.8  median qi       
#> 11 Central  0.539  0.230   0.918   0.95 median qi       
#> 12 East     0.383  0.0491  0.696   0.95 median qi       
#> 13 North    0.424  0.118   0.771   0.95 median qi       
#> 14 South    0.509  0.204   0.872   0.95 median qi       
#> 15 West     0.108 -0.334   0.473   0.95 median qi

# Mean and HDI (highest density interval)
theta_tidy %>%
  group_by(region) %>%
  mean_hdi(.value, .width = 0.95)
#> # A tibble: 5 × 7
#>   region  .value  .lower .upper .width .point .interval
#>   <chr>    <dbl>   <dbl>  <dbl>  <dbl> <chr>  <chr>    
#> 1 Central 0.549   0.198   0.881   0.95 mean   hdi      
#> 2 East    0.378   0.0620  0.706   0.95 mean   hdi      
#> 3 North   0.427   0.118   0.771   0.95 mean   hdi      
#> 4 South   0.517   0.194   0.856   0.95 mean   hdi      
#> 5 West    0.0997 -0.295   0.502   0.95 mean   hdi

Custom Summaries with dplyr

# Probability of positive effect
theta_tidy %>%
  group_by(region) %>%
  summarise(
    mean_effect = mean(.value),
    sd_effect = sd(.value),
    prob_positive = mean(.value > 0),
    prob_clinically_meaningful = mean(.value > 0.3),
    .groups = "drop"
  ) %>%
  arrange(desc(prob_positive))
#> # A tibble: 5 × 5
#>   region  mean_effect sd_effect prob_positive prob_clinically_meaningful
#>   <chr>         <dbl>     <dbl>         <dbl>                      <dbl>
#> 1 Central      0.549      0.179         1.000                      0.934
#> 2 South        0.517      0.171         1.000                      0.904
#> 3 North        0.427      0.162         0.995                      0.787
#> 4 East         0.378      0.162         0.988                      0.699
#> 5 West         0.0997     0.211         0.688                      0.182

Computing Contrasts

# Method 1: Using shrinkr's built-in function
L <- rbind(
  "South - North" = c(-1, 1, 0, 0, 0),
  "Central - North" = c(-1, 0, 0, 0, 1),
  "South - West" = c(0, 1, 0, -1, 0)
)
contrasts <- theta_contrasts(fit, L, labels = rownames(L))
summarise_draws(contrasts)
#> # A tibble: 3 × 10
#>   variable        mean median    sd   mad       q5   q95  rhat ess_bulk ess_tail
#>   <chr>          <dbl>  <dbl> <dbl> <dbl>    <dbl> <dbl> <dbl>    <dbl>    <dbl>
#> 1 South - North 0.0904 0.0715 0.219 0.198 -0.251   0.460  1.00    5281.    3574.
#> 2 Central - No… 0.123  0.102  0.226 0.216 -0.226   0.505  1.00    5179.    3583.
#> 3 South - West  0.417  0.404  0.289 0.312 -0.00425 0.923  1.00    2129.    2363.

# Method 2: Using tidybayes compare_levels
theta_wide %>%
  compare_levels(theta, by = region, comparison = "pairwise") %>%
  group_by(region) %>%
  median_qi(theta) %>%
  arrange(desc(theta))
#> # A tibble: 10 × 7
#>    region            theta .lower .upper .width .point .interval
#>    <chr>             <dbl>  <dbl>  <dbl>  <dbl> <chr>  <chr>    
#>  1 South - East     0.119  -0.283 0.615    0.95 median qi       
#>  2 South - North    0.0715 -0.334 0.550    0.95 median qi       
#>  3 North - East     0.0336 -0.366 0.501    0.95 median qi       
#>  4 South - Central -0.0211 -0.481 0.398    0.95 median qi       
#>  5 North - Central -0.102  -0.593 0.302    0.95 median qi       
#>  6 East - Central  -0.151  -0.670 0.241    0.95 median qi       
#>  7 West - East     -0.257  -0.797 0.135    0.95 median qi       
#>  8 West - North    -0.306  -0.907 0.0952   0.95 median qi       
#>  9 West - South    -0.404  -1.03  0.0373   0.95 median qi       
#> 10 West - Central  -0.437  -1.07  0.0277   0.95 median qi

Modern Visualizations with ggdist

ggdist provides publication-ready distribution visualizations.

Halfeye Plots

Eye + interval visualization:

theta_tidy %>%
  ggplot(aes(y = region, x = .value)) +
  stat_halfeye(
    .width = c(0.66, 0.95),
    fill = "steelblue"
  ) +
  geom_vline(xintercept = 0, linetype = "dashed", alpha = 0.5) +
  labs(
    title = "Regional Treatment Effects",
    subtitle = "Posterior distributions with median and 66%/95% intervals",
    x = "Treatment Effect",
    y = NULL
  )

Slab + Interval

Density with separate interval layer:

theta_tidy %>%
  ggplot(aes(y = region, x = .value)) +
  stat_slab(aes(fill_ramp = after_stat(level)), fill = "steelblue", alpha = 0.8) +
  stat_pointinterval(.width = c(0.66, 0.95), position = position_nudge(y = -0.15)) +
  scale_fill_ramp_discrete(range = c(1, 0.2), guide = "none") +
  labs(
    title = "Posterior Densities with Quantile Intervals",
    x = "Treatment Effect",
    y = NULL
  )

Quantile Dotplots

Each dot = quantile of the distribution:

theta_tidy %>%
  ggplot(aes(y = region, x = .value)) +
  stat_dots(quantiles = 100) +
  geom_vline(xintercept = 0, linetype = "dashed", alpha = 0.5) +
  labs(
    title = "Quantile Dotplots",
    subtitle = "Each dot represents 1% of the posterior",
    x = "Treatment Effect",
    y = NULL
  )

Gradient Intervals

Continuous representation of uncertainty:

theta_tidy %>%
  ggplot(aes(y = region, x = .value)) +
  stat_gradientinterval(.width = ppoints(50)) +
  scale_color_brewer(palette = "Blues", guide = "none") +
  labs(
    title = "Gradient Interval Representation",
    x = "Treatment Effect",
    y = NULL
  )

Comparing Pre- and Post-Shrinkage

Extract Both Estimates

# Get pre-shrunk estimates from mixture
pre_shrunk <- summarise_theta(fit) %>%
  mutate(type = "Pre-shrunk")

# Get post-shrunk estimates
post_shrunk <- summarise_theta(fit) %>%
  mutate(type = "Post-shrunk")

# Or use shrinkr's built-in plot
plot(fit, group_names = region_names)

Custom Comparison Plot

# Get the hierarchical mean (mu)
mu_draws <- draws %>% spread_draws(mu)
mu_mean <- mean(mu_draws$mu)

# Combine with Stage 1 samples
stage1_draws <- lapply(seq_along(samples_list), function(i) {
  data.frame(
    region = region_names[i],
    .value = samples_list[[i]][,1],
    type = "Stage 1"
  )
}) %>% bind_rows()

stage2_draws <- theta_tidy %>%
  mutate(type = "Stage 2 (Shrunk)")

# Plot side by side
bind_rows(stage1_draws, stage2_draws) %>%
  ggplot(aes(y = region, x = .value, fill = type)) +
  stat_halfeye(alpha = 0.7, position = position_dodge(width = 0.4)) +
  geom_vline(xintercept = 0, linetype = "dashed", alpha = 0.5, color = "gray50") +
  geom_vline(xintercept = mu_mean, linetype = "solid", alpha = 0.8, 
             color = "darkred", linewidth = 1) +
  annotate("text", x = mu_mean, y = 0.5, 
           label = sprintf("Global mean (μ) = %.2f", mu_mean),
           hjust = -0.1, color = "darkred", size = 3.5) +
  scale_fill_manual(values = c("Stage 1" = "gray70", "Stage 2 (Shrunk)" = "steelblue")) +
  labs(
    title = "Stage 1 vs Stage 2: Effect of Hierarchical Shrinkage",
    subtitle = "Stage 2 estimates are pulled toward the global mean",
    x = "Treatment Effect",
    y = NULL,
    fill = NULL
  ) +
  theme(legend.position = "bottom")

Complete Workflow Example

Here’s a typical analysis workflow using tidy principles:

# 1. Extract and prepare data
analysis_data <- draws %>%
  spread_draws(mu, tau, theta[i]) %>%
  mutate(region = region_names[i])

# 2. Compute summaries
summary_table <- analysis_data %>%
  group_by(region) %>%
  summarise(
    mean = mean(theta),
    median = median(theta),
    sd = sd(theta),
    q025 = quantile(theta, 0.025),
    q975 = quantile(theta, 0.975),
    prob_positive = mean(theta > 0),
    prob_clinically_important = mean(theta > 0.3),
    .groups = "drop"
  ) %>%
  arrange(desc(median))

print(summary_table)
#> # A tibble: 5 × 8
#>   region    mean median    sd    q025  q975 prob_positive prob_clinically_impo…¹
#>   <chr>    <dbl>  <dbl> <dbl>   <dbl> <dbl>         <dbl>                  <dbl>
#> 1 Central 0.549   0.539 0.179  0.230  0.918         1.000                  0.934
#> 2 South   0.517   0.509 0.171  0.204  0.872         1.000                  0.904
#> 3 North   0.427   0.424 0.162  0.118  0.771         0.995                  0.787
#> 4 East    0.378   0.383 0.162  0.0491 0.696         0.988                  0.699
#> 5 West    0.0997  0.108 0.211 -0.334  0.473         0.688                  0.182
#> # ℹ abbreviated name: ¹​prob_clinically_important

# 3. Create advanced figure
library(patchwork)

p1 <- analysis_data %>%
  ggplot(aes(y = reorder(region, theta), x = theta)) +
  stat_halfeye(.width = c(0.66, 0.95), fill = "steelblue") +
  geom_vline(xintercept = 0, linetype = "dashed", alpha = 0.5) +
  labs(
    title = "A. Regional Treatment Effects",
    x = "Effect Size",
    y = NULL
  )

p2 <- analysis_data %>%
  dplyr::ungroup() %>%
  dplyr::select(mu, tau, .draw) %>%
  dplyr::distinct() %>%
  tidyr::pivot_longer(cols = c(mu, tau), names_to = "name", values_to = "value") %>%
  ggplot(aes(x = value, fill = name)) +
  stat_halfeye(alpha = 0.7) +
  facet_wrap(~name, scales = "free", labeller = label_both) +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "B. Hyperparameters",
    x = "Value",
    y = "Density"
  ) +
  theme(legend.position = "none")

p3 <- analysis_data %>%
  dplyr::ungroup() %>%
  dplyr::select(.draw, region, theta) %>%
  compare_levels(theta, by = region) %>%
  ggplot(aes(y = region, x = theta)) +
  stat_halfeye(fill = "coral", alpha = 0.7) +
  geom_vline(xintercept = 0, linetype = "dashed", color = "red", alpha = 0.5) +
  labs(
    title = "C. Pairwise Regional Comparisons",
    x = "Difference in Effect Size",
    y = NULL
  )

p4 <- analysis_data %>%
  dplyr::ungroup() %>%
  dplyr::select(.draw, mu, tau) %>%
  dplyr::distinct() %>%
  ggplot(aes(x = mu, y = tau)) +
  geom_hex(bins = 30) +
  stat_ellipse(level = 0.95, color = "red", linewidth = 1) +
  scale_fill_viridis_c() +
  labs(
    title = "D. Hyperparameter Correlation",
    x = expression(mu~"(global mean)"),
    y = expression(tau~"(heterogeneity)")
  )

(p1 + p2) / (p3 + p4) +
  plot_annotation(
    title = "Complete Bayesian Shrinkage Analysis",
    subtitle = sprintf(
      "Global effect: %.2f [%.2f, %.2f] | Heterogeneity (tau): %.2f",
      median(analysis_data$mu),
      quantile(analysis_data$mu, 0.025),
      quantile(analysis_data$mu, 0.975),
      median(analysis_data$tau)
    )
  )

Advanced: Custom Analyses

Probability Statements

# Which region is best?
analysis_data %>%
  group_by(.draw) %>%
  slice_max(theta, n = 1) %>%
  ungroup() %>%
  count(region) %>%
  mutate(probability = n / sum(n)) %>%
  arrange(desc(probability))
#> # A tibble: 5 × 3
#>   region      n probability
#>   <chr>   <int>       <dbl>
#> 1 Central  1679      0.420 
#> 2 South    1260      0.315 
#> 3 North     625      0.156 
#> 4 East      395      0.0988
#> 5 West       41      0.0102

# Alternative: probability each region is best
analysis_data %>%
  group_by(.draw) %>%
  mutate(rank = rank(-theta)) %>%
  ungroup() %>%
  group_by(region) %>%
  summarise(
    prob_best = mean(rank == 1),
    prob_top2 = mean(rank <= 2),
    mean_rank = mean(rank),
    .groups = "drop"
  ) %>%
  arrange(mean_rank)
#> # A tibble: 5 × 4
#>   region  prob_best prob_top2 mean_rank
#>   <chr>       <dbl>     <dbl>     <dbl>
#> 1 Central    0.420     0.709       2.02
#> 2 South      0.315     0.630       2.23
#> 3 North      0.156     0.372       2.87
#> 4 East       0.0988    0.254       3.20
#> 5 West       0.0102    0.0357      4.67

# Pairwise comparisons: Probability that South > North
# Create wide format for comparisons
theta_wide_for_contrasts <- analysis_data %>%
  ungroup() %>%
  dplyr::select(.draw, region, theta) %>%
  tidyr::pivot_wider(names_from = region, values_from = theta)

theta_wide_for_contrasts %>%
  summarise(
    prob_south_beats_north = mean(South > North),
    prob_south_beats_north_by_02 = mean((South - North) > 0.2),
    prob_central_beats_all = mean(
      Central > North & Central > South & 
      Central > East & Central > West
    )
  )
#> # A tibble: 1 × 3
#>   prob_south_beats_north prob_south_beats_north_by_02 prob_central_beats_all
#>                    <dbl>                        <dbl>                  <dbl>
#> 1                  0.657                        0.289                  0.420

Tail Probabilities

# Classify effects into categories
theta_tidy %>%
  group_by(region) %>%
  summarise(
    prob_harm = mean(.value < -0.1),
    prob_null = mean(abs(.value) < 0.1),
    prob_small_benefit = mean(.value > 0.1 & .value < 0.3),
    prob_large_benefit = mean(.value > 0.3),
    .groups = "drop"
  ) %>%
  arrange(desc(prob_large_benefit))
#> # A tibble: 5 × 5
#>   region  prob_harm prob_null prob_small_benefit prob_large_benefit
#>   <chr>       <dbl>     <dbl>              <dbl>              <dbl>
#> 1 Central   0         0.00325             0.0632              0.934
#> 2 South     0         0.003               0.0932              0.904
#> 3 North     0.001     0.019               0.193               0.787
#> 4 East      0.00325   0.043               0.254               0.699
#> 5 West      0.170     0.316               0.331               0.182

# Visualize classification
theta_tidy %>%
  mutate(
    category = case_when(
      .value < -0.1 ~ "Harm",
      abs(.value) < 0.1 ~ "Null",
      .value > 0.1 & .value < 0.3 ~ "Small Benefit",
      .value > 0.3 ~ "Large Benefit"
    )
  ) %>%
  count(region, category) %>%
  group_by(region) %>%
  mutate(probability = n / sum(n)) %>%
  ggplot(aes(x = probability, y = region, fill = category)) +
  geom_col(position = "stack") +
  scale_fill_manual(
    values = c(
      "Harm" = "red",
      "Null" = "gray",
      "Small Benefit" = "lightblue",
      "Large Benefit" = "darkblue"
    )
  ) +
  labs(
    title = "Classification of Treatment Effects",
    x = "Probability",
    y = NULL,
    fill = "Effect Category"
  ) +
  theme(legend.position = "bottom")

Ranking Analysis

# Compute ranks for each draw
rank_data <- analysis_data %>%
  group_by(.draw) %>%
  mutate(rank = rank(-theta)) %>%
  ungroup()

# Summary statistics
rank_summary <- rank_data %>%
  group_by(region) %>%
  summarise(
    mean_rank = mean(rank),
    median_rank = median(rank),
    prob_rank1 = mean(rank == 1),
    prob_rank2 = mean(rank == 2),
    prob_top3 = mean(rank <= 3),
    .groups = "drop"
  ) %>%
  arrange(mean_rank)

print(rank_summary)
#> # A tibble: 5 × 6
#>   region  mean_rank median_rank prob_rank1 prob_rank2 prob_top3
#>   <chr>       <dbl>       <dbl>      <dbl>      <dbl>     <dbl>
#> 1 Central      2.02           2     0.420      0.289     0.874 
#> 2 South        2.23           2     0.315      0.314     0.854 
#> 3 North        2.87           3     0.156      0.216     0.662 
#> 4 East         3.20           3     0.0988     0.155     0.53  
#> 5 West         4.67           5     0.0102     0.0255    0.0788

# Visualize ranking distribution
rank_data %>%
  ggplot(aes(x = rank, y = reorder(region, -theta))) +
  stat_dots(quantiles = 100) +
  scale_x_continuous(breaks = 1:5) +
  labs(
    title = "Ranking Distribution",
    subtitle = "Each dot represents 1% of posterior draws",
    x = "Rank (1 = best, 5 = worst)",
    y = NULL
  )


# Alternative: bar chart of ranking probabilities
rank_data %>%
  count(region, rank) %>%
  group_by(region) %>%
  mutate(probability = n / sum(n)) %>%
  ggplot(aes(x = rank, y = probability, fill = region)) +
  geom_col() +
  facet_wrap(~region, ncol = 1) +
  scale_x_continuous(breaks = 1:5) +
  scale_fill_brewer(palette = "Set2") +
  labs(
    title = "Probability of Each Rank by Region",
    x = "Rank (1 = best)",
    y = "Probability"
  ) +
  theme(legend.position = "none")

Further Reading

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.