Density and intensity¶
fdatools.density: penalised log-density (fit_density) and log-intensity
(fit_intensity) estimation with a basis expansion for the log function,
fitted by damped Newton with exact integrals.
Penalised density and intensity estimation.
Two estimators replace R fda's density.fd (dropped from fda 6.3.0) and
intensity.fd. Both write a positive function as the exponential of a basis
expansion W(x) = Σ_k c_k φ_k(x) and maximise a penalised log-likelihood.
:func:fit_density estimates a probability density
p(x) = C exp W(x) from a sample x_1 .. x_n by minimising
.. math::
F(c) = -\sum_{i=1}^{n} W(x_i) + n \log \int e^{W(u)}\,du
+ \lambda \int (LW)^2 ,
with C = 1 / ∫ e^W the normalising constant (Ramsay, Hooker & Graves
2009, § 5.4; Silverman 1986 for penalised log-density estimation).
:func:fit_intensity estimates the intensity μ(t) = exp W(t) of a
non-homogeneous Poisson process from its event times by minimising the
penalised negative log-likelihood of the process,
.. math::
F(c) = -\sum_{i=1}^{n} W(x_i) + \int e^{W(u)}\,du + \lambda \int (LW)^2 .
The penalty is λ cᵀRc with R the roughness matrix of the linear
differential operator L (:meth:fdatools.Basis.penalty), the same scaling as
R. Both criteria are convex in c. They are minimised by Newton's method
with the exact Hessian and a backtracking (Armijo) line search; the integrals
are computed by composite Gauss-Legendre quadrature, 12 nodes on each of the
basis's refined quadrature panels, which is exact to rounding for the
exponential of a polynomial piece of the size met in practice.
Convergence: the iteration stops once a full Newton step, which is then
taken, moves no coefficient by more than tol * (1 + max|c|) (default
tol = 1e-10); Newton's quadratic convergence leaves the result about
tol² from the optimum. If rounding hides every further decrease of F
first, the fit counts as converged when the last proposed step was within
sqrt(tol) * (1 + max|c|).
When the basis reproduces the constant function and the penalty does not
charge it (every derivative operator Dᵐ with m >= 1), the density
criterion does not change under W -> W + s. The fit then returns the
member of that family with ∫ W = 0 over the domain; the density itself,
C exp W, is unique.
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.density import fit_density
>>> rng = np.random.default_rng(0)
>>> x = rng.normal(size=300)
>>> basis = fdt.BSpline(domain=(-5.0, 5.0), n_basis=11)
>>> result = fit_density(x, basis=basis, lam=1e-2)
>>> grid = np.linspace(-5.0, 5.0, 2001)
>>> mass = float(np.sum(result(grid)) * (grid[1] - grid[0]))
>>> round(mass, 3)
1.0
DensityResult
dataclass
¶
DensityResult(
fd: FData,
normaliser: float,
lam: float,
penalty_matrix: Array,
criterion: float,
log_likelihood: float,
gradient_norm: float,
n_iter: int,
converged: bool,
)
Outcome of :func:fit_density.
The estimated density is p(x) = normaliser * exp(W(x)).
Attributes:
| Name | Type | Description |
|---|---|---|
fd |
FData
|
The log-density function |
normaliser |
float
|
|
lam |
float
|
The smoothing parameter. |
penalty_matrix |
array
|
The roughness matrix |
criterion |
float
|
The minimised penalised negative log-likelihood |
log_likelihood |
float
|
|
gradient_norm |
float
|
Euclidean norm of the gradient of |
n_iter |
int
|
Number of Newton iterations taken. |
converged |
bool
|
Whether the step tolerance was met within |
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.density import fit_density
>>> x = np.array([0.1, 0.2, 0.25, 0.4, 0.45, 0.5, 0.55, 0.7, 0.8])
>>> res = fit_density(x, basis=fdt.BSpline(domain=(0.0, 1.0), n_basis=6), lam=1e-3)
>>> res.converged
True
>>> bool(np.all(res(np.array([0.0, 0.5, 1.0])) > 0))
True
__call__ ¶
__call__(t: Any) -> Array
Evaluate the density p(t) = C exp W(t).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
array_like
|
One-dimensional evaluation points inside the basis domain. A
:class: |
required |
Returns:
| Type | Description |
|---|---|
array
|
Density values, shape |
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.density import fit_density
>>> x = np.array([0.2, 0.3, 0.5, 0.6, 0.65, 0.9])
>>> res = fit_density(x, basis=fdt.BSpline(domain=(0.0, 1.0), n_basis=5), lam=1.0)
>>> res(np.array([0.5])).shape
(1,)
log_density ¶
log_density(t: Any) -> Array
Evaluate the log-density log p(t) = W(t) + log C.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
array_like
|
One-dimensional evaluation points inside the basis domain. |
required |
Returns:
| Type | Description |
|---|---|
array
|
Log-density values, shape |
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.density import fit_density
>>> x = np.array([0.2, 0.3, 0.5, 0.6, 0.65, 0.9])
>>> res = fit_density(x, basis=fdt.BSpline(domain=(0.0, 1.0), n_basis=5), lam=1.0)
>>> t = np.array([0.25, 0.75])
>>> bool(np.allclose(np.exp(res.log_density(t)), res(t)))
True
IntensityResult
dataclass
¶
IntensityResult(
fd: FData,
lam: float,
penalty_matrix: Array,
criterion: float,
log_likelihood: float,
expected_count: float,
gradient_norm: float,
n_iter: int,
converged: bool,
)
Outcome of :func:fit_intensity.
The estimated intensity is μ(t) = exp(W(t)).
Attributes:
| Name | Type | Description |
|---|---|---|
fd |
FData
|
The log-intensity function |
lam |
float
|
The smoothing parameter. |
penalty_matrix |
array
|
The roughness matrix |
criterion |
float
|
The minimised penalised negative log-likelihood |
log_likelihood |
float
|
The Poisson-process log-likelihood |
expected_count |
float
|
|
gradient_norm |
float
|
Euclidean norm of the gradient of |
n_iter |
int
|
Number of Newton iterations taken. |
converged |
bool
|
Whether the step tolerance was met within |
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.density import fit_intensity
>>> events = np.cumsum(np.full(40, 0.25))
>>> res = fit_intensity(events, basis=fdt.BSpline(domain=(0.0, 10.0), n_basis=5), lam=10.0)
>>> round(res.expected_count, 6)
40.0
__call__ ¶
__call__(t: Any) -> Array
Evaluate the intensity μ(t) = exp W(t).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
t
|
array_like
|
One-dimensional evaluation points inside the basis domain. A
:class: |
required |
Returns:
| Type | Description |
|---|---|
array
|
Intensity values (events per unit time), shape |
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.density import fit_intensity
>>> events = np.linspace(0.5, 9.5, 19)
>>> basis = fdt.BSpline(domain=(0.0, 10.0), n_basis=4)
>>> res = fit_intensity(events, basis=basis, penalty=1, lam=1e6)
>>> np.round(res(np.array([2.0, 8.0])), 3).tolist()
[1.9, 1.9]
fit_density ¶
fit_density(
x: Any,
*,
basis: Basis | None = None,
domain: tuple[float, float] | None = None,
lam: float = 0.0,
penalty: int | LDO = 2,
start: Any = None,
tol: float = DEFAULT_TOL,
max_iter: int = DEFAULT_MAX_ITER,
) -> DensityResult
Estimate a probability density by penalised maximum likelihood.
Replaces R fda's density.fd. The density is
p(x) = C exp W(x) with W a basis expansion; W minimises
-Σ W(x_i) + n log ∫ exp W + lam * ∫ (L W)².
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
One-dimensional sample (a :class: |
required |
basis
|
Basis
|
Basis for |
None
|
domain
|
tuple of float
|
Support used for the default basis; default |
None
|
lam
|
float
|
Smoothing parameter |
0.0
|
penalty
|
int or LDO
|
Derivative order or linear differential operator |
2
|
start
|
FData or array_like
|
Starting coefficients of |
None
|
tol
|
float
|
Newton step tolerance: stop once a full Newton step moves no
coefficient by more than |
DEFAULT_TOL
|
max_iter
|
int
|
Iteration cap. |
DEFAULT_MAX_ITER
|
Returns:
| Type | Description |
|---|---|
DensityResult
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Warns:
| Type | Description |
|---|---|
RuntimeWarning
|
If the Newton iteration has not converged after |
Notes
If the basis reproduces constants and L annihilates them, W is
fixed only up to an additive constant; the returned W has ∫ W = 0.
In that case the fit coincides with :func:fit_intensity on the same
data, normalised: p = μ / n.
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.density import fit_density
>>> rng = np.random.default_rng(1)
>>> x = rng.exponential(size=500)
>>> res = fit_density(x, domain=(0.0, 10.0), lam=1e-2)
>>> bool(res(np.array([0.1]))[0] > res(np.array([3.0]))[0])
True
fit_intensity ¶
fit_intensity(
x: Any,
*,
basis: Basis | None = None,
domain: tuple[float, float] | None = None,
lam: float = 0.0,
penalty: int | LDO = 2,
start: Any = None,
tol: float = DEFAULT_TOL,
max_iter: int = DEFAULT_MAX_ITER,
) -> IntensityResult
Estimate the intensity of a Poisson process by penalised likelihood.
Replaces R fda's intensity.fd. The intensity is
μ(t) = exp W(t); W minimises
-Σ W(x_i) + ∫ exp W + lam * ∫ (L W)² over the basis domain.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
array_like
|
One-dimensional event times (a :class: |
required |
basis
|
Basis
|
Basis for |
None
|
domain
|
tuple of float
|
Observation window for the default basis; default |
None
|
lam
|
float
|
Smoothing parameter |
0.0
|
penalty
|
int or LDO
|
Derivative order or operator |
2
|
start
|
FData or array_like
|
Starting coefficients of |
None
|
tol
|
float
|
Newton step tolerance: stop once a full Newton step moves no
coefficient by more than |
DEFAULT_TOL
|
max_iter
|
int
|
Iteration cap. |
DEFAULT_MAX_ITER
|
Returns:
| Type | Description |
|---|---|
IntensityResult
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Warns:
| Type | Description |
|---|---|
RuntimeWarning
|
If the Newton iteration has not converged after |
Examples:
>>> import numpy as np
>>> import fdatools as fdt
>>> from fdatools.density import fit_intensity
>>> rng = np.random.default_rng(2)
>>> events = np.cumsum(rng.exponential(scale=0.5, size=200))
>>> res = fit_intensity(events, penalty=1, lam=10.0)
>>> round(res.expected_count, 6)
200.0