Diagnostics and Spike-and-Slab Summaries

Introduction

This vignette illustrates how to inspect convergence diagnostics and how to interpret spike-and-slab summaries in bgms models. For some of the model variables spike-and-slab priors introduce binary indicator variables that govern whether the effect is included or not. Their posterior distributions can be summarized with inclusion probabilities and Bayes factors.

Example fit

We use a subset of the Wenchuan dataset:

library(bgms)
data = Wenchuan[, 1:5]
fit = bgm(data, seed = 1234)

Convergence diagnostics

The quality of the Markov chain can be assessed with common MCMC diagnostics:

summary(fit)$pairwise
#>                          mean         mcse         sd     n_eff share_incl
#> intrusion-dreams  0.314978026 0.0005097229 0.03234831 4027.4928  0.0000000
#> intrusion-flash   0.168835906 0.0005032092 0.03118034 3839.4093  0.0000000
#> intrusion-upset   0.097580633 0.0018139307 0.03512899  375.0507  0.8858647
#> intrusion-physior 0.099049581 0.0017877360 0.03445517  371.4514  0.9080269
#> dreams-flash      0.249685675 0.0004714174 0.03031425 4135.0649  0.0000000
#> dreams-upset      0.113241752 0.0007722999 0.02761804 1278.8331  0.2983476
#> dreams-physior    0.003157521 0.0004473601 0.01370966  939.1583  0.9069453
#> flash-upset       0.004351199 0.0004855319 0.01632962 1131.1408  0.8138045
#> flash-physior     0.153083191 0.0004645719 0.02649353 3252.1731  0.0000000
#> upset-physior     0.354810254 0.0004896631 0.02986192 3719.1250  0.0000000
#>                        Rhat
#> intrusion-dreams  1.0004346
#> intrusion-flash   1.0009076
#> intrusion-upset   1.0020848
#> intrusion-physior 1.0037932
#> dreams-flash      0.9999779
#> dreams-upset      1.0001267
#> dreams-physior    1.0067551
#> flash-upset       1.0004642
#> flash-physior     1.0004835
#> upset-physior     1.0006071

The 1.01 rule is a guideline for continuous parameters. It should not be applied directly to the binary edge (or difference) indicators. A decisive edge produces a nearly saturated indicator chain — the inclusion probability sits close to 0 or 1 and the indicator only rarely flips — so R-hat carries little information and can be NA (all chains identical) or large (chains stuck in different structures). Read the indicator R-hat together with the directional transition counts (n0->1 and n1->0, reported in the indicator summary) and the posterior inclusion probability: for a saturated or barely-flipping indicator the statistic is uninformative rather than a sign of trouble. The indicator R-hat is most useful when it is large while the inclusion probability is near 0.5 and the transition counts show the chains disagreeing on inclusion.

Diagnostics for edge-selected parameters

With edge or difference selection active, the effect parameters are governed by spike-and-slab priors. The corresponding parameter is set to exactly zero when the effect is excluded, rather than being removed from the model. Because the parameter has a well-defined value at every iteration, mean, sd and Rhat are computed from the full chain, zeros included. The reported n_eff is deliberately not a full-chain ESS: run through the standard autocorrelation machinery, the spike-and-slab chain inflates the ESS in cells where the inclusion indicator, rather than the weight itself, drives the variability. The package therefore reports a composite estimator built from the two Monte Carlo errors that actually limit the model-averaged weight.

The inclusion table carries its own diagnostics:

summary(fit)$indicator
#>                         mean        mcse           sd    n_eff      Rhat n0->1
#> intrusion-dreams  1.00000000          NA 0.000000e+00       NA        NA     0
#> intrusion-flash   1.00000000          NA 2.206767e-11       NA 0.9999966     0
#> intrusion-upset   0.95575792 0.016726253 2.018832e-01 145.6811 1.0075613     9
#> intrusion-physior 0.96092900 0.016528122 1.901965e-01 132.4214 1.0180140    11
#> dreams-flash      1.00000000          NA 0.000000e+00       NA        NA     0
#> dreams-upset      0.99478669 0.003705567 7.028964e-02 359.8101 1.0030833     3
#> dreams-physior    0.07122807 0.009175093 2.134445e-01 541.1889 1.0073597    81
#> flash-upset       0.09252153 0.009286142 2.523770e-01 738.6335 1.0012897   102
#> flash-physior     1.00000000          NA 2.638065e-10       NA 0.9999541     0
#> upset-physior     1.00000000          NA 0.000000e+00       NA        NA     0
#>                   n1->0
#> intrusion-dreams      0
#> intrusion-flash       0
#> intrusion-upset       9
#> intrusion-physior    11
#> dreams-flash          0
#> dreams-upset          3
#> dreams-physior       81
#> flash-upset         101
#> flash-physior         0
#> upset-physior         0

