The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

Intermediate AMM Specifications: B-spline W Bases and Heterogeneous Families per Slot

Tutorial for users moving past the polynomial / homogeneous baseline (Path 1)

José Mauricio Gómez Julián

2026-07-06


1. What this vignette covers

The AMM canonical decomposition

\[\theta_i = \theta_{\text{ref}} + a(x_i) + b(x_i) \odot \theta_{\text{ref}} + W(\theta_{\text{ref}})\,x_i,\]

where \(\odot\) denotes the Hadamard (elementwise) product, coherent with the canonical notation of vignette("v00_framework_overview", package = "gdpar") §8.2 and vignette("v01_amm_identifiability", package = "gdpar") §3.3,

allows two refinements that take a model out of the polynomial / homogeneous baseline and into the intermediate regime — that is, still a single-anchor canonical fit, but with a richer modulating geometry and per-slot heterogeneous families when the user runs distributional regression (K > 1). This vignette documents both refinements in the order in which a user typically encounters them.

  1. B-spline W bases (sub-phase 8.3.8). The default W_basis(type = "polynomial", dim = 2L) is sufficient when the modulating effect \(W(\theta_{\text{ref}})\) is smooth and low-curvature. For non-monotone or sharply varying effects, a B-spline basis preserves the AMM canonical form while giving local flexibility.
  2. Heterogeneous families per slot (sub-phase 8.3.7). For distributional regression K > 1, the default homogeneous family (the same gdpar_family for every slot) can be replaced by a named list of gdpar_family objects, one per slot, when the location slot’s likelihood admits multiple compatible auxiliary parametrizations (e.g., a Gaussian location with a Beta-distributed dispersion slot).

Both refinements are orthogonal: a fit may combine a B-spline W with heterogeneous families, or use one without the other.

For the API of K > 1 distributional regression as a whole and for the residual / DHARMa workflow that complements these fits, see vignette("vop05_distributional_K_dharma", package = "gdpar").


2. B-spline W bases

2.1. When to switch from polynomial to B-spline

W(\theta_{\text{ref}}) is a univariate function of the anchor evaluated at each observation’s posterior reference value. The polynomial basis of dimension \(\dim_W\) writes

\[W(\theta_{\text{ref}}) = \sum_{j=1}^{\dim_W} W_j \,(\theta_{\text{ref}}^j - \theta_{\text{anchor}}^j),\]

which is smooth and parsimonious but globally rigid. For modulating effects that are piecewise smooth — for example, a saturating curve or a sigmoidal shift — a B-spline basis evaluates a local set of basis functions, each supported on a few neighbouring knots, and gives finer control over local curvature without inflating the global degree.

Rule of thumb. Start with W_basis(type = "polynomial", dim = 2L) (the default). Switch to B-spline when (i) posterior predictive checks (see vignette("vop05_distributional_K_dharma")) show systematic deviations of W away from a global low-degree polynomial, or (ii) you have prior knowledge that the modulating effect is non-monotone over the operative range of theta_ref.

2.2. API

library(gdpar)

# Polynomial baseline (default)
W_poly <- W_basis(type = "polynomial", dim = 2L)

# B-spline with internal knots given explicitly, cubic by default
W_bs <- W_basis(
  type           = "bspline",
  knots          = c(-0.5, 0.0, 0.5),
  degree         = 3L,
  boundary_knots = c(-1.5, 1.5)
)

Three points of contract:

2.3. End-to-end example

set.seed(2026L)
n <- 80L
theta_ref_true <- 0.5
# A sigmoidal modulating effect that the polynomial baseline cannot
# represent without raising dim_W substantially.
x_var <- runif(n, -2, 2)
W_true <- function(z) 1.2 / (1 + exp(-3 * z)) - 0.6
y <- theta_ref_true + 0.4 * (x_var - mean(x_var)) +
  W_true(theta_ref_true) * (x_var - mean(x_var)) * 0.7 +
  rnorm(n, sd = 0.2)
d <- data.frame(y = y, x = x_var)

library(gdpar)
fit_bs <- gdpar(
  y ~ a(x) + W(),
  data   = d,
  family = gdpar_family("gaussian"),
  W      = W_basis(type = "bspline",
                   knots = c(-1, 0, 1),
                   degree = 3L,
                   boundary_knots = c(-2.2, 2.2)),
  chains = 2L, iter_warmup = 400L, iter_sampling = 400L,
  refresh = 0L
)

co <- coef(fit_bs)
co

The coef() output reports the per-basis coefficient W_raw[j, k] summarised by posterior mean and 5%/50%/95% quantiles, exactly as in the polynomial case. Compare with a polynomial fit of the same dim_W to see whether the B-spline absorbs structure that the polynomial flattens.

2.4. Diagnostics and known limitations


3. Heterogeneous families per slot (K > 1)

3.1. The default homogeneous case

For distributional regression K > 1, gdpar() accepts a single gdpar_family and replicates the canonical per-slot parametrization across every slot. For Gaussian K = 2, slot 1 is the location (mu with identity link) and slot 2 is the dispersion (sigma with log link). The two slots share the same Gaussian likelihood family; only the slot-specific link differs.

fit <- gdpar(
  gdpar_bf(y ~ a(x1), sigma ~ a(x2)),
  data   = d,
  family = gdpar_family("gaussian"),
  chains = 2L, iter_warmup = 400L, iter_sampling = 400L
)

3.2. When to declare heterogeneous families

The heterogeneous-per-slot path (sub-phase 8.3.7, decision D5) targets the use case where the auxiliary slots (slot \(k > 1\)) have a different parametric structure than the location slot. Two motivating examples:

