Source code for liesel_gam.term_builder

from __future__ import annotations

import logging
from collections.abc import Callable, Mapping, Sequence
from typing import Any, Literal

import jax
import jax.numpy as jnp
import liesel.goose as gs
import liesel.model as lsl
import pandas as pd
import tensorflow_probability.substrates.jax.bijectors as tfb
from liesel.model.model import TemporaryModel

from .basis_builder import BasisBuilder
from .names import NameManager
from .registry import CategoryMapping, PandasRegistry
from .term import (
    LinTerm,
    MRFTerm,
    RITerm,
    StrctInteractionTerm,
    StrctLinTerm,
    StrctTensorProdTerm,
    StrctTerm,
)
from .var import ScaleIG, VarIGPrior

InferenceTypes = Any

Array = jax.Array
ArrayLike = jax.typing.ArrayLike

BasisTypes = Literal["tp", "ts", "cr", "cs", "cc", "bs", "ps", "cp", "gp"]


logger = logging.getLogger(__name__)


def labels_to_integers(newdata: dict, mappings: dict[str, CategoryMapping]) -> dict:
    # replace categorical inputs with their index representation
    # create combined input matrices from individual variables, if desired
    newdata = newdata.copy()

    # replace categorical variables by their integer representations
    for name, mapping in mappings.items():
        if name in newdata:
            newdata[name] = mapping.labels_to_integers(newdata[name])

    return newdata


