import numpy as np
from whittaker.smooths import CyclicCRS
x = np.linspace(0, 2 * np.pi, 100)
basis = CyclicCRS(k=10).fit(x)
B = basis.basis_matrix(x)
S = basis.penalty_matrix()
B.shape, S.shape((100, 9), (9, 9))
Cyclic Cubic Regression Spline (periodic natural cubic spline).
Usage
A cyclic (periodic) cubic regression spline is the natural extension of CRS to covariates that wrap around, such as time of day, day of year, or wind direction, where the value and slope at the end of the range must match the value and slope at the start. Equivalent to mgcv’s bs="cc" basis. The spline is periodic over the range of the training data: f(x_min) = f(x_max), f'(x_min) = f'(x_max), and f''(x_min) = f''(x_max). Values outside the training range are mapped into the periodic domain via modular arithmetic, so predictions remain well-defined for any x. Choose CyclicCRS (rather than plain CRS) whenever the covariate is inherently circular; using a non-cyclic basis on such data would otherwise produce an artificial discontinuity at the wrap-around point.
The cyclic constraint absorbs one degree of freedom, so k knots produce k-1 basis functions. The penalty null space is 1-dimensional (constant functions only as linear functions are no longer unpenalized under periodicity).
k: int = 104, which yields at least 3 basis functions (n_basis = k - 1) after the cyclic constraint removes one degree of freedom. As with CRS, larger k gives more local flexibility, with overall smoothness controlled by the penalty and lambda rather than by k alone. The default is 10.
Knots are placed at evenly-spaced quantiles of the training data (falling back to an evenly-spaced grid when quantile ties would produce duplicates), exactly as in CRS, but the second-derivative relationship between the coefficient vector and the knot second derivatives is built on a circular tridiagonal system: _build_cyclic_Q() and _build_cyclic_R() construct the periodic analogues of CRS’s Q and R matrices, wrapping the sub/super-diagonal entries around modulo m = k - 1 (the number of free coefficients once f(x_min) = f(x_max) is imposed). Concretely, the periodic constraint identifies knot t_{k-1} with t_0, so \boldsymbol{\beta} has only m = k - 1 free entries, and second derivatives satisfy \mathbf{R}_c \mathbf{d} = \mathbf{Q}_c^\top \boldsymbol{\beta} with Q_c, R_c both m \times m and circulant-tridiagonal (each row’s neighbors wrap around index m). Unlike CRS, there are no natural boundary conditions — periodicity itself replaces them — so the penalty null space drops from dimension 2 (constant and linear) to dimension 1 (constant only): a periodic function cannot be a nonzero linear function of x, since a nonzero slope is incompatible with f(x_min) = f(x_max).
The penalty is the same quadratic form as in CRS, computed from the circular matrices,
\mathbf{S} = \mathbf{Q}_c \mathbf{R}_c^{-1} \mathbf{Q}_c^\top,
an m \times m (m = k - 1) positive semi-definite matrix of rank m - 1, whose one-dimensional null space is the constant sequence. At evaluation time, basis_matrix() maps any x into the periodic domain [x_min, x_max) via knots[0] + (x - knots[0]) % period before evaluating the same piecewise-cubic construction as CRS, so extrapolation is not linear (as in CRS) but exactly periodic.
| Name | Description |
|---|---|
| knots | Knot locations used by the fitted basis. |
| n_basis | Total number of basis functions. |
Knot locations used by the fitted basis.
knots: NDArray
The k locations, placed at evenly-spaced quantiles of the training data (or an evenly-spaced grid as a fallback), spanning one full period [x_min, x_max]. The last knot is identified with the first under the periodicity constraint, leaving k - 1 free coefficients.
Total number of basis functions.
n_basis: int
Equal to k - 1: the periodicity constraint f(x_min) = f(x_max) identifies the last knot with the first, so only k - 1 of the k knot values are free coefficients.
| Name | Description |
|---|---|
| basis_matrix() |
Evaluate the cyclic CRS basis at x.
|
| fit() |
Fit the cyclic CRS to training data x.
|
| identifiability_constraints() | Return the sum-to-zero constraint row for the intercept. |
| null_space_dimension() | Return the dimension of the penalty null space. |
| penalty_matrix() |
Return the (k - 1) x (k - 1) cyclic penalty matrix S = Q_c R_c^{-1} Q_c'.
|
Evaluate the cyclic CRS basis at x.
Usage
Any x outside the training range is first mapped into the periodic domain [x_min, x_max) via modular arithmetic, so the returned basis values are exactly periodic with period x_max - x_min.
x: NDArray(n,) or (n, 1).
NDArray(n, k - 1).
Fit the cyclic CRS to training data x.
Usage
Places k knots at evenly-spaced quantiles of x (spanning one full period), then pre-computes the circular second-derivative operator and penalty matrix.
x: NDArray(n,) or (n, 1). The period is inferred as x.max() - x.min().
CyclicCRSself for method chaining.
ValueErrorn < k, or if x is not 1-D / (n, 1).
Return the sum-to-zero constraint row for the intercept.
Usage
Evaluates the fitted basis at the free knots (excluding the duplicated wrap-around knot) and averages each column, giving a (1, k - 1) row C such that C @ beta == 0 constrains the smooth to have mean zero over the training data.
NDArray(1, k - 1) constraint matrix.
Return the dimension of the penalty null space.
Usage
Always 1 for a cyclic CRS: only constant functions have zero penalty, since a nonzero linear term would violate the periodicity constraint f(x_min) = f(x_max).
int1.
Return the (k - 1) x (k - 1) cyclic penalty matrix S = Q_c R_c^{-1} Q_c'.
Usage
S is positive semi-definite with rank k - 2. Its one-dimensional null space is spanned by the constant function (linear functions are no longer unpenalized under periodicity).
NDArray(k - 1, k - 1).