vignette("diagnostics", package = "frmtmb") asks three
questions of a maximum-likelihood fit: did the optimizer converge, does
the family describe the data, and are the reported uncertainties
trustworthy.
A sampled fit reorders that list. There is no optimizer to interrogate, so the first question becomes whether the sampler explored the posterior it was given. The third question changes character: the posterior IS the uncertainty statement, so the interesting comparison is against the maximum-likelihood fit that the same objective produced.
Every chunk here is shown and not run, because every one needs a
sampler this package only suggests. vignette("sampling")
says more about why, and its own prior tables do run.
library(frmtmb)
library(frmtmb.sample)
set.seed(9)
dd <- data.frame(x = rnorm(80), g = factor(rep(1:8, 10)))
dd$y <- rnorm(80, 1 + 0.5 * dd$x + rnorm(8, 0, 0.5)[dd$g], 1)
fit <- frm(bf(y ~ x + (1 | g)) + gaussian(), data = dd)
ds <- frm_sample(fit, chains = 4)Did the sampler explore the posterior?
summary(ds) puts Rhat,
Bulk_ESS and Tail_ESS beside each estimate,
which is where to start. Read the pattern and not only the worst entry.
One parameter with a low effective sample size and a clean
Rhat is a slow chain. Several at once usually means the
geometry is wrong rather than the run too short, and sampling longer
will not fix it. A high Bulk_ESS with a low
Tail_ESS says the centre of the posterior is well explored
and the extreme quantiles are not, so an interval is less trustworthy
than a mean.
summary(ds)
rhat(ds)
neff_ratio(ds)These are brms’s diagnostics throughout, computed by the posterior
package on these draws and reported under brms’s names, which
variables(ds) lists. rhat() is the
rank-normalized split-R-hat, neff_ratio() is
min(ess_bulk, ess_tail) / ndraws, and
summary(ds) reports the same Rhat with the two
effective sizes it takes the minimum of. The three columns and the two
accessors are one set of numbers: summary(ds)[, "Rhat"] IS
rhat(ds) on those rows.
None of them is what the sampler itself recorded. Stan’s own classic
split-R-hat and n_eff are a different definition of the
same idea, and they are in
rstan::summary(ds$stanfit)$summary if you want to compare.
Expect them to disagree by about the size of the excess over 1 that
either R-hat reports.
The sampler’s own record is in nuts_params(), with the
divergent transitions, the tree depths and the step size per iteration,
and the log posterior is log_posterior(). Both feed
bayesplot’s mcmc_nuts_*() displays.
np <- nuts_params(ds)
sum(np$Value[np$Parameter == "divergent__"]) # zero is the target
mcmc_plot(ds, type = "trace")
mcmc_plot(ds, type = "rank_overlay")
pairs(ds, variable = c("theta_1", "r_g[1,Intercept]"))Divergences concentrate in the funnel of a random-effect block, so
the pairs() display to draw is a group’s log standard
deviation against something it multiplies.
Three moves, in the order worth trying:
-
Give the variance parameters a prior.
reparameterize = TRUEis the default, but a block qualifies for the non-centered form only when every one of its variance parameters carries a prior, and the call reports each block it left centered and why. Underprior = "flat"a standard deviation is flat on its own scale, as brms’s(flat)is, which closes the tail atsd = 0, so a block qualifies there too.prior = set_prior("exponential(1)", class = "sd")is the usual fix, and it fixes two things at once: it closes the flat tail atsd = 0and it unlocks the reparameterization that removes the funnel. -
Sample longer.
iter =andchains =pass straight to tmbstan. A lowBulk_ESSwithRhatnear 1 is answered here. -
Tighten the sampler.
frm_sample(fit, control = list(adapt_delta = 0.99))is brms’s spelling and brms’s meaning: the list goes to rstan through tmbstan. Raisingadapt_deltashortens the step size the adaptation settles on, which is what removes the divergences a residual funnel produces, and the cost is wall-clock time.max_treedepth = 12answers the companion warning. Read the step size back withnuts_params(ds)to see what the change bought. The fit-time options offrmtmb_control()arefit_controlin this function.
One check comes before all of these. A tmbstan built against
StanHeaders 2.39 samples a standard normal instead of the model, and
every diagnostic on this page looks perfect when it does.
frm_sample() refuses such a build before sampling; the
sampling vignette has the detail.
Does the model describe the data?
pp_check() overlays the observed response with responses
drawn from the posterior predictive distribution. This is the check brms
users already run, on the same bayesplot displays, and it is a stronger
statement than the maximum-likelihood version in core, because the draws
carry parameter uncertainty into the simulated responses.
loo() is the real leave-one-out here, not the
AIC() substitute core offers. It runs Pareto-smoothed
importance sampling on the log_lik() matrix and returns the
loo package’s own object, so print() and
loo::pareto_k_table() work on it unchanged.
loo(ds)
# a comparison is two sampled models, each with its own elpd
ds0 <- frm_sample(bf(y ~ 1 + (1 | g)) + gaussian(), data = dd, chains = 4)
loo_compare(ds, ds0)A high Pareto k is a real finding here, because the refit-based
remedies are absent: reloo(), kfold(),
loo_moment_match() and loo_subsample() all
refuse, and say so, since each needs to refit the model on modified
data. What to do instead depends on why k is high. Under
prior = "flat" the elpd is unregularized and a model with
many group-level parameters will produce those warnings by construction,
so the first move is to sample under the default priors and read the
number again. If the warnings survive that, the observations loo names
are genuinely influential: compare the models by AIC() on
the maximum-likelihood fits, or measure the influence directly with
frmtmb::frm_bootstrap().
Read every one of these numbers as a posterior quantity. Both of
frm_sample()’s routes apply brms’s default priors, so an
elpd is regularized the way brms regularizes it unless the call opted
out.
Does the Laplace approximation hold?
frmtmb::frm() integrates random effects out with a
Laplace approximation and reports Wald standard errors. Both assumptions
degrade in the same places: variance components from few groups, binary
data in small clusters. Core routes around that with
confint(method = "profile"), frm_bootstrap()
and frm(quadrature = TRUE). This package measures it
instead.
check_laplace() samples the density the fit maximized,
and no other. It hands Stan fit$obj as it stands, which is
why it opts out of frm_sample()’s default priors explicitly
rather than inheriting them: a prior nothing in the fit ever saw would
change the very thing being measured. For an ordinary fit that objective
is the likelihood, so the run is flat. For a MAP fit
(frm(prior = )) the penalty is taped into the objective, so
the run carries it, which is right, because the mode and the Wald
standard errors it compares against come from the same penalized
Hessian.
cl <- check_laplace(fit, chains = 2, iter = 1000)
clOne row per outer parameter, with the maximum-likelihood estimate
(ml), the posterior mean (post_mean), the two
uncertainty statements (wald_se, post_sd), and
the two comparisons worth reading:
-
z_shiftis(post_mean - ml) / post_sd, the distance from the mode to the posterior mean in posterior standard deviations. It measures asymmetry the quadratic approximation cannot see. -
sd_ratioispost_sd / wald_se. Above one, the Wald interval is too narrow.
# the parameters whose Wald interval to replace with a profile or
# bootstrap one
cl[abs(cl$z_shift) > 0.3 | cl$sd_ratio > 1.3, ]Those thresholds are working values, not a test. A variance component estimated from eight groups will fail them and should: the log standard deviation has a skewed likelihood at that sample size, and the skew is the finding.
Because the defaults are off, this function samples the centered
parameterization on an ordinary fit. That costs it nothing. The
non-centered form is offered only to blocks whose variance parameters
carry priors, and the flat prior that makes this comparison meaningful
is exactly the one that leaves a tail at sd = 0 for a
non-centered chain to walk into. Give the variance parameters a prior
through prior = and the run does non-center, but it is then
measuring the Laplace approximation of a different posterior, which is
usually not the question.
Which page to read
| question | where |
|---|---|
| did the optimizer converge | vignette("diagnostics", package = "frmtmb") |
| does the family describe the data (ML) | same page: dharma_residuals(),
residuals(type = "osa")
|
| did the chains converge | here |
| does the model describe the data (posterior) | here: pp_check(), loo()
|
| is the Laplace approximation good enough | here: check_laplace()
|
vignette("sampling", package = "frmtmb.sample") is the
page on getting the draws in the first place: the two routes, the
default priors, and the non-centered parameterization.