plot() methods for
model comparisons, family and transition enumerations, pooled
imputations, cross-level and general transition fits, and
predict() for growth mixtures, which shows the refusal and
where the trajectories are.?get_results.latents_transition_enumeration fits the first
50 students, so it runs in a few seconds on Windows as well (it took 12
s there).tests/equivalence/: the depmixS4 and LMest fits,
exact path sums, dense likelihoods, enumerated EM steps and independent
Hessians, together with their reference helpers and fixtures. The
installed tests no longer contain any such comparison.lpa, lca and lta. Its model fits
made it most of the vignette build time.print(),
summary(), as.data.frame(),
coef(), vcov(), confint(),
logLik() and nobs() methods that had a page
each are documented together on four pages
(?"latents-print", ?"latents-summary",
?"latents-as-data-frame",
?"latents-model-methods"). The methods are unchanged.get_results(),
plot(), simulate()) and
compare_models() open with a fit on the first 60 students
of growth_scores, which runs in about a second; the
full-data examples stay in \donttest{}. The
growth_scores, growth_schools and
student_esm pages open with a summary of the data. The
compare_models() example no longer fits a random intercept
that course_engagement cannot support.lpa,
lca, lta and growth-mixture. The
workflow guides (workflow-lpa, workflow-lca)
and the guides to evaluation, covariates, mixture regression, latent
class growth analysis and group-class models move to articles on the
package website, with unchanged addresses. This keeps the installed
package small and the checks quick.numDeriv, mclust, MASS,
glm(), Latent GOLD and Mplus results, reference
implementations, the numbers in the workflow guides) move to
tests/equivalence/, which is not part of the package and
runs in its own CI job. numDeriv is no longer a suggested
package. Slow tests that refit many models are skipped on CRAN; every
model family keeps tests that run there.model = "VVI"), as candidate_fit() needs once
several structures are crossed, and its text matches the regenerated
output.Vignettes and worked examples save and restore the user’s display
options (digits and width) after use,
including the scripts installed in inst/doc.
Corrects regression and growth inference: robust covariance checks the number and rank of independent units, multilevel growth BIC uses clusters, enumerated models retain the appropriate inference, and boundary parameters no longer erase unrelated delta-method standard errors.
Stabilizes Poisson and negative-binomial regression densities, NB dispersion derivatives near the Poisson limit, and growth residual calculations for outcomes with large locations. Single-class binary regressions are accepted; nonfinite outcomes and evaluated predictors are refused explicitly.
Trajectory tables include offsets and report response-scale standard errors. Alternate time columns work in tables and plots; random-effect spread holds other predictors fixed, and observed means respect sampling weights.
Withholds regression bootstrap likelihood-ratio p-values when refits fail validation. Model comparisons also check trials, weights and cluster layout.
Handles unassigned regression classes, weighted transition class counts and matrix-valued prediction inputs correctly. Noise cases retain their mass in profile percentages and appear as class zero in sequence tables and plots.
MASS and withr, used by the
tests, in Suggests (the 0.9.13 check warned about them as
undeclared).validation/ENGINE_DESIGN.md): one EM driver (with SQUAREM
for the profile models), one start runner, one quasi-Newton finish, one
measurement block (Gaussian, categorical, ordinal and count indicators)
shared by the profile, covariate, cross-level and both transition
engines, one forward-backward pass for both transition engines, shared
latent structures for the regression and growth models, and shared
inference, alignment and simulation services. Results are unchanged: 251
recorded fits of every engine and option reproduce bit for bit
(equivalence/engine-golden/), except where a defect below
was fixed.mixture_regression(cluster = ) nests the persons
(id) in clusters (schools, clinics) with
n_group_classes group classes of clusters that shift how
probable each trajectory class is, and group_membership
covariates of the group class. The clusters are the independent units of
the likelihood, standard errors and BIC. New tables
"group_classes" and "clusters";
enumerate_regressions() compares the number of group
classes; simulate() draws the two levels. New dataset
growth_schools; the growth vignette has a section on
it.mixture_regression(family = "negative_binomial"), NB2 with
a dispersion per class or shared (variance = "equal"). One
class reproduces MASS::glm.nb(). A dispersion at the
Poisson limit warns (latents_boundary) and the other
standard errors are conditional on it.mixture_regression(family = "ordinal"), a proportional-odds
(cumulative logit) regression within each class with class-specific
ordered thresholds in place of the intercept. The outcome is an ordered
factor, a factor or whole-number categories. Thresholds are rows
"threshold:<lower>|<upper>" of the coefficient
table; class means are expected category scores;
predict(type = "probabilities") gives the category
probabilities and simulate() draws categories in the
outcome’s own type. One class reproduces MASS::polr().
Mixtures that one outcome per class assignment cannot identify (two
categories, or no predictors) are refused with
latents_not_identified.vignette("trajectory-classes")): trajectory classes
without random effects, every plot view, curved trajectories, pass/fail
and ordered-band trajectories, distal outcomes, and when to prefer the
growth mixture model. The growth mixture vignette now focuses on random
effects and the multilevel model, with its plots.summary() and
get_results(x, "all") no longer fail;
"classification_errors" and "bch_weights"
(which were wrong for noise units) refuse with
latents_unsupported_noise. sensitivity() no
longer reports NA agreement for noise fits.parameter_inference() of a general lta()
fit now uses the caller’s step in its table, not only in
the attached covariance.get_tna() and get_group_tna() on a general
lta() fit refuse with latents_unsupported_tna
instead of a bare “no applicable method”.latents_singular_information instead of an
unclassed Cholesky error.latents_bad_data,
latents_all_starts_failed).mixture_regression() gains random,
random_covariance and random_diagonal: random
effects within classes, so persons scatter around their class trajectory
(the Gaussian growth mixture model). The likelihood is exact; estimation
is EM with the random effects as missing data, finished by quasi-Newton
with analytic scores. Validated against an independent dense likelihood,
numerical derivatives and lcmm::hlme() (six configurations;
the same likelihood at hlme’s estimates and at least its
maximum).random = "wave", random = "intercept",
common = "x", membership = "age".latents_random_boundary) and reports no interval for
it.compare_models(): fits to the same data side by
side, with BIC differences and Schwarz weights; plot()
draws the comparison.id with
class_level = "group", with or without random)
share new ggplot2 views: class trajectories with confidence bands, the
spread of persons around them and observed class means; individual
trajectories; a coefficient forest; predicted random effects; and a
classification structure plot.[low, high], APA p-values, fit
statistics as a card) while remaining plain numeric data frames.enumerate_regressions(),
including its bootstrap likelihood-ratio test, have a
simulate() method, and support distal outcomes in
three_step() (person-level, bias-corrected);
three_step() results print in the report layout.
predict() and r3step() refuse growth fits with
classed conditions.growth_scores and vignette “Growth mixture
models”.tol. In 120 negative-binomial test datasets 5
were flagged unconverged at points with scores of about 1e-6 that a
restart improved by less than 1e-12; none are now. This, not the
iteration cap, is what failed the 0.9.8-0.9.10 CI checks on macOS and R
4.1 (the 0.9.10 test change is reverted).max_iter = 5, which held on some
platforms only (macOS CI and R 4.1 needed more iterations, and the
package correctly warned). The test now allows 500 iterations; its
checks of dispersion floors, monotone likelihood and stationary scores
are unchanged.bootstrap_lrt() compares them. Each profile’s
rows are drawn through the Cholesky factor of its covariance; diagonal
fits keep their random stream.parameter_inference(method = "bootstrap", data = )
works on transition fits. Persons are resampled with replacement and
refitted; each replicate’s profiles are matched to the original’s on the
measurement and its group classes on what the matched profiles make of
them, with initial logits rebased when the reference profile moves.
Standard errors are the replicates’ standard deviation and intervals
their percentiles. This gives inference for extended models whose Wald
inference is unavailable (full covariance structures) and supports
sampling weights.boundary = "fix", which
get_results(fit, "responses") and summary()
request, so categorical transition response tables lost their standard
errors. Transition inference holds nothing at a bound, so
"fix" now agrees with "error"; a fit with an
active bound is still refused with
latents_boundary_fit.lca(),
multilca() and enumerate_lca(), as documented.
Bootstrap and imputation label alignment move their measurement
parameters together, restore the ordinal reference profile, and match
profiles using the extra indicators.coef() now carries complete
measurement covariance coordinates for all fourteen structures, with
likelihood-preserving decoding. The existing restrictions on
extended-model Wald inference remain explicit.bootstrap_lrt() refuses a full-covariance transition
null up front with latents_unsupported_inference, rather
than failing every replicate and returning p_value = NA
with a generic warning.parameter_inference() on transition fits refuses
method = "bootstrap" and boundary = "fix" with
latents_unsupported_inference instead of silently returning
the Wald table. On general lta() fits, adjust
now produces a p_adjusted column.bootstrap_lrt() accepts the caller’s own data frame for
single-level fits."classification" table’s
estimated_n and estimated_proportion (and so
the odds of correct classification and diagnostics()’
smallest class) use weighted posterior totals. The same applies to
restricted cross-level composition and
group_classes counts.profile_covariates,
group_covariates) and lta() (every extension,
invariant or occasion-specific measurement) now take
ordinal and count indicators, with Wald
inference, weights and simulation (bootstrap_lrt() for
transition fits).count_model = "negative_binomial" models counts more
variable than a Poisson within a profile (NB2: variance
mu + alpha mu^2), with
count_dispersion = "varying" (per profile) or
"equal" (shared).
get_results(fit, "count_means") adds the dispersion and its
standard error. A dispersion at zero is the Poisson limit: the fit warns
(latents_boundary) and Wald inference is refused
there.poisson overdispersed), class-varying and shared
dispersion: log likelihoods agree to 4e-5 and parameters to its printed
precision; seven reference cases in all, stored as a test fixture.multilpa() with profile_covariates or
group_covariates silently fitted any covariance structure
other than EEI, VVI, EEE and VVV as one of those four (for example
model = "VEI" gave VVI) and did not record the structure.
It now refuses them with latents_unsupported_structure; fit
the covariate-free model with the structure and use
three_step() or r3step().?"latents-conditions" documents the condition classes
added in 0.9.3–0.9.6.multilpa(), lpa(), lca() and
multilca() take ordinal and
count, naming indicators in vars:
ordinal: an adjacent-category logit with category
intercepts shared by every profile and one location per profile (Latent
GOLD’s default ordinal model): (K - 1) + (C - 1) parameters
per indicator instead of C (K - 1) as
categorical. Ordered factors or whole numbers.count: Poisson, one mean per profile.get_results(fit, "ordinal") (category
probabilities and locations, with standard errors) and
get_results(fit, "count_means").missing = "fiml" and
weights. Wald (analytic scores) and bootstrap inference,
bootstrap_lrt(), predict(),
sensitivity() and enumerate_classes() handle
them.equivalence/latentgold-ordinal/); the results are a test
fixture.start,
fixed, prior, noise,
lta() or the group-class families
(latents_unsupported_indicator).profile_covariates, group_covariates) now
solves its weighted multinomial logits by Newton-Raphson with the exact
Hessian instead of BFGS. BFGS stalled above its score tolerance on about
half of the steps of an ordinary fit, which left fits flagged
unconverged (and refused standard errors) on some platforms but not
others. Estimates are unchanged; fits converge in fewer EM
iterations.mice (its current dependency chain needs R
>= 4.4), one lint is fixed, and the checkout action is updated.multilpa(), lpa(), lca(),
multilca(), lta() and
mixture_regression() take weights, the name of
a column holding one sampling weight per independent unit (each
id group, or each row of a single-level fit). The fit
maximizes the pseudo log likelihood sum_j w_j log L_j, with
the weights scaled to sum to the number of units (Mplus’s convention),
so AIC and BIC stay on the sample’s scale.missing = "fiml", membership covariates, every
family (additive, dispersion, additive-dispersion,
restricted and full cross-level), every extension of lta(),
and all three nestings of mixture_regression().TYPE = COMPLEX, WEIGHT)
and closed-form weighted least squares
(equivalence/weights/).parameter_inference(), vcov(),
confint() and get_results() default to
vcov_type = "robust" for a weighted fit and refuse
"observed" and "opg"
(latents_unsupported_weights).
method = "bootstrap" resamples units with their
weights.get_results(fit, "model") gains a
weights column. Effective counts and proportions are
weighted; classification and posterior tables report each unit’s own
posterior.latents_unsupported_weights:
bootstrap_lrt() and
enumerate_regressions(bootstrap = ) (the parametric
bootstrap ignores the design), three_step(),
r3step(), prior and noise.
Weights that vary within a unit, are negative or missing raise
latents_bad_weights.enumerate_classes(..., time = ) enumerates latent
transition models over n_profiles and
n_group_classes (and covariance structures named in
model), with every other lta() argument held
fixed; read with get_results(), plot() and
candidate_fit(grid, n_profiles = , n_group_classes = ).bootstrap_lrt() compares nested transition fits
(data required): more profiles or group classes,
occasion-varying against homogeneous transitions, added covariates,
occasion-specific measurement, second order, a stayer class. The null
model is simulated with the data’s groups, occasions, covariates and
missing values kept.lta(mover_stayer = TRUE) adds a class of stayers who
never change profile (Goodman’s mover-stayer model), with its own
initial distribution.missing = "fiml" and covariance structures
(model =, covariance_model = "full"); standard
errors with diagonal covariances (including FIML).latents_boundary,
standard errors withheld) instead of as non-convergence.lta() gains arguments that relax each default
assumption of the model:
transition_covariates: covariates (fixed or changing
over occasions) shift the transition probabilities through a multinomial
logit per origin profile, the log odds of moving to each other profile
rather than staying;initial_covariates: covariates shift the starting
profile;transitions = "occasion": a separate transition matrix
for each move;measurement = "occasion": profile means and variances
(or response probabilities) per occasion;order = 2: second-order transitions, the next profile
depending on the two previous ones;model =: the fourteen covariance structures
("VEI", "EEV", …) for the homogeneous model,
as in multilpa().multilpa_lta fit with tidy
tables (get_results(fit, "transition_coefficients"),
"transitions" per occasion,
"second_order_transitions",
"initial_coefficients", "profiles", …), Wald
standard errors from analytic scores (observed, robust or OPG),
coef(), vcov(), logLik(),
nobs(), BIC() and plot(). EM is
finished by a quasi-Newton search on the exact likelihood.equivalence/lta-extensions/): Mplus User’s Guide examples
8.13 and 8.14 and an Mplus model with occasion-specific thresholds
(log-likelihoods within Mplus’s printed precision, estimates within
2e-3), depmixS4 covariate transitions (log-likelihood within 1e-7) and
LMest time-heterogeneous transitions (within 4e-8). Second-order
transitions, which no external program fits, are checked against an
exact sum over every profile path.multilpa(family = ) fits the multilevel latent profile
families of Houle, Morin & Harvey (2026) beside the existing profile
model (family = "profiles", their dispersion-heterogeneity
model):
"additive"
(classes differ in means; within-group variance shared),
"dispersion" (classes differ in within-group variances) and
"additive_dispersion" (both);
between_variance = "varying" or "equal". The
likelihood is exact (no quadrature); zero between-group variances are
reached by a boundary maximization with a Karush-Kuhn-Tucker check;
interior fits are finished by Newton steps."restricted_cross_level" and
"full_cross_level". Their likelihood is a working
likelihood (group means reuse the ratings), so it compares only
cross-level fits of the same data; no standard errors.get_results()
("parameters", "group_classes",
"groups", "intercepts",
"recovery", … for the group-class families;
"profiles", "composition", … for the
cross-level ones), summary(), plot(),
coef(), vcov(), confint(),
logLik(), nobs() and
parameter_inference() (observed, cluster-robust or OPG
covariance from analytic group scores).enumerate_classes(family = c("additive", "dispersion", "additive_dispersion"), n_group_classes = )
crosses families, between-variance restrictions and class counts;
candidate_fit(grid, n_group_classes = , model = ) picks
one. bootstrap_lrt() tests nested group-class fits by
parametric bootstrap.latents_weak_class warning with
effective_groups in the "group_classes" table:
a class supported by fewer than 50 effective groups (small, poorly
separated, or both) has intervals that can be miscalibrated. Threshold
set by a predeclared rule and evaluated on held-out simulations (flags
100% of a rare, weakly separated condition and 0.8% of well-separated
ones). Also flags designs with fewer than about 50 groups per
class.validation/ADDITIVE_SIMULATION.md,
equivalence/latentgold-families/): predeclared simulations
with 1000 datasets per condition (additive: 31 of 34 reference-condition
parameters meet every gate; dispersion and additive-dispersion: 45 of
46; every miss is the ML small-sample downward bias of a between-group
variance, coverage 0.925-0.947). Latent GOLD 6.1 agrees on parameter
counts in all eight external cases, and its likelihoods and posteriors
converge to latents’ as its quadrature is refined (largest remaining
gaps 1.6e-4 and 2.0e-4). Robust standard errors are needed for
heavy-tailed ratings.vignette("additive").plot() on a fit (multilpa, covariate and
transition fits), on an enumeration and on a diagnostics()
result now returns ggplot objects instead of drawing with base graphics.
Print one to draw it, save it with ggplot2::ggsave(), or
restyle it with + ggplot2::theme(). ggplot2 stays in
Suggests: a plot without it raises latents_missing_package,
and diagnostics() and report() print a message
and carry on.what = "all", plot() on a diagnostics
result and an enumeration plotted with combine = FALSE
return a latents_plots list, named by view, that draws
every plot when printed.palette, symbols,
linetypes, style and the style constants
passed through ... are gone; an unknown argument now raises
latents_bad_argument instead of being ignored."parallel" (every case as a line, one panel
per profile), "pairs" (scatter-plot matrix with each
profile’s 95% covariance ellipse) and, for enumerations,
what = "tree" (an icicle of how profiles split as more are
added). plot_views() lists them.plot(diagnostics(fit)) on a one-profile fit returns the
sizes and average posterior views instead of refusing.multilpa(),
multilca() and enumerate_classes() with
id = NULL is now one line (“Fitted a single-level latent
profile model.”). The error for a missing id is shorter
too.plot() still draws the
base-graphics views; the switch comes in a later release.r3step(by_group_class = TRUE) now handles the
one-group-class limit and agrees with the pooled regression for both
observed and robust covariance.enumerate_lpa() preserves an omitted
model, allowing the documented covariance switches and
all-categorical inputs through the wrapper.bootstrap_lrt() refuses prior-fitted models whose
likelihoods are evaluated at posterior modes. Its fixed-mask missingness
assumptions are clarified, and multiple-imputation guidance
distinguishes algebraic equivalence from evidence about coverage and
imputation-model validity.lpa() and lca() fit single-level
latent profile and latent class models. They are
multilpa(id = NULL) and multilca(id = NULL)
under their ordinary names, return the same object, and refuse
id and n_group_classes.multilpa() and lpa() take
model, the three-letter covariance code
("EEE", "VVI", …), as an alternative to
variance_model / covariance_model or
volume / shape / orientation;
giving both is an error.enumerate_lpa() and enumerate_lca()
compare single-level latent profile and latent class models, as
lpa() and lca() fit one:
enumerate_lpa(data, vars, n_profiles = 1:6) crosses the
counts with the covariance structures, and
enumerate_lca(data, vars, n_classes = 1:6) treats every
indicator as categorical. enumerate_classes() remains the
verb for nested data and needs id.multilpa(),
multilca(), enumerate_classes()) with
id = NULL say which analysis was estimated – a single-level
latent profile or latent class analysis – and name the single-level
verbs; called without id they are refused with the same
pointer.enumerate_classes(model = ) now defaults to
"basic": the four structures that combine equal or varying
variances with covariances absent or present (EEI,
VVI, EEE, VVV).
"all" fits the 14 structures; codes can still be named.
With one continuous indicator the structures that coincide are fitted
once; with only categorical indicators no structure is crossed; a
structure set through the other arguments replaces the default, and
model = NULL restores the previous grid. Because a grid now
holds several structures per class count, candidate_fit()
needs model to pick one of them.srl: five self-regulated learning scales
for 300 respondents simulated by a large language model.vignette("workflow-lpa") and
vignette("workflow-lca"): a latent profile and a latent
class analysis from the data to a reported model.get_results(fit, "profiles") and
get_results(fit, "responses") carry
mean_standard_error, variance_standard_error
and probability_standard_error without the data being
passed; they are NA, with a message, when the information
matrix cannot be formed.diagnostics() draws its classification plots by default
(plots = TRUE): profile sizes, posterior probabilities,
entropy contributions and the average posterior probability matrix.plot(what = "raincloud"): for each profile and
indicator, the density, a quartile box and the observations.confint() returns exactly the intervals
parameter_inference() reports.boundary = "fix" could not
hold.r3step() and three_step() name their
adjusted p-value p_adjusted and their classes
profile_1, group_class_1, as
parameter_inference() does (were
p_value_adjusted and class_1).id = NULL now raises its
latents_single_level notice as a message, not a warning: a
single-level model is a legitimate choice.diagnostics() prints only the individual level for a
fit with one group class, and a single-level fit’s assignments carry no
group_class column.plot(what = "profiles") draws 95% intervals when the
fit has standard errors (intervals = TRUE, profiles dodged
apart); plot(what = "responses") draws intervals too.plot(what = "bars") and the profile plot no longer fail
on a bound-active or unconverged fit: they draw without whiskers and the
subtitle says why.plot(what = "heatmap") on an all-categorical fit draws
the response probabilities: one row per class, one column per category,
values printed where they fit."bars", "heatmap", "raincloud",
"sizes", "avepp" are new for them).summary() of an enumeration prints a compact candidates
table and names get_results() for the rest.(Intercept), not group_class_1.$ or bracket indexing; a test keeps it that way.bootstrap_lrt() now accepts fits made with
missing = "fiml": every simulated replicate is given the
observed missing cells before it is refitted, so the reference
distribution loses the same information as the observed statistic. The
pattern is treated as fixed (independent of the profiles).bootstrap_lrt() now accepts membership-covariate fits:
the covariates are held at their observed values and memberships are
drawn from the fitted logits. A one-group-class null may be compared
with an alternative that adds group covariates or slopes by group class;
otherwise both models must use the same membership regressions
(latents_bad_nesting). A covariate replicate whose
likelihood has converged counts as valid even if a membership logit is
still drifting (an over-fitted class), since the statistic reads only
the maximized likelihood; the replicate table’s new
logits_settled column records it.latents_incomparable_models).parameter_inference(), vcov() and
confint() now work on lta() fits instead of
refusing them: observed-information, robust (clustered on sequences) and
OPG standard errors for the measurement model, the group-class shares,
the initial profile probabilities and the transition probabilities. The
scores follow the Fisher identity (expected counts from the
forward-backward pass minus the counts the fitted probabilities imply)
and match numerical differentiation of the likelihood; the standard
errors match a Richardson-extrapolated Hessian to 1e-6 and depmixS4’s
numerical ones to within its own finite-difference error. Over 300
simulated panels, 95% intervals covered 0.947-0.960. The
latents_no_inference condition is retired.coef() on an lta() fit now uses the names
and order of vcov(): under
variance_model = "equal" it reports the shared variances
once, under covariance_model = "full" the covariance
matrices, and the group-class shares come before the initial and
transition probabilities.pool_imputations(): fits multilpa() to
every completed data set (a list of data frames, or a mids
object from mice), aligns each fit’s profile and group-class labels to
the first, and pools with Rubin’s rules. The table adds df,
within, between, riv and
fmi to the columns of parameter_inference();
get_results(x, "imputations") and
get_results(x, "fits") show each imputation and the
relabelling it needed, and plot() shows the imputations’
estimates beside the pooled interval. This is the route for missing
covariates, which missing = "fiml" cannot integrate out.
Aligning a covariate fit reparameterizes its membership logits, whose
reference category moves with the labels. Pooled values agree with
mice::pool.scalar() to machine precision. A failing
imputation raises latents_pooling_failed rather than being
dropped.parameter_inference(method = "bootstrap") now accepts a
fit_staged() result instead of refusing it. Every resample
of the groups refits both stages, so the standard errors and percentile
intervals carry the measurement’s sampling variability into the
group-class estimates; the Wald default still conditions on the
measurement. The table reports every parameter, the measurement
included. On a simulated two-class design the within-class profile
probabilities’ errors were 14% and 32% wider than the conditional ones,
and a bootstrap that held the first stage reproduced the conditional
Wald errors to within 3%.multilpa(profile_covariates = , profile_slopes = "group_class")
lets each group class have its own profile-covariate slopes, so a
covariate can predict profile membership differently in different kinds
of group (a cross-level interaction). The coefficients are reported with
terms such as z:group_class_1, and inference, robust and
OPG covariances cover them. The default, "shared", is the
previous model. With one group class the two are the same model. Naming
it without profile_covariates raises
latents_bad_argument.multilpa(profile_covariates = , group_covariates = , missing = "fiml")
now fits: missing indicators are integrated out of the measurement
density exactly as in the covariate-free model, for diagonal and full
residual covariance and for categorical and mixed indicators. Previously
this combination was refused with latents_bad_argument.
Standard errors (observed, robust and OPG) cover it through the Fisher
identity: a missing value enters the scores through its conditional mean
and covariance given the row’s observed values. Covariates themselves
must still be complete; a missing covariate now raises the classed
latents_bad_data.multilpa(acceleration = "squarem"), the new default,
accelerates EM with SQUAREM (Varadhan & Roland 2008). A cycle
extrapolates along the last two EM steps and finishes with an ordinary
EM step, falling back to plain EM whenever the extrapolation would lower
the likelihood, so the path stays monotone; convergence is judged over
the whole cycle, so it is never looser than plain EM’s.
acceleration = "none" reproduces plain EM’s path (use it to
follow mclust or Mplus step by step). Prior fits, membership-covariate
models always use plain EM. Results can differ slightly from earlier
versions: the accelerated fit usually ends closer to the maximum, and on
a flat, weakly identified likelihood it may stop at a different point of
the ridge.multilpa() is now Ward’s
hierarchical clustering, with each cluster’s own means and spread; the
rest remain random k-means. Over 14 structures on 3 datasets it never
lowered the best-of-10 likelihood and raised it in 3 of 36 cases, for
example on iris VVV from -186.57 to mclust’s -180.19. Fits may therefore
land on a better maximum, or on the same maximum with profiles numbered
differently.enumerate_classes(id = NULL) enumerates single-level
models, the search mclust::mclustBIC() performs, with one
single-level notice for the grid.predict() method for multilpa() and
multilca() fits: the modal profile ("class"),
every profile’s probability ("posterior"), or each row’s
log density under the fitted mixture ("density") for new
rows, prepared as the fit prepared its own (categorical levels,
centering, missing data, noise). Groups in the new data are classified
as new groups. On the training rows it reproduces the fit’s posteriors,
and it matches mclust::estep() at the same parameters.descriptives(by = ) lists strata in a fixed order (a
factor’s levels, otherwise sorted) rather than in order of first
appearance.apply(): about 14 times faster per iteration, with
identical results.mclust::Mclust() from one
start (VVV is the exception), and 13 of 14 reach a higher
likelihood.mixture_regression() fits finite mixtures of
regressions (clusterwise, latent class or regression-mixture models) for
"gaussian", "binomial" (0/1, factor or
cbind(successes, failures)) and "poisson"
(with offset()) outcomes. Classes can belong to each row,
to a whole group (class_level = "group", flexmix’s
y ~ x | id), or to each row with a second-level group class
that shifts the class shares within groups
(n_group_classes, Vermunt 2003). Membership covariates at
both levels (membership, group_membership),
coefficients shared across classes (common), and equal
residual variances (variance = "equal").id for a
single-level fit) and outer-product standard errors; validated against a
numerical Hessian of the likelihood in every nesting. A Monte Carlo
study (validation/mixture-regression-recovery.R, 200
replications) found 91.5–97% coverage of nominal 95% intervals for the
two-level model.get_results() tables: coefficients (with
Benjamini-Hochberg-adjusted slopes and odds/rate ratios),
classes, membership,
group_classes, fit (AIC, BIC, SABIC, ICL,
entropy), assignments, groups,
fitted, starts, classification
and recovery (against a known classification).
predict(), simulate(), plot(),
summary(), coef(), vcov(),
confint(), logLik() and nobs()
methods.enumerate_regressions() compares class counts in
one table, with an optional parametric bootstrap likelihood-ratio
test.latents_not_identified), since that mixture is not
identified.equivalence/test-mixture-regression-flexmix.R).study_hours and
vignette("mixture-regression").multilca(id = NULL) now fits one group class by
default, as multilpa() does, instead of refusing the
default n_group_classes = 2.equivalence/run.R evaluated tests in the retired
multilpa package’s namespace when it was still installed;
it now uses latents.parameter_inference(), vcov() and
confint() give Wald standard errors for all fourteen
covariance structures, not only EEI, VVI, EEE and VVV. The ten that
constrain the volume, the shape or the orientation across profiles are
differentiated in their own free coordinates — log volumes, log shapes
with the determinant-one constraint built in, and Cayley-transform
orientations (log-Cholesky factors for VEE and EVV) — with exactly as
many dimensions as the structure has parameters, and carried to the
reported variances and covariances by the delta method. Robust
("robust", "opg") errors work for them too.
Validated against an independent numerical Hessian of the observed-data
likelihood in a different chart.multilpa(prior = prior_control()) gives maximum a
posteriori estimates under the conjugate prior of Fraley and Raftery
(2007), mclust’s priorControl(): the same default
hyperparameters and the same M-steps as mclust::me(), for
the ten structures mclust defines a prior for.
log_likelihood, and the criteria built from it, are the
unpenalized likelihood at the posterior mode, as in mclust. Wald
inference is refused for such a fit; the bootstrap refits with the
prior. New condition class latents_unsupported_prior.multilpa(noise = TRUE) adds mclust’s uniform noise
component over the data’s hypervolume (hypvol()), for one
group class and continuous, complete indicators, with any covariance
structure and with or without prior. The noise is profile
0 in get_results(fit, "assignments") (with a
posterior_noise column), "posteriors",
"profile_probabilities" and the classification diagnostics.
The component counts two parameters, as in mclust, so BIC equals
mclust’s. New condition class latents_unsupported_noise,
raised also by the verbs that do not yet account for a noise
component.parameter_inference() and vcov() give
standard errors for a one-step covariate model with categorical or mixed
indicators (multilca(profile_covariates = ),
multilpa(categorical = , profile_covariates = )). Each
category’s probability is reported with a delta-method standard error
and no Wald test, as for the covariate-free model.parameter_inference(vcov_type = "opg") inverts the
outer product of the group scores (the BHHH estimate). It is the
estimator glca reports, and reproduces its standard errors;
"observed" remains the default.parameter_inference(boundary = "fix") holds categorical
response probabilities that sit on min_probability at that
bound and reports the other parameters conditionally on them. The
default, "error", refuses as before, now with a message
that names the alternative.multilpa(), multilca() and
lta() gain select_start.
"converged" reports the best converged start whenever one
converged, instead of an unconverged start that edges it out by a
negligible likelihood.r3step() gains by_group_class.
TRUE gives every group class its own profile intercepts,
with shared slopes and one latent group class per group, the two-level
form of the one-step model. The pooled regression attenuates the slopes
when group classes differ in their profile mix.r3step() now defaults to
vcov_type = "robust", standard errors clustered on groups,
matching three_step(). Pass
vcov_type = "observed" for the previous behaviour.min_probability, so
inference recognises response probabilities on their bound and refuses
with latents_boundary_fit instead of a numerically singular
information.multilpa(), multilca(), lta()),
as do the result classes. Condition classes now use the
latents_ prefix (for example
latents_bad_argument, previously
multilpa_bad_argument), multilpa_plot_types()
is plot_views(), and the conditions catalogue is
?"latents-conditions". The vignettes are
vignette("lpa", package = "latents"),
"evaluation", "covariates", "lca"
and "lta".multilca() fits a two-level latent class model:
multilpa() with every indicator categorical, so the items
are named once. Mixed models remain
multilpa(categorical = ).enumerate_classes(structure =) and
candidate_fit(structure =) are now model =,
and the candidate grid’s structure column is
model. An argument multilpa() does not take,
including the old structure, is refused with
multilpa_bad_argument before any candidate is fitted,
instead of failing inside every candidate.boundary
and n_best_replicated.plot() on an enumeration grid draws several criteria as
line plots, one panel per criterion and one line per covariance model
and group-class count; the default shows AIC, both BICs and ICL.
combine = FALSE draws each criterion as a separate
figure.?course_engagement and the case studies now say that
the indicators are simulated on the log1p scale, not
transformed from counts.student_esm; the two latent transition case studies are
merged into one. Case studies and vignettes print whole tables instead
of filtering them.student_esm: experience-sampling data on the leisure
activities of 100 university students at 2,582 non-study prompts, with
four affect ratings, from openESM dataset 0062 (Neubauer &
Schmiedek, 2024; CC-BY 4.0). Eight yes/no activity items make it the
categorical counterpart of course_engagement, used for
two-level latent class and mixed-measurement examples.seed (and each of
sensitivity()’s seeds) is any whole number
that set.seed() accepts, negative ones included.
multilpa(), fit_staged(), lta(),
the covariate fit, bootstrap_lrt() and
sensitivity() previously refused negative seeds, and the
bootstrap in parameter_inference() accepted a fraction that
set.seed() truncates, so seeds 1.2 and 1.7 drew the same
stream. Every refusal is now multilpa_bad_argument.multilpa_indicator_reused from both verbs, alongside
multilpa_bad_outcome from three_step() and the
new multilpa_bad_covariate from r3step()
(previously multilpa_bad_argument).r3step() raises multilpa_bad_covariate for
a predictor that varies within a group at level = "groups"
(previously multilpa_bad_outcome) and for a rank-deficient
design (previously multilpa_bad_inference_data).data with the wrong number of rows raises
multilpa_bad_inference_data from three_step(),
r3step() and the bivariate residuals, as it already did
from the assignments table and sensitivity(). These were
unclassed.get_tna() is computed for every state in one matrix product
rather than a loop; the result is bit-identical.get_results(x, "assignments", truth = ) counts each
group once when a truth column is compared against group classes, rather
than once per observation, so larger groups no longer weigh more in
recovery. Unused factor levels are omitted.multilpa_unverified_alignment warning now also
fires when the supplied data shares only columns that
cannot establish row order, such as a repeated group identifier on its
own.three_step() and r3step() refuse a
membership-covariate fit (multilpa_unsupported_three_step)
and refuse measurement indicators as outcomes or predictors. Both now
check data alignment against the fit.bootstrap_lrt() requires matching covariance structure
and centering, repeats grand-mean centering in every refit, and refuses
person-centred fits (multilpa_unsupported_bootstrap).get_tna() and get_group_tna() no longer
turn a state with no outgoing moves into a certain self-transition. The
fit’s own row is kept and multilpa_empty_transition_row is
warned.fit_staged(measurement = ) refuses a first stage with
more than one group class (multilpa_bad_stage).sensitivity() works on fit_staged()
results, refuses directly fixed fits, checks data
alignment, and requires non-negative whole-number seeds.sensitivity() now refits with the reference fit’s own
max_iter and tol; multilpa() did
not store them, so the defaults were always used.group_posteriors, group_classes and
effective_group_counts.indicator_data, which coerced the matrix to character.get_results(x, "data") and bootstrap
simulation.three_step() no longer reports a spurious
significant contrast for a constant outcome; zero standard errors give
NA statistics and p-values in three_step() and
r3step().descriptives() excludes rows with a missing group ID
from n_groups and icc, and describes repeated
vars once.vignettes/figure, a leftover knitr figure
directory from a preview render, is added to .Rbuildignore.
It was entering the tarball and drawing a “looks like a leftover from
‘knitr’” NOTE. The tarball’s vignettes/ now holds the four
.Rmd sources and nothing else.
get_data() is
renamed get_results()Breaking, and deliberately without an alias. The package has never been on CRAN, so there is no installed base to migrate.
The old name was ambiguous in a way that showed up inside the verb
itself: get_data(x, what = "data") used the same word for
the accessor and for one of its own values, and “data” in an R package
normally means the input rather than what a model produced.
get_results() says what comes back, and keeps the
get_* family consistent with get_tna() and
get_group_tna().
All thirteen S3 methods, the four vignettes and the README move with
it. The what = values are unchanged, so
get_results(x, what = "profiles") returns exactly what
get_data(x, what = "profiles") did. Earlier entries in this
file still say get_data(), because that is what those
versions shipped.
future/The package now exports 17 verbs rather than 19. The test applied was
whether an export makes a claim the package can back; two did not, and
they are kept in future/, which .Rbuildignore
excludes, with a note on what would bring each one back.
fit_random_intercept() is removed. Its
independent validation reached only the one-profile limit –
multiple-profile parameter recovery had never been checked against
anything – and it refused parameter_inference(),
vcov() and confint() outright, so nothing in
the package could say when it was wrong. It is also a different model
family: a continuous group effect rather than the discrete group classes
the package is named for. It appeared in no vignette.lmr_lrt() is removed. It returned a
p_value column that was always NA, because the
Vuong-Lo-Mendell-Rubin reference distribution is not reproduced.
bootstrap_lrt() gives a calibrated p-value for the same
comparison. The Lo-Mendell-Rubin adjustment factor itself had been
verified against two genuine Mplus TECH11 runs; that
evidence is recorded in future/README.md against the day it
is restored.Fifteen S3 methods and twelve help pages go with them.
multilpa_plot_types() no longer lists
"random_intercepts", and get_data() no longer
offers the "random_intercepts" table.
Two conditions changed status as a consequence, both of which had the random intercept as their only trigger:
multilpa_quadrature_check is removed from
?"multilpa-conditions". Nothing raises it any more.multilpa_no_group_classes is still raised by guards in
R/diagnostics.R and R/get-data.R, but every
model family this version fits has discrete group classes, so no call
can reach it. The catalogue says so rather than describing a refusal a
caller cannot produce.sensitivity()
asks whether the solution survives a different seedEM converges to a local maximum from the starts it was given. The package could report how many starts within one seed’s stream reached the best likelihood, and nothing else: a user could not vary the seed on their own data at all. That left the package unable to meet, on a user’s data, the standard it applies to its own.
sensitivity(fit, seeds = ) refits under each seed and
returns one tidy row per seed: log_likelihood,
converged, iterations, optimum
(which distinct maximum it reached, 1 being the best),
best, and agreement (the proportion of
observations assigned as the reference fit assigned them).
agreement is computed after each refit’s arbitrary
profile labels have been matched to the reference fit’s, so two
identical solutions that happened to number their profiles differently
report agreement of one rather than zero.
Writing it exposed a defect in
.multilpa_permute_profiles(), which has been fixed. It
permuted means, variances,
covariances, profile_probabilities and
response_probabilities, but left
subject_posteriors, subject_profiles and
effective_profile_counts behind. The bootstrap it was
written for reads only parameters, so a permuted fit that described one
labelling and assigned another went unnoticed.
subject_profiles holds labels rather than positions and
takes the inverse permutation.
multilpa() fits only. lta() and covariate
fits raise multilpa_unsupported_sensitivity, and a seed
whose refit fails contributes an NA row with a
multilpa_sensitivity_dropped warning naming how many.
fit_transitions()
is renamed lta()Breaking, and deliberately without an alias. The package has never been on CRAN, so there is no installed base to migrate and a compatibility alias would be permanent debt paid for nobody.
The old name described the mechanism; every other fitting verb here
is named for its method. multilpa() is latent profile
analysis, so latent transition analysis is lta(). Under the
old name the capability was undiscoverable by the name the literature
uses: searching the index for “lta” found nothing.
multilta() was considered and rejected. It is one
character from multilpa(), and the two take the same
leading arguments, so a typo would fit a different model and still
return a result.
Nothing else moves. The fitted object is still
multilpa_transitions, the table is still
get_data(x, "transitions"), and the help page is now
?lta.
plot() on an lta() fit raised
multilpa_no_plot for every view. That was a blanket
refusal, not a limitation: the measurement model is the one
multilpa() fits, and the fit already carried
means, variances,
subject_posteriors, subject_profiles and
sequence_lengths. Six of the seven plot helpers worked
against one unchanged; the seventh was called with the wrong
signature.
plot.multilpa_transitions() now offers ten views:
"transitions", "profiles",
"bars", "heatmap", "responses",
"sequences", "sizes", "entropy",
"posteriors", "avepp", and
"all".
what = "transitions" is new and is this family’s own
view: the estimated transition matrix as a heatmap, one panel per group
class, row the current profile and column the next, so the diagonal is
persistence. Every cell prints its probability, and the fill is the
white-to-blue ramp the average-posterior matrix already uses, so darker
is a higher probability anywhere in the package. A row with no data
support has its label parenthesised: such a row is uniform by
construction rather than estimated.
"bars" draws point estimates with no whiskers here. The
interval it draws on a multilpa() fit is a Wald interval,
and this family has no standard errors, so there is none to draw; the
subtitle says so rather than the plot implying an uncertainty it does
not have.
multilpa_plot_types() lists thirteen views.
multilpa_no_plot keeps its entry in
?"multilpa-conditions", reworded to what still raises it:
what = "all" on an object whose method names no views.
get_tna()
and get_group_tna() are documentedBoth were added in 0.11.7 and appeared nowhere in the README. The README now carries a section and a feature-matrix row for them, and every one of the 17 exports appears there again.
^future$, ^docs$,
^vignettes/\.beatrina and ^vignettes/.*\.tex$
are added to .Rbuildignore. The hidden
vignettes/.beatrina-* directories were in
.gitignore but not in .Rbuildignore, so they
stayed out of version control and shipped in the tarball anyway, along
with a leftover vignettes/multilpa.tex from a preview
render.
R CMD check --as-cran on this version reports 1 NOTE, 0
errors and 0 warnings. The NOTE is “New submission”.
Every number in the prose of all four vignettes was checked against the value its own chunk actually prints, and the stale ones corrected. Seventy-one figures changed. Three passages needed rewriting rather than renumbering, because the conclusion moved with the data:
converged FALSE: its membership logits still carry a
non-negligible score at max_iter. The text says so, and
reads the coefficients as the direction of the association rather than
as a converged maximum.Sonsoles López-Pernas is recorded as an author, and both authors
carry their ORCIDs. The README’s installation section now gives
remotes::install_github("mohsaqr/multilpa") before the
local R CMD INSTALL ., which was the only instruction it
offered.
vignette("multilpa") now opens with what the model
found. The order is the profile means, then how big each profile and
class is, then the profile probabilities, then everything else –
estimation diagnostics, the plots, the sequences. Printing the fit gives
the means one row per profile with each profile’s size, so the guide
starts from a result rather than from plumbing.
Two passages are gone. One introduced the notation
pi_{k|m} and omega_m, said the probabilities
lie between zero and one and sum to one, and never used the symbols
again; what it was reaching for is now said where the table is read. The
other listed the model’s assumptions in a block, of which the clause
that does any work – the default diagonal covariance treats the
indicators as independent within a profile, which a shared association
between activity measures can violate – has moved to Limitations, where
a reader can act on it. A sentence restating the ICC formula’s symbols
in words went with them.
Every figure in the text was recomputed against the standardized indicators.
get_tna() and
get_group_tna()A fitted transition model hands itself to the tna package:
moves <- fit_transitions(course_engagement, vars = activity, id = "student",
time = "sequence", n_profiles = 3, n_group_classes = 3)
get_tna(moves) # one network for the whole sample
get_group_tna(moves) # one network per latent group classget_tna() returns a tna model and
get_group_tna() a group_tna, so every verb of
that package applies: centralities(),
communities(), cliques(),
compare(), plot(). tna is in
Suggests and is required only by these two.
What is handed over is the estimates. tna’s own
method for a fitted mixture model, group_model.mhmm(),
takes the model’s grouping and counts transitions between modal
assignments, discarding the estimated matrices; these verbs pass the
matrices themselves, maximised over the full posteriors, so an
observation split 0.6/0.4 between two profiles contributes to both
rather than entirely to one. The two answer different questions and will
differ wherever classification is uncertain.
The whole-sample network is the marginal transition
matrix: the expected counts summed across classes and then normalised,
not the class-probability average of the class matrices. A class holding
a tenth of the units but a fifth of the transitions counts for its
transitions, which is what a marginal probability means. A state no
observation ever leaves is reported as a self-transition rather than as
NaN.
id = NULLmultilpa() accepts id = NULL, which treats
the observations as independent – each row is its own unit,
n_group_classes becomes one – and fits the ordinary
Gaussian or latent-class mixture the two-level model reduces to:
multilpa(faithful, vars = c("eruptions", "waiting"), id = NULL, n_profiles = 3)id has no default, and omitting it
still raises, now with multilpa_bad_argument naming both
ways forward rather than R’s bare “argument "id" is missing”. A
single-level model is not the default in a package for multilevel
models, and a forgotten grouping must not quietly become a different
model: on data with 106 students in 1,422 enrolments it would report
1,422 independent observations and change every standard error. Every
id = NULL fit raises a multilpa_single_level
warning saying what it fitted.
Nothing about the likelihood changes, and the fit is identical to the
one you get by numbering the rows yourself and asking for one group
class; that equivalence is asserted in the tests. Every verb of the
package works on it, including parameter_inference(), whose
cluster bootstrap degenerates to the ordinary nonparametric bootstrap
when each unit holds one row – which is the right bootstrap for
independent observations – and vcov_type = "robust", whose
per-group scores become per-row scores.
The unit column is fabricated internally as
.observation. It is not handed back by
get_data(x, "data") or
get_data(x, "assignments"), and data that already carry a
column of that name raise multilpa_bad_data rather than
having it silently overwritten. Asking for more than one group class
without an id raises multilpa_bad_argument:
one observation per unit leaves no composition for a second-level class
to differ in.
A single-level fit prints as what it is —
Latent profile analysis: 2 profiles over
160 observations — rather than reporting one group class
and 160 groups of one.
fit_staged() and fit_transitions() still
require id, and say so in their own documentation rather
than inheriting multilpa()’s: both have a second level by
construction.
parameter_inference() gains
method = "bootstrap". It resamples groups with
replacement, refits inside the same covariance family, undoes the
relabelling each refit comes back with, and reports the percentile
interval and the standard deviation of the replicates:
fit <- multilpa(course_engagement, activity, id = "student", n_profiles = 2,
n_group_classes = 1, volume = "varying", shape = "equal",
orientation = "axis")
parameter_inference(fit, method = "bootstrap", iter = 999, seed = 1)This closes the gap the fourteen structures opened. Ten of them —
EII, VII, VEI, EVI, VEE, EVE, VVE, EEV, VEV and EVV — constrain the
volume, the shape or the orientation, which the Wald coordinates cannot
express, so the Wald path refused them.
enumerate_classes(structure = ) could therefore select a
model the package would not then do inference on: on this package’s own
course_engagement data the best of the fourteen is
VEI. Every structure the grid can select now reports an
interval.
vcov() and confint() reach the same path
through .... confint() returns the percentile
interval the table reports rather than rebuilding
estimate +/- z * se from the bootstrap standard error, so
one fit does not give two different intervals.
Groups are the resampling unit, not rows, so the interval carries the
same independence assumption as vcov_type = "robust" and
not the stronger one "observed" makes. Against the
cluster-robust sandwich on course_engagement, where both
are available, the bootstrap standard errors agree to within 11% across
every estimated parameter. statistic, p_value
and p_adjusted are NA on this path: the
interval is the inference, and putting a normal approximation back on
top of the replicates it was read from is the assumption the path exists
to avoid.
A fit that holds a measurement block is refused with
multilpa_unsupported_inference, because the held values
came from another fit and resampling these data does not resample
them.
fit_staged() accepted a measurement fitted with a
constrained covariance structure and silently fitted the second stage as
the nearest structure variance_model and
covariance_model can name: a VEI measurement
came back recorded as VVI. The held means and variances
were the right numbers, but under the wrong model, and
n_parameters_with_measurement counted the spread as
VVI’s. multilpa() already refuses to hold the
variances of such a structure, so a staged fit of one was never
supported; it is now refused up front with
multilpa_bad_stage, naming the structure. Refit the
measurement as EEI, VVI, EEE or VVV, or fit both levels together with
multilpa(), which estimates all fourteen directly.
bootstrap_lrt() refitted each replicate from
variance_model and covariance_model alone,
which cannot express a constrained structure: a VEI model
came back as VVI, two parameters wider, so the reference
distribution belonged to a different pair of models than the statistic
compared against it. Replicates are now refitted from the structure the
model records.
The 0.11.3 section was written in two passes and the earlier one was left standing. Two of its statements are wrong, and are corrected here rather than edited away:
mclust’s fourteen models — the ones with a
non-axis-parallel orientation — remain unavailable” was superseded
within that same release by “All fourteen mclust covariance structures”.
All fourteen ship, and their parameter counts match
mclust’s own.parameter_inference(),
vcov() and confint() refuse EII, VII, VEI and
EVI” named four of the ten that were actually refused. The rule, rather
than the list: Wald inference was available exactly for the four
structures reachable by variance_model and
covariance_model alone — EEI, VVI, EEE and VVV. As of this
release the bootstrap covers all fourteen.Every test that compares multilpa against other software or published
output now lives in equivalence/ in the source repository,
and that folder is not part of the built package. This covers Mplus,
mclust, tidySEM, depmixS4 and the in-house enumeration. The shipped
tests/testthat/ keeps unit and invariant tests only. Run
the comparisons with Rscript equivalence/run.R.
mclust and tidySEM are no longer in
Suggests; nothing shipped uses them.
fit_transitions() is now checked against the external
references pinned by the JStats project: depmixS4 Gaussian and binary
panel models, two Mplus LTA runs, and the plain-LTA row of Table 5 of
Muthen & Asparouhov (2022, Psychological Methods).
Log-likelihoods, criteria and parameters agree to the reference’s
precision. On one binary depmixS4 fixture (K = 3), the recorded depmixS4
solution is a local maximum: multilpa reaches a likelihood 0.016 higher.
A brute-force path-sum likelihood confirms both values.
multilpa(centering = "person") subtracts each group’s
own mean from its rows before the measurement model sees them, so the
profiles become profiles of change rather than of level. This
is the person-mean-centred, within-person design (Quintana, 2021;
Voelkle, Brose, Schmiedek, & Lindenberger, 2014), and until now it
meant transforming the data frame first — which left the fit holding
numbers that no longer matched the frame it was given, so every verb
that checks row alignment was comparing centred values against raw
ones.
multilpa(course_engagement,
c("browse", "lectures", "forum_read", "forum_post", "attendance"),
id = "student", n_profiles = 3, n_group_classes = 1,
centering = "person")The offsets stay on the fit, so nothing downstream has to guess which
scale it is looking at: get_data(x, "data") returns the
columns you supplied, alignment is still checked against them, and the
bivariate residuals are computed on the scale the model was estimated
on. "grand" subtracts one mean per indicator instead.
"none" is the default and changes nothing.
Centring removes exactly the between-unit variation, so
"person" refuses with multilpa_bad_data when
it leaves an indicator constant — which is what happens when a unit has
one observation of it. With "person" the group classes
become types of change pattern, not types of unit.
volume, shape and orientation
are the three pieces a covariance decomposes into —
Sigma_k = lambda_k * D_k * A_k * D_k' — and naming them
reaches every model mclust fits, where
variance_model and covariance_model reached
four.
volume |
shape |
orientation |
model |
|---|---|---|---|
| equal / varying | spherical | — | EII, VII |
| equal / varying | equal / varying | axis | EEI, VEI, EVI, VVI |
| equal / varying | equal / varying | equal | EEE, VEE, EVE, VVE |
| equal / varying | equal / varying | varying | EEV, VEV, EVV, VVV |
All three default to NULL, which follows
variance_model and covariance_model, so a call
that names none of them fits exactly what it did before. An ellipsoidal
structure carries a full covariance array whatever
covariance_model said, because an orientation cannot live
in a diagonal block.
The estimates are those of Celeux and Govaert (1995). Ten have a closed form or a scalar fixed point; EVE and VVE share one orientation across profiles while letting the shapes differ, which has no closed form, and use the minorize-maximize step of Browne and McNicholas (2014) — majorize each trace term linearly, then solve the resulting orthogonal Procrustes problem by singular value decomposition.
Verified against mclust. Parameter
counts match its own df for all fourteen. Against
mclust::mstep() on the same responsibilities, twelve agree
to 1e-11 or better. EVE and VVE differ by at most 5.7e-4 — and attain a
lower value of the objective the M-step minimizes, by 2.1e-7
and 2.8e-5, so the difference is mclust stopping short
rather than a disagreement about the estimate.
enumerate_classes(structure = ) crosses the covariance
structures with the class counts, which is the grid model-based
clustering is usually selected over, and adds a structure
column to the candidate table:
enumerate_classes(course_engagement, activity, id = "student",
n_profiles = 2:4, n_group_classes = 1,
structure = c("EEI", "VEI", "EVI", "VVI"),
centering = "person")candidate_fit() gains structure to name one
candidate out of a grid where the class counts alone name several;
asking without it raises multilpa_unknown_candidate listing
the structures it could have meant.
The two structures whose M-step iterates over orientations now warm start from the previous iteration’s answer, which is worth about 1.2 to 1.3 times on them. An M-step that stops at its iteration cap is counted and reported once per fit, with the count, rather than warning from inside every M-step: one step in three hundred is a different thing from all of them, and a warning raised hundreds of times says neither.
get_data(x, "assignments") gains an
uncertainty column: one minus the posterior of the profile
each row was assigned to, which is exactly what modal assignment
discards.
variance_model ties a profile’s covariance volume to its
shape: "equal" constrains both and "varying"
frees both. volume and shape free them
separately, which makes the six axis-parallel mclust models
reachable instead of two:
volume |
shape |
model | spread parameters |
|---|---|---|---|
| equal | spherical | EII | 1 |
| varying | spherical | VII | K |
| equal | equal | EEI | d |
| varying | equal | VEI | K + d − 1 |
| equal | varying | EVI | 1 + K(d − 1) |
| varying | varying | VVI | Kd |
Each profile’s covariance decomposes as
Sigma_k = lambda_k * A_k, a volume
lambda_k = |Sigma_k|^(1/d) and a shape A_k
with determinant one; the estimates are those of Celeux and Govaert
(1995). volume/shape default to
NULL, which follows variance_model, so every
existing fit is unchanged.
Verified against mclust: the parameter counts match its
own for all six models, and at mclust’s maximum-likelihood
estimate the log likelihood agrees with one written from the mixture
definition to 1e-10. The maximized likelihoods are monotone along the
whole nesting lattice.
Three consequences worth knowing:
parameter_inference(), vcov() and
confint() refuse EII, VII, VEI and EVI with
multilpa_unsupported_inference. The free coordinates this
package differentiates are log variances, one per profile and indicator,
which is the wrong chart for a constrained volume or shape — it has more
coordinates than the model has parameters. EEI, VVI, EEE and VVV are
unaffected.variances block;
fixed = "means" is fine.start taken from a wider structure is projected onto
the requested family before the first iteration, because EM only
promises a likelihood that does not decrease within the family
it is maximizing over.covariance_model = "full" constrains neither volume nor
shape, so naming either argument with it raises
multilpa_bad_argument. Six of mclust’s
fourteen models — the ones with a non-axis-parallel orientation — remain
unavailable.
Eleven table verbs and the what = argument of seven
as.data.frame() methods are replaced by one
generic, get_data(x, what = ). A reader no longer
has to know which verb owns which table before they can ask for it:
get_data(fit) # the primary table, the measurement model
get_data(fit, "entropy") # was entropy_table(fit)
get_data(fit, "transitions") # was transitions(fit)
get_data(fit, "profile_probabilities") # was as.data.frame(fit, what = "...")
names(get_data(fit, "all")) # every table this fit can producewhat = "all" returns a named list of every table, built
from the same definitions a single what uses, so the two
cannot disagree. A table this particular fit cannot produce – sequences
for a fit made without time, bivariate residuals for a
family with no discrete group classes – is left out rather than
erroring, so the names of the list say what was available.
summary() now carries every table and prints all of
them, truncated to rows = 10 each with the
get_data() call that returns the rest.
as.data.frame() is plain coercion to the primary table; it
no longer takes what, and an argument it cannot use raises
multilpa_bad_argument naming it rather than quietly
returning a different table.
| Was | Now |
|---|---|
assignments(fit, data) |
get_data(fit, "assignments", data = data) |
average_posteriors(fit) |
get_data(fit, "average_posteriors") |
bch_weights(fit) |
get_data(fit, "bch_weights") |
bivariate_residuals(fit, data) |
get_data(fit, "residuals", data = data) |
classification_errors(fit) |
get_data(fit, "classification_errors") |
classification_table(fit) |
get_data(fit, "classification") |
entropy_table(fit) |
get_data(fit, "entropy") |
information_criteria(fit) |
get_data(fit, "information_criteria") |
sequences(fit) |
get_data(fit, "sequences") |
sequence_summary(fit) |
get_data(fit, "sequence_summary") |
transitions(fit) |
get_data(fit, "transitions") |
as.data.frame(fit, what = "x") |
get_data(fit, "x") |
as.data.frame(summary(fit), what = "fit") |
get_data(fit, "model") |
as.data.frame(diagnostics(fit), what = "posteriors") |
get_data(diagnostics(fit), "average_posteriors") |
fit_covariates(...) |
multilpa(..., profile_covariates = ) |
Three tables that could only be reached through a
summary() object are now on the fit itself:
"counts" (effective class memberships),
"covariances" (the within-profile residual covariance
matrices) and "model" (the one-row fit summary, previously
what = "fit"). An enumeration grid gains
"criteria", the per-criterion minima its summary already
printed.
"classification", "average_posteriors",
"classification_errors" and "bch_weights"
default to both levels on a fit that has discrete group
classes, where the old verbs defaulted to "individuals".
This is the rule diagnostics() already followed. Pass
level = "individuals" for the old behaviour.
"classification_errors" and "bch_weights"
accept level = "both" for the first time; weight a
regression with one level, not with both.
as.data.frame() on a fitted transition model returns the
measurement model, not the transition matrix, because
the primary table is now the same kind of thing for every fitted family.
get_data(fit, "transitions") is the transition matrix.
multilpa(), not a separate verbfit_covariates() is removed. Membership
covariates are part of the model multilpa() fits, so they
are arguments to it:
multilpa(course_engagement, activity, id = "student", n_profiles = 2,
n_group_classes = 2, profile_covariates = "previous_grade")The result is a multilpa_covariates object exactly as
before, and it records the multilpa() call the caller
wrote. The covariate likelihood has no observed-data form, no
starting-value contract of the shape the covariate-free EM uses and no
held-measurement machinery, so start,
missing = "fiml" and fixed are refused by name
with multilpa_bad_argument rather than accepted and
ignored.
plot(x, what = "all") draws every view the fit has the
ingredients for, re-issuing the caller’s own call once per view so style
arguments carry into each panel. A view that refuses is named at the end
rather than stopping the sequence.
get_data(x, "assignments", truth = ) cross-tabulates the
model’s labels against known ones, so checking a fit against a
generating truth no longer needs xtabs() on the assignment
frame:
get_data(fit, "assignments", data = course_engagement,
truth = c("engagement", "student_type"))Each truth column is compared against the level it describes, read
off the data rather than guessed: a column taking one value within every
group is a property of the group and goes against
group_class, and one that varies inside any group goes
against profile. The assignment column records
which comparison was made.
print() on any fitted model now shows the measurement
model under the header, so fit alone is the whole result.
fit followed by as.data.frame(fit) was two
calls showing one thing.
fit <- multilpa(course_engagement,
c("browse", "lectures", "forum_read", "forum_post", "attendance"),
id = "student", n_profiles = 2, n_group_classes = 2)
fit
#> Two-level latent profile analysis: 2 profiles, 2 group classes
#> ...
#> profile indicator mean variance standard_deviation
#> 1 browse 2.854 0.3521 0.5934
#> ...
#> Every other table: get_data(x, what = ), or get_data(x, "all").An all-categorical fit prints its response probabilities instead,
rather than an empty block under a heading promising means.
rows bounds the printout; get_data() returns
any table whole.
get_data(x, "model") renames the plain fit’s
bic column to bic_groups, which is what the
covariate and random-intercept families already called it, so one verb
no longer spells the same convention two ways. The table is
family-shaped rather than a padded union: a random-intercept fit reports
its integration diagnostics and a covariate fit its predictor counts.
"information_criteria" is the table with one column set for
every family, and is the one to compare fits across families with.report() gains rows, forwarded to
print(summary(x)), because a first look at a fit with
thousands of observations would otherwise be mostly posteriors.report() and diagnostics() are unchanged
in purpose. diagnostics() reports its tables through
get_data() and renames its average-posterior table from
"posteriors" to "average_posteriors", which no
longer collides with the individual posteriors.multilpa_removed_argument is removed
from the condition catalogue. It could only be raised by
classification_table(detail = ), and that verb no longer
exists, so nothing can raise it. ?"multilpa-conditions" no
longer claims otherwise.multilpa_no_group_classes instead of an unclassed
error, so get_data(x, "all") can tell a table that does not
apply from a defect.school_engagement and engagement_panel are
removed and replaced by course_engagement:
1422 enrolments of 106 students across 32 courses, ordered within each
student. The same rows now carry every model the package fits.
activity <- c("browse", "lectures", "forum_read", "forum_post", "attendance")
multilpa(course_engagement, activity, id = "student",
n_profiles = 2, n_group_classes = 2) # two-level
fit_transitions(course_engagement, activity, id = "student",
time = "sequence", n_profiles = 2) # the same students, in order
fit_covariates(course_engagement, activity, id = "student",
n_profiles = 2, n_group_classes = 1,
profile_covariates = "previous_grade") # and what predicts itThe old pair could not do this. school_engagement’s
term column was not time – it indexed twelve different
students within a school – so anything about order needed the second
dataset and a different set of people, and neither dataset carried a
covariate at all.
Two new columns make that possible. previous_grade is
the standardised grade the student earned in the course before
this one, so it predicts engagement rather than summarising it.
attendance is the count of days the student was active,
following the same engagement as the click measures but recorded one
tick per day rather than one per click, so it carries more noise than
they do.
The generating truth still ships beside the observations –
engagement for the enrolment pattern and
student_type for the kind of student – so a fitted model
can still be checked against what produced it:
xtabs(~ profile + engagement, data = assignments(fit, data = course_engagement))The truth column is called engagement rather than
profile because assignments() adds a column
named profile, and a dataset already carrying that name
would make the verb refuse to join its own output onto it.
The data are simulated. The simulation is calibrated to the shape of
a real learning-analytics export, using only aggregate constants from
it: marginal means and spreads on the log scale, the separation between
an engaged and a disengaged pattern, and how persistent that pattern is
across a student’s courses. No row, identifier or value of any real
student is present, and the source is not distributed. See
data-raw/course-engagement.R.
The activity indicators are natural-log counts,
log1p(events). Raw learning-analytics counts are strongly
right-skewed and on an unlogged draft the skew alone looked like an
extra class, exactly as a floor effect does.
This release is a correctness release. An independent review of 0.10.0 found that several advertised paths returned plausible numbers that were wrong, rather than failing. Every one of them is fixed below, each with a regression test that reproduces the original defect. Nothing here adds a model family.
fit_covariates() ran its membership M-step on the
coefficients as supplied. BFGS stops on a relative tolerance
measured in the coordinates it is given, so a predictor in small units
stalled the search at a point where the score was still large, and
optim() reported success. Rescaling one covariate by
1e-7 moved the log likelihood from -620.1281
to -730.5748 and drove the slope to zero, with no warning.
The search now runs on root-mean-square-scaled coordinates and
convergence is judged by a unit-free score, not by the optimiser’s
return code. Estimates are returned on your scale, unchanged:
# identical to machine precision at every scale
fit_covariates(data, "y", "g", 2, profile_covariates = "z")A held measurement was passed between stages as an unlabelled,
positional list, while the compatibility check compared only the
sorted set of indicator names – which a permutation passes.
Reordering categorical = c("a", "b") to
c("b", "a") swapped whole response distributions
(-587.5909 to -671.5937); reversing a factor’s
levels swapped categories within an item (-1047.9689).
starting_values() now keeps the indicator names and
category labels, and multilpa() aligns on them. An encoding
that cannot be aligned raises multilpa_bad_start or
multilpa_bad_stage instead of returning a number. An
unlabelled start is still read positionally, so stored starts keep
working.
bootstrap_lrt() accepted fixed-measurement models but
dropped fixed and the held values from every refit, so the
observed statistic compared constrained fits while the null distribution
came from unconstrained ones – a 1-versus-3 parameter statistic referred
to a 9-versus-11 parameter null. Refits now carry the constraint, and a
pair whose constrained nesting cannot be established is refused with
multilpa_bad_nesting.
Expected transition counts exponentiated a scaled forward factor and
an unscaled backward factor separately, so on a long sequence one
overflowed to Inf where the other underflowed to
0. A converged 600-occasion fit reported NaN
for every count, and fitting 1,800-occasion sequences failed outright.
The complete pair log probability is now formed before exponentiating,
which cannot overflow. Counts agree with explicit path enumeration to
1.8e-15.
descriptives()
no longer invents or replaces valuesAn NA stratifier produced NA row selectors,
and x[NA, ] fabricates a row rather than dropping one:
every stratum gained a phantom missing observation while the real row
vanished. Missing strata are now a labelled stratum, and the per-stratum
counts are asserted to account for every input row. Separately, the
profile assignment was written into the same frame as the indicators, so
an indicator named profile or group_class was
overwritten by its own class labels. The assignment is now kept beside
the frame, never in it.
three_step() and r3step() summed
estimating-equation contributions within groups without requiring enough
independent groups. Those contributions sum to zero at the estimate –
that sum is the stationarity condition – so a single group gave
a standard error of 5.9e-17 and a confidence interval of
zero width, and r3step()’s robust path reported p-values of
exactly zero. Both now require more independent groups than the
quantities reported, raising multilpa_too_few_groups.
three_step(vcov_type = "independent") offers an explicit,
labelled unclustered alternative.
diagnostics()
no longer ignores the argument you passeddiagnostics() and report() documented that
... reached the verbs they gather, but discarded it.
Because by is a real argument of
bivariate_residuals(),
diagnostics(fit, by = "overall") silently returned
per-profile residuals under the name you asked to pool. Both verbs now
take by explicitly and refuse any argument they cannot
forward.
bootstrap_lrt() judged a replicate invalid if its
statistic fell below a fixed -1e-5. But the statistic is
2 * (ll_alt - ll_null) on log likelihoods of order 10^3 to
10^4, while EM stops on a relative tolerance, and under the
null the alternative converges to the null solution – so every replicate
sits at the boundary with noise of about
2 * tol * |log likelihood|. On
course_engagement that is 1.6e-04, sixteen times the
window. Replicates that had all converged were reported as
“Nonconvergence or reversed likelihood” and the p-value withheld. The
window now scales with tol and the likelihood, so the same
comparison returns every replicate valid and a p-value.
parameter_inference() cut a fixed 0.01 on
the scaled score. On 720 observations at the default
tol = 1e-8 that fires on a fit which converged with all 20
starts replicating. The criterion is now the displacement the score
implies, expressed in the estimate’s own standard errors: a score
g against information I moves the estimate by
g / I, and the standard error is sqrt(1 / I),
so g * SE is that displacement and is dimensionless. The
default-tolerance fit sits 0.17% of a standard error from stationarity
and is now quiet; a fit at tol = 1e-4 sits 5% away and
still warns, now saying so in those words.
assignments() documented that it checked row alignment
but compared only the row count, and bivariate_residuals()
trusted row order. A reversed frame was paired with the wrong posteriors
and returned as a result. Both now compare every column the fit
recognises and raise multilpa_bad_inference_data on a
mismatch; a frame sharing no column with the fit warns
multilpa_unverified_alignment rather than passing in
silence.
parameter_inference(), vcov() and
confint() work on fixed and staged fits, reporting standard
errors conditional on the held measurement – which
?fit_staged already promised. They previously failed with
length(theta) == object$n_parameters is not TRUE. Held
blocks enter the likelihood at their held values and contribute no row
to the information matrix; the conditional likelihood matches an
independent enumeration to 5.7e-14 and the standard errors
match an independent numerical information matrix to
1.3e-07 relative.assignments() and
descriptives(by = "profile") work on a random-intercept
fit. Both were documented to, but the fit stored no modal assignment, so
the first failed with an unclassed cbind() error.multilpa_plot_types() lists
"random_intercepts", which
plot.multilpa_random_intercept() has always accepted.fit_transitions(max_iter = 0) returns a usable fit
instead of failing with “attempt to set an attribute on NULL”.plot() draws what = "entropy" and
what = "posteriors" for covariate and random-intercept
fits, so plot(diagnostics(fit)) works for every
family.max_iter = 0 with a supplied start now
evaluates that start alone and ignores n_starts, which is
what ?multilpa promised. It previously scored all random
starts and returned the best of them.which.max(), which was choosing between equally valid
optima on floating-point noise.parameter_inference() refuses it instead of
returning Wald intervals around an unbounded coefficient.multilpa_extreme_coefficients,
multilpa_quadrature_check,
multilpa_empty_transition_row,
multilpa_unverified_alignment,
multilpa_no_free_parameters,
multilpa_held_parameter.three_step() gains vcov_type;
diagnostics() and report() gain
by; plot.multilpa_covariates() and
plot.multilpa_random_intercept() gain
"entropy" and "posteriors". All are additive
and keep positional calls.multilpa_bootstrap_lrt objects carry
fixed, and as.data.frame(what = "test") gains
a fixed column.parameter_inference() on a fixed fit reports no row for
a held block and carries a fixed attribute. The table is on
the natural scale, where every probability in a set is reported, so it
has n_parameters rows plus one for each reference category
the estimation scale drops. vcov() is
n_parameters square on the unconstrained scale; on the
natural scale it is correspondingly larger and singular.descriptives(by = ) can return one additional row: the
labelled missing stratum.profile_prevalence for a transition fit is read from
the final expectation rather than the last M-step. These agree at
convergence; the new value is the correct one for an unconverged or
zero-iteration fit.starting_values() keeps names and column labels on
categorical response blocks, and
as.data.frame(what = "responses") now reports those labels
rather than positions, matching what the fitted object’s own response
table reports for the same quantity. A start carrying no labels is still
read positionally and its tidy view still reports positions.assignments() raises
multilpa_bad_inference_data rather than
multilpa_bad_nesting for a row-count mismatch, which is a
data contract failure, not a model-nesting one.bootstrap_lrt() and fit_transitions() now
raise their catalogued class instead of a bare simpleError,
so ?"multilpa-conditions" is true of them.multilpa(fixed = ) fit records
n_parameters_with_measurement, which only
fit_staged() set before. print() consequently
reported the same count twice on such a fit; it now reports the two
counts it names.parameter_inference() results carry
score_displacement beside scaled_score, the
quantity the convergence warning is judged on.bivariate_residuals() gains adjust, and
its table a p_adjusted column. Every pair of indicators is
a separate test, so a five-indicator fit asks ten questions at once; the
family size is now the verb’s to declare rather than the reader’s to
reconstruct. The default is "none", so
p_adjusted equals p_value unless asked
otherwise.Depends floor; a second workflow runs
lintr.DESCRIPTION gains URL and
BugReports.Post-fit verbs no longer ask for data the fit is already holding. The model frame is rebuilt from the uncentred indicators, the categorical codes and their levels, the identifier and the occasion — all of which were stored already — so this costs no extra memory, and the round trip is exact for every model family.
parameter_inference(fit) # was parameter_inference(fit, students)
bivariate_residuals(fit)
vcov(fit); confint(fit)
bootstrap_lrt(smaller, larger)
as.data.frame(fit, what = "data") # the columns the model was fitted toSupplying data still works and is still the stronger
check, since a frame that does not reproduce the fitted likelihood is
rejected as before. three_step() and r3step()
keep requiring data, because an outcome or a covariate is a
column the model never saw. The multilpa_data_required
condition is retired: there is now something sensible to default to.
descriptives(x, vars, id) — one row per variable with
n, missing, mean, sd, range, and the intraclass
correlation: the share of variance lying between groups. An ICC
near zero says the groups do not differ, so a model built to tell them
apart has nothing to find. It accepts a data frame before fitting or a
fit afterwards, and by = "profile" splits it by assigned
profile.diagnostics(x) — entropy, classification quality,
average posteriors and bivariate residuals in one call, as a classed
object with print(), as.data.frame(x, what = )
and plot(). plots = TRUE draws them too. A
model family that cannot supply a table says so rather than
failing.report(x) — summary, descriptives, diagnostics and
every plot the fit supports, in one call. It skips views a given fit has
no ingredients for rather than stopping at the first refusal.assignments()Every observation with the profile it was assigned to, the group class, and its posteriors – optionally with a frame of your own columns kept alongside:
labelled <- assignments(fit, data = course_engagement)
xtabs(~ profile + engagement, data = labelled)Comparing an assignment with anything else means putting them in the same row, and doing that with two separate objects assumes they share an order. This verb owns the alignment: a frame with the wrong number of rows is refused, and a column the assignments would overwrite is an error rather than a replacement.
It is a verb rather than another as.data.frame(what = )
value because it computes a join with a frame the caller supplies. The
line: as.data.frame() represents what a fit already holds,
and anything bringing in columns the model never saw gets its own verb
with its own documented arguments.
plot(fit, what = "bars")
draws its intervals without being handed dataThe fit carries the columns it was built from, so the 95% intervals
are drawn by default; data is now only an override. A model
family whose standard errors are not implemented gets bars without
whiskers rather than an error.
report() likewise asks each plot() method
what views it accepts instead of assuming the full catalogue, and skips
a view the method refuses – naming what it could not draw rather than
dropping it silently.
1 rather than
NA when a variable has no within-group variance. That is a
well-defined case — every observation equals its group’s own value — not
a degenerate one.information_criteria()
returns the shape people reportThe default is now one row, with one column per criterion — the shape
a model-comparison table is published in, and the same column names
enumerate_classes() uses, so a chosen model’s row sits
under the grid it was chosen from without renaming anything.
information_criteria(fit)
#> log_likelihood n_parameters aic kic bic_groups bic_individual ...
#> 1 -3778.259 15 7586.518 7604.518 7617.933 7655.207 ...
information_criteria(fit, format = "long") # the previous shapepenalty column has been removed. No convention
reports it — not Mplus, not tidyLPA::get_fit(), not mclust
— and it is the difference between two numbers the table already
carries.definitions = TRUE describes one criterion per row, so
it now requires format = "long" and raises
multilpa_bad_argument otherwise.as.data.frame(fit, what = "information_criteria")
follows the verb, and its format argument now actually
reaches it. Previously format was captured by the
accessor’s own signature and never forwarded.convention remains NA_character_ for
deviance, aic and kic, now
documented as meaning the question does not arise rather than
a value is missing.school_engagement (720 students in 60
schools) and engagement_panel (120 students over four
waves). Both are simulated and both carry the kind each row was
generated from — engaged and state — so a
fitted model can be checked against what produced it.vignette("multilpa") is rebuilt on those datasets and
gains a section cross-tabulating the fitted profiles against the truth
column.NestimateNo deprecation shim: the old spellings are gone and raise
unused argument.
| was | is | where |
|---|---|---|
object |
x |
first formal of this package’s own verbs |
indicators |
vars |
multilpa(), fit_covariates(),
fit_staged(), fit_transitions(),
fit_random_intercept(),
enumerate_classes() |
group |
id |
the same six |
level_ci |
ci_level |
three_step(), r3step() |
p_adjust |
adjust |
parameter_inference(), three_step(),
r3step() |
n_boot |
iter |
bootstrap_lrt() |
profiles, group_classes |
n_profiles, n_group_classes |
enumerate_classes() |
# before
multilpa(students, indicators = vars, group = "school", n_profiles = 2)
# after
multilpa(students, vars = vars, id = "school", n_profiles = 2)The S3 methods for the base generics summary(),
coef(), vcov(), confint(),
logLik() and nobs() keep object,
because those generics own that name.
Tidy output columns did not change. Every posterior,
sequence and classification table keeps its group column,
level keeps the value "group", and fitted
objects keep group_classes and
n_group_classes.
multilpa_plot_types() lists every view the
plot() methods accept, with the group each belongs to and
what it answers. It carried @export in 0.7.0 but was
missing from NAMESPACE, so it could not be called.plot() gains four views: "bars" (profile
means as grouped bars, with 95% intervals when data is
supplied), "heatmap" (means as deviations from each
indicator’s grand mean), "entropy" and
"posteriors" (per-case classification quality as
ridges).mfg as part of
par(no.readonly = TRUE) switches new on as a
documented side effect, so a view that validated and raised before
drawing left every subsequent plot ready to overlay onto a blank
panel.plot(fit) encodes prevalence absolutely
rather than being min-max normalised within the plot, which had mapped
any spread onto the full size range and so carried no information.data.frame. See
CHANGES.md in the source repository for the full
sweep.