The canonical decision D5 (sub-phase 8.3.7) also restricts heterogeneous specifications to compatible families: slot 1 (“location”) must belong to the canonical primary {gaussian, poisson, neg_binomial_2, beta, gamma, lognormal_loc_scale} family-id set, and slot \(k > 1\) must declare a family whose support is coherent with the slot’s canonical role. Incompatible combinations (e.g., a Gaussian location with a Bernoulli dispersion) are rejected by .gdpar_validate_heterogeneous_family_K with gdpar_input_error.

The pattern name lognormal_loc_scale listed in the primary family-id set above is not part of the enum of gdpar_family(name): the package registers it as a K = 2 custom-family pattern (canonised in Sub-phase 8.3.4), accessed via gdpar_family_custom_K(stan_lpdf_id = "lognormal_loc_scale", ...). See vignette("vop05_distributional_K_dharma", package = "gdpar") §2.4 for the literal recipe and vignette("vop05_distributional_K_dharma", package = "gdpar") §6 for the complementary gdpar_family_custom() (K = 1) constructor.

On the Stan side, the per-slot dispatch of the inverse link is implemented by the helper function apply_inv_link_by_id(link_id, eta) canonised in inst/stan/amm_distrib_K.stan:228. It maps the integer link_id carried per slot in the data block (inv_link_id_per_slot) to the corresponding inverse-link evaluation, so that each slot’s linear predictor is transformed by its own inv_link without conditional R-side branching at compile time. This is the Stan-side mechanism that makes the heterogeneous-per-slot path of Sub-phase 8.3.7 transparent to the model template.

3.3. API

fit_het <- gdpar(
  gdpar_bf(y ~ a(x1), sigma ~ a(x2)),
  data   = d,
  family = list(
    mu    = gdpar_family("gaussian"),
    sigma = gdpar_family("beta")
  ),
  chains = 2L, iter_warmup = 400L, iter_sampling = 400L
)

The named list keys must match the slot names of the gdpar_formula_set (the bf() constructor’s left-hand side names, or the param_specs names of the location family). Mismatches raise gdpar_input_error with the expected slot names listed.

Sub-phase 8.3.7 supports heterogeneous specifications for K = 2 in three combinations of practical interest:

Other combinations are queued for sub-phase 8.4 (heterogeneous K = 3+ families) and emit gdpar_unsupported_feature_error at construction time with a message pointing to Session 8.4 deudas.

3.4. End-to-end example

set.seed(818L)
n <- 100L
x1 <- rnorm(n); x2 <- rnorm(n)
mu_true   <- 0.3 + 0.6 * (x1 - mean(x1))
sigma_eta <- 0.5 + 0.3 * (x2 - mean(x2))
# Beta-distributed sigma in (0, 1) via inverse-logit of sigma_eta
sigma_p <- 1 / (1 + exp(-sigma_eta))
y <- rbeta(n, shape1 = 2 + 5 * sigma_p, shape2 = 5 - 4 * sigma_p)
d <- data.frame(y = y, x1 = x1, x2 = x2)

fit_het <- gdpar(
  gdpar_bf(y ~ a(x1), sigma ~ a(x2)),
  data   = d,
  family = list(
    mu    = gdpar_family("beta"),
    sigma = gdpar_family("beta")
  ),
  chains = 2L, iter_warmup = 400L, iter_sampling = 400L,
  refresh = 0L
)

co <- coef(fit_het)
str(co, max.level = 2L)

coef.gdpar_fit for K > 1 returns a named list of gdpar_coef objects, one per slot (decision E4.A, sub-phase 8.3.10). Each entry follows the standard scalar contract (theta_ref, a, b, W sub-components with posterior summaries) and can be coerced to a long-tidy data frame via as.data.frame.

3.5. Identifiability and information diagnostics

The heterogeneous path runs the same identifiability diagnostics as the homogeneous one:

When the dispersion slot’s canonical kind is Beta or Gamma, the prior on its anchor theta_ref_k[k] is the canonical one of the slot’s family (not the location’s), via the placeholder THETA_REF_PRIOR_BLOCK resolved at codegen time. This is the decision D-ID per-slot of sub-phase 8.3.4 generalised to heterogeneous specifications.


4. Combining B-spline W with heterogeneous families

The two refinements are orthogonal at the design level. A typical combined fit declares:

fit_combo <- gdpar(
  gdpar_bf(y ~ a(x1) + W(), sigma ~ a(x2)),
  data   = d,
  family = list(
    mu    = gdpar_family("gaussian"),
    sigma = gdpar_family("beta")
  ),
  W      = W_basis(type = "bspline", knots = c(-1, 0, 1),
                   degree = 3L, boundary_knots = c(-2.5, 2.5)),
  chains = 2L, iter_warmup = 400L, iter_sampling = 400L
)

The W block is globally shared across slots (canonical decision: “Scope of W: global”, handoff 28). The same W_raw and sigma_W enter the linear predictor of every slot, evaluated at the slot’s own theta_ref_k[k] and anchored at the slot’s theta_anchor_K[k]. This is by design and reflects the role of W as a structural coupler in the AMM canonical form; the alternatives (W-per-slot, W-only-in-mu) are queued as scope deudas for Session 8.4.


5. Known limitations and future work

For the residual diagnostics that complement these intermediate specifications — including gdpar_dharma_object() and the three-layer G1 / G2 / G3 residual hierarchy — see vignette("vop05_distributional_K_dharma", package = "gdpar").


References

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.