Markov random field basis for areal spatial data.
MRFBasis(
k=-1,
neighborhood=None,
)
A Markov random field (MRF) smooth represents spatial structure over discrete areal units — counties, districts, postcodes, grid cells on a lattice — where the covariate is a categorical label rather than a continuous coordinate, and the only spatial information available is which units are adjacent to which. It is equivalent to mgcv’s bs="mrf" basis. Each unique region gets its own basis function (an indicator column), and smoothness across the map is enforced directly through the neighborhood graph rather than through any distance metric: the penalty discourages the fitted values of neighboring regions from differing, so choose this basis over TPRS-style continuous smooths whenever the domain is a set of discrete areas linked by an adjacency structure (e.g. shared borders) instead of by Euclidean coordinates.
Parameters
k: int = -1
-
Maximum number of regions to retain. If -1 (the default), all observed levels of the grouping variable are kept as basis functions. If a positive integer smaller than the number of observed levels, only the first k (in sorted level order) are used; this is rarely what a user wants for MRF smooths (unlike continuous bases, reducing k does not give a lower-rank approximation of the same structure — it silently drops regions), so in most workflows the default of -1 should be left alone.
neighborhood: dict | NDArray | None = None
-
The neighborhood structure that defines which regions are considered adjacent. Either a
dict mapping region labels to lists of neighbor labels (only pairs need to be listed once; the adjacency is symmetrized automatically), or a square, symmetric adjacency matrix (ndarray) whose row/column order matches the sorted unique levels of the fitted grouping variable. This argument is required — there is no sensible default neighborhood structure.
Notes
Let there be k unique regions after fit(). The basis matrix B is the n x k matrix of region indicators, B[i, j] = 1 if observation i belongs to region j and 0 otherwise — identical in structure to RandomEffectBasis. What distinguishes an MRF smooth is its penalty. Writing A for the symmetric adjacency matrix (A[i, j] = 1 if regions i and j are neighbors) and D = \operatorname{diag}(A \mathbf{1}) for the diagonal matrix of neighbor counts, the penalty matrix is the graph Laplacian
\mathbf{L} = \mathbf{D} - \mathbf{A},
so that the roughness penalty takes the form
\boldsymbol{\beta}^\top \mathbf{L} \boldsymbol{\beta}
= \sum_{(i,j) \, \in \, \text{neighbors}} (\beta_i - \beta_j)^2 .
This penalizes exactly the pairwise differences between the fitted level for each region and the fitted levels of its geographic neighbors, pulling adjacent regions toward a common value as lambda grows, while leaving regions that are far apart on the map free to differ. The graph Laplacian of a connected neighborhood graph has exactly one zero eigenvalue, with eigenvector proportional to the all-ones vector; null_space_dimension() therefore returns 1 for a fully connected graph (the penalty cannot shrink a common overall level, only differences between neighbors), but can be larger if the neighborhood graph has multiple disconnected components, since each component then has its own unpenalized constant. Because the raw indicator basis shares an unpenalized constant with the model intercept, a sum-to-zero constraint (returned by identifiability_constraints()) is needed for identifiability when fitting alongside an intercept term. fit() raises if fewer than two regions are present, since a spatial smooth is meaningless with only one area, and raises if no neighborhood is supplied.
Examples
import numpy as np
from whittaker.smooths.mrf import MRFBasis
rng = np.random.default_rng(0)
regions = np.array(["A", "B", "C", "D"])
x = rng.choice(regions, size=40)
neighborhood = {
"A": ["B"],
"B": ["A", "C"],
"C": ["B", "D"],
"D": ["C"],
}
basis = MRFBasis(neighborhood=neighborhood).fit(x)
B = basis.basis_matrix(x)
S = basis.penalty_matrix()
B.shape, S.shape
Attributes
|
Name
|
Description
|
|
k
|
Requested or actual number of region levels.
|
|
levels
|
Sorted array of unique region labels retained during fit().
|
|
n_basis
|
Number of basis functions, i.e. the number of retained region levels.
|
k
Requested or actual number of region levels.
Before fit() is called, returns the k value passed at construction (the requested cap on the number of levels, or -1 for “all levels”). After fit(), returns the actual number of region levels retained.
levels
Sorted array of unique region labels retained during fit().
n_basis
Number of basis functions, i.e. the number of retained region levels.
Raises
RuntimeError
-
If accessed before
fit() has been called.
Methods
basis_matrix()
Evaluate the MRF indicator basis at x.
Parameters
x: NDArray
-
Grouping variable values. Shape
(n,). Values not seen during fit() produce an all-zero row (no region indicator is set).
Returns
NDArray
-
Design matrix of shape
(n, k) where k is the number of region levels retained during fit(). Each row has at most one entry equal to 1.0.
fit()
Fit the MRF basis to training data.
Determines the set of unique region levels present in x, builds the symmetric adjacency matrix from the supplied neighborhood structure, and computes the graph Laplacian penalty.
Parameters
x: NDArray
-
Training grouping variable (region labels). Shape
(n,), of any hashable dtype (strings, integers, etc.).
Returns
MRFBasis
-
Returns
self for method chaining.
Raises
ValueError
-
If fewer than 2 unique levels are present in x, if neighborhood was not supplied, if an adjacency matrix is provided with the wrong shape, or if k is requested but is less than 2.
TypeError
-
If
neighborhood is neither a dict nor an ndarray.
identifiability_constraints()
Return the sum-to-zero constraint row for the intercept.
identifiability_constraints()
Returns
NDArray
-
A
(1, k) matrix of equal weights 1 / k whose product with the coefficient vector forces the mean fitted region effect to be zero, resolving the confound between the MRF smooth’s unpenalized constant and the model intercept.
null_space_dimension()
Return the number of zero eigenvalues of the graph Laplacian.
This equals the number of connected components of the neighborhood graph: 1 for a fully connected map, and more than 1 if some regions have no path of neighbors linking them to the rest, since each disconnected component then carries its own unpenalized constant.
Returns
int
-
Dimension of the penalty null space.
penalty_matrix()
Return the (k, k) graph Laplacian penalty matrix L = D - A.
Returns
NDArray
-
Symmetric positive semi-definite matrix of shape
(k, k), where k is the number of region levels.