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.
We use a subset of the Wenchuan dataset:
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.0006071The 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.
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 0NA when that draw carries too little variability for the
machinery to work on, which happens by two routes. The first is an
indicator whose Rao-Blackwellized draws are constant to double
precision, where the inclusion probability sits at its numerical bound;
sd is then exactly 0. The second is a draw
whose variance is non-zero but falls below the numerical floor of the
autocovariance kernel: mcse and n_eff come
back NA while sd and Rhat are
still reported. In the table above, intrusion-flash takes
the second route — an sd of about 2e-11 with
NA mcse and n_eff and a finite
Rhat. Either way the reading is the same: the inclusion
probability is pinned, not poorly estimated.n_eff.
Few flips are expected on an edge whose inclusion probability sits near
0 or 1, and the asymmetry between the two counts is itself
informative.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])
}
}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.00000000When 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:
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):
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.
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] FALSEE-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 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".
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.
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".
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.9101709The returned list contains the following fields (one value per chain):
TRUE when any of the indicators below suggest the chain may
not have reached stationarity.TRUE if the energy
slope is statistically significant (p < 0.01).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.
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 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):
harm_pred, one of the two fields to watch.TRUE when the chain’s flip rate
exceeds the 1% tolerance after allowing for reference noise.flip_rate against the tolerance plus this floor,
so a chain is only flagged when the effect is larger than the
reference’s own uncertainty.se_mean: se_mcse is the
contribution of the exact reference’s own noise, and se_se
combines it with the spread across audited moves. The harm channel only
flags a lean that is resolved above twice its own uncertainty
(accounting for repeated audits of the same edge and for the reference
noise), so a flag reflects a measured effect rather than noise.amplification is the feedback factor estimated
from the chain’s own inclusion probabilities and the edge prior;
kappa is the predicted shift of the mean inclusion
probability per nat of coherent error (a regime summary: how exposed
this fit is to approximation error, independent of how large the error
turned out to be); harm_pred is the projected distortion
itself; harm_flag is TRUE when that projection
exceeds 0.01 and is resolved. A fit can be flagged here with a quiet
flip rate: that combination means the error is not changing individual
decisions but is consistently leaning the recovered graph in one
direction.n_ent is the number of non-trivial edge moves the gauge
saw, n_ref the number it actually checked against the exact
calculation, and n_capped the number left unchecked because
a per-sweep budget was reached. A small n_ref means both
channels are based on few comparisons and should be read as a rough
indication. On a dense large model the gap is wide by design — a few
tens of checked moves against tens of thousands of non-trivial ones — so
read the audit as a sample. A sample of that size resolves coherent
error, error that leans the same way across edges, which is what
harm_pred targets and what moves a recovered graph. It does
not resolve rare edge-specific failures, and flip_rate is
the flip rate among the audited decisions rather than a guarantee about
every edge.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.
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).