Skip to main content

MultinomialSavedModel

Struct MultinomialSavedModel 

Source
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: String

The 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: usize

Index 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: TermCollectionSpec

Resolved 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: usize

P — coefficient count per active class. Matches the column count of the design matrix the saved resolved_termspec produces.

§n_active_classes: usize

Number 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: String

Container 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: usize

Newton 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: f64

Penalized negative log-likelihood at the returned β̂.

§deviance: f64

Unpenalized 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: usize

Number 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

Source

pub fn validate(&self) -> Result<(), EstimationError>

Source

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.

Source

pub fn training_design(&self) -> Result<Array2<f64>, EstimationError>

Training design as an (n, P) ndarray.

Source

pub fn joint_penalty(&self) -> Result<Array2<f64>, EstimationError>

Joint penalty S_λ as a (P·M, P·M) ndarray.

Source

pub fn coefficients_active(&self) -> Result<Array2<f64>, EstimationError>

Active-class coefficient block as an (P, K-1) ndarray view.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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 g

with 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.

Source

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].

Source

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.

Source

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

Source§

fn clone(&self) -> MultinomialSavedModel

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for MultinomialSavedModel

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl<'de> Deserialize<'de> for MultinomialSavedModel

Source§

fn deserialize<__D>(__deserializer: __D) -> Result<Self, __D::Error>
where __D: Deserializer<'de>,

Deserialize this value from the given Serde deserializer. Read more
Source§

impl Serialize for MultinomialSavedModel

Source§

fn serialize<__S>(&self, __serializer: __S) -> Result<__S::Ok, __S::Error>
where __S: Serializer,

Serialize this value into the given Serde serializer. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Allocation for T
where T: RefUnwindSafe + Send + Sync,

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> ByRef<T> for T

Source§

fn by_ref(&self) -> &T

Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DeserializeOwned for T
where T: for<'de> Deserialize<'de>,

Source§

impl<T> DistributionExt for T
where T: ?Sized,

Source§

fn rand<T>(&self, rng: &mut (impl Rng + ?Sized)) -> T
where Self: Distribution<T>,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Imply<T> for U
where T: ?Sized, U: ?Sized,

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self>

Converts 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 more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self>
where F: FnOnce(&Self) -> bool,

Converts 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 more
Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<SS, SP> SupersetOf<SS> for SP
where SS: SubsetOf<SP>,

Source§

fn to_subset(&self) -> Option<SS>

The inverse inclusion map: attempts to construct self from the equivalent element of its superset. Read more
Source§

fn is_in_subset(&self) -> bool

Checks if self is actually part of its subset T (and can be converted to it).
Source§

fn to_subset_unchecked(&self) -> SS

Use with care! Same as self.to_subset but without any property checks. Always succeeds.
Source§

fn from_subset(element: &SS) -> SP

The inclusion map: converts self to the equivalent element of its superset.
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V