[docs] class TermBuilder: r""" Initializes structured additive model terms. The terms returned by the methods of this class are all instances of :class:`liesel.model.Var`, or of its subclasses. Among other things, the term builder automatically assigns unique names to the created variables. Parameters ---------- registry Provides an interface to a data frame used to set up the model terms. prefix_names_by Names created by this TermBuilder will be prefixed by the string supplied here. default_inference Defines the default inference specification for terms created by this builder. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default ``gs.MCMCSpec(gs.IWLSKernel.untuned)`` is an iteratively-reweighted least squares kernel without step size tuning, see :meth:`liesel.goose.IWLSKernel.untuned`. default_scale_fn A function or :class:`.VarIGPrior` object that defines the default scale for structured additive terms initialized by this builder. If this is a function, it must take no arguments and return a :class:`liesel.model.Var` that acts as the scale. If it is a :class:`.VarIGPrior`, the default scale will be ``scale = sqrt(var)``, where ``var ~ InverseGamma(concentration, scale)``, with concentration and scale given by the :class:`.VarIGPrior` object. For most terms, this will mean that a fitting Gibbs sampler can be automatically set up for ``var``. The exceptions to this rule are :meth:`.tf` and :meth:`.tx`. Note that, if you supply a custom default scale function, you should make sure that the ``inference`` attribute of your custom scale is defined, otherwise your custom scale may not be included in MCMC sampling. The default is ``VarIGPrior(1.0, 0.005)``, which leads to an inverse Gamma prior on the level of the variance parameter, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(1.0, 0.005)`. See Also -------- .BasisBuilder : Initializes :class:`.Basis` objects with penalty matrices. Notes ------ The terms created by this builder generally have the form .. math:: s(\mathbf{x}_i) = \sum_{j=1}^J B_j(\mathbf{x}_i) \beta_j = \mathbf{b}(\mathbf{x}_i)^\top \boldsymbol{\beta} where - :math:`i=1, \dots, N` is the observation index, - :math:`\mathbf{x}_i^\top = [x_{i,1}, \dots, x_{i,M}]` are covariate observations, where :math:`M` denotes the number of covariates, - :math:`\mathbf{b}(\mathbf{x}_i)^\top = [B_1(\mathbf{x}_i), \dots, B_J(\mathbf{x}_i)]` are a set of basis function evaluations, and - :math:`\boldsymbol{\beta}^\top = [\beta_1, \dots, \beta_J]` are the corresponding coefficients. In many cases, :math:`\mathbf{x}_i` will consist of only one covariate, except for linear effects (:meth:`.lin`, :meth:`.slin`) or tensor product smooths (:meth:`.tf`, :meth:`.tx`). The basis matrix for such a term is .. math:: \mathbf{B} = \begin{bmatrix} \mathbf{b}(\mathbf{x}_1)^\top \\ \vdots \\ \mathbf{b}(\mathbf{x}_N)^\top \end{bmatrix}. The coefficient receives a potentially rank-deficient multivariate normal prior .. math:: p(\boldsymbol{\beta}) \propto \left( \frac{1}{\tau^2}\right)^{\operatorname{rk}(\mathbf{K})/2} \exp \left( - \frac{1}{\tau^2} \boldsymbol{\beta}^\top \mathbf{K} \boldsymbol{\beta} \right) with the potentially rank-deficient penalty matrix :math:`\mathbf{K}` of rank :math:`\operatorname{rk}(\mathbf{K})`. The variance parameter :math:`\tau^2` acts as an inverse smoothing parameter. The choice of basis functions :math:`B_j` and penalty matrix :math:`\mathbf{K}` determines the nature of the term. .. rubric:: Sampling specification By default, the coefficients for each term created with this termbuilder will be equipped with the TermBuilder's default inference specification. This default specification corresponds to a blockwise sampling scheme, with each term's coefficients forming a single block. See the examples for how to modify the default blockwise setup. .. rubric:: Overview of commonly used terms .. note:: **Basic terms** - :meth:`.lin` : Linear term. - :meth:`.slin` : Linear term with iid penalty (ridge prior). - :meth:`.ps` : P-spline. - :meth:`.tp` : Thin plate spline. - :meth:`.ri` : Random intercept. - :meth:`.mrf` : Markov random field (discrete spatial effect). - :meth:`.kriging` : Low-rank gaussian process with fixed range. **Combined terms and tensor products** - :meth:`.rs` : Random slope. - :meth:`.vc` : Varying coefficient. - :meth:`.tx` : Tensor product interaction without main effects. - :meth:`.tf` : Full tensor product with main effects. **Specialized smooths** - :meth:`.np` : P-splines without linear trend. - :meth:`.cp` : Cyclic P-splines **Custom smooths** - :meth:`.f` : Supply your own basis function and penalty matrix. - :class:`.StrctTerm` : Initialize a term independetly, potentially supplying a constant basis matrix and your own penalty matrix. .. tip:: If your model somewhere contains a categorical variable, pay attention to the method :meth:`labels_to_integers`; this helps you bring a ``newdata`` dictionary into a form understood by :meth:`liesel.model.Model.predict` easily by turning string labels into their integer representations. Examples -------- Basic example using defaults, initializing a P-spline: >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.ps("x_nonlin", k=20) StrctTerm(name="ps(x_nonlin)") Changing the default inference. This still means, each term is sampled in a separate block. >>> import liesel.goose as gs >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df, default_inference=gs.MCMCSpec(gs.NUTSKernel)) >>> tb.ps("x_nonlin", k=20) StrctTerm(name="ps(x_nonlin)") Changing default inference, such that all terms' coefficients are collected in the same block. Note that the scales are still sampled individually; by default with Gibbs kernels. >>> import liesel.goose as gs >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df( ... df, ... default_inference=gs.MCMCSpec(gs.NUTSKernel, kernel_group="1"), ... ) >>> tb.ps("x_nonlin", k=20) StrctTerm(name="ps(x_nonlin)") Changing the default scale. Here, we change the default scales to have a HalfNormal prior, and sample them on inverse-softplus level using independent IWLS kernels. >>> import jax.numpy as jnp >>> import liesel.model as lsl >>> import tensorflow_probability.substrates.jax.bijectors as tfb >>> import tensorflow_probability.substrates.jax.distributions as tfd >>> import liesel_gam as gam >>> def scale_fn(): ... prior = lsl.Dist( ... tfd.HalfNormal, ... scale=jnp.array(20.0), ... ) ... scale = lsl.Var.new_param( ... jnp.array(0.1), ... distribution=prior, ... name="{x}", # placeholder for the automatically generated name ... ) ... scale.transform( ... tfb.Softplus(), ... inference=gs.MCMCSpec(gs.IWLSKernel.untuned), # inference for scales ... name="h({x})", # {x} is a placeholder ... ) ... return scale >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df, default_scale_fn=scale_fn) >>> tb.ps("x_nonlin", k=20) StrctTerm(name="ps(x_nonlin)") Using a name prefix: >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df, prefix_names_by="loc.") >>> tb.ps("x_nonlin", k=20) StrctTerm(name="loc.ps(loc.x_nonlin)") If you don't want the name prefix to appear on the covariate names, too, initialize the :class:`.PandasRegistry` individually. This way, you can for example use the same registry for two TermBuilder instances. >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> registry = gam.PandasRegistry(df) >>> tb1 = gam.TermBuilder(registry, prefix_names_by="loc.") >>> tb1.ps("x_nonlin", k=20) StrctTerm(name="loc.ps(x_nonlin)") >>> tb2 = gam.TermBuilder(registry, prefix_names_by="scale.") >>> tb2.ps("x_nonlin", k=20) StrctTerm(name="scale.ps(x_nonlin)") """ def __init__( self, registry: PandasRegistry, prefix_names_by: str = "", default_inference: InferenceTypes | None = gs.MCMCSpec(gs.IWLSKernel.untuned), default_scale_fn: Callable[[], lsl.Var] | VarIGPrior = VarIGPrior(1.0, 0.005), ) -> None: self.registry = registry self.names = NameManager(prefix=prefix_names_by) self.bases = BasisBuilder(registry, names=self.names) self.default_inference = default_inference self._default_scale_fn = default_scale_fn def _get_inference( self, inference: InferenceTypes | None | Literal["default"] = "default", ) -> InferenceTypes | None: if inference == "default": return self.default_inference else: return inference
[docs] def init_scale( self, scale: lsl.Var | ScaleIG | float | Literal["default"] | VarIGPrior, term_name: str, ) -> lsl.Var: """ Initializes a scale variable with a term-related name. Parameters ---------- scale Scale object. term_name Name of the term this scale corresponds to. If you supply a :class:`liesel.model.Var`, you can use the place- holder ``{x}`` in its name to allow this method to fill in the ``term_name``. Notes ----- The behavior depends on the type of the ``scale`` argument. - If it is ``"default"``, the return will be created based on the ``default_scale_fn`` argument supplied to the TermBuilder upon initialization. - If it is a :class:`.VarIGPrior`, the return will be ``scale = sqrt(var)``, where ``var ~ InverseGamma(concentration, scale)``, with concentration and scale given by the :class:`.VarIGPrior` object. For most terms, this will mean that a fitting Gibbs sampler can be automatically set up for ``var``. The exceptions to this rule are :meth:`.ta`, :meth:`.tf`, and :meth:`.tx`. - If it is a ``float``, the return will be ``lsl.Var.new_value`` holding this float. - If it is a :class:`liesel.model.Var` object, the return will be this object. If you supply a :class:`liesel.model.Var`, you can use the place- holder ``{x}`` in its name to allow this method to fill in an automatically generated name based on the ``term_name``. """ if scale == "default": if isinstance(self._default_scale_fn, VarIGPrior): scale_var: lsl.Var | ScaleIG = ScaleIG( value=self._default_scale_fn.value, concentration=self._default_scale_fn.concentration, scale=self._default_scale_fn.scale, name="{x}", variance_name="{x}^2", ) else: scale_var = self._default_scale_fn() elif isinstance(scale, VarIGPrior): scale_var = ScaleIG( value=scale.value, concentration=scale.concentration, scale=scale.scale, name="{x}", variance_name="{x}^2", ) elif isinstance(scale, float): scale_var = lsl.Var.new_value(scale, name="{x}") elif isinstance(scale, lsl.Var | ScaleIG): scale_var = scale else: raise TypeError(f"Unexpected scale type: {type(scale)}") if scale_var.name: scale_name = self.names.tau(term_name) scale_var = _format_name(scale_var, fill=scale_name) return scale_var
[docs] @classmethod def from_dict( cls, data: dict[str, ArrayLike], prefix_names_by: str = "", default_inference: InferenceTypes | None = gs.MCMCSpec(gs.IWLSKernel.untuned), default_scale_fn: Callable[[], lsl.Var] | VarIGPrior = VarIGPrior(1.0, 0.005), ) -> TermBuilder: """ Initializes a TermBuilder from a dictionary that holds the data. Internally, this will create a :class:`.PandasRegistry` with ``na_action="drop"``. The other arguments are passed on to the init. """ return cls.from_df( pd.DataFrame(data), prefix_names_by=prefix_names_by, default_inference=default_inference, default_scale_fn=default_scale_fn, )
[docs] @classmethod def from_df( cls, data: pd.DataFrame, prefix_names_by: str = "", default_inference: InferenceTypes | None = gs.MCMCSpec(gs.IWLSKernel.untuned), default_scale_fn: Callable[[], lsl.Var] | VarIGPrior = VarIGPrior(1.0, 0.005), ) -> TermBuilder: """ Initializes a TermBuilder from a pandas dataframe. Internally, this will create a :class:`.PandasRegistry` with ``na_action="drop"``. The other arguments are passed on to the init. """ registry = PandasRegistry( data, na_action="drop", prefix_names_by=prefix_names_by ) return cls( registry, prefix_names_by=prefix_names_by, default_inference=default_inference, default_scale_fn=default_scale_fn, )
[docs] def labels_to_integers(self, newdata: dict) -> dict: """ Processes a ``newdata`` dictionary, replacing labels of caterogical variables with their integer representation, such that they can be understood by :meth:`liesel.model.Model.predict`. """ return labels_to_integers(newdata, self.bases.mappings)
# formula
[docs] def lin( self, formula: str, prior: lsl.Dist | None = None, inference: InferenceTypes | None | Literal["default"] = "default", context: dict[str, Any] | None = None, prefix: str = "", name: str | None = None, ) -> LinTerm: """ Linear term. Parameters ---------- formula Right-hand side of a model formula, as understood by formulaic_. Most of formulaic's grammar_ is supported. See notes for details. prior An optional prior for this term's coefficient. The default is a constant prior. inference An optional :class:`liesel.goose.MCMCSpec` instance (or other valid inference object). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. context Dictionary of additional Python objects that should be made available to formulaic when constructing the design matrix. Gets passed to ``formulaic.ModelSpec.get_model_matrix()``. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .slin : Linear term with identity penalty matrix, leading to a ridge prior. Notes ----- This term evaluates to :math:`\\mathbf{X}\\boldsymbol{\\beta}`, where :math:`\\mathbf{X}` is a linear-effect design matrix. The coefficient vector receives a constant prior by default, :math:`\\boldsymbol{\\beta} \\sim \\text{const}`, but a custom prior can be passed in the argument ``prior`` as a :class:`liesel.model.Dist`. The following formulaic syntax is supported: - ``+`` for adding a term - ``a:b`` for simple interactions - ``a*b`` for expanding to ``a + b + a:b`` - ``(a + b)**n`` for n-th order interactions - ``a / b`` for nesting - ``C(a, ...)`` for categorical effects (see formulaic_categorical_ for details) - ``b %in% a`` for inverted nesting - ``{a+1}`` for quoted Python code to be executed - ```weird name``` backtick-strings for weird names - Other transformations like ``center(a)``, ``scale(a)``, or ``lag(a)``, see grammar_. - Python functions Not supported: - String literals - Numeric literals - Wildcard ``"."`` - ``\\|`` for splitting a formula - ``"~"`` in formula, since this method supports only the right-hand side of a Wilkinson formula. - ``1 +``, ``0 +``, or ``-1`` in formula, since intercept addition is handled via the argument ``include_intercept``. References ---------- - Python library formulaic: https://matthewwardrop.github.io/formulaic/latest/ Examples -------- Simple example: >>> import liesel_gam as gam >>> df = gam.demo_data(n=100) >>> registry = gam.PandasRegistry(df) >>> bb = gam.BasisBuilder(registry) >>> bb.lin("x_lin + x_nonlin + x_cat") LinBasis(name="X") Customized categorical encoding: >>> import liesel_gam as gam >>> df = gam.demo_data(n=100) >>> registry = gam.PandasRegistry(df) >>> bb = gam.BasisBuilder(registry) >>> bb.lin("x_lin + x_nonlin + C(x_cat, contr.sum)") LinBasis(name="X") Interaction: >>> import liesel_gam as gam >>> df = gam.demo_data(n=100) >>> registry = gam.PandasRegistry(df) >>> bb = gam.BasisBuilder(registry) >>> bb.lin("x_lin * x_cat") LinBasis(name="X") .. _formulaic: https://matthewwardrop.github.io/formulaic/latest/ .. _formulaic_categorical: https://matthew.wardrop.casa/formulaic/latest/guides/contrasts/#contrast-codings .. _grammar: https://matthewwardrop.github.io/formulaic/latest/guides/grammar/ """ include_intercept = False basis = self.bases.lin( formula, xname="", basis_name="X", include_intercept=include_intercept, context=context, ) term_name = self.names.create(prefix + "lin" + "(" + basis.name + ")") term_name = prefix + name if name is not None else term_name coef_name = self.names.beta(term_name) term = LinTerm( basis, prior=prior, name=term_name, inference=self._get_inference(inference), coef_name=coef_name, ) term.model_spec = basis.model_spec term.mappings = basis.mappings term.column_names = basis.column_names return term
[docs] def slin( self, formula: str, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", context: dict[str, Any] | None = None, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> StrctLinTerm: """ Linear term with an identity penalty matrix, leading to a ridge prior. Parameters ---------- formula Right-hand side of a model formula, as understood by formulaic_. Most of formulaic's grammar_ is supported. See notes for details. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \\sim \\operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference An optional :class:`liesel.goose.MCMCSpec` instance (or other valid inference object). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. context Dictionary of additional Python objects that should be made available to formulaic when constructing the design matrix. Gets passed to ``formulaic.ModelSpec.get_model_matrix()``. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .lin : Linear term with constant prior. Notes ----- The following formulaic syntax is supported: - ``+`` for adding a term - ``a:b`` for simple interactions - ``a*b`` for expanding to ``a + b + a:b`` - ``(a + b)**n`` for n-th order interactions - ``a / b`` for nesting - ``C(a, ...)`` for categorical effects (see formulaic_categorical_ for details) - ``b %in% a`` for inverted nesting - ``{a+1}`` for quoted Python code to be executed - ```weird name``` backtick-strings for weird names - Other transformations like ``center(a)``, ``scale(a)``, or ``lag(a)``, see grammar_. - Python functions Not supported: - String literals - Numeric literals - Wildcard ``"."`` - ``\\|`` for splitting a formula - ``"~"`` in formula, since this method supports only the right-hand side of a Wilkinson formula. - ``1 +``, ``0 +``, or ``-1`` in formula, since intercept addition is handled via the argument ``include_intercept``. References ---------- - Python library formulaic: https://matthewwardrop.github.io/formulaic/latest/ Examples -------- Simple example: >>> import liesel_gam as gam >>> df = gam.demo_data(n=100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.slin("x_lin") StrctLinTerm(name="slin(X)") .. _formulaic: https://matthewwardrop.github.io/formulaic/latest/ .. _formulaic_categorical: https://matthew.wardrop.casa/formulaic/latest/guides/contrasts/#contrast-codings .. _grammar: https://matthewwardrop.github.io/formulaic/latest/guides/grammar/ """ include_intercept = False basis = self.bases.lin( formula, xname="", basis_name="X", include_intercept=include_intercept, context=context, ) basis._penalty = lsl.Value(jnp.eye(basis.nbases)) fname = self.names.create(prefix + "slin" + "(" + basis.name + ")") term_name = prefix + name if name is not None else fname fname = term_name term = StrctLinTerm( basis=basis, penalty=basis.penalty, scale=self.init_scale(scale, fname), name=fname, inference=self._get_inference(inference), coef_name=self.names.beta(fname), ) if factor_scale: term.factor_scale() term.model_spec = basis.model_spec term.mappings = basis.mappings term.column_names = basis.column_names return term
[docs] def cr( self, x: str | lsl.Var, *, k: int, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", penalty_order: int = 2, knots: ArrayLike | None = None, absorb_cons: bool = True, diagonal_penalty: bool = True, scale_penalty: bool = True, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> StrctTerm: r""" Cubic regression spline. Parameters ---------- x Name of input variable. k Number of (unconstrained) bases. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. penalty_order Order of the penalty. knots Knots used to set up the basis. If ``None`` (default), a set of equidistant knots will be set up automatically, with the domain boundaries inferred from the minimum and maximum of the observed values. absorb_cons Whether the default identification constraint should be applied by reparameterization and absorbing the reparameterization matrix into the basis and penalty matrices for computational efficiency. If ``False``, the basis is unconstrained, if ``True`` it receives a sum to zero constrained. Also see :meth:`.Basis.constrain`. diagonal_penalty Whether the penalty matrix associated with this term should be reparameterized into a diagonal matrix. In this case, the basis matrix is reparameterized accordingly. This can be beneficial for posterior geometry, which is why it is the default. Also see :meth:`.Basis.diagonalize_penalty`. scale_penalty Whether the penalty matrix should be scaled such that its infinity norm is one. This can improve numerical stability, which is why it is done by default. Also see :meth:`.Basis.scale_penalty`. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .cs : Cubic regression splines with additinal shrinkage on the null space. .BasisBuilder : Initializes the basis and penalty. Notes ----- This method internally calls the R package mgcv to set up the basis and penalty. The mgcv documentation provides further details. References ---------- - Wood, S.N. (2017) Generalized Additive Models: An Introduction with R (2nd edition). Chapman and Hall/CRC. - R package mgcv https://cran.r-project.org/web/packages/mgcv/index.html Examples --------- >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.cr("x_nonlin", k=20) StrctTerm(name="cr(x_nonlin)") """ basis = self.bases.cr( x=x, k=k, penalty_order=penalty_order, knots=knots, absorb_cons=absorb_cons, diagonal_penalty=diagonal_penalty, scale_penalty=scale_penalty, basis_name="B", ) fname = self.names.fname(prefix + "cr", basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = StrctTerm( basis=basis, penalty=basis.penalty, scale=self.init_scale(scale, fname), name=fname, inference=self._get_inference(inference), coef_name=coef_name, ) if factor_scale: term.factor_scale() return term
[docs] def cs( self, x: str | lsl.Var, *, k: int, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", penalty_order: int = 2, knots: ArrayLike | None = None, absorb_cons: bool = True, diagonal_penalty: bool = True, scale_penalty: bool = True, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> StrctTerm: r""" Cubic regression spline with additional null space shrinkage. Parameters ---------- x Name of input variable. k Number of (unconstrained) bases. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. penalty_order Order of the penalty. knots Knots used to set up the basis. If ``None`` (default), a set of equidistant knots will be set up automatically, with the domain boundaries inferred from the minimum and maximum of the observed values. absorb_cons Whether the default identification constraint should be applied by reparameterization and absorbing the reparameterization matrix into the basis and penalty matrices for computational efficiency. If ``False``, the basis is unconstrained, if ``True`` it receives a sum to zero constrained. Also see :meth:`.Basis.constrain`. diagonal_penalty Whether the penalty matrix associated with this term should be reparameterized into a diagonal matrix. In this case, the basis matrix is reparameterized accordingly. This can be beneficial for posterior geometry, which is why it is the default. Also see :meth:`.Basis.diagonalize_penalty`. scale_penalty Whether the penalty matrix should be scaled such that its infinity norm is one. This can improve numerical stability, which is why it is done by default. Also see :meth:`.Basis.scale_penalty`. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .cr : Cubic regression splines. .BasisBuilder : Initializes the basis and penalty. Notes ----- This method internally calls the R package mgcv to set up the basis and penalty. The mgcv documentation provides further details. References ---------- - Wood, S.N. (2017) Generalized Additive Models: An Introduction with R (2nd edition). Chapman and Hall/CRC. - R package mgcv https://cran.r-project.org/web/packages/mgcv/index.html Examples --------- >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.cs("x_nonlin", k=20) StrctTerm(name="cs(x_nonlin)") """ basis = self.bases.cs( x=x, k=k, penalty_order=penalty_order, knots=knots, absorb_cons=absorb_cons, diagonal_penalty=diagonal_penalty, scale_penalty=scale_penalty, basis_name="B", ) fname = self.names.fname(prefix + "cs", basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = StrctTerm( basis=basis, penalty=basis.penalty, scale=self.init_scale(scale, fname), name=fname, inference=self._get_inference(inference), coef_name=coef_name, ) if factor_scale: term.factor_scale() return term
[docs] def cc( self, x: str | lsl.Var, *, k: int, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", penalty_order: int = 2, knots: ArrayLike | None = None, absorb_cons: bool = True, diagonal_penalty: bool = True, scale_penalty: bool = True, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> StrctTerm: r""" Cyclic version of cubic regression spline. Parameters ---------- x Name of input variable. k Number of (unconstrained) bases. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. penalty_order Order of the penalty. knots Knots used to set up the basis. If ``None`` (default), a set of equidistant knots will be set up automatically, with the domain boundaries inferred from the minimum and maximum of the observed values. absorb_cons Whether the default identification constraint should be applied by reparameterization and absorbing the reparameterization matrix into the basis and penalty matrices for computational efficiency. If ``False``, the basis is unconstrained, if ``True`` it receives a sum to zero constrained. Also see :meth:`.Basis.constrain`. diagonal_penalty Whether the penalty matrix associated with this term should be reparameterized into a diagonal matrix. In this case, the basis matrix is reparameterized accordingly. This can be beneficial for posterior geometry, which is why it is the default. Also see :meth:`.Basis.diagonalize_penalty`. scale_penalty Whether the penalty matrix should be scaled such that its infinity norm is one. This can improve numerical stability, which is why it is done by default. Also see :meth:`.Basis.scale_penalty`. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .cr : Cubic regression splines. .cs : Cubic regression splines with additinal shrinkage on the null space. .BasisBuilder : Initializes the basis and penalty. Notes ----- This method internally calls the R package mgcv to set up the basis and penalty. The mgcv documentation provides further details. References ---------- - Wood, S.N. (2017) Generalized Additive Models: An Introduction with R (2nd edition). Chapman and Hall/CRC. - R package mgcv https://cran.r-project.org/web/packages/mgcv/index.html Examples --------- >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.cc("x_nonlin", k=20) StrctTerm(name="cc(x_nonlin)") """ basis = self.bases.cc( x=x, k=k, penalty_order=penalty_order, knots=knots, absorb_cons=absorb_cons, diagonal_penalty=diagonal_penalty, scale_penalty=scale_penalty, basis_name="B", ) fname = self.names.fname(prefix + "cc", basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = StrctTerm( basis=basis, penalty=basis.penalty, scale=self.init_scale(scale, fname), name=fname, inference=self._get_inference(inference), coef_name=coef_name, ) if factor_scale: term.factor_scale() return term
[docs] def bs( self, x: str | lsl.Var, *, k: int, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", basis_degree: int = 3, penalty_order: int | Sequence[int] = 2, knots: ArrayLike | None = None, absorb_cons: bool = True, diagonal_penalty: bool = True, scale_penalty: bool = True, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> StrctTerm: r""" B-spline with integrated squared derivative penalties. Parameters ---------- x Name of input variable. k Number of (unconstrained) bases. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. basis_degree Degree of the polynomials used in the B-spline basis function. Default is 3 for cubic B-splines. penalty_order Order of the penalty. If this is a sequence of integers, a penalty of the integer's order is added for each entry in the sequence. knots Knots used to set up the basis. If ``None`` (default), a set of equidistant knots will be set up automatically, with the domain boundaries inferred from the minimum and maximum of the observed values. absorb_cons Whether the default identification constraint should be applied by reparameterization and absorbing the reparameterization matrix into the basis and penalty matrices for computational efficiency. If ``False``, the basis is unconstrained, if ``True`` it receives a sum to zero constrained. Also see :meth:`.Basis.constrain`. diagonal_penalty Whether the penalty matrix associated with this term should be reparameterized into a diagonal matrix. In this case, the basis matrix is reparameterized accordingly. This can be beneficial for posterior geometry, which is why it is the default. Also see :meth:`.Basis.diagonalize_penalty`. scale_penalty Whether the penalty matrix should be scaled such that its infinity norm is one. This can improve numerical stability, which is why it is done by default. Also see :meth:`.Basis.scale_penalty`. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .ps : P-splines. .BasisBuilder : Initializes the basis and penalty. Notes ----- This method internally calls the R package mgcv to set up the basis and penalty. The mgcv documentation provides further details. References ---------- - Wood, S.N. (2017) Generalized Additive Models: An Introduction with R (2nd edition). Chapman and Hall/CRC. - R package mgcv https://cran.r-project.org/web/packages/mgcv/index.html Examples --------- >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.bs("x_nonlin", k=20) StrctTerm(name="bs(x_nonlin)") """ basis = self.bases.bs( x=x, k=k, basis_degree=basis_degree, penalty_order=penalty_order, knots=knots, absorb_cons=absorb_cons, diagonal_penalty=diagonal_penalty, scale_penalty=scale_penalty, basis_name="B", ) fname = self.names.fname(prefix + "bs", basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = StrctTerm( basis=basis, penalty=basis.penalty, scale=self.init_scale(scale, fname), name=fname, inference=self._get_inference(inference), coef_name=coef_name, ) if factor_scale: term.factor_scale() return term
# P-spline
[docs] def ps( self, x: str | lsl.Var, *, k: int, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", basis_degree: int = 3, penalty_order: int = 2, knots: ArrayLike | None = None, absorb_cons: bool = True, diagonal_penalty: bool = True, scale_penalty: bool = True, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> StrctTerm: r""" P-spline: A B-spline with a discrete penalty matrix. Parameters ---------- x Name of input variable. k Number of (unconstrained) bases. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. basis_degree Degree of the polynomials used in the B-spline basis function. Default is 3 for cubic B-splines. penalty_order Order of the penalty. knots Knots used to set up the basis. If ``None`` (default), a set of equidistant knots will be set up automatically, with the domain boundaries inferred from the minimum and maximum of the observed values. absorb_cons Whether the default identification constraint should be applied by reparameterization and absorbing the reparameterization matrix into the basis and penalty matrices for computational efficiency. If ``False``, the basis is unconstrained, if ``True`` it receives a sum to zero constrained. Also see :meth:`.Basis.constrain`. diagonal_penalty Whether the penalty matrix associated with this term should be reparameterized into a diagonal matrix. In this case, the basis matrix is reparameterized accordingly. This can be beneficial for posterior geometry, which is why it is the default. Also see :meth:`.Basis.diagonalize_penalty`. scale_penalty Whether the penalty matrix should be scaled such that its infinity norm is one. This can improve numerical stability, which is why it is done by default. Also see :meth:`.Basis.scale_penalty`. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .np : P-spline without linear trend. .cp : Cyclic P-spline. .BasisBuilder : Initializes the basis and penalty. Notes ----- This basis is initialized with ``use_callback=True`` and ``cache_basis=True``. See :class:`.Basis` for details. This method internally calls the R package mgcv to set up the basis and penalty. References ---------- - Lang, S., & Brezger, A. (2004). Bayesian P-splines. Journal of Computational and Graphical Statistics, 13(1), 183–212. https://doi.org/10.1198/1061860043010 - Wood, S.N. (2017) Generalized Additive Models: An Introduction with R (2nd edition). Chapman and Hall/CRC. - R package mgcv https://cran.r-project.org/web/packages/mgcv/index.html Examples --------- >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.ps("x_nonlin", k=20) StrctTerm(name="ps(x_nonlin)") The default is a constrained basis: >>> tb.ps("x_nonlin", k=20).basis.value.shape (100, 19) The constraint can be turned off by passing ``absorb_cons=False``: >>> tb.ps("x_nonlin", k=20, absorb_cons=False).basis.value.shape (100, 20) """ basis = self.bases.ps( x=x, k=k, basis_degree=basis_degree, penalty_order=penalty_order, knots=knots, absorb_cons=absorb_cons, diagonal_penalty=diagonal_penalty, scale_penalty=scale_penalty, basis_name="B", ) fname = self.names.fname(prefix + "ps", basis.x.name) term_name = prefix + name if name is not None else fname coef_name = self.names.beta(term_name) term = StrctTerm( basis=basis, penalty=basis.penalty, scale=self.init_scale(scale, term_name), name=term_name, inference=self._get_inference(inference), coef_name=coef_name, ) if factor_scale: term.factor_scale() return term
[docs] def np( self, x: str | lsl.Var, *, k: int, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", basis_degree: int = 3, penalty_order: int = 2, knots: ArrayLike | None = None, diagonal_penalty: bool = True, scale_penalty: bool = True, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> StrctTerm: r""" P-spline without linear trend. Parameters ---------- x Name of input variable. k Number of (unconstrained) bases. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. basis_degree Degree of the polynomials used in the B-spline basis function. Default is 3 for cubic B-splines. penalty_order Order of the penalty. knots Knots used to set up the basis. If ``None`` (default), a set of equidistant knots will be set up automatically, with the domain boundaries inferred from the minimum and maximum of the observed values. absorb_cons Whether the default identification constraint should be applied by reparameterization and absorbing the reparameterization matrix into the basis and penalty matrices for computational efficiency. If ``False``, the basis is unconstrained, if ``True`` it receives a sum to zero constrained. Also see :meth:`.Basis.constrain`. diagonal_penalty Whether the penalty matrix associated with this term should be reparameterized into a diagonal matrix. In this case, the basis matrix is reparameterized accordingly. This can be beneficial for posterior geometry, which is why it is the default. Also see :meth:`.Basis.diagonalize_penalty`. scale_penalty Whether the penalty matrix should be scaled such that its infinity norm is one. This can improve numerical stability, which is why it is done by default. Also see :meth:`.Basis.scale_penalty`. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .ps : P-spline. .cp : Cyclic P-spline. .BasisBuilder : Initializes the basis and penalty. Notes ----- This basis is initialized with ``use_callback=True`` and ``cache_basis=True``. See :class:`.Basis` for details. This method internally calls the R package mgcv to set up the basis and penalty. References ---------- - Lang, S., & Brezger, A. (2004). Bayesian P-splines. Journal of Computational and Graphical Statistics, 13(1), 183–212. https://doi.org/10.1198/1061860043010 - Wood, S.N. (2017) Generalized Additive Models: An Introduction with R (2nd edition). Chapman and Hall/CRC. - R package mgcv https://cran.r-project.org/web/packages/mgcv/index.html Examples --------- >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.np("x_nonlin", k=20) StrctTerm(name="np(x_nonlin)") """ basis = self.bases.ps( x=x, k=k, basis_degree=basis_degree, penalty_order=penalty_order, knots=knots, absorb_cons=False, diagonal_penalty=False, scale_penalty=False, basis_name="B", ) basis.constrain("constant_and_linear") if scale_penalty: basis.scale_penalty() if diagonal_penalty: basis.diagonalize_penalty() fname = self.names.fname(prefix + "np", basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = StrctTerm( basis=basis, penalty=basis.penalty, scale=self.init_scale(scale, fname), name=fname, inference=self._get_inference(inference), coef_name=coef_name, ) if factor_scale: term.factor_scale() return term
[docs] def cp( self, x: str | lsl.Var, *, k: int, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", basis_degree: int = 3, penalty_order: int = 2, knots: ArrayLike | None = None, absorb_cons: bool = True, diagonal_penalty: bool = True, scale_penalty: bool = True, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> StrctTerm: r""" Cyclic P-spline. Parameters ---------- x Name of input variable. k Number of (unconstrained) bases. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. basis_degree Degree of the polynomials used in the B-spline basis function. Default is 3 for cubic B-splines. penalty_order Order of the penalty. knots Knots used to set up the basis. If ``None`` (default), a set of equidistant knots will be set up automatically, with the domain boundaries inferred from the minimum and maximum of the observed values. absorb_cons Whether the default identification constraint should be applied by reparameterization and absorbing the reparameterization matrix into the basis and penalty matrices for computational efficiency. If ``False``, the basis is unconstrained, if ``True`` it receives a sum to zero constrained. Also see :meth:`.Basis.constrain`. diagonal_penalty Whether the penalty matrix associated with this term should be reparameterized into a diagonal matrix. In this case, the basis matrix is reparameterized accordingly. This can be beneficial for posterior geometry, which is why it is the default. Also see :meth:`.Basis.diagonalize_penalty`. scale_penalty Whether the penalty matrix should be scaled such that its infinity norm is one. This can improve numerical stability, which is why it is done by default. Also see :meth:`.Basis.scale_penalty`. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .ps : P-spline. .np : P-spline without linear trend. .BasisBuilder : Initializes the basis and penalty. Notes ----- This basis is initialized with ``use_callback=True`` and ``cache_basis=True``. See :class:`.Basis` for details. This method internally calls the R package mgcv to set up the basis and penalty. References ---------- - Lang, S., & Brezger, A. (2004). Bayesian P-splines. Journal of Computational and Graphical Statistics, 13(1), 183–212. https://doi.org/10.1198/1061860043010 - Wood, S.N. (2017) Generalized Additive Models: An Introduction with R (2nd edition). Chapman and Hall/CRC. - R package mgcv https://cran.r-project.org/web/packages/mgcv/index.html Examples --------- >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.cp("x_nonlin", k=20) StrctTerm(name="cp(x_nonlin)") """ basis = self.bases.cp( x=x, k=k, basis_degree=basis_degree, penalty_order=penalty_order, knots=knots, absorb_cons=absorb_cons, diagonal_penalty=diagonal_penalty, scale_penalty=scale_penalty, basis_name="B", ) fname = self.names.fname(prefix + "cp", basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = StrctTerm( basis=basis, penalty=basis.penalty, scale=self.init_scale(scale, fname), name=fname, inference=self._get_inference(inference), coef_name=coef_name, ) if factor_scale: term.factor_scale() return term
# random intercept
[docs] def ri( self, cluster: str, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", penalty: ArrayLike | None = None, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> RITerm: r""" Random intercept. Parameters ---------- cluster Name of the cluster variable. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. penalty Custom penalty matrix to use. Default is an iid penalty. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .rs : Random slope. .BasisBuilder : Initializes the basis and penalty. Notes ------ If the penalty is iid, then each column of the basis consists only of binary (0/1) entries, and each row has only one non-zero entry. In this case it is not necessary to store the full matrix in memory and evaluate the term as a dot product ``basis @ coef``. Instead, we can simply store a 1d array of indices, identifying the nonzero column for each row of the basis matrix, and use this index to access the corresponding coefficient. This scenario is common for independent random intercepts. This method returns such a sparse representation of the random intercept basis if ``penalty=None``. Examples --------- >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.ri("x_cat") RITerm(name="ri(x_cat)") """ basis = self.bases.ri(cluster=cluster, basis_name="B", penalty=penalty) fname = self.names.fname(prefix + "ri", basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = RITerm( basis=basis, penalty=basis.penalty, coef_name=coef_name, inference=self._get_inference(inference), scale=self.init_scale(scale, fname), name=fname, ) if factor_scale: term.factor_scale() mapping = self.bases.mappings[cluster] term.mapping = mapping term.labels = list(mapping.labels_to_integers_map) nparams = len(mapping.labels_to_integers_map) if basis.penalty is None and nparams != basis.nbases: # this takes care of increasing the parameter number in case this term # covers unobserved clusters term.coef.value = jnp.zeros(nparams) return term
# random scaling
[docs] def rs( self, x: str | StrctTerm | LinTerm, cluster: str, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", penalty: ArrayLike | None = None, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> lsl.Var: r""" Random slope. Create a Liesel variable that evaluates to ``x * ri(cluster)``, where ``ri(cluster)`` is a random intercept initialized via :meth:`.ri` and ``x`` is either a covariate directly, or a smooth term. The ``scale`` argument of this method is the random intercept's scale, it is passed to :meth:`.ri`. The same goes for ``penalty`` and ``factor_scale``. Parameters ---------- x Name of input variable, or a smooth represented by a :class:`.StrctTerm`. cluster Name of the cluster variable. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. penalty Custom penalty matrix to use. Default is an iid penalty. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. See Also -------- .ri : Random intercept. .BasisBuilder : Initializes the basis and penalty. Examples --------- Random slope: >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.rs(x="x_lin", cluster="x_cat") Var(name="rs(x_lin|x_cat)") Random scaling of a smooth term: >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> psx = tb.ps("x_nonlin", k=20) >>> tb.rs(x=psx, cluster="x_cat") Var(name="rs(ps(x_nonlin)|x_cat)") """ ri = self.ri( cluster=cluster, scale=scale, inference=self._get_inference(inference), penalty=penalty, factor_scale=factor_scale, ) if isinstance(x, str): x_var = self.registry.get_numeric_obs(x) xname = x else: x_var = x xname = x_var.name fname = self.names.create("rs(" + xname + "|" + cluster + ")") term_name = prefix + name if name is not None else fname fname = term_name term = lsl.Var.new_calc( lambda x, cluster: x * cluster, x=x_var, cluster=ri, name=fname, ) return term
# varying coefficient
[docs] def vc( self, x: str | lsl.Var, by: StrctTerm, prefix: str = "", name: str | None = None, ) -> lsl.Var: r""" Varying coefficient term. Parameters ---------- x Name of input variable. by Smooth term, a :class:`.StrctTerm` that represents the smoothly varying coefficient of this term, for example a P-spline :meth:`.ps`. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. Notes ----- A varying coefficient term can be written as .. math:: x \beta(z), where :math:`x` is a covariate, and :math:`\beta(z)` is the linear effect of this covariate, which smoothly varies as a function of another variable :math:`z`. Examples -------- >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> psx = tb.ps("x_nonlin", k=20) >>> tb.vc(x="x_lin", by=psx) Var(name="x_lin*ps(x_nonlin)") """ x_var = self.bases._get_var_and_value(x)[0] fname = self.names.create(x_var.name + "*" + by.name) term_name = prefix + name if name is not None else fname fname = term_name term = lsl.Var.new_calc( lambda x, by: x * by, x=x_var, by=by, name=fname, ) return term
# general smooth with MGCV bases def _s( self, *x: str | lsl.Var, k: int, bs: BasisTypes, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", m: str = "NA", knots: ArrayLike | None = None, absorb_cons: bool = True, diagonal_penalty: bool = True, scale_penalty: bool = True, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> StrctTerm: basis = self.bases._s( *x, k=k, bs=bs, m=m, knots=knots, absorb_cons=absorb_cons, diagonal_penalty=diagonal_penalty, scale_penalty=scale_penalty, basis_name="B", ) fname = self.names.fname(prefix + bs, basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = StrctTerm( basis, penalty=basis.penalty, name=fname, coef_name=coef_name, scale=self.init_scale(scale, fname), inference=self._get_inference(inference), ) if factor_scale: term.factor_scale() return term # markov random field
[docs] def mrf( self, x: str, k: int = -1, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", polys: dict[str, ArrayLike] | None = None, nb: Mapping[str, ArrayLike | list[str] | list[int]] | None = None, penalty: ArrayLike | None = None, absorb_cons: bool = True, diagonal_penalty: bool = True, scale_penalty: bool = True, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> MRFTerm: r""" Gaussian Markov random field. The preferred way to initialize these is by supplying ``polys``, because this enables plotting via :func:`.plot_regions`. Parameters ---------- x Name of the region variable. k If ``-1``, this is a "full-rank" (up to identifiability constraint) Markov random field. If ``k`` is an integer smaller than the number of unique regions, a low-rank field will be returned, see Wood (2017), Sections 5.8.1 and 5.4.2. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. polys Dictionary of arrays. The keys of the dict are the region labels. The corresponding values define the region by defining polygons. The neighborhood structure can be inferred from this polygon information. nb Dictionary of array. The keys of the dict are the region labels. The corresponding values indicate the neighbors of the region. If the values are lists or arrays of strings, the values are the labels of the neighbors. If they are lists or arrays of integers, the values are the indices of the neighbors. Indices correspond to regions based on an alphabetical ordering of regions. penalty If a penalty is supplied explicitly, it takes precedence over a potential penalty derived from both nb and polys. penalty_labels If a penalty is supplied explicitly, labels must also be specified. The labels create the association between penalty columns and region labels. The values of this sequence should be the string labels of unique regions in ``x``. absorb_cons Whether the default identification constraint should be applied by reparameterization and absorbing the reparameterization matrix into the basis and penalty matrices for computational efficiency. If ``False``, the basis is unconstrained, if ``True`` it receives a sum to zero constrained. Also see :meth:`.Basis.constrain`. diagonal_penalty Whether the penalty matrix associated with this term should be reparameterized into a diagonal matrix. In this case, the basis matrix is reparameterized accordingly. This can be beneficial for posterior geometry, which is why it is the default. Also see :meth:`.Basis.diagonalize_penalty`. scale_penalty Whether the penalty matrix should be scaled such that its infinity norm is one. This can improve numerical stability, which is why it is done by default. Also see :meth:`.Basis.scale_penalty`. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .plot_regions : Plots MCMC results on a map of the regions. .plot_polys : Plots a map based on polygons. .plot_forest : Plots regions with uncertainty in a forest plot. Notes ----- This method internally calls the R package mgcv to set up the basis and penalty. The mgcv documentation provides further details. Returns ------- Comments on the additional attributes available on the returned :class:`.MRFTerm` variable: - If either polys or nb are supplied, the returned term will contain information in :attr:`.MRFTerm.neighbors`. - If only a penalty matrix is supplied, the returned MRFSpec will *not* contain information in :attr:`.MRFTerm.neighbors`. - :attr:`.MRFTerm.mapping` contains the map of region labels to integer codes. - :attr:`.MRFTerm.labels` contains the region labels. - Returning the label order only makes sense if the basis is *not* reparameterized, because only then we have a clear correspondence of parameters to labels. If the basis is reparameterized, with ``absorb_cons=True`` or of low rank with ``k ≠ -1``, there is no such correspondence in a clear way, so the label order in :attr:`.MRFTerm.ordered_labels` is None. Examples -------- >>> import liesel_gam as gam >>> df = gam.demo_data(n=100) >>> nb = {"a": ["b", "c"], "b": ["a"], "c": ["a"]} >>> print(df.x_cat.unique().tolist()) ['a', 'b', 'c'] >>> tb = gam.TermBuilder.from_df(df) >>> tb.mrf("x_cat", nb=nb) MRFTerm(name="mrf(x_cat)") References ---------- - Wood, S.N. (2017) Generalized Additive Models: An Introduction with R (2nd edition). Chapman and Hall/CRC. - R package mgcv https://cran.r-project.org/web/packages/mgcv/index.html """ basis = self.bases.mrf( x=x, k=k, polys=polys, nb=nb, penalty=penalty, absorb_cons=absorb_cons, diagonal_penalty=diagonal_penalty, scale_penalty=scale_penalty, basis_name="B", ) fname = self.names.fname(prefix + "mrf", basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = MRFTerm( basis, penalty=basis.penalty, name=fname, scale=self.init_scale(scale, fname), inference=self._get_inference(inference), coef_name=coef_name, ) if factor_scale: term.factor_scale() term.polygons = basis.mrf_spec.polys term.neighbors = basis.mrf_spec.nb if basis.mrf_spec.ordered_labels is not None: term.ordered_labels = basis.mrf_spec.ordered_labels term.labels = list(basis.mrf_spec.mapping.labels_to_integers_map) term.mapping = basis.mrf_spec.mapping return term
# general basis function + penalty smooth
[docs] def f( self, *x: str | lsl.Var, basis_fn: Callable[[Array], Array], scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", use_callback: bool = True, cache_basis: bool = True, penalty: ArrayLike | None = None, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> StrctTerm: r""" General :class:`.StrctTerm`, initialized by passing a custom basis function. .. note:: You can use :meth:`.StrctTerm.constrain` to apply linear constraints to your custom term. Parameters ---------- *x Variable number of input variable names. Can be one or more. basis_fn Basis function. Must take a 2d-array as input and return a 2d array. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. use_callback If *True*, the basis function is evaluated using a Python callback, which means that it does not have to be jit-compatible via JAX. This also means that the basis must remain constant throughout estimation. Passed on to :class:`.Basis`. cache_basis If ``True`` the computed basis is cached in a persistent calculation node (``lsl.Calc``), which avoids re-computation when not required. Passed on to :class:`.Basis`. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .StrctTerm.constrain : Apply constraints to a term after initialization. .StrctTerm.diagonalize_penalty : Diagonalize the penalty of a term after initialization. .StrctTerm.scale_penalty : Scale the penalty of a term after initialization. .BasisBuilder.basis : Used by this method to set up the basis. Examples -------- Manually set up a P-spline: >>> from liesel.contrib.splines import ( ... basis_matrix, ... equidistant_knots, ... pspline_penalty, ... ) >>> import liesel_gam as gam >>> df = gam.demo_data(n=100) >>> tb = gam.TermBuilder.from_df(df) Set up basis function: >>> knots = equidistant_knots(df["x_nonlin"].to_numpy(), n_param=20) >>> pen = pspline_penalty(d=20) >>> def bspline_basis(x_mat): ... # x_mat is shape (n, 1) ... x_vec = x_mat.squeeze() # shape (n,) ... return basis_matrix(x_vec, knots=knots) Initialize the term: >>> fx = tb.f("x_nonlin", basis_fn=bspline_basis, penalty=pen) The term currently has a basis of dimension (100, 20), since it is unconstrained. >>> fx.basis.value.shape (100, 20) You can use :meth:`.StrctTerm.constrain` to apply a constraint: >>> fx.constrain("sumzero_term") StrctTerm(name="f(x_nonlin)") Now the basis dimension is reduced: >>> fx.basis.value.shape (100, 19) """ basis = self.bases.basis( *x, basis_fn=basis_fn, use_callback=use_callback, cache_basis=cache_basis, penalty=penalty, basis_name="B", ) fname = self.names.fname(prefix + "f", basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = StrctTerm( basis, penalty=basis.penalty, name=fname, scale=self.init_scale(scale, fname), inference=self._get_inference(inference), coef_name=coef_name, ) if factor_scale: term.factor_scale() return term
[docs] def kriging( self, *x: str | lsl.Var, k: int, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", kernel_name: Literal[ "spherical", "power_exponential", "matern1.5", "matern2.5", "matern3.5", ] = "matern1.5", linear_trend: bool = True, range: float | None = None, power_exponential_power: float = 1.0, knots: ArrayLike | None = None, absorb_cons: bool = True, diagonal_penalty: bool = True, scale_penalty: bool = True, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> StrctTerm: r""" Gaussian process models with a fixed range parameter in a basis-penalty-parameterization, often referred to as Kriging. Parameters ---------- *x Name of input variables (one or more). k Number of (unconstrained) bases. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. kernel_name Selects the kernel / covariance function to use. linear_trend Whether to include or remove a linear trend. range Range parameter. If ``None``, estimated as in Kamman & Wand (2003). power_exponential_power Power for the power exponential kernel. absorb_cons Whether the default identification constraint should be applied by reparameterization and absorbing the reparameterization matrix into the basis and penalty matrices for computational efficiency. If ``False``, the basis is unconstrained, if ``True`` it receives a sum to zero constrained. Also see :meth:`.Basis.constrain`. diagonal_penalty Whether the penalty matrix associated with this term should be reparameterized into a diagonal matrix. In this case, the basis matrix is reparameterized accordingly. This can be beneficial for posterior geometry, which is why it is the default. Also see :meth:`.Basis.diagonalize_penalty`. scale_penalty Whether the penalty matrix should be scaled such that its infinity norm is one. This can improve numerical stability, which is why it is done by default. Also see :meth:`.Basis.scale_penalty`. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .BasisBuilder : Initializes the basis and penalty. Notes ----- This method internally calls the R package mgcv to set up the basis and penalty. The mgcv documentation provides further details. References ---------- - Kammann, E. E. and M.P. Wand (2003) Geoadditive Models. Applied Statistics 52(1):1-18. - Wood, S.N. (2017) Generalized Additive Models: An Introduction with R (2nd edition). Chapman and Hall/CRC. - R package mgcv https://cran.r-project.org/web/packages/mgcv/index.html Examples -------- >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.kriging("x_nonlin", k=20) StrctTerm(name="kriging(x_nonlin)") """ basis = self.bases.kriging( *x, k=k, kernel_name=kernel_name, linear_trend=linear_trend, range=range, power_exponential_power=power_exponential_power, knots=knots, absorb_cons=absorb_cons, diagonal_penalty=diagonal_penalty, scale_penalty=scale_penalty, basis_name="B", ) fname = self.names.fname(prefix + "kriging", basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = StrctTerm( basis, penalty=basis.penalty, name=fname, scale=self.init_scale(scale, fname), inference=self._get_inference(inference), coef_name=coef_name, ) if factor_scale: term.factor_scale() return term
[docs] def tp( self, *x: str | lsl.Var, k: int, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", penalty_order: int | None = None, knots: ArrayLike | None = None, absorb_cons: bool = True, diagonal_penalty: bool = True, scale_penalty: bool = True, factor_scale: bool = False, remove_null_space_completely: bool = False, prefix: str = "", name: str | None = None, ) -> StrctTerm: r""" Thin plate spline. Parameters ---------- *x Names of input variables (one or more). k Number of (unconstrained) bases. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. penalty_order Order of the penalty. Quote from mgcv: "The default is to set this to the smallest value satisfying ``2*penalty_order > d+1`` where ``d`` is the number of covariates of the term." knots Knots used to set up the basis. If ``None`` (default), a set knots will be set up automatically. absorb_cons Whether the default identification constraint should be applied by reparameterization and absorbing the reparameterization matrix into the basis and penalty matrices for computational efficiency. If ``False``, the basis is unconstrained, if ``True`` it receives a sum to zero constrained. Also see :meth:`.Basis.constrain`. diagonal_penalty Whether the penalty matrix associated with this term should be reparameterized into a diagonal matrix. In this case, the basis matrix is reparameterized accordingly. This can be beneficial for posterior geometry, which is why it is the default. Also see :meth:`.Basis.diagonalize_penalty`. scale_penalty Whether the penalty matrix should be scaled such that its infinity norm is one. This can improve numerical stability, which is why it is done by default. Also see :meth:`.Basis.scale_penalty`. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. remove_null_space_completely If ``True``, the unpenalized part of the smooth, corresponding to the null space of the penalty matrix, is removed completely. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .BasisBuilder : Initializes the basis and penalty. Notes ----- This method internally calls the R package mgcv to set up the basis and penalty. The mgcv documentation provides further details. References ---------- - Wood, S.N. (2003) Thin-plate regression splines. Journal of the Royal Statistical Society (B) 65(1):95-114. - Wood, S.N. (2017) Generalized Additive Models: An Introduction with R (2nd edition). Chapman and Hall/CRC. - R package mgcv https://cran.r-project.org/web/packages/mgcv/index.html Examples -------- >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.tp("x_nonlin", k=20) StrctTerm(name="tp(x_nonlin)") """ basis = self.bases.tp( *x, k=k, penalty_order=penalty_order, knots=knots, absorb_cons=absorb_cons, diagonal_penalty=diagonal_penalty, scale_penalty=scale_penalty, basis_name="B", remove_null_space_completely=remove_null_space_completely, ) fname = self.names.fname(prefix + "tp", basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = StrctTerm( basis, penalty=basis.penalty, name=fname, scale=self.init_scale(scale, fname), inference=self._get_inference(inference), coef_name=coef_name, ) if factor_scale: term.factor_scale() return term
[docs] def ts( self, *x: str | lsl.Var, k: int, scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] = "default", inference: InferenceTypes | None | Literal["default"] = "default", penalty_order: int | None = None, knots: ArrayLike | None = None, absorb_cons: bool = True, diagonal_penalty: bool = True, scale_penalty: bool = True, factor_scale: bool = False, prefix: str = "", name: str | None = None, ) -> StrctTerm: r""" Thin plate spline with additional null space penalty. Parameters ---------- *x Names of input variables (one or more). k Number of (unconstrained) bases. scale Scale parameter passed to the coefficient prior, :attr:`.StrctTerm.scale`. - If ``"default"``, the scale will be initialized according to the default scale function defined for this :class:`.TermBuilder` instance. Please refer to the TermBuilder documentation for more information. - If you pass a ``float``, this will be taken as the constant value of the scale, and the scale will not be estimated as part of the model without further action. - If you pass a :class:`liesel.model.Var`, this will be used as the scale. Make sure to define the ``inference`` attribute of your custom scale variable (or a latent, transformed version of it). - If you pass a :class:`.VarIGPrior`, a scale variable will be set up for you using :class:`.ScaleIG`. This means, the scale will be :math:`\tau`, with an iverse Gamma prior on its square, i.e. :math:`\tau^2 \sim \operatorname{InverseGamma}(a, b)`, where a and b are taken from the :class:`.VarIGPrior` object. A fitting Gibbs kernel will be set up automatically to sample :math:`\tau^2` in this case, see :class:`.ScaleIG` for details. inference Inference specification for this term's coefficient. Note that this inference is only used for the coefficient variables of the terms created by this builder (:attr:`.StrctTerm.coef`), *not* for the scale variables (:attr:`.StrctTerm.scale`). The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. penalty_order Order of the penalty. Quote from mgcv: "The default is to set this to the smallest value satisfying ``2*penalty_order > d+1`` where ``d`` is the number of covariates of the term." knots Knots used to set up the basis. If ``None`` (default), a set knots will be set up automatically. absorb_cons Whether the default identification constraint should be applied by reparameterization and absorbing the reparameterization matrix into the basis and penalty matrices for computational efficiency. If ``False``, the basis is unconstrained, if ``True`` it receives a sum to zero constrained. Also see :meth:`.Basis.constrain`. diagonal_penalty Whether the penalty matrix associated with this term should be reparameterized into a diagonal matrix. In this case, the basis matrix is reparameterized accordingly. This can be beneficial for posterior geometry, which is why it is the default. Also see :meth:`.Basis.diagonalize_penalty`. scale_penalty Whether the penalty matrix should be scaled such that its infinity norm is one. This can improve numerical stability, which is why it is done by default. Also see :meth:`.Basis.scale_penalty`. factor_scale Whether to factor out the scale in the prior for this term, turning it into a partially (or fully) standardized form. See :meth:`.StrctTerm.factor_scale` for details. remove_null_space_completely If ``True``, the unpenalized part of the smooth, corresponding to the null space of the penalty matrix, is removed completely. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. See Also -------- .BasisBuilder : Initializes the basis and penalty. Notes ----- This method internally calls the R package mgcv to set up the basis and penalty. The mgcv documentation provides further details. References ---------- - Wood, S.N. (2003) Thin-plate regression splines. Journal of the Royal Statistical Society (B) 65(1):95-114. - Wood, S.N. (2017) Generalized Additive Models: An Introduction with R (2nd edition). Chapman and Hall/CRC. - R package mgcv https://cran.r-project.org/web/packages/mgcv/index.html Examples -------- >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> tb.ts("x_nonlin", k=20) StrctTerm(name="ts(x_nonlin)") """ basis = self.bases.ts( *x, k=k, penalty_order=penalty_order, knots=knots, absorb_cons=absorb_cons, diagonal_penalty=diagonal_penalty, scale_penalty=scale_penalty, basis_name="B", ) fname = self.names.fname(prefix + "ts", basis.x.name) term_name = prefix + name if name is not None else fname fname = term_name coef_name = self.names.beta(fname) term = StrctTerm( basis, penalty=basis.penalty, name=fname, scale=self.init_scale(scale, fname), inference=self._get_inference(inference), coef_name=coef_name, ) if factor_scale: term.factor_scale() return term
def _ta( self, *marginals: StrctTerm, common_scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] | None = None, inference: InferenceTypes | None | Literal["default"] = "default", scales_inference: InferenceTypes | None | Literal["default"] = gs.MCMCSpec( gs.HMCKernel ), include_main_effects: bool = False, _fname: str = "ta", prefix: str = "", name: str | None = None, ) -> StrctInteractionTerm: r""" General anisotropic tensor product term. .. warning:: This method remove any default gibbs samplers and replace them with ``scales_inference`` on log level, since the full conditional for the variance parameters is not known in closed form for an anisotropic tensor product. Parameters ---------- *marginals Marginal terms, subclasses of :class:`.StrctTerm`. common_scale A single, common scale to cover all marginal dimensions, resulting in an isotropic tensor product. This mean setting :math:`\tau^2_1 = \\dots = \tau^2_M = \tau^2` for all marginal smooths in the notation used in :class:`.StrctTensorProdTerm`. inference Inference specification for this term's coefficient. scales_inference If ``"default"``, uses the default inference passed to the TermBuilder upon initialization. include_main_effects If ``True``, the marginal terms will be added to this term's value. """ inputs = ",".join( list(StrctInteractionTerm._input_obs([t.basis for t in marginals])) ) fname = self.names.create(prefix + f"{_fname}(" + inputs + ")") term_name = prefix + name if name is not None else fname fname = term_name basis_name = self.names.create("B(" + inputs + ")") coef_name = self.names.beta(fname) if common_scale is not None and not isinstance(common_scale, float): common_scale = self.init_scale(common_scale, fname) _biject_and_replace_star_gibbs_with( common_scale, self._get_inference(scales_inference) ) elif common_scale is not None: common_scale = self.init_scale(common_scale, fname) term = StrctInteractionTerm( *marginals, common_scale=common_scale, name=fname, inference=self._get_inference(inference), coef_name=coef_name, include_main_effects=include_main_effects, basis_name=basis_name, ) if not common_scale: for scale in term.scales: if not isinstance(scale, lsl.Var): raise TypeError( f"Expected scale to be a liesel.model.Var, got {type(scale)}" ) _biject_and_replace_star_gibbs_with( scale, self._get_inference(scales_inference) ) return term
[docs] def tx( self, *marginals: StrctTerm, common_scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] | None = None, inference: InferenceTypes | None | Literal["default"] = "default", scales_inference: InferenceTypes | None | Literal["default"] = gs.MCMCSpec( gs.HMCKernel ), prefix: str = "", name: str | None = None, ) -> StrctInteractionTerm: r""" General anisotropic tensor product interaction term without main effects. Includes only the tensor product interaction. Corresponds to ``mgcv::ti``. .. warning:: This method removes any default gibbs samplers and replaces them with ``scales_inference`` on log level, since the full conditional for the variance parameters is not known in closed form for an anisotropic tensor product. Parameters ---------- *marginals Marginal terms, subclasses of :class:`.StrctTerm`. common_scale A single, common scale to cover all marginal dimensions, resulting in an isotropic tensor product. This mean setting :math:`\tau^2_1 = \dots = \tau^2_M = \tau^2` for all marginal dimensions of this interaction in the notation used in :class:`.StrctInteractionTerm`. This does not affect the scales of the supplied marginals (main effects). inference Inference specification for this term's coefficient. The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. scales_inference If ``"default"``, uses the default inference passed to the TermBuilder upon initialization. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. Notes ----- .. note:: The methods :meth:`.tf` and :meth:`.tx` are closely related. The former loosely corresponds to ``mgcv::ti``, and the latter loosely corresponds to ``mgcv::te``, meaning that, when you supply centered marginals, :class:`.tx` will *only* include the highest-order interaction of the supplied marginals, while :class:`.tf` will include the highest-order interaction *and* all lower-order interactions, including the main effects. See Also -------- .StrctInteractionTerm : The term class returned by this method; includes further details. .tf : Full tensor product, including main effects. Examples -------- Using only the interaction term: >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> pred = gam.AdditivePredictor(name="loc") >>> ps1 = tb.ps("x_nonlin", k=7) >>> ps2 = tb.ps("x_lin", k=7) >>> pred += tb.tx(ps1, ps2) >>> pred.terms {'tx(x_nonlin,x_lin)': StrctInteractionTerm(name="tx(x_nonlin,x_lin)")} .. rubric:: Anova decomposition Including the main effects (this corresponds to :meth:`.tf`): >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> pred = gam.AdditivePredictor(name="loc") >>> ps1 = tb.ps("x_nonlin", k=7) >>> ps2 = tb.ps("x_lin", k=7) >>> pred += ps1, ps2, tb.tx(ps1, ps2) >>> len(pred.terms) 3 .. rubric:: Isotropic tensor product interaction >>> import liesel_gam as gam >>> import tensorflow_probability.substrates.jax.bijectors as tfb >>> import tensorflow_probability.substrates.jax.distributions as tfd >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> pred = gam.AdditivePredictor(name="loc") Marginal smooths: >>> ps1 = tb.ps("x_nonlin", k=7) >>> ps2 = tb.ps("x_lin", k=7) Initializing the scale variable: >>> scale = lsl.Var.new_param( ... 1.0, ... distribution=lsl.Dist(tfd.HalfNormal, scale=20.0), ... name="{x}", ... ) >>> log_scale = scale.transform( ... tfb.Exp(), ... inference=gs.MCMCSpec(gs.HMCKernel), ... name="ln({x})", ... ) Initializing the interaction term: >>> tx1 = tb.tx(ps1, ps2, common_scale=scale) The :attr:`.StrctTensorProdTerm.scales` list now contains the same scale twice, leading to an isotropic tensor product. >>> tx1.scales[0] Var(name="$\tau_{tx(x_nonlin,x_lin)}$") >>> tx1.scales[1] Var(name="$\tau_{tx(x_nonlin,x_lin)}$") .. rubric:: Marginals with different dimensions In the Anova decomposition of a tensor product, you can supply marginals with more bases than the marginals used in the interaction term. This can be helpful for managing the curse of dimensionality, as the parameter count in the interaction term grows rapidly. >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> pred = gam.AdditivePredictor(name="loc") Independent marginal terms: >>> ps1 = tb.ps("x_nonlin", k=20) >>> ps2 = tb.ps("x_lin", k=20) >>> pred += ps1, ps2 Adding an interaction term using marginals with fewer bases: >>> pred += tb.tx( ... tb.ps("x_nonlin", k=7), ... tb.ps("x_lin", k=7), ... ) >>> len(pred.terms) 3 Note that this term has four variance parameters: Four of them govern the independent marginals, and the other two govern the interaction, because we initialize new marginals with new variance parameters for the interaction term. This added flexibility when using :meth:`.tx` is a further difference to :meth:`.tf`. .. rubric:: Three-dimensional interaction Note that this term has six variance parameters: Three of them govern the independent marginals, and the other three are used in the interaction terms, because we initialize new marginals with new variance parameters for the interaction terms. This added flexibility when using :meth:`.tx` is a further difference to :meth:`.tf`. >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> pred = gam.AdditivePredictor(name="loc") >>> ps1 = tb.ps("x_nonlin", k=20) >>> ps2 = tb.ps("x_lin", k=20) >>> ps3 = tb.ps("x", k=20) We first add the main effects: >>> pred += ps1, ps2, ps3 Then we initialize a second set of marginals: >>> ps1x = tb.ps("x_nonlin", k=7) >>> ps2x = tb.ps("x_lin", k=7) >>> ps3x = tb.ps("x", k=7) Then the three bivariate interactions: >>> pred += tb.tx(ps1x, ps2x) >>> pred += tb.tx(ps1x, ps3x) >>> pred += tb.tx(ps2x, ps3x) And finally the trivariate interaction: >>> pred += tb.tx(ps1x, ps2x, ps3x) >>> len(pred.terms) 7 """ term = self._ta( *marginals, common_scale=common_scale, inference=self._get_inference(inference), scales_inference=scales_inference, include_main_effects=False, _fname="tx", prefix=prefix, name=name, ) return term
[docs] def tf( self, *marginals: StrctTerm, common_scale: ScaleIG | lsl.Var | float | VarIGPrior | Literal["default"] | None = None, order: Sequence[int] | None = None, inference: InferenceTypes | None | Literal["default"] = "default", scales_inference: InferenceTypes | None | Literal["default"] = gs.MCMCSpec( gs.HMCKernel ), prefix: str = "", name: str | None = None, group_terms_by_order: bool = False, ) -> StrctTensorProdTerm: r""" General full anisotropic tensor product term, including main effects and interactions. Corresponds to ``mgcv::te``. .. warning:: This method removes any default gibbs samplers and replaces them with ``scales_inference`` on log level, since the full conditional for the variance parameters is not known in closed form for an anisotropic tensor product. Parameters ---------- *marginals Marginal terms, subclasses of :class:`.StrctTerm`. common_scale A single, common scale to cover all marginal dimensions, resulting in an isotropic tensor product. This mean setting :math:`\tau^2_1 = \dots = \tau^2_M = \tau^2` for all marginal smooths in the notation used in :class:`.StrctInteractionTerm`. Note that this *will* also change the scales of the supplied marginals (main effects). order Sequence of intergers identifying the orders of interactions to be included in this term. For example, if you want to include only the bi- and trivariate interactions when supplying three marginals, pass ``order=(2,3)``. The default ``order=None`` means that *all* orders will be included; also the main effects. inference Inference specification for this term's coefficient. The default (``"default"``) uses the :class:`.TermBuilder`'s default inference specification defined during initialization. Please refer to the TermBuilder documentation for more information. scales_inference If ``"default"``, uses the default inference passed to the TermBuilder upon initialization. prefix A string prefix to be added to the returned term's name. name Manually defined name of the term. If a prefix is specified, the prefix will be added to this name. group_terms_by_order If ``True``, an intermediate variable object will be created for each order of interactions in this term. This can help to better organize the plotted graph of a term, but otherwise has no effect except using slightly more memory. Notes ----- .. note:: The methods :meth:`.tf` and :meth:`.tx` are closely related. The former loosely corresponds to ``mgcv::ti``, and the latter loosely corresponds to ``mgcv::te``, meaning that, when you supply centered marginals, :class:`.tx` will *only* include the highest-order interaction of the supplied marginals, while :class:`.tf` will include the highest-order interaction *and* all lower-order interactions, including the main effects. See Also -------- .StrctTensorProdTerm : The term class returned by this method; includes further details. .tx : Tensor product interaction term, without main effects. Examples -------- Using only the interaction term: >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> pred = gam.AdditivePredictor(name="loc") >>> ps1 = tb.ps("x_nonlin", k=7) >>> ps2 = tb.ps("x_lin", k=7) >>> pred += tb.tf(ps1, ps2) >>> pred.terms {'tf(x_nonlin,x_lin)': StrctTensorProdTerm(name="tf(x_nonlin,x_lin)")} To illustrate the difference to :meth:`.tx`, consider this example, which is practically equivalent: >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> pred = gam.AdditivePredictor(name="loc") >>> ps1 = tb.ps("x_nonlin", k=7) >>> ps2 = tb.ps("x_lin", k=7) >>> pred += ps1, ps2, tb.tx(ps1, ps2) >>> len(pred.terms) 3 .. rubric:: Three-dimensional tensor product >>> import liesel_gam as gam >>> df = gam.demo_data(100) >>> tb = gam.TermBuilder.from_df(df) >>> pred = gam.AdditivePredictor(name="loc") >>> pred += tb.tf( ... tb.ps("x_nonlin", k=7), ... tb.ps("x_lin", k=7), ... tb.ps("x", k=7), ... ) >>> len(pred.terms) 1 The attribute ``StrctTensorProd.terms_by_order`` organizes the individual terms created by the full tensor product by interaction order. For example, these are the main effects: >>> pred.terms["tf(x_nonlin,x_lin,x)"].terms_by_order[1] # doctest: +ELLIPSIS [StrctTerm(name="ps(x_nonlin)"), ...] These are the bivariate interactions: >>> pred.terms["tf(x_nonlin,x_lin,x)"].terms_by_order[2] # doctest: +ELLIPSIS [StrctInteractionTerm(name="tx(x_nonlin,x_lin)"), ...] And this is the trivariate interaction: >>> pred.terms["tf(x_nonlin,x_lin,x)"].terms_by_order[3] [StrctInteractionTerm(name="tx(x_nonlin,x_lin,x)")] We can inspect the number of coefficients in the trivariate interaction term: >>> interaction = pred.terms["tf(x_nonlin,x_lin,x)"].terms_by_order[3] >>> interaction[0] StrctInteractionTerm(name="tx(x_nonlin,x_lin,x)") >>> interaction[0].coef.value.shape (216,) """ inputs = ",".join( list(StrctInteractionTerm._input_obs([t.basis for t in marginals])) ) fname = self.names.create(prefix + "tf(" + inputs + ")") term_name = prefix + name if name is not None else fname fname = term_name if common_scale is not None and not isinstance(common_scale, float): common_scale = self.init_scale(common_scale, fname) _biject_and_replace_star_gibbs_with( common_scale, self._get_inference(scales_inference) ) elif common_scale is not None: common_scale = self.init_scale(common_scale, fname) term = StrctTensorProdTerm( *marginals, common_scale=common_scale, order=order, inference=self._get_inference(inference), coef_name=r"\beta", basis_name="B", tx_name="tx", tf_name="tf", names_prefix=prefix, group_terms_by_order=group_terms_by_order, ) term.name = term_name first_order_bases = [] for term_ in term.terms_by_order[1]: first_order_bases.append(term_.basis) for i in term.order: if i == 1: continue for term_ in term.terms_by_order[i]: assert hasattr(term_, "bases") for basis in term_.bases: if basis not in first_order_bases: basis.name = self.names.create(basis.name) for order_, subterms in term.terms_by_order.items(): if order_ == 1: continue for subterm in subterms: subterm.coef.name = self.names.create(subterm.coef.name) subterm.name = self.names.create(subterm.name) if not common_scale: for scale in term.scales: if not isinstance(scale, lsl.Var): raise TypeError( f"Expected scale to be a liesel.model.Var, got {type(scale)}" ) _biject_and_replace_star_gibbs_with( scale, self._get_inference(scales_inference) ) return term
def _find_parameter(var: lsl.Var) -> lsl.Var: """ Intended for the following use case: 'var' is a parameter that may be a weak transformation of a strong latent parameter, we want to find this strong latent parameter. Returns the strong latent parameter, if it can be determined unambiguously. """ if var.strong and var.parameter: return var with TemporaryModel(var, to_float32=False, silent=True) as model: params = model.parameters if not params: raise ValueError(f"No parameter found in the graph of {var}.") if len(params) > 1: raise ValueError( f"In the graph of {var}, there are {len(params)} parameters, " "so we cannot return a unique parameter." ) param = list(model.parameters.values())[0] return param def _biject_and_replace_star_gibbs_with( var: lsl.Var, inference: InferenceTypes | None ) -> lsl.Var: """ If var is a ScaleIG, it is the square root of a variance parameter that may have a default Gibbs kernel. This function removes any such Gibbs kernel and then transforms the variance parameter using the default event space bijector and sets the inference to the 'inference' supplied to the function. """ param = _find_parameter(var) if param.inference is not None: if isinstance(param.inference, gs.MCMCSpec): try: is_star_gibbs = param.inference.kernel.__name__ == "StarVarianceGibbs" # type: ignore if not is_star_gibbs: return var except AttributeError: # in this case, we assume that the inference has been set intentionally # so we don't change anything return var else: # in this case, we assume that the inference has been set intentionally # so we don't change anything return var if param.name: trafo_name = "ln(" + param.name + ")" else: trafo_name = None transformed = param.transform( bijector=tfb.Exp(), inference=inference, name=trafo_name ) if trafo_name is None: transformed.name = "" return var def _has_star_gibbs(var: lsl.Var) -> bool: try: param = _find_parameter(var) except ValueError: return False if param.inference is None: # no inference means no StarVarianceGibbs return False inferences = [] if isinstance(param.inference, gs.MCMCSpec): inferences.append(param.inference) elif isinstance(param.inference, Mapping): try: for v in param.inference.values(): if isinstance(v, gs.MCMCSpec): inferences.append(v) except Exception as e: raise TypeError( f"Could not handle type {type(param.inference)}, expected " "liesel.goose.MCMCSpec or dict." ) from e else: raise TypeError( f"Could not handle type {type(param.inference)}, expected " "liesel.goose.MCMCSpec or dict." ) if not inferences: # no gs.MCMCSpecs present, so there cannot be StarVarianceGibbs return False for inference in inferences: try: is_star_gibbs = inference.kernel.__name__ == "StarVarianceGibbs" # type: ignore if is_star_gibbs: return True # if we find any StarVarianceGibbs, return True except Exception: # very liberal about errors here pass # by this point, we did not find any StarVarianceGibbs return False def _format_name(var: lsl.Var, fill: str) -> lsl.Var: with TemporaryModel(var, to_float32=False, silent=True) as model: nodes = dict(model.nodes) vars_ = dict(model.vars) nodes_and_vars = nodes | vars_ for node in nodes_and_vars.values(): node.name = node.name.format(name=fill, x=fill) if "$" in node.name: node.name = node.name.replace("$", "") node.name = "$" + node.name + "$" if not var.name: var.name = fill return var