pub struct MultinomialSavedModel {Show 27 fields
pub formula: String,
pub class_levels: Vec<String>,
pub reference_class_index: usize,
pub resolved_termspec: TermCollectionSpec,
pub coefficients_flat: Vec<f64>,
pub p_per_class: usize,
pub n_active_classes: usize,
pub training_headers: Vec<String>,
pub training_table_kind: String,
pub lambdas: Vec<f64>,
pub lambdas_per_block: Vec<usize>,
pub iterations: usize,
pub separation_evidence: Option<String>,
pub penalized_neg_log_likelihood: f64,
pub deviance: f64,
pub edf_per_class: Option<Vec<f64>>,
pub edf_per_penalty: Option<Vec<f64>>,
pub coefficient_covariance_flat: Vec<f64>,
pub smoothing_correction_flat: Option<Vec<f64>>,
pub coefficient_influence_flat: Option<Vec<f64>>,
pub smooth_term_spans: Vec<MultinomialSmoothTermSpan>,
pub training_design_flat: Vec<f64>,
pub training_rows: usize,
pub training_class_index: Vec<u32>,
pub training_weights: Vec<f64>,
pub joint_penalty_flat: Vec<f64>,
pub lambda_labels: Vec<String>,
}Expand description
Saved-model payload for a multinomial fit driven by a Wilkinson formula.
This is what the FFI returns to Python. It carries everything the Python
MultinomialModel.predict path needs to evaluate softmax(X_new · β) on
fresh data using the training basis / penalty structure (no refit on
predict, no re-derivation of class levels).
Fields§
§formula: StringThe training formula, verbatim. Stored so Python’s summary() and
any round-trip persistence path can echo what was fit.
class_levels: Vec<String>Names of the training response levels in canonical order. The last
entry is the reference class (η = 0); the first K - 1 carry the
active linear-predictor blocks. Class permutations are forbidden:
this list is fixed at fit time and predictions emit columns in the
same order.
reference_class_index: usizeIndex of the reference class within class_levels — currently always
class_levels.len() - 1, exposed as a field so future “user-pinned
reference” gauges (e.g. family='multinomial', reference='setosa')
can land without changing the on-disk shape.
resolved_termspec: TermCollectionSpecResolved term-collection spec used to build X at fit time. Replayed
on predict via gam_terms::smooth::build_term_collection_design.
coefficients_flat: Vec<f64>Active-class coefficient block, shape (P, K-1). Column a is the
coefficient vector for class class_levels[a]. Stored flat in
row-major order to keep the serde payload self-describing.
p_per_class: usizeP — coefficient count per active class. Matches the column count of
the design matrix the saved resolved_termspec produces.
n_active_classes: usizeNumber of active classes (K - 1).
training_headers: Vec<String>Original training column headers, in dataset-column order. Needed at
predict time so the FFI can align a fresh Dataset to the training
schema before evaluating the basis.
training_table_kind: StringContainer type of the training table. "unknown" is the explicit value
for Rust/CLI callers without a typed table container; the field is always
present so persistence never invents presentation state while loading.
lambdas: Vec<f64>REML/LAML-selected smoothing parameters, one per (active class, smooth term), flattened in block-major order: all of class 0’s per-term λ,
then class 1’s, and so on. Per-term penalties (#561) mean each active
class block selects an independent λ for every smooth term, so this
vector has length Σ_a (#terms in class a) = (K − 1) · #terms. Use
MultinomialSavedModel::lambdas_per_block to segment it by class. An
unpenalized model (no smooth terms) yields an empty vector.
lambdas_per_block: Vec<usize>Number of smoothing parameters (smooth terms) in each active class
block, parallel to class_levels[0..K-1]. Segments the flat lambdas
vector: class a’s λ are lambdas[Σ_{b<a} lambdas_per_block[b] ..][.. lambdas_per_block[a]]. Every entry is identical in the shared-design
architecture (all classes share the same term structure), but it is
stored explicitly so consumers never have to assume that.
iterations: usizeNewton iterations executed; recorded for the summary report.
separation_evidence: Option<String>The separation evidence that armed the Jeffreys/Firth proper prior on
this fit, or None when the unbiased penalized-REML criterion was
accepted with the prior disarmed (#2612).
The two branches publish different estimands. Disarmed, the
coefficients are the exact penalized-REML mode with zero Firth bias;
armed, they are the mode of that objective plus the proper prior
Φ = ½ log|ZᵀHZ|, which pulls fitted class probabilities toward the
uniform simplex 1/K by an O(1/n) amount that #715 measured as a real
truth-RMSE cost on interior data. A consumer scoring calibration, a
reader comparing two fits, and the CLI summary all need to know which
objective produced the numbers in front of them, and until #2612 the
decision existed only in a log::info! line the caller never sees.
The string is the certificate itself, not a flag: a verdict that carries the spectrum it was taken on can be checked, and one that carries only a boolean cannot.
penalized_neg_log_likelihood: f64Penalized negative log-likelihood at the returned β̂.
deviance: f64Unpenalized deviance −2 log L(β̂).
edf_per_class: Option<Vec<f64>>Per-active-class effective degrees of freedom (hat-matrix trace),
length K - 1. Populated when the REML driver reports an
inference block; falls back to None for the legacy fixed-λ path.
edf_per_penalty: Option<Vec<f64>>Per-PENALTY effective degrees of freedom, one entry per smoothing
parameter (length == lambdas.len()), aligned block-major with the flat
Self::lambdas / Self::lambdas_per_block layout. Each entry is the
penalty-block trace EDF rank(S_k) − λ_k·tr(H⁻¹ S_k), clamped to
[0, rank(S_k)]. This is the per-(class, term, penalty) resolution that
the per-class Self::edf_per_class SUM deliberately hides: only the
per-penalty vector reveals whether an individual smooth collapsed onto its
polynomial null space (its wiggliness λ driven to the λ-cap), which a
per-class total cannot show. Populated whenever the REML driver reports an
inference block; None on the legacy fixed-λ path or when the trace
channel is mis-shaped. Unlike edf_per_class, the entries do NOT sum to
the model EDF when several penalties share one coefficient range (a
double-penalty smooth has Σ_k rank(S_k) > p_per_class).
coefficient_covariance_flat: Vec<f64>Joint posterior coefficient covariance H⁻¹ (#1101), block-ordered to
match the stacked active-class coefficient vector β = [β_0; …; β_{K-2}]
(class a’s P coefficients occupy rows/cols a·P .. (a+1)·P). This is
the Laplace covariance the REML driver already computes from the factored
penalized Hessian; storing it makes posterior-mean prediction and its
integrated uncertainty well-defined. Flattened row-major over the
(P·M)×(P·M) matrix. This is required by the versioned persistence
schema: a payload without covariance is not a usable multinomial model.
smoothing_correction_flat: Option<Vec<f64>>The first-order smoothing-parameter-uncertainty correction
C = J·Var(ρ̂)·Jᵀ (#2346), in the SAME raw units and block order as
Self::coefficient_covariance_flat, flattened row-major over the
(P·M)×(P·M) matrix.
Self::coefficient_covariance_flat is V_cond = Var(β | λ̂): it answers
“how wide is the posterior once the smoothing parameters are known
exactly”, and λ̂ is an estimate, not a known. Every other family in this
library publishes the pair — InferenceCovarianceMode::{Conditional, SmoothingCorrected} in gam-predict, beta_covariance /
beta_covariance_corrected on FitInference — and the multinomial is
the one that never got it, so its bands were conditional-only
(gam#2612, and the #1871 defect one family over).
C is stored rather than V_c = V_cond + C deliberately: the sum is
recoverable exactly by addition (see
Self::coefficient_covariance_corrected) while the difference is not
— subtracting two nearly-equal covariances would lose every digit of a
correction that is small relative to V_cond, which is the regime this
matrix is most often in.
None is a TYPED ABSENCE, never a silent substitution: the outer solve
retained no ρ curvature, or every ρ coordinate is railed and so has no
finite variance to propagate (#2337 Thm 2.3).
coefficient_influence_flat: Option<Vec<f64>>Joint coefficient-space influence matrix F = H⁻¹ X'WX (#1101),
block-ordered identically to Self::coefficient_covariance_flat.
Its per-term diagonal block trace is the term’s effective degrees of
freedom and its tr(F_jj)²/tr(F_jj²) the Wood reference d.f., feeding
the rank-truncated Wald smooth-term test in summary(). Flattened
row-major over the (P·M)×(P·M) matrix. None when unavailable.
smooth_term_spans: Vec<MultinomialSmoothTermSpan>Per-(active class, smooth term) coefficient column range and unpenalized
nullspace dimension within the P-wide class block (#1101). Parallel to
the smooth terms the design produced; replicated across classes by the
shared-design architecture. Drives the Wald smooth-term table in
summary(). Empty for a wholly parametric (no-smooth) model.
training_design_flat: Vec<f64>Training design in the RAW basis, flattened row-major over (n, P).
§Why a saved multinomial model carries its own rows (#2612)
The published prediction is the posterior MEAN probability
E[softmax(x'β)], and a Laplace summary — β̂ plus
Self::coefficient_covariance_flat — cannot produce it. That summary
IS the quadratic model of the log-posterior, and the quadratic model is
exactly what fails: integrating softmax against N(β̂, H⁻¹) keeps the
curvature half of the O(n⁻¹) correction to a posterior mean and drops
the skewness half, which on a (quasi-)separated fit is neither small nor
the same sign. Computing the estimand honestly means evaluating the
posterior away from the mode, which means the likelihood, which means the
rows. See crate::multinomial_predictive.
This is the same choice mgcv makes when it stores the model frame with
the fitted object, and it is required rather than optional: a payload
without it cannot answer the question predict is asked.
training_rows: usizeNumber of training rows n; segments Self::training_design_flat.
training_class_index: Vec<u32>Training class index per row, values in 0..K, aligned to
Self::class_levels.
training_weights: Vec<f64>Training row weights, length n.
joint_penalty_flat: Vec<f64>The joint penalty S_λ at the selected smoothing parameters, flattened
row-major over (P·M, P·M) in the same stacked class-major order as
Self::coefficient_covariance_flat.
Stored rather than rebuilt because the coupled Σ_t λ_t (M ⊗ S_t)
carrier (#1587) is assembled from the family’s equivariant specs, which
the saved payload does not otherwise carry — and because reconstructing
it as Σ⁻¹(I − F) would put an inverse of the very matrix whose
conditioning is the problem on the prediction path.
lambda_labels: Vec<String>One descriptive label per penalty component within a single active-class
block, parallel to that block’s λ slice (i.e. length
lambdas_per_block[0]). The Marra–Wood double penalty (and tensor /
operator smooths) emit more than one penalty component — hence more
than one λ — per smooth term, so this is NOT 1:1 with
Self::smooth_term_spans: a single s(x) term contributes a primary
wiggliness λ labelled s(x) and a null-space shrinkage λ labelled
s(x) [null space]. The summary renderer pairs lambdas with these
labels component-for-component so no λ is ever dropped (#1544). Built from
the per-component term name + penalty role at fit time; empty only for a
wholly parametric model.
Implementations§
Source§impl MultinomialSavedModel
impl MultinomialSavedModel
pub fn validate(&self) -> Result<(), EstimationError>
Sourcepub fn predictive_model<'a>(
&'a self,
training_design: ArrayView2<'a, f64>,
training_weights: ArrayView1<'a, f64>,
joint_penalty: ArrayView2<'a, f64>,
) -> MultinomialPredictiveModel<'a>
pub fn predictive_model<'a>( &'a self, training_design: ArrayView2<'a, f64>, training_weights: ArrayView1<'a, f64>, joint_penalty: ArrayView2<'a, f64>, ) -> MultinomialPredictiveModel<'a>
The training frame and penalty this model carries, as the borrowed view
crate::multinomial_predictive consumes.
Sourcepub fn training_design(&self) -> Result<Array2<f64>, EstimationError>
pub fn training_design(&self) -> Result<Array2<f64>, EstimationError>
Training design as an (n, P) ndarray.
Sourcepub fn joint_penalty(&self) -> Result<Array2<f64>, EstimationError>
pub fn joint_penalty(&self) -> Result<Array2<f64>, EstimationError>
Joint penalty S_λ as a (P·M, P·M) ndarray.
Sourcepub fn coefficients_active(&self) -> Result<Array2<f64>, EstimationError>
pub fn coefficients_active(&self) -> Result<Array2<f64>, EstimationError>
Active-class coefficient block as an (P, K-1) ndarray view.
Sourcepub fn coefficient_covariance(&self) -> Result<Array2<f64>, EstimationError>
pub fn coefficient_covariance(&self) -> Result<Array2<f64>, EstimationError>
Reconstruct the joint posterior covariance H⁻¹ as a (P·M)×(P·M)
ndarray, block-ordered to match the stacked coefficient vector
θ[a·P + i] = β[i, a] (#1101).
Sourcepub fn smoothing_correction(&self) -> Option<Array2<f64>>
pub fn smoothing_correction(&self) -> Option<Array2<f64>>
Reconstruct the first-order smoothing correction C = J·Var(ρ̂)·Jᵀ as a
(P·M)×(P·M) ndarray, block-ordered like
Self::coefficient_covariance (#2346, gam#2612). None is the typed
absence documented on Self::smoothing_correction_flat.
Sourcepub fn coefficient_covariance_corrected(&self) -> Option<Array2<f64>>
pub fn coefficient_covariance_corrected(&self) -> Option<Array2<f64>>
The smoothing-CORRECTED joint posterior covariance V_c = V_cond + C,
the multinomial’s InferenceCovarianceMode::SmoothingCorrected matrix.
None exactly when Self::smoothing_correction is None. This never
falls back to the conditional matrix: a caller that asks for the
unconditional covariance and is handed the conditional one cannot tell
that its interval is narrower than it asked for, which is the whole
defect the mode axis exists to make visible.
Sourcepub fn coefficient_influence(&self) -> Option<Array2<f64>>
pub fn coefficient_influence(&self) -> Option<Array2<f64>>
Reconstruct the joint influence matrix F = H⁻¹ X'WX as a
(P·M)×(P·M) ndarray, block-ordered like
Self::coefficient_covariance (#1101). None when unavailable.
Sourcepub fn predict_probabilities(
&self,
x_new: ArrayView2<'_, f64>,
) -> Result<Array2<f64>, EstimationError>
pub fn predict_probabilities( &self, x_new: ArrayView2<'_, f64>, ) -> Result<Array2<f64>, EstimationError>
Default posterior-mean class probabilities. This integrates
softmax(eta) under the per-row Gaussian predictor posterior rather than
evaluating softmax at the coefficient mode.
Sourcepub fn predict_probabilities_with_se(
&self,
x_new: ArrayView2<'_, f64>,
) -> Result<(Array2<f64>, Array2<f64>), EstimationError>
pub fn predict_probabilities_with_se( &self, x_new: ArrayView2<'_, f64>, ) -> Result<(Array2<f64>, Array2<f64>), EstimationError>
Posterior-mean class probabilities and integrated marginal standard
deviations at fresh design rows, under the covariance definition this
model can support — Self::predict_probabilities_with_se_in_mode with
SmoothingCorrected when the correction is present and Conditional
when it is not.
The DEFAULT is the corrected definition, matching
PredictUncertaintyOptions::default() for every other family
(gam-predict), because λ̂ is an estimate and a band that conditions on
it being exact is answering a narrower question than the caller asked.
Sourcepub fn predict_probabilities_with_se_and_source(
&self,
x_new: ArrayView2<'_, f64>,
) -> Result<(Array2<f64>, Array2<f64>, InferenceCovarianceMode), EstimationError>
pub fn predict_probabilities_with_se_and_source( &self, x_new: ArrayView2<'_, f64>, ) -> Result<(Array2<f64>, Array2<f64>, InferenceCovarianceMode), EstimationError>
The same pair, plus the covariance definition it was actually built from. A caller that cannot tell a conditional band from an unconditional one cannot reason about the interval it was handed, so the provenance is returned rather than inferred.
Sourcepub fn predict_probabilities_with_se_in_mode(
&self,
x_new: ArrayView2<'_, f64>,
mode: InferenceCovarianceMode,
) -> Result<(Array2<f64>, Array2<f64>), EstimationError>
pub fn predict_probabilities_with_se_in_mode( &self, x_new: ArrayView2<'_, f64>, mode: InferenceCovarianceMode, ) -> Result<(Array2<f64>, Array2<f64>), EstimationError>
Posterior-mean class probabilities and their marginal standard deviations under an explicitly named covariance definition.
§What the two modes are
InferenceCovarianceMode::Conditional is sd(p_c | λ̂), computed by
crate::multinomial_predictive as √(E[p_c²] − E[p_c]²) from two
ratios of normalising constants at the selected smoothing. It is the
exact posterior spread of the probability GIVEN that λ̂ is the truth.
InferenceCovarianceMode::SmoothingCorrected additionally propagates
the uncertainty in λ̂ itself, by the law of total variance:
Var(p_c) = E_ρ[ Var(p_c | ρ) ] + Var_ρ( E[p_c | ρ] )
≈ Var(p_c | ρ̂) + (∂E[p_c|ρ]/∂ρ)ᵀ V_ρ (∂E[p_c|ρ]/∂ρ)and the second term needs no new object. To the order the correction is
itself computed at, E[p_c | ρ] moves with ρ through the mode
θ̂(ρ), so ∂E[p_c|ρ]/∂ρ = gᵀ ∂θ̂/∂ρ with g = ∂p_c/∂θ the softmax
Jacobian at the mode, and
Var_ρ( E[p_c|ρ] ) = gᵀ (J V_ρ Jᵀ) g = gᵀ C gwith C exactly the matrix Self::smoothing_correction carries. That
is the response-scale statement of V_c = V_cond + C: the same
correction, contracted through the same delta-method Jacobian the rest of
the library uses on the response scale, with no new constant and no new
approximation order.
g is evaluated at the PLUG-IN probabilities softmax(x'β̂) rather than
at the published posterior mean. Those differ by O(n⁻¹), and g
multiplies a term that is itself the first-order correction, so the
difference enters at an order neither term is accurate to; the plug-in
Jacobian is the exact derivative of the leading term.
§Errors
Asking for SmoothingCorrected on a model that carries no correction is
an ERROR, not a silent downgrade — the same contract
gam-predict’s SmoothingCorrected mode holds.
Sourcepub fn stacked_mode(&self) -> Result<Array1<f64>, EstimationError>
pub fn stacked_mode(&self) -> Result<Array1<f64>, EstimationError>
The posterior mode in the stacked class-major order the joint covariance,
the joint penalty and crate::multinomial_predictive all use:
θ[a·P + i] = β[i, a].
Sourcepub fn smooth_significance(&self) -> Vec<MultinomialSmoothSignificance>
pub fn smooth_significance(&self) -> Vec<MultinomialSmoothSignificance>
Wood (2013) rank-truncated Wald smooth-significance test per
(active class, smooth term) (#1101), reusing the exact scalar-summary
kernel gam_terms::inference::smooth_test::wood_smooth_test. For active
class a and term span [c0, c1) within the class block, the global
coefficient range is a·P + c0 .. a·P + c1; the joint covariance and
influence are sliced there. The term EDF is the influence-block trace
tr(F_jj) (when present) and the reference d.f. uses tr(F_jj)²/tr(F_jj²),
exactly as the scalar path. The multinomial softmax is a known-dispersion
family, so the χ²_{ref_df} branch applies. Returns one row per
(class label, term label, edf, ref_df, statistic, p_value); empty when
no covariance/smooth terms are available.
Sourcepub fn sample_replicate_classes(
&self,
x_new: ArrayView2<'_, f64>,
n_draws: usize,
seed: u64,
) -> Result<Array2<u32>, EstimationError>
pub fn sample_replicate_classes( &self, x_new: ArrayView2<'_, f64>, n_draws: usize, seed: u64, ) -> Result<Array2<u32>, EstimationError>
Draw n_draws posterior-predictive replicate class assignments at fresh
rows (#1101). Each draw independently samples every row’s class from
Categorical(p_row) with p = E[softmax(eta) | data], so coefficient
uncertainty is integrated before adding categorical observation noise.
The returned (n_draws, N) matrix holds class
INDICES 0..K, aligned to Self::class_levels. The draw stream is a
StdRng seeded by seed, so (x_new, n_draws, seed) reproduce
bit-identically — the engine for posterior-predictive checks and
simulation-based calibration. x_new must have self.p_per_class
columns (built from the same resolved_termspec as fit time).
Trait Implementations§
Source§impl Clone for MultinomialSavedModel
impl Clone for MultinomialSavedModel
Source§fn clone(&self) -> MultinomialSavedModel
fn clone(&self) -> MultinomialSavedModel
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
source. Read moreSource§impl Debug for MultinomialSavedModel
impl Debug for MultinomialSavedModel
Source§impl<'de> Deserialize<'de> for MultinomialSavedModel
impl<'de> Deserialize<'de> for MultinomialSavedModel
Source§fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>where
__D: Deserializer<'de>,
Auto Trait Implementations§
impl Freeze for MultinomialSavedModel
impl RefUnwindSafe for MultinomialSavedModel
impl Send for MultinomialSavedModel
impl Sync for MultinomialSavedModel
impl Unpin for MultinomialSavedModel
impl UnsafeUnpin for MultinomialSavedModel
impl UnwindSafe for MultinomialSavedModel
Blanket Implementations§
impl<T> Allocation for T
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
Source§impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> CloneToUninit for Twhere
T: Clone,
impl<T> DeserializeOwned for Twhere
T: for<'de> Deserialize<'de>,
Source§impl<T> DistributionExt for Twhere
T: ?Sized,
impl<T> DistributionExt for Twhere
T: ?Sized,
impl<T, U> Imply<T> for U
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left is true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
self into a Left variant of Either<Self, Self>
if into_left(&self) returns true.
Converts self into a Right variant of Either<Self, Self>
otherwise. Read moreSource§impl<T> Pointable for T
impl<T> Pointable for T
impl<T> Read<Exclusive, BecauseExclusive> for Twhere
T: ?Sized,
Source§impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
impl<SS, SP> SupersetOf<SS> for SPwhere
SS: SubsetOf<SP>,
Source§fn to_subset(&self) -> Option<SS>
fn to_subset(&self) -> Option<SS>
self from the equivalent element of its
superset. Read moreSource§fn is_in_subset(&self) -> bool
fn is_in_subset(&self) -> bool
self is actually part of its subset T (and can be converted to it).Source§fn to_subset_unchecked(&self) -> SS
fn to_subset_unchecked(&self) -> SS
self.to_subset but without any property checks. Always succeeds.Source§fn from_subset(element: &SS) -> SP
fn from_subset(element: &SS) -> SP
self to the equivalent element of its superset.