Source code for ewoksxas.fit.data
"""Shared 1D x/y data primitives and goodness-of-fit metrics for the backends.
Both fitting backends (:mod:`ewoksxas.fit.lca_fit` and
:mod:`ewoksxas.fit.peak_fit`) use these: the ``ArrayF64``/``IterableF64``/
``DataDict`` aliases, the :func:`as_1d_array` helper, the immutable
:class:`XYData` container for observations and fitted curves, and the
:func:`r2`/:func:`rmse` goodness-of-fit metrics (the latter also used by the
interactive Orange fit panel to report the quality of a fitted model).
"""
from __future__ import annotations
from collections.abc import Iterable
from dataclasses import asdict, dataclass
import numpy as np
import numpy.typing as npt
ArrayF64 = npt.NDArray[np.float64]
IterableF64 = Iterable[np.float64]
DataDict = dict[str, npt.NDArray[np.float64]]
[docs]
def as_1d_array(iterable: IterableF64) -> ArrayF64:
"""Converts a 1D numeric iterable into a flattened 1D numpy array.
Args:
iterable: One-dimensional sequence of floats.
Raises:
TypeError: If iterable is not type Iterable or is type string or bytes.
ValueError: If iterable is scalar or has two or more dimensions.
"""
if not isinstance(iterable, Iterable) or isinstance(iterable, (str, bytes)):
raise TypeError(f"Expected an array, got {type(iterable)}.")
array = np.asarray(iterable, dtype=np.float64)
if not array.size or array.ndim == 1:
return array
if array.size in array.shape:
return array.flatten()
raise ValueError(f"Array is scalar or has 2+ dimensions: shape={array.shape}.")
[docs]
@dataclass(kw_only=True, frozen=True, slots=True)
class XYData:
"""An immutable container for 1D X and Y data.
Used to hold the observations to be fitted as well as the fitted peak,
baseline and total-model curves returned by a fit.
Attributes:
x: 1D array of x-values.
y: 1D array of y-values.
"""
x: ArrayF64
y: ArrayF64
def __post_init__(self) -> None:
"""Converts inputs to 1D numpy arrays and checks shape matching.
Raises:
ValueError: If self.x and self.y do not have matching shapes.
"""
object.__setattr__(self, "x", as_1d_array(self.x))
object.__setattr__(self, "y", as_1d_array(self.y))
if self.x.size != self.y.size:
raise ValueError(f"Arrays don't match: x={self.x.shape}, y={self.y.shape}.")
[docs]
def as_dict(self) -> DataDict:
"""Returns the dataclass attributes as a dictionary."""
return asdict(self)
[docs]
def rmse(observed: ArrayF64, model: ArrayF64) -> float:
"""Root-mean-square error of a model against the observed data.
Args:
observed: 1D array of observed y-values.
model: 1D array of model y-values on the same grid.
"""
residual = observed - model
return float(np.sqrt(np.mean(np.square(residual))))
[docs]
def r2(observed: ArrayF64, model: ArrayF64) -> float:
"""Coefficient of determination of a model against the observed data.
Args:
observed: 1D array of observed y-values.
model: 1D array of model y-values on the same grid.
Returns:
The R² value, or NaN when the observed data has zero variance.
"""
residual = observed - model
ss_res = float(np.sum(np.square(residual)))
ss_tot = float(np.sum(np.square(observed - observed.mean())))
return 1.0 - ss_res / ss_tot if ss_tot > 0 else float("nan")