Traceplots

Users can inspect traceplots by extracting raw samples directly. Here is an example for the pairwise effect parameter.

param_index = 1
chains = fit$raw_samples$pairwise
nchains = length(chains)
cols = c("firebrick", "steelblue", "darkgreen", "goldenrod")

plot(chains[[1]][, param_index],
  type = "l", col = cols[1],
  xlab = "Iteration", ylab = "Value",
  main = "Traceplot of pairwise[1]",
  ylim = range(sapply(chains, function(ch) range(ch[, param_index])))
)
if(nchains > 1) {
  for(c in 2:nchains) {
    lines(chains[[c]][, param_index], col = cols[c])
  }
}

Spike-and-slab summaries

The spike-and-slab prior yields posterior inclusion probabilities for edges:

coef(fit)$indicator
#>           intrusion     dreams      flash      upset    physior
#> intrusion 0.0000000 1.00000000 1.00000000 0.95575792 0.96092900
#> dreams    1.0000000 0.00000000 1.00000000 0.99478669 0.07122807
#> flash     1.0000000 1.00000000 0.00000000 0.09252153 1.00000000
#> upset     0.9557579 0.99478669 0.09252153 0.00000000 1.00000000
#> physior   0.9609290 0.07122807 1.00000000 1.00000000 0.00000000

Bayes factors

When the prior inclusion probability for an edge is equal to 0.5 (e.g., using edge_prior = bernoulli_prior(0.5), the default, or a symmetric Beta prior edge_prior = beta_bernoulli_prior(alpha, beta) with alpha == beta), we can directly transform inclusion probabilities into Bayes factors for edge presence vs absence:

# Example for one edge
p = coef(fit)$indicator[1, 5]
BF_10 = p / (1 - p)
BF_10
#> [1] 24.59443

Here the Bayes factor in favor of inclusion (H1) is 24.59: the data are about 25 times more likely under a model that includes the intrusion-physior edge than under one that excludes it, which is substantial evidence for inclusion. On the natural log scale that the package uses elsewhere this is a log Bayes factor of 3.2, and verdicts() — see the model-checking vignette — labels this edge a presence.

Since the Bayes factor is transitive, the same number expressed the other way around gives the evidence in favor of exclusion (H0):

1 / BF_10
#> [1] 0.04065962

This is the Bayes factor for absence, BF_01, and at 0.041 it sits well below 1: it is evidence against absence rather than for it — the mirror image of the reading above. Absence is what a large BF_01 would show: a value of, say, 10 or more, which is the same as a BF_10 of 0.1 or less, would be strong evidence that there is no edge between two variables.

NUTS diagnostics

When using update_method = "nuts" (the default), additional diagnostics are available to assess the quality of the Hamiltonian Monte Carlo sampling. These can be accessed via fit$nuts_diag:

fit$nuts_diag$summary
#> $total_divergences
#> [1] 0
#> 
#> $max_tree_depth_hits
#> [1] 0
#> 
#> $min_ebfmi
#> [1] 0.9418742
#> 
#> $mean_accept_prob
#> [1] 0.8717974
#> 
#> $warmup_incomplete
#> [1] FALSE

E-BFMI

E-BFMI (Energy Bayesian Fraction of Missing Information) measures how efficiently the sampler explores the posterior. It compares the typical size of energy changes between successive samples to the overall spread of energies. Values close to 1 indicate that the sampler moves freely across the energy landscape; values below 0.3 suggest the sampler may be getting stuck or that the chain has not yet settled into its stationary distribution.

A low E-BFMI does not necessarily mean your results are wrong, but it does warrant further investigation. In models with edge selection, the most common cause is that the warmup period was too short for the discrete graph structure to equilibrate. Increasing warmup often resolves this.

Divergent transitions

Divergent transitions occur when the numerical integrator encounters regions of the posterior where the curvature changes too rapidly for the current step size. A small number of divergences (say, fewer than 0.1% of samples) is generally acceptable. However, many divergences indicate that the sampler may be missing important parts of the posterior.

If you see a large number of divergences, consider increasing target_accept (which makes the sampler use a smaller step size) and, if this does not fix it, switching to update_method = "adaptive-metropolis".

Tree depth

