# AdaptiveTPRS


Adaptive Thin Plate Regression Spline.


Usage

``` python
AdaptiveTPRS(
    k=10,
    m=2,
    n_penalties=-1,
)
```


An ordinary [TPRS](TPRS.md#whittaker.TPRS) uses a single smoothing parameter `lambda` to control wiggliness uniformly across the whole covariate domain, which is a poor fit when the true function is smooth in some regions and rapidly varying in others (e.g. a signal with a sharp local feature embedded in an otherwise flat trend). [AdaptiveTPRS](AdaptiveTPRS.md#whittaker.AdaptiveTPRS) addresses this by decomposing the ordinary TPRS penalty matrix into its eigenvectors and turning each eigenvector (or a contiguous block of them) into its own separate penalty term with its own smoothing parameter, following the "adaptive smoothing" construction of Wood (2000, 2017, sec. 5.4.2). Because each eigenvector of the original penalty corresponds to a distinct spatial pattern of wiggliness, giving each its own `lambda` lets the fitted smoothing-parameter-selection procedure impose more smoothing where the data support a flat fit and less where they support local structure. Choose [AdaptiveTPRS](AdaptiveTPRS.md#whittaker.AdaptiveTPRS) over plain [TPRS](TPRS.md#whittaker.TPRS) when there is a-priori reason to expect the required smoothness to vary spatially and the extra smoothing parameters (and their computational cost during fitting) can be afforded; otherwise, use [TPRS](TPRS.md#whittaker.TPRS).

The number of adaptive penalty components is controlled by `n_penalties`. With `n_penalties=k-M` (the default), every eigenvector gets its own λ. Smaller values group eigenvectors into blocks for computational efficiency.


## Parameters


`k: int = ``10`  
Total number of basis functions, exactly as in [TPRS](TPRS.md#whittaker.TPRS) (the default is `10`). Since [AdaptiveTPRS](AdaptiveTPRS.md#whittaker.AdaptiveTPRS) uses the same underlying basis functions as [TPRS](TPRS.md#whittaker.TPRS) and only changes how the penalty is structured, the same guidance for choosing `k` applies.

`m: int = ``2`  
Spline order, exactly as in [TPRS](TPRS.md#whittaker.TPRS) (the default is `2`). Controls the order of derivative that the *unweighted* thin plate penalty targets before it is decomposed into adaptive components.

`n_penalties: int = ``-1`  
Number of adaptive penalty components to construct from the eigendecomposition of the base TPRS penalty. If `-1` (the default), one penalty is created per retained eigenvector (`k - M` penalties total, the maximum granularity and maximum flexibility for spatially varying smoothness). If a smaller positive integer is given, the eigenvectors (already sorted by decreasing eigenvalue during [TPRS.fit()](TPRS.md#whittaker.TPRS.fit)) are grouped into that many contiguous blocks of roughly equal size, each sharing one smoothing parameter; this reduces the number of smoothing parameters that must be estimated, trading some spatial adaptivity for faster, more stable fitting.


## Notes

After `fit()` (inherited unchanged from [TPRS](TPRS.md#whittaker.TPRS)), the basis matrix and its first `M` null-space columns are identical to plain [TPRS](TPRS.md#whittaker.TPRS); only the penalty differs. Let `D_r = \operatorname{diag}(d_1, \ldots, d_r)` be the diagonal matrix of the `r = k - M` eigenvalues retained by [TPRS.fit()](TPRS.md#whittaker.TPRS.fit) (in descending order), so that the ordinary TPRS penalty restricted to the range space is `D_r` itself. [AdaptiveTPRS](AdaptiveTPRS.md#whittaker.AdaptiveTPRS) partitions the index set `\{1, \ldots, r\}` into `n_penalties` contiguous blocks `B_1, \ldots, B_{n_penalties}` of (approximately) equal size and returns one `k x k` penalty matrix per block,

 \mathbf{S}\_b = \operatorname{diag}\bigl(0, \ldots, 0,\\ \max(d_i, \epsilon) \cdot \[i \in B_b\], \ldots\bigr), \qquad b = 1, \ldots, n\_\text{penalties}, 

where each `S_b` is zero everywhere except in the diagonal entries corresponding to eigenvectors in its block, and `\epsilon = 10^{-10}` guards against the (numerically possible) tiny negative eigenvalues that arise from the projection step in [TPRS.fit()](TPRS.md#whittaker.TPRS.fit). The total penalty used during model fitting is the weighted sum `\sum_b \lambda_b \, \boldsymbol{\beta}^\top \mathbf{S}_b \boldsymbol{\beta}`, with one smoothing parameter `\lambda_b` per block selected by the outer GCV/REML procedure -- this is what allows the fitted smoothness to vary spatially: blocks corresponding to slowly varying eigenvectors can receive small `\lambda_b` (little shrinkage, high local flexibility) while blocks corresponding to rapidly varying eigenvectors receive large `\lambda_b` (heavy shrinkage), or vice versa, according to what the data support in different parts of the covariate space. Since the block matrices `S_b` are mutually orthogonal (each touches disjoint diagonal entries) and together cover exactly the `k - M` range-space coefficients, `null_space_dimension()` is unchanged from [TPRS](TPRS.md#whittaker.TPRS) and equals `M`. Because more penalties mean more smoothing parameters to estimate, `n_penalties` close to `r` can make outer optimization slower and, on small or noisy datasets, less numerically stable; the default of `n_penalties=-1` should be reduced if fitting becomes unreliable.


## Examples


``` python
import numpy as np
from whittaker.smooths.adaptive import AdaptiveTPRS

rng = np.random.default_rng(0)
x = rng.uniform(0, 1, 100)

basis = AdaptiveTPRS(k=10, n_penalties=3).fit(x)
B = basis.basis_matrix(x)
penalties = basis.penalty_matrices()
B.shape, len(penalties), penalties[0].shape
```


    ((100, 10), 3, (10, 10))


## Methods

| Name | Description |
|----|----|
| [null_space_dimension()](#null_space_dimension) | Return `M`, the dimension of the (unchanged) TPRS polynomial null space. |
| [penalty_matrices()](#penalty_matrices) | Return the decomposed penalty matrices, one per eigenvector group. |

------------------------------------------------------------------------


### null_space_dimension()


Return `M`, the dimension of the (unchanged) TPRS polynomial null space.


Usage

``` python
null_space_dimension()
```


Splitting the range-space penalty into blocks does not add or remove any unpenalized basis functions, so this is identical to the value returned by the parent [TPRS](TPRS.md#whittaker.TPRS) class.


#### Returns


`int`  
The polynomial null-space dimension `M = C(m - 1 + d, d)`.


------------------------------------------------------------------------


### penalty_matrices()


Return the decomposed penalty matrices, one per eigenvector group.


Usage

``` python
penalty_matrices()
```


Each returned matrix is `k x k` and zero everywhere except on the diagonal entries belonging to its group of eigenvectors of the underlying TPRS penalty, so that summing all returned matrices reconstructs the ordinary (non-adaptive) TPRS penalty restricted to the range space.


#### Returns


`list[NDArray]`  
List of `n_penalties` matrices (or `k - M` matrices if `n_penalties=-1`), each of shape `(k, k)`. Intended to be combined with one smoothing parameter per matrix during model fitting.
