Decomposition¶
fdatools.decomposition: functional principal component analysis (FPCA) and
functional canonical correlation analysis (FCCA), both scikit-learn
estimators.
Functional principal components and canonical correlation.
Two estimators replace R fda's pca.fd, varmx.pca.fd and cca.fd.
Both work on the basis coefficients of an :class:~fdatools.core.FData and both
follow the scikit-learn estimator API, so they drop straight into a
:class:~sklearn.pipeline.Pipeline behind :class:~fdatools.smoothing.Smoother.
Functional PCA with a roughness penalty (Ramsay & Silverman §9.4) maximises the
sample variance of the scores :math:\int \xi(t) x_i(t)\,dt while charging the
harmonic :math:\xi for its own roughness. Writing :math:W = \int
\varphi\varphi^{T} for the Gram matrix of the basis, :math:R = \int
(L\varphi)(L\varphi)^{T} for the roughness penalty and :math:V = C C^{T}/N
for the sample covariance of the centred coefficients, the harmonic
coefficients solve the symmetric generalised eigenproblem
.. math::
W V W\,b = \mu\,(W + \lambda R)\,b ,
\qquad b^{T}(W + \lambda R)\,b = 1 .
With :math:\lambda = 0 this collapses to the unpenalised problem
:math:V W b = \mu b, i.e. the eigen-decomposition of the covariance operator.
Functional CCA (chapter 11) is the same construction on the cross-covariance of
two samples: with :math:S_{xy} = W_1 C_x C_y^{T} W_2 / N the canonical weights
solve
.. math::
\begin{pmatrix} 0 & S_{xy} \\ S_{yx} & 0 \end{pmatrix} v
= \rho \begin{pmatrix} S_{xx} + \lambda_1 R_1 & 0 \\
0 & S_{yy} + \lambda_2 R_2 \end{pmatrix} v .
Every Gram matrix is exact (Gauss-Legendre on the basis breaks), where R's
pca.fd uses a Romberg approximation good to four or five digits; see
tests/parity/test_decomposition.py for the measured consequences.
Examples:
>>> import numpy as np
>>> from fdatools import BSpline, FData
>>> from fdatools.decomposition import FPCA
>>> rng = np.random.default_rng(0)
>>> basis = BSpline(domain=(0.0, 1.0), n_basis=8)
>>> fd = FData(rng.standard_normal((8, 30)), basis)
>>> pca = FPCA(n=3).fit(fd)
>>> pca.scores.shape
(30, 3)
>>> bool(np.all(np.diff(pca.values) <= 1e-12))
True
FPCA ¶
FPCA(
n: SupportsIndex = 2,
*,
lam: float | str = 0.0,
penalty: int | LDO = 2,
center: bool = True,
basis: Basis | None = None,
)
Bases: TransformerMixin, BaseEstimator
Functional principal component analysis, optionally with a roughness penalty.
Replaces R's pca.fd and varmx.pca.fd.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int or SupportsIndex
|
Number of harmonics to keep. Default |
2
|
lam
|
float or str
|
Roughness penalty on the harmonics. |
0.0
|
penalty
|
int or LDO
|
Roughness operator |
2
|
center
|
bool
|
Subtract the sample mean function before decomposing. Default |
True
|
basis
|
Basis
|
Basis to attach to a plain coefficient matrix. Ignored when |
None
|
Attributes:
| Name | Type | Description |
|---|---|---|
harmonics |
FData
|
The |
values |
array
|
The full eigenvalue spectrum, descending -- one entry per basis
function (times the number of variables), as R reports it. After
:meth: |
scores |
array
|
Score matrix of shape |
varprop |
array
|
Proportion of total variance explained by each retained harmonic. |
mean_fd |
FData
|
The sample mean function (a zero function when |
rotation |
array or None
|
The rotation applied by :meth: |
Notes
Eigenvector signs are mathematically arbitrary. fdatools makes the
coefficient sum of every harmonic positive -- the rule R's pca.fd
follows -- so repeated fits give identical output and match R's signs.
Examples:
>>> import numpy as np
>>> from fdatools import BSpline, FData
>>> from fdatools.decomposition import FPCA
>>> rng = np.random.default_rng(1)
>>> fd = FData(rng.standard_normal((7, 40)), BSpline(domain=(0.0, 1.0), n_basis=7))
>>> pca = FPCA(n=2).fit(fd)
>>> pca.harmonics.n_curves
2
>>> bool(pca.varprop.sum() <= 1.0 + 1e-12)
True
fit ¶
fit(X: Any, y: Any = None) -> FPCA
Decompose X into functional principal components.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
FData or array of shape (n_samples, n_basis)
|
Curves, or their basis coefficients one curve per row. |
required |
y
|
ignored
|
Present for API compatibility. |
None
|
Returns:
| Type | Description |
|---|---|
FPCA
|
The fitted estimator. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import numpy as np
>>> from fdatools import Fourier, FData
>>> from fdatools.decomposition import FPCA
>>> rng = np.random.default_rng(3)
>>> fd = FData(rng.standard_normal((5, 20)), Fourier(domain=(0.0, 1.0), n_basis=5))
>>> FPCA(n=2).fit(fd).harmonics.n_curves
2
transform ¶
transform(X: Any) -> Array
Return the principal component scores of X.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
FData or array of shape (n_samples, n_basis)
|
Curves to project onto the fitted harmonics. |
required |
Returns:
| Type | Description |
|---|---|
array
|
Scores of shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import numpy as np
>>> from fdatools import BSpline, FData
>>> from fdatools.decomposition import FPCA
>>> rng = np.random.default_rng(4)
>>> fd = FData(rng.standard_normal((6, 25)), BSpline(domain=(0.0, 1.0), n_basis=6))
>>> pca = FPCA(n=2).fit(fd)
>>> bool(np.allclose(pca.transform(fd), pca.scores))
True
inverse_transform ¶
inverse_transform(X: Any) -> FData
Rebuild curves from scores.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
array of shape (n_samples, n)
|
Principal component scores. |
required |
Returns:
| Type | Description |
|---|---|
FData
|
|
Examples:
>>> import numpy as np
>>> from fdatools import BSpline, FData
>>> from fdatools.decomposition import FPCA
>>> rng = np.random.default_rng(5)
>>> fd = FData(rng.standard_normal((4, 30)), BSpline(domain=(0.0, 1.0), n_basis=4))
>>> pca = FPCA(n=4).fit(fd)
>>> bool(np.allclose(pca.inverse_transform(pca.scores).coefs, fd.coefs))
True
rotate ¶
rotate(method: str = 'varimax') -> FPCA
Return a copy of this fit with rotated harmonics.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
method
|
str
|
Only |
'varimax'
|
Returns:
| Type | Description |
|---|---|
FPCA
|
A new fitted estimator whose |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> import numpy as np
>>> from fdatools import BSpline, FData
>>> from fdatools.decomposition import FPCA
>>> rng = np.random.default_rng(6)
>>> fd = FData(rng.standard_normal((8, 30)), BSpline(domain=(0.0, 1.0), n_basis=8))
>>> rotated = FPCA(n=3).fit(fd).rotate("varimax")
>>> bool(np.allclose(rotated.rotation.T @ rotated.rotation, np.eye(3)))
True
plot ¶
plot(ax: Any = None, *, n_points: int = 201) -> list[Any]
Plot each harmonic as a perturbation of the mean.
Every panel shows the mean function and the mean plus and minus the
harmonic scaled by the standard deviation of its scores -- the display
R's plot.pca.fd produces.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ax
|
matplotlib.axes.Axes or sequence of Axes
|
Axes to draw on, one per harmonic. |
None
|
n_points
|
int
|
Number of evaluation points. Default |
201
|
Returns:
| Type | Description |
|---|---|
list of matplotlib.axes.Axes
|
The axes drawn on. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If fewer axes than harmonics are supplied. |
Examples:
>>> import matplotlib
>>> matplotlib.use("Agg")
>>> import numpy as np
>>> from fdatools import BSpline, FData
>>> from fdatools.decomposition import FPCA
>>> rng = np.random.default_rng(7)
>>> fd = FData(rng.standard_normal((6, 20)), BSpline(domain=(0.0, 1.0), n_basis=6))
>>> len(FPCA(n=2).fit(fd).plot())
2
FCCA ¶
FCCA(
n: SupportsIndex = 2,
*,
lam1: float = 0.0,
lam2: float = 0.0,
penalty: int | LDO = 2,
center: bool = True,
)
Bases: BaseEstimator
Functional canonical correlation analysis.
Replaces R's cca.fd.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
n
|
int or SupportsIndex
|
Number of canonical variate pairs to keep. Default |
2
|
lam1
|
float
|
Roughness penalties on the first and second set of canonical weights. Without a penalty the problem is degenerate whenever there are fewer curves than basis functions (every correlation is one). |
0.0
|
lam2
|
float
|
Roughness penalties on the first and second set of canonical weights. Without a penalty the problem is degenerate whenever there are fewer curves than basis functions (every correlation is one). |
0.0
|
penalty
|
int or LDO
|
Roughness operator |
2
|
center
|
bool
|
Subtract each sample mean before decomposing. Default |
True
|
Attributes:
| Name | Type | Description |
|---|---|---|
weights1, weights2 |
FData
|
Canonical weight functions, normalised to unit :math: |
correlations |
array
|
The full canonical correlation spectrum, descending, as R reports it. |
scores1, scores2 |
array
|
Canonical variate scores of shape |
Examples:
>>> import numpy as np
>>> from fdatools import BSpline, FData
>>> from fdatools.decomposition import FCCA
>>> rng = np.random.default_rng(2)
>>> basis = BSpline(domain=(0.0, 1.0), n_basis=6)
>>> a = rng.standard_normal((6, 25))
>>> fd1 = FData(a, basis)
>>> fd2 = FData(a + 0.1 * rng.standard_normal((6, 25)), basis)
>>> cca = FCCA(n=2, lam1=1e-6, lam2=1e-6).fit(fd1, fd2)
>>> bool(cca.correlations[0] > 0.9)
True
fit ¶
fit(X: FData, y: FData) -> FCCA
Find the canonical weight functions of two samples of curves.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
FData
|
Two samples observed on the same curves, in that order. |
required |
y
|
FData
|
Two samples observed on the same curves, in that order. |
required |
Returns:
| Type | Description |
|---|---|
FCCA
|
The fitted estimator. |
Raises:
| Type | Description |
|---|---|
TypeError
|
If either argument is not an :class: |
ValueError
|
If the samples hold different numbers of curves, are multivariate, or a parameter is out of range. |
Examples:
>>> import numpy as np
>>> from fdatools import Fourier, FData
>>> from fdatools.decomposition import FCCA
>>> rng = np.random.default_rng(8)
>>> basis = Fourier(domain=(0.0, 1.0), n_basis=5)
>>> x = rng.standard_normal((5, 40))
>>> cca = FCCA(n=1, lam1=1e-3, lam2=1e-3).fit(FData(x, basis), FData(-x, basis))
>>> cca.scores1.shape
(40, 1)
transform ¶
transform(X: FData, y: FData) -> tuple[Array, Array]
Return the canonical variate scores of two new samples.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
X
|
FData
|
Curves to project, in the order given to :meth: |
required |
y
|
FData
|
Curves to project, in the order given to :meth: |
required |
Returns:
| Type | Description |
|---|---|
tuple of array
|
Scores of shape |
Raises:
| Type | Description |
|---|---|
ValueError
|
If a sample's basis size differs from the one seen during |
Examples:
>>> import numpy as np
>>> from fdatools import Fourier, FData
>>> from fdatools.decomposition import FCCA
>>> rng = np.random.default_rng(9)
>>> basis = Fourier(domain=(0.0, 1.0), n_basis=5)
>>> x = FData(rng.standard_normal((5, 30)), basis)
>>> y = FData(rng.standard_normal((5, 30)), basis)
>>> cca = FCCA(n=2, lam1=1e-2, lam2=1e-2).fit(x, y)
>>> bool(np.allclose(cca.transform(x, y)[0], cca.scores1))
True