NUTS builds trajectories by repeatedly doubling their length until a “U-turn” criterion is satisfied. If the trajectory frequently reaches the maximum allowed depth (nuts_max_depth, default 10), it suggests the sampler may benefit from longer trajectories to explore the posterior efficiently. Hitting the maximum depth occasionally is normal; hitting it on most iterations may indicate challenging posterior geometry. If this happens, consider increasing nuts_max_depth.

Non-reversible steps

For MRFs with continuous variables, the leapfrog integrator enforces equality constraints through a projection step. After each forward step, the integrator checks whether reversing the step returns to the starting point. When the round-trip error exceeds a tolerance scaled by the square of the step size, the step is flagged as non-reversible.

A small number of non-reversible steps is not a concern. A large number indicates that the step size is too large for the constraint geometry. Because the step size is tuned during warmup, the most effective remedy is to increase warmup so the adapter has more time to find an appropriate step size. If non-reversible steps persist after increasing warmup, switch to update_method = "adaptive-metropolis".

Warmup and equilibration

Standard HMC/NUTS warmup is designed to tune the step size and mass matrix for the continuous parameters. In models with edge selection, the discrete graph structure may take longer to reach its stationary distribution than the continuous parameters. As a result, even after warmup completes, the first portion of the sampling phase may still show transient behavior (i.e., non-stationarity).

The warmup_check component provides simple diagnostics that compare the first and second halves of the post-warmup samples:

fit$nuts_diag$warmup_check
#> $warmup_incomplete
#> [1] FALSE FALSE
#> 
#> $energy_slope
#>     time_idx     time_idx 
#> 0.0001064247 0.0004421248 
#> 
#> $slope_significant
#> time_idx time_idx 
#>    FALSE    FALSE 
#> 
#> $ebfmi_first_half
#> [1] 0.9438559 0.9885577
#> 
#> $ebfmi_second_half
#> [1] 0.9400481 0.9605412
#> 
#> $var_ratio
#> [1] 1.0093544 0.9101709

The returned list contains the following fields (one value per chain):

If these diagnostics suggest the chain was still settling, increase warmup and re-run the model. If diagnostics remain problematic after a substantial increase (for example, doubling or tripling warmup), consider re-fitting with update_method = "adaptive-metropolis" and comparing the posterior summaries. If the two samplers produce similar results, the estimates are likely trustworthy despite the warnings; if they differ substantially, that warrants further investigation of the model or data.

Hierarchical prior trust gauge

When you use the hierarchical graph prior (precision_graph_prior = "hierarchical"), the sampler must weigh, for every candidate edge, how the model’s normalizing constant changes when that edge is added or removed. Computing this exactly at every step would be prohibitively slow, so the sampler uses a fast approximation in its place. The approximation is accurate across the prior scales and model sizes it was calibrated on, but it can drift outside that range, most often when there are many variables or a very wide prior scale. When it drifts far enough, it starts to influence which edges the sampler includes, so the recovered graph reflects the shortcut rather than the data and prior alone. The trust gauge exists to tell you whether that is happening in your fit.

The two alarms

The gauge runs by default. It audits the chain’s decisions in a short set of assessment sweeps after sampling: on a subset of the edge moves the chain actually made, it redoes the add/remove decision the exact (slow) way and checks whether the outcome would have been different. Auditing the chain’s own moves is deliberate: it measures the approximation where the sampler is actually spending its time, rather than at arbitrary test configurations. The audit is produced under the hierarchical prior, and its summary is stored in fit$zratio_diag, with one row per chain. Turn it off with options(bgms.zratio_gauge_sweeps = 0L) before fitting, which leaves fit$zratio_diag as NULL.

Only edge moves whose mediating block holds two or more variables are audited. On a sparse posterior no block is that large, the normalizer ratio is exact, and the gauge reports nothing because there is nothing to audit: silence there means the approximation was never exercised, not that it went unchecked. Its cost follows the same rule. Two sweeps audit a capped number of moves regardless of how long the chain ran, so the gauge adds nothing to a sparse fit and about 5 to 10 seconds per chain to a dense fit at 100 to 200 variables — negligible against a production-length run, and worth switching off for short exploratory ones.

The gauge reports two complementary alarms. The first is flip_rate, the fraction of audited decisions that would have come out differently under the exact calculation. A chain is flagged on this channel above a 1% tolerance, after allowing for the small amount of Monte Carlo noise in the exact reference itself. Because the flip rate counts decisions rather than measuring a distance in abstract units, the 1% tolerance means the same thing regardless of the number of variables or the choice of prior.

