Source code for circust.fitting.rhythm_model
"""
Interfaz compartida para todos los modelos de ajuste ritmico del pipeline CIRCUST.
Nota de diseno
--------------
``FitResult`` es un contenedor de datos puro: almacena todo lo que ``fit()``
produce, incluido el tiempo de pico precalculado por el modelo. Asi el
resultado es autosuficiente — nadie necesita llamar al modelo despues de
obtenerlo.
La logica especifica de cada modelo vive exclusivamente en ``RhythmModel`` y
sus subclases:
``fit()`` — abstracto, produce un FitResult con peak_time ya calculado.
Para el calculo del pico con floats sueltos (reparametrizacion manual) cada
modelo expone un metodo estatico auxiliar, p.ej. ``FMMModel.peak_time()``.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
import numpy as np
# ---------------------------------------------------------------------------
# Contenedor de resultados — datos puros, sin logica de modelo
# ---------------------------------------------------------------------------
[docs]
@dataclass
class FitResult:
"""
Resultado de cualquier modelo de ajuste ritmico.
Attributes
----------
fitted : np.ndarray, dim (n_muestras,)
Valores predichos por el modelo en cada punto temporal.
params : dict
Parametros estimados del modelo.
Claves Cosinor : ``M``, ``A``, ``phi``
- M : mesor (nivel medio)
- A : amplitud
- phi : acrofase en [0, 2*pi)
Claves FMM : ``M``, ``A``, ``alpha``, ``beta``, ``omega``
- M : mesor
- A : amplitud
- alpha : parametro de localizacion en [0, 2*pi)
- beta : parametro de forma en [0, 2*pi)
- omega : parametro de asimetria en (0, 1]
peak_time : float
Tiempo de pico en [0, 2*pi), precalculado por el modelo al hacer fit().
Cosinor: phi mod 2*pi (phi es directamente el tiempo de pico).
FMM: formula compUU — alpha + 2*atan2(...) mod 2*pi.
r2 : float
Coeficiente de determinacion R-cuadrado = 1 - SS_res / SS_tot.
residuals : np.ndarray, forma (n_muestras,)
Residuos brutos: datos - ajustado.
residuals_std : np.ndarray, forma (n_muestras,)
Residuos estandarizados: (residuos - media) / desv_std.
sse : float
Suma de errores al cuadrado.
model_name : str
``"cosinor"`` o ``"fmm"``.
"""
fitted: np.ndarray
params: dict
peak_time: float
r2: float
residuals: np.ndarray
residuals_std: np.ndarray
sse: float
model_name: str
[docs]
def summary(self) -> str:
lines = [
f"=== Ajuste {self.model_name.upper()} ===",
f" R-cuadrado : {self.r2:.4f}",
f" peak_time : {self.peak_time:.4f} rad",
f" SSE : {self.sse:.6f}",
f" parametros : { {k: round(float(v), 5) for k, v in self.params.items()} }",
]
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Modelo base abstracto
# ---------------------------------------------------------------------------
[docs]
class RhythmModel(ABC):
"""
Interfaz comun para todos los modelos de ajuste de ritmos circadianos.
Las subclases deben implementar :meth:`fit`.
Los parametros pasados al constructor son hiperparametros que permanecen
constantes entre llamadas a ``fit()`` (p.ej. tamanos de rejilla para FMM).
"""
[docs]
@abstractmethod
def fit(
self,
data: np.ndarray,
time_points: np.ndarray,
) -> FitResult:
"""
Ajusta el modelo a ``data`` observados en ``time_points``.
El ``FitResult`` devuelto incluye ``peak_time`` ya calculado.
Parameters
----------
data : np.ndarray, forma (n_muestras,)
Valores de expresion normalizados (tipicamente en [-1, 1]).
time_points : np.ndarray, forma (n_muestras,)
Eje temporal circular en [0, 2*pi), producido por CPCA.
Returns
-------
FitResult
"""
# ------------------------------------------------------------------
# Utilidades compartidas por todos los modelos
# ------------------------------------------------------------------
@staticmethod
def _r2(data: np.ndarray, fitted: np.ndarray) -> float:
"""R-cuadrado = 1 - SS_res / SS_tot."""
ss_res = np.sum((data - fitted) ** 2)
ss_tot = np.sum((data - data.mean()) ** 2)
if ss_tot == 0:
return 0.0
return float(1.0 - ss_res / ss_tot)
@staticmethod
def _standardise_residuals(residuals: np.ndarray, ddof: int = 1) -> np.ndarray:
"""(res - media) / desv_std con ``ddof`` grados de libertad."""
std = residuals.std(ddof=ddof)
if std == 0:
return np.zeros_like(residuals)
return (residuals - residuals.mean()) / std