Diagnostics, reproducibility, and migration
Source:vignettes/WorkflowReliability.Rmd
WorkflowReliability.RmdFit once and keep the inference settings
data(example_data)
model <- wsmed_model(
outcome = c(before = "D1", after = "D2"),
mediators = list(A = c(before = "A1", after = "A2")),
moderator = list(
variable = "D3",
interactions = "A -> Y"
)
)
fit <- wsmed_fit(
model,
example_data
)
inference <- wsmed_infer(
fit,
draws = 1000,
seed = 604,
level = .90
)This tutorial uses 1,000 draws for speed; the default is 20,000 MC
draws. Summary, interval extraction, and plotting keep the stored
confidence level. An explicit level changes the quantiles
computed from stored joint draws. It does not refit, reimpute, or draw
new samples.
effects <- wsmed_effects(
inference,
at = list(D3 = c(.2, .5)),
scale = "marginal"
)
summary(effects)
#> wsMed indirect | marginal scale | mc
#> 90% percentile intervals; 1000 joint draws; SE from effect draws
#> Condition contrast: after - before
#> Moderator: D3 in raw units; centering reference = 0.452271
#> Standardization: effect / marginal model-implied SD(D2 - D1); condition contrast is unscaled.
#> Plug-in marginal outcome-difference SD: 0.169194
#> The denominator is not a moderator-specific conditional SD.
#> Estimate Std. Error CI lower CI upper
#> indirect_1@1: A [D3 = 0.2] 0.01950 0.03638 -0.03063 0.08758
#> indirect_1@2: A [D3 = 0.5] 0.00886 0.01873 -0.02071 0.04337
#> Inference draws: 1000 retained of 1000 ; 0 excluded before effect extraction.
#> Use as.data.frame() for the full-precision table and metadata.
confint(effects) # stored 90% level
#> 5 % 95 %
#> indirect_1@1 -0.03062835 0.08758182
#> indirect_1@2 -0.02071058 0.04336693
confint(
effects,
level = .95
) # explicit override
#> 2.5 % 97.5 %
#> indirect_1@1 -0.04736114 0.10345303
#> indirect_1@2 -0.02713894 0.05320883
plot(
effects,
view = "conditional"
)
confint(inference) extracts primitive model-parameter
intervals. Use confint(effects) for the selected indirect
or conditional effects. When a one-call result contains both MC and
bootstrap inference, choose method explicitly. Each method
keeps its own stored level.
Effect extraction and its associated methods use percentile
intervals, including when
wsmed_infer(..., method = "bootstrap", interval = "bc")
requested a bias-corrected legacy parameter table. These are different
interval conventions. The existing plot_effects() helper
preserves that parameter table; plot() uses the extracted
percentile intervals. bca.simple uses bias correction with
zero acceleration, not a full BCa acceleration estimate. See also the plotting tutorial.
Inspect fitted datasets and inference separately
fit
#> wsMed fit: P | DE | 1 fitted dataset(s)
#> Observations: 100 of 100 input rows
#> Converged: 1 of 1
#> Condition contrast: after - before
#> Moderator: D3 in raw units; centering reference = 0.452271
#> Post-fit admissibility: 1 of 1 ; excluded rows: 0
diagnostics <- wsmed_inspect(
fit,
"diagnostics"
)
diagnostics$by_dataset
#> dataset n.used converged admissible covariance.finite
#> 1 1 100 TRUE TRUE TRUE
#> covariance.min.eigenvalue complete.cases complete.case.rank
#> 1 3.988869e-08 100 6
#> complete.case.columns warnings message n.excluded
#> 1 6 0 0
wsmed_inspect(
inference,
"diagnostics"
)
#> $requested
#> [1] 1000
#>
#> $valid
#> [1] 1000
#>
#> $invalid
#> [1] 0
#>
#> $reasons
#> integer(0)
#>
#> $invalid_draw_ids
#> integer(0)
#>
#> $warnings
#> character(0)For each dataset, the report records the cases used and excluded, optimizer convergence, the backend’s post-fit admissibility check, finite parameter covariance, its smallest eigenvalue, and warning messages. These checks assess different properties: optimizer convergence alone does not establish an admissible solution or a substantively appropriate model. MI reports every completed dataset separately. Non-converged MI fits are not silently pooled; inference rejects known inadmissible fits.
Complete-case matrix rank is descriptive and may be unavailable when
there are too few complete rows; it is not a test of FIML
identification. Recorded category counts refer to the fitted rows in
each prepared dataset (completed data in MI). Counts below five are
flagged descriptively, without treating five as a general validity
threshold. Transformed variables with no usable variation stop fitting
and are named in the error, for example M1diff for the
first mediator difference.
Inference diagnostics count exclusions before effect extraction. Standardized effect diagnostics separately count additional invalid scale draws and their reasons. An excluded draw’s original index remains available. Do not interpret a low failure count as proof of good model fit or adequate simulation precision.
Interpret the effect being reported
The summary states the condition order, moderator units and centering reference, and categorical reference groups. For marginally standardized direct and indirect condition effects, it identifies the model-implied SD of the outcome difference and reports its plug-in value. This is a marginal SD, not a conditional SD at each moderator value. Draw-specific scales continue to vary jointly with coefficients. Contrasts display signed weights and their source effects/probes.
The generated SEM now accounts for the dependence of endogenous
products on upstream mediator disturbances. printGM() shows
its auxiliary product moment equations and residual covariances
separately from the mediation paths. These auxiliary equations
parameterize means and covariances, not new causal paths. For the same
data and model, changing a categorical reference preserves the marginal
standardizer and conditional effects, up to fitting precision.
group_model <- wsmed_model(
c(before = "D1", after = "D2"),
list(A = c(before = "A1", after = "A2")),
moderator = list(
variable = "Group",
interactions = "A -> Y"
)
)
group_fit <- wsmed_fit(
group_model,
example_data
)
recoded_data <- example_data
recoded_data$Group <- relevel(recoded_data$Group, "low")
recoded_fit <- wsmed_fit(
group_model,
recoded_data
)
probes <- list(Group = c("high", "low", "med"))
original_effects <- wsmed_effects(
group_fit,
at = probes,
scale = "marginal"
)
recoded_effects <- wsmed_effects(
recoded_fit,
at = probes,
scale = "marginal"
)
data.frame(
group = probes$Group,
original = coef(original_effects),
recoded = coef(recoded_effects)
)
#> group original recoded
#> indirect_1@1 high -0.001614355 -0.001614355
#> indirect_1@2 low 0.089299551 0.089299550
#> indirect_1@3 med -0.003012100 -0.003012105
stopifnot(isTRUE(all.equal(unname(coef(original_effects)),
unname(coef(recoded_effects)), tolerance = 1e-6)))For MI moderated models, marginal variances are estimated in each completed dataset and pooled jointly with coefficients. Their within-imputation covariance uses the delta method; Rubin’s rules include between-imputation uncertainty. MC draws retain the covariance between coefficients and standardizers. A coding comparison must use the same completed datasets: repeating stochastic imputation or finite MC sampling can itself change numerical results.
Models saved before this correction must be refitted and their inference rerun. Re-extracting an old object’s results cannot repair its fitted covariance model.
Saved-object compatibility
Model and sampler identifiers are recorded separately from the
package version, because different development builds can share
1.1.0.9000. Before new inference or effect extraction,
endogenous-product fits must have compatible model and MI pooling
identifiers. Missing or incompatible identifiers stop with instructions
to refit from the original data. The package does not silently update an
old fit. If the fit is compatible but its stored MI sampler is outdated,
reuse the fit:
group_fit$provenance$algorithms
#> $product_moments
#> [1] "endogenous-products-v1"
#>
#> $mi_marginal_pooling
#> [1] "joint-rubin-v1"
new_inference <- wsmed_infer(
group_fit,
draws = 100,
seed = 190
)
new_inference$provenance$algorithms
#> $mi_variance_sampler
#> [1] "symmetric-psd-v1"
saved <- tempfile(fileext = ".rds")
saveRDS(new_inference, saved)
restored <- readRDS(saved)
stopifnot(identical(coef(restored), coef(new_inference)))
unlink(saved)For an MI object, the same
wsmed_infer(compatible_fit, ...) call regenerates its
uncertainty estimates without reimputing or refitting. Stored
coefficients, covariances and
print(result, detail = "legacy") remain available for
historical inspection. An old object without algorithm metadata can be
statistically valid, but the package cannot safely certify its moment
model from its version alone.
Cross-platform checks also compare against a frozen candidate
numerical baseline; all platforms changing together must not silently
change the accepted results. The separate simulation study in the
repository’s .github/validation directory evaluates bias
and interval coverage against analytical population targets. Passing
software tests and reproducing a numerical result do not establish
coverage for every sample size, missingness mechanism or imputation
model.
Export a compact analysis manifest
report <- wsmed_reproducibility(effects)
report$analysis$missing
#> [1] "DE"
report$analysis$reference$policy
#> [1] "legacy: per-imputation centering; first-imputation probes"
report$inference$mc$controls
#> $draws
#> [1] 1000
#>
#> $seed
#> [1] 604
#>
#> $level
#> [1] 0.9
#>
#> $interval
#> [1] "percentile"
#>
#> $decomposition
#> NULL
#>
#> $pd
#> [1] TRUE
#>
#> $tol
#> [1] 1e-06
#>
#> $RNGkind
#> [1] "Mersenne-Twister" "Inversion" "Rejection"
report$fitting_versions
#> $R
#> [1] "R version 4.6.1 (2026-06-24)"
#>
#> $wsMed
#> [1] '1.2.0'
#>
#> $algorithms
#> $algorithms$product_moments
#> [1] "endogenous-products-v1"
#>
#> $algorithms$mi_marginal_pooling
#> [1] "joint-rubin-v1"
#>
#>
#> $lavaan
#> [1] '0.7.3'
#>
#> $mice
#> NULL
#>
#> $platform
#> [1] "x86_64-pc-linux-gnu"
#>
#> $RNGkind
#> [1] "Mersenne-Twister" "Inversion" "Rejection"
#>
#> $marginal_standardization
#> [1] "Fitted model-implied marginal variances"
path <- tempfile(fileext = ".rds")
wsmed_reproducibility(
effects,
file = path
)
stopifnot(identical(readRDS(path), report))
unlink(path)The manifest records the resolved model, variable roles and coding, missing-data and inference settings, recorded versions, diagnostics, and effect/contrast query. It contains no participant-level data or simulation draw matrices. It is useful alongside the analysis script, data, and dependency records, but cannot reproduce the analysis by itself. Existing report files are not overwritten. Versions from fitting and inference remain separate from the environment used to export. Missing provenance in older objects is not filled using current versions.
Move from the previous one-call workflow
| Previous usage or expectation | Current workflow |
|---|---|
wsMed(...) performs the full analysis |
Still supported; staged calls use the same engines |
| Printing shows all tables |
print(result) shows core effects;
summary(result) provides structured tables; use
detail = "legacy" for historical output |
| Read effects from nested fields |
wsmed_effects() followed by
as.data.frame() or confint()
|
| Change MC to bootstrap by rerunning everything | Pass an existing fit to wsmed_infer();
bootstrap still fits resampled datasets |
| Inspect the generated syntax |
printGM(result) or
wsmed_inspect(fit, "syntax")
|
| Use the named plotting helpers | Still supported; plot() also accepts
workflow objects |
| Expect implicit 95% intervals everywhere | Methods keep the saved level; specify
level = .95 when intended |
| Numeric 0/1 predictors detected automatically | Use factors with explicit levels for categorical predictors in both interfaces; existing one-call recognition defaults are retained |
Older saved one-call objects retain the legacy fields and plotting helpers. They cannot acquire missing staged metadata without refitting from the original script and data. The new methods report this limitation rather than refitting automatically. MI-bootstrap and categorical outcome/mediator models remain outside the supported scope of this release.
Statistical scope of validation
The recorded eight-scenario screening study found undercoverage for some standardized conditional indirect effects under default multiple imputation (the lowest observed coverage of nominal 95% intervals was 89.5%). Passing software checks does not establish nominal coverage for every model. The imputation model should preserve the substantive interaction structure; the study does not isolate the cause of the observed bias and undercoverage. See the development repository simulation report for the design, Monte Carlo uncertainty and retained results.
A model-compatible external imputer can now supply its completed
datasets via mi = list(completed = completed) or one-call
mi_args = list(completed = completed). This does not
reimpute or change the pooling/standardization engine. See the model-compatible imputation
tutorial for a scoped SMC-FCS example and the paired follow-up
study. Save external imputation settings and convergence diagnostics
separately; they cannot be inferred from completed data.
Internal MI with moderator interactions emits a typed compatibility
warning once per fit. This is retained in fit diagnostics and analysis
manifests even when fitted objects are never printed. The warning does
not establish that an external imputer is appropriate; its statistical
and numerical checks remain necessary. Numeric 0/1 moderator
interpretation messages are separately retained in
diagnostics$input_notes.