The flip rate only registers error that lands near an accept/reject boundary, so a small but consistent error can pass it silently while still shifting the recovered inclusion probabilities: applied to every move a chain makes, a consistent lean relocates where the chain settles even when no single decision visibly changes. The second channel, harm_pred, projects the measured error onto that scale directly: it averages each audited edge’s signed error weighted by that edge’s own inclusion sensitivity (an error on an edge the data have pinned near 0 or 1 cannot move the result and does not count), then multiplies by the feedback amplification of the edge prior (learned inclusion probabilities amplify a consistent lean; a fixed inclusion probability does not). The result is the first-order predicted distortion of the mean posterior inclusion probability, flagged when it exceeds 0.01 and the weighted error is resolved above its own Monte Carlo uncertainty. It is a targeted alarm for a coherent lean, not a bound on all approximation error: edge-specific distortions that cancel in the mean are outside its scope. This channel is computed for the Bernoulli and Beta-Bernoulli edge priors; under other priors it is reported as NA.

The per_chain data frame in fit$zratio_diag records the following fields (one row per chain):

Fits that skip the approximation instead

The approximation described above corrects each edge weight for the mediating structure around that edge: the other variables that connect its two endpoints. How much that correction matters depends on the precision diagonal’s prior. A gamma_prior() shape above 10 concentrates the diagonal enough that the precision matrix becomes diagonally dominant and the mediating correction all but disappears, and past that shape the approximation has not been scored.

bgms handles that case by not approximating. Above shape 10 it drops the mediating correction and uses the isolated-edge value, which is exact for an edge with no mediating structure at all, so the entire error is the mediation left out. Measured against the slow exact calculation at shapes 12, 15 and 20, that is at most 0.00028 nats — roughly a tenth of the 0.003 nats the approximation is held to inside its scored range. A fit that takes this route says so in a note and records the count as n_isolated in its per-chain counters.

The note carries one caveat worth reading. The mediating correction grows with the standardized diagonal rate (eta), and the bound above was measured only at rates up to 2. Above that rate the same route is used, and the note says plainly that the measured bound does not cover your fit. The trust gauge still runs, and on such a fit it measures that residual directly rather than relying on the bound, so it remains the thing to read.

What to do when a chain is flagged

A flagged chain tells you the approximation is affecting that chain’s recovered structure — a flag on the flip channel means individual edge decisions are changing; a harm_flag means a consistent lean is shifting the inclusion probabilities — so the structure should be treated with caution. The approximation is a smooth surface fitted once per analysis over a range of component sizes and densities; it drifts when a fit visits graphs outside that trained range, most often in large or unusually dense models. Work through the following in order.

First, read what was measured. The flagged bullet reports the error share and the mediating-block sizes the audit covered (block_lo and block_hi in per_chain), and n_ref against n_ent says how much of the chain the audit saw. A flag driven by blocks well beyond the anchored range is a different situation from one driven by ordinary-sized blocks. The surface is anchored to components of up to 80 variables, and beyond that it is continued along its own boundary slope; a fit that relies on the continuation says so in a separate note, with the retained share and the largest block involved.

Second, resolve whether the signal is real. Raise options(bgms.zratio_gauge_sweeps) above its default of 2 and re-fit. More sweeps audit more moves, which narrows the Monte Carlo uncertainty on both channels; a flag that survives a larger audit is a measured effect, and one that disappears was noise. Narrowing the prior scale toward its trained range is worth trying at this point too, since a very wide scale is the other common cause of drift.

Third, and only if a flag persists, consider the joint prior specification, precision_graph_prior = "joint", which evaluates the edge weights without this approximation. Note what that change does: the joint specification is a different model, not a more accurate version of this one. Its graph marginal is the edge prior reweighted by the per-graph normalizer rather than the edge prior itself, so the inferential target moves. bgms will never make that switch for you.

You can also keep the recovered structure and treat the flagged edges as provisional, confirming them another way, for example by re-fitting the neighborhood of those edges with a narrower prior and checking that they survive.

For evidence-free fits (prior-only sampling, or data too weak to pin the edges) under a Beta-Bernoulli edge prior, a harm_pred flag deserves extra weight: the learned inclusion probability amplifies a small per-edge lean, and a narrower scale may not remove the residual. A direct check is available: under either specification the average posterior inclusion probability of a prior-only fit must equal the edge prior’s mean, which for beta_bernoulli_prior(a, b) is a / (a + b).

Next steps

Vehtari, A., Gelman, A., Simpson, D., Carpenter, B., & Bürkner, P.-C. (2021). Rank-normalization, folding, and localization: An improved \(\hat{R}\) for assessing convergence of MCMC. Bayesian Analysis, 16, 667–718. https://doi.org/10.1214/20-BA1221