The two fitting options every mixture-type family refuses, in its own name.
Source:R/families.R, R/predict.R, R/structure.R
frmtmb-extension-api.RdA mixture likelihood is invariant to permuting its components, so the
location coefficients enter a multimodal objective. Both REML and
profile = TRUE integrate those coefficients out with a Laplace
approximation about a single inner mode, which is not defined here:
the inner Newton solve walks between the component modes and the fit
either dies at "NA/NaN gradient evaluation" or reports an optimum
with a gradient near 1e9. Quadrature is unaffected, because it
marginalizes the random effects, not the coefficients.
The conservative all-FALSE default is right for a family whose
likelihood does not factorize: it starts fully refused and opts in.
It is exactly wrong for a family whose likelihood DOES factorize and
which carries a structure only to hold the two or three refusals that
belong to it, because there every other capability already works and
a flag would only take it away. This is the starting point for that
second kind, so such a family names what it refuses and nothing else.
The read-only surface a frmtmb_structure() slot may use. A
structure's fitted_mean, fitted_var, latent_probs and sim_ctx
run at the estimates and outside the AD tape, and they are handed a
frmtmb_fit; these are how such a slot reads what it needs out of
one without reaching into the fit's internal layout. The rest of a
family's needs are already public: frm(), bf(), fixef(),
set_prior(), posterior_predict(), conditional_effects() and
hypothesis() among them.
Usage
response_mean(fam, dpars, aterms)
mixture_multimodal_refusals(what)
mixture_posterior(fit)
as_frmtmb_family(x)
eval_dpars(fit, b = fit$estimates[["b"]])
single_response(fit, what)
fit_extras(fit, resp = NULL)
dpar_linpred(frame, params, resp, dpar)
structure_supports_all(...)
frame_block_of(frame, resp)Arguments
- fam
A
frmtmb_family.- dpars
Named list of numeric dpar values, as one element of
eval_dpars().- aterms
Evaluated addition terms for the response,
fit$frame$aterm_values[[resp]].- what
For
single_response(), the calling function as it should open a refusal:"latent_probs()". Formixture_multimodal_refusals(), the family as the refusal should name it:"an lca() family".- fit
A
frmtmb_fit.- x
A family: a
frmtmb_family, astats::family()object, a family constructor, or a family name.- b
Random-effect vector to evaluate at, defaulting to the fit's own estimates.
NULLdrops the random-effect contribution.- resp, dpar
The response name and the distributional parameter name, as
eval_dpars()returns them.- frame
A
fit$frame, or the frame acheck_fitslot is given.- params
A parameter list in the frame's layout: a fit's
estimates, or the starting template acheck_fitslot is given.- ...
Capability flags to override, named as in the
supportstable offrmtmb_structure().
Value
single_response() a response spec; eval_dpars() a nested
list of numeric vectors; fit_extras() a named list or NULL;
dpar_linpred() a numeric vector or NULL; response_mean() a
numeric vector; as_frmtmb_family() a frmtmb_family;
frame_block_of() a list or NULL; structure_supports_all() a
named list of logicals; mixture_posterior() a matrix of class
probabilities; mixture_multimodal_refusals() a list of two
refusal strings.
Details
what is the family as the sentence should name it. These used to be
one message listing every mixture-type family, raised from one gate
in fit.R that knew all three by name; each family states its own now.
They are documented together because they are one contract with one promise: what they return is stable, so a family in another package can be written against them. Everything else in a fit, a frame or a family object is internal and may be renamed without notice.
single_response()The one response spec of a univariate fit, or the refusal a method owes a multivariate one.
whatnames the caller and opens the message. Almost every structured family is univariate, so this is the first line of most slots. It was nameduni_resp()while it was internal.eval_dpars()The distributional parameters of every response at the estimates, on their natural (response) scale, evaluated over the training rows:
out[[response]][[dpar]]is a numeric vector of lengthnor1.bsupplies a different random-effect vector, which is how a family evaluates at the modes of a resampled fit. This is the same evaluationpredict(),fitted()andresiduals()run.fit_extras()The family's extra (non-dpar) parameters at the estimates, as a named list in the order
extra_parsdeclared them, orNULLwhen the family declared none. Item profiles, ordinal thresholds and class covariances arrive here. In a multivariate model each response's block is stored under a name that carries the response;respadds that response's block back under the names its family declared, which is the list its density and simulator read.dpar_linpred()The FIXED-effect part of one dpar's linear predictor, on the link scale, at a parameter list in the frame's own layout, or
NULLwhen that response and dpar have no linear predictor with columns. Random effects are excluded on purpose: this exists for acheck_fitslot, which runs before the inner modes are solved and cannot see them. Not to be confused with thedpar_*accessors of frmtmb-robust-dpars, which take adparslist rather than a frame and run inside a density while the objective is taped.response_mean()The expected response of a family at given dpar values, with
trunc()bounds applied when the response carries them. A structured family composing per-state or per-class means calls this once per state rather than reaching into the wrapped family'spostslot.as_frmtmb_family()The internal representation of a family given as a
frmtmb_family, astats::family(), a bare family constructor or a name. A family that WRAPS another one calls this on its argument, so thathmm(2, gaussian),hmm(2, gaussian())andhmm(2, "gaussian")all mean the same thing.frame_block_of()The block a
frame_blockslot built for one response, orNULLwhen it built none. This is the read half of that slot: the block is written once at frame assembly and read back by every slot that runs at the estimates, and where a frame keeps its blocks is not a layout an extension should spell for itself.structure_supports_all()A
supportslist with every capability TRUE and the named ones overridden. The starting point for the second kind of structure: a family whose likelihood DOES factorize over rows and which declares a structure only to carry the two or three refusals that belong to it. Writing the TRUEs out by hand instead would silently take away each new capability frmtmb adds.mixture_posterior()The posterior class probabilities of any family that implements the
fam$mixcomponent interface, which is whatmixture(), a multivariate normal mixture and a latent-class family all do. A family implementing that interface uses this as its wholelatent_probsslot rather than deriving the same quantity a second time. See "The mixture component interface" below for the fields it reads.mixture_multimodal_refusals()The
refusalsentries forremlandprofileowed by a family whose likelihood is invariant to permuting its components.whatis the family as the sentence should name it, for example"an lca() family". The fact refused is the same one in every such family, so the sentence is issued from one place rather than copied.
The mixture component interface
A family is a finite mixture to frmtmb when its family object has a
mix element (fam[["mix"]] <- list(...), set after
frmtmb_family() returns). frmtmb then treats it as one:
mixture_probs() and pp_mixture() accept its fits, a prior must
name the dpar it means (several locations), and ar()-type terms
and incl_thres are refused. mixture_posterior() reads three
fields, which such a family must supply:
KThe number of classes, an integer.
log_pi(dpars)The log mixing weights: a list of
Kvectors, each of length 1 or the number of rows, from the family'sdparslist. They must sum to one on the natural scale in every row.comp_lpdf(y, dpars, aterms, k, extra)The log density of each row under class
k, a vector with one value per row ofy(NROW(y), so a matrix response gives one per row). The fifth argument, the family's extra parameters asfit_extras()returns them, is passed only when the family declaresextra_pars; a family without extra parameters takes four arguments.
The other fields that mixture() stores in mix (ord, ref,
type, comp_dpars, comp_sim and its component bookkeeping) are
read only for mixture() itself; a family that leaves them out is
not read through them. A family may store fields of its own there,
as frmtmb.latent's mixture_mvn() stores D and sigma.
How to add a covariance structure
A package that supplies a structured covariance, or a precision
matrix that depends on parameters, registers it with
frmtmb_register_covstruct() from its .onLoad(). frmtmb.spatial
does this for car() and spde(). The steps:
Decide how a formula writes the structure. A bar structure,
name(terms | g), covaries the coefficients of one level. A field, a special such asname(M, gr = g), has one intercept per level ofgrand covaries the levels through fixed data; give it atermslot.Write
lpdf(b, theta, blk), the log density of the block on the tape, with RTMB's densities and no loop over levels, andvcov(theta, blk), the covariance within one level, on numbers. Name the theta entries that are log standard deviations insd_idx.For a field, give
natural, the rowsconfint_varcorr()reports, anddraw, one draw of the whole field. Setlevels_coupled = TRUEwhen the levels are correlated, so that an unseen level gets no law it does not have.Register the rows the structure adds to
frm_compat()withfrmtmb_register_compat(), AFTER the structure: the structure itself joins the vocabulary when it is registered.Test the fit against a likelihood you compute without the registry, in a test that calls
frmtmb_local_registries()first, so that every registry is restored when the test ends.
Use exported functions only, never frmtmb:::. When a user writes
the structure while its package is not loaded, core refuses it by
name if its table of extension homes lists the package, which is how
car() points at frmtmb.spatial.
A term that correlates the RESIDUALS of the response, rather than
adding a latent vector to the linear predictor, is not a covariance
structure: it changes the response's density, and it may change its
mean. Register it with frmtmb_register_autocor() instead, as
frmtmb.spatial does for sar(). Such a term is one affine map from
the response to independent innovations, and core writes its
density, mean and draws from the map.
What the registries hold you to
The registries an extension fills from its own .onLoad()
check what they are given, because a registration that is accepted
and then does nothing is indistinguishable from one that worked.
frmtmb_register_compat()refuses a rule whose sides it cannot resolve. A side names a feature, a kind, a group or"*"; anything else matches no pair, so the rule is dropped in silence and the table reads as complete while covering less. Name a feature your package supplies infeatures =, and one another package supplies inexpects =. A nameexpects =gives that the vocabulary already holds is refused too, because it exempts nothing; one still unresolved whenfrm_compat()is called is reported there rather than dropped.frmtmb_register_aterm()adds the term to that vocabulary itself, as"<name>()"of kind"aterm". A termfrm()accepts andfrm_compat()cannot describe is a gap by construction, and the seam that creates the term is the only place that can close it without asking the registrant to remember. Register the term BEFORE the rules that name it; declaring it infeatures =anyway is a no-op. A name the vocabulary already holds under another kind is refused, and nothing is registered on either side.frmtmb_register_covstruct()checks every slot before it commits anything, adds the structure to the compatibility vocabulary as a feature of kind"covstruct", and refuses a name that frmtmb's own structures or formula specials hold.frmtmb_register_autocor()does the same for a residual correlation term, as a feature"<name>()"of kind"autocor", writes the rows offrm_compat()that refuse what every such term refuses, and checksnparandstartonce per fit.frmtmb_family()takesrequired_atermsas a conjunction (a character vector) or as one with choices in it (a list, whose elements of length more than one are alternatives). Declare the choice rather than refusing it by hand invalid_y: the framework refuses before the frame is built and names every alternative.
See also
frmtmb_structure() for the protocol these serve,
frmtmb_family() for the family object they read,
frmtmb-robust-dpars for the accessors a DENSITY uses while the
objective is taped, frmtmb-conditions for frm_stop(),
frm_warning() and frm_message(), which an extension raises its
conditions with, and the
registries an extension fills from its own .onLoad():
frmtmb_register_frame_check(), frmtmb_register_aterm(),
frmtmb_register_covstruct() and frmtmb_register_compat()
Examples
set.seed(1)
dd <- data.frame(x = rnorm(50))
dd$y <- rnorm(50, 1 + 2 * dd$x, 0.5)
fit <- frm(bf(y ~ x) + gaussian(), data = dd)
rspec <- single_response(fit, "my_family_probs()")
dp <- eval_dpars(fit)[[rspec$resp_name]]
str(dp)
#> List of 2
#> $ mu : Named num [1:50] -0.178 1.424 -0.591 4.215 1.712 ...
#> ..- attr(*, "names")= chr [1:50] "1" "2" "3" "4" ...
#> $ sigma: Named num [1:50] 0.479 0.479 0.479 0.479 0.479 ...
#> ..- attr(*, "names")= chr [1:50] "1" "2" "3" "4" ...
head(response_mean(rspec$family, dp, list()))
#> 1 2 3 4 5 6
#> -0.1776897 1.4240552 -0.5912755 4.2151811 1.7124621 -0.5613004
fit_extras(fit) # NULL: no extra parameters
#> NULL
head(dpar_linpred(fit$frame, fit$estimates, rspec$resp_name, "mu"))
#> 1 2 3 4 5 6
#> -0.1776897 1.4240552 -0.5912755 4.2151811 1.7124621 -0.5613004
as_frmtmb_family(gaussian)
#>
#> Family: gaussian
#> Link function: identity
#> Parameters: mu (identity), sigma (log)
#>