Tensor product of marginal smooth bases (te()-style interaction smooth).
TensorProductBasis(marginals)
A tensor product smooth builds a multivariate smooth function of two or more covariates out of one-dimensional (or otherwise lower-dimensional) marginal smooths, one per covariate, by taking the row-wise outer product of their basis matrices. This is the standard way to represent an interaction between covariates that are measured on very different scales or units (e.g. a spatial coordinate combined with time, or a covariate in meters combined with one in years) — unlike an isotropic basis such as TPRS, which assumes all covariates share a common notion of distance, a tensor product smooth applies a separate marginal penalty (and, in principle, a separate smoothing parameter) to each covariate direction, so that the anisotropic scaling of the covariates does not distort the fitted surface. Choose TensorProductBasis over TPRS whenever the covariates involved are not naturally on comparable scales, and use TensorInteractionBasis instead when a decomposition into separate main-effect and pure-interaction terms (an ANOVA-style model) is wanted.
Parameters
marginals: list[SmoothBasis]
-
List of (typically unfitted) marginal basis objects, one per covariate dimension to be combined, e.g.
[TPRS(k=10), TPRS(k=8)] for a bivariate smooth. Each marginal is fit independently to its own column of x inside fit(). At least 2 marginals are required; for a single covariate, use the marginal basis directly instead of wrapping it in a tensor product.
Notes
Given d marginal bases with basis matrices B_1, \ldots, B_d (each B_j of shape (n, k_j)), the tensor product basis matrix is the row-wise Kronecker product
\mathbf{B}[i, :] = \mathbf{B}_1[i, :] \otimes \mathbf{B}_2[i, :] \otimes \cdots \otimes
\mathbf{B}_d[i, :], \qquad i = 1, \ldots, n,
which has k = k_1 k_2 \cdots k_d columns in total — every combination of one marginal basis function from each dimension. This basis represents all smooth functions expressible as sums of products of the marginal bases, including both pure main effects and their interaction, so unlike TensorInteractionBasis, no separate main-effect terms need to be added to the model for identifiability of low-order structure (though it is common practice in mgcv-style formulas to do so anyway for a cleaner ANOVA decomposition). For each marginal direction j with own penalty S_j, the tensor product carries one whole-basis penalty per marginal direction,
\mathbf{S}_j^{\text{tensor}} = \mathbf{I}_{k_1} \otimes \cdots \otimes \mathbf{I}_{k_{j-1}}
\otimes \mathbf{S}_j \otimes \mathbf{I}_{k_{j+1}} \otimes \cdots \otimes \mathbf{I}_{k_d},
returned in order by penalty_matrices(); penalty_matrix() sums these into a single matrix only for compatibility with the base SmoothBasis interface — for a proper anisotropic fit (one smoothing parameter per marginal direction), use penalty_matrices() directly rather than penalty_matrix(). The null-space dimension of the combined penalty is the product of the marginal null-space dimensions, M = M_1 \cdot M_2 \cdots M_d (the multivariate polynomials that are simultaneously in the null space of every marginal penalty). Because k grows multiplicatively with the number of marginals and their individual sizes, tensor products become expensive quickly in more than two or three dimensions; for higher-dimensional smooths of covariates on comparable scales, an isotropic basis such as TPRS is usually preferable.
Examples
import numpy as np
from whittaker.smooths.tensor import TensorProductBasis
from whittaker.smooths.tprs import TPRS
rng = np.random.default_rng(0)
x = rng.uniform(0, 1, (100, 2))
basis = TensorProductBasis([TPRS(k=6), TPRS(k=5)]).fit(x)
B = basis.basis_matrix(x)
penalties = basis.penalty_matrices()
B.shape, len(penalties)
Attributes
|
Name
|
Description
|
|
marginals
|
The list of marginal basis objects, fitted after fit() is called.
|
|
n_basis
|
Total number of basis functions.
|
marginals
The list of marginal basis objects, fitted after fit() is called.
marginals: list[SmoothBasis]
Each marginal is fit independently to its own covariate column; their basis matrices are combined via the row-wise Kronecker product to form basis_matrix().
n_basis
Total number of basis functions.
Equal to the product k_1 * k_2 * ... * k_d of the marginal basis dimensions, since every combination of one marginal basis function from each dimension contributes one column of the tensor product basis matrix.
Methods
basis_matrix()
Evaluate the tensor product basis at x.
Builds each marginal’s basis matrix independently and combines them with the row-wise Kronecker (tensor) product, so that column j1 * k_2 * ... * k_d + j2 * k_3 * ... + ... of the result is the product of the corresponding marginal basis function values.
Parameters
x: NDArray
-
Covariate values, shape
(n, d) where d matches the number of marginals.
Returns
NDArray
-
Design matrix of shape
(n, k) where k = self.n_basis is the product of the marginal basis dimensions.
fit()
Fit each marginal basis to its corresponding column of x.
Parameters
x: NDArray
-
Training covariates, shape
(n, d) where d is the number of marginals. Column j is passed to marginals[j].fit().
Returns
TensorProductBasis
-
Returns
self for method chaining.
Raises
ValueError
-
If the number of columns in
x does not match the number of marginal bases.
identifiability_constraints()
Return a sum-to-zero constraint on the tensor product basis, if derivable.
identifiability_constraints()
Attempts to recover each marginal’s training covariate values (via a _x_train or _knots attribute) to build a constraint that forces the tensor surface to have mean zero over the training data. If no marginal exposes its training data in a recognized form, no constraint can be derived and None is returned.
Returns
NDArray or None
-
A
(1, k) matrix of the mean basis-function values over the training grid, or None if the training covariates could not be recovered from the marginals.
null_space_dimension()
Return the product of the marginal null-space dimensions.
The multivariate null space of a tensor product penalty is spanned by the tensor products of each marginal’s own null-space basis functions (e.g. for two thin plate marginals with linear null spaces, the constant, and each marginal’s linear term, giving a bilinear null space), so its dimension multiplies rather than adds across marginals.
Returns
int
-
M = M_1 * M_2 * ... * M_d.
penalty_matrices()
Return one penalty matrix per marginal direction.
For d marginals with basis dimensions k_1, ..., k_d and marginal penalties S_1, ..., S_d, penalty j is the Kronecker product I_{k_1} ⊗ ... ⊗ S_j ⊗ ... ⊗ I_{k_d} (the marginal penalty in position j, identity elsewhere), so that penalizing with matrix j alone penalizes exactly the roughness of the tensor surface along covariate direction j, holding the other directions fixed.
Returns
list[NDArray]
-
List of d matrices, each of shape
(k, k) where k is the total basis dimension. Intended to be combined with one smoothing parameter per marginal direction during model fitting.
penalty_matrix()
Sum of all marginal penalty matrices, for single-penalty compatibility.
This collapses the per-direction penalties returned by penalty_matrices() into one matrix, which is convenient when a caller only supports a single penalty but discards the ability to give each marginal direction its own smoothing parameter. Prefer penalty_matrices() for a proper anisotropic fit.
Returns
NDArray
-
The
(k, k) sum of all per-marginal penalty matrices.