Source code for leaspy.models.lme

from typing import Optional

import numpy as np
import statsmodels.api as sm
import torch

from leaspy.io.outputs import IndividualParameters
from leaspy.utils.typing import DictParamsTorch

from .stateless import StatelessModel

__all__ = ["LMEModel"]


[docs] class LMEModel(StatelessModel): r"""LMEModel is a benchmark model that fits and personalize a linear mixed-effects model. The model specification is the following: .. math:: y_{ij} = fixed_{intercept} + random_{intercept_i} + (fixed_{slopeAge} + random_{slopeAge_i}) * age_{ij} + \epsilon_{ij} with: * :math:`y_{ij}`: value of the feature of the i-th subject at his j-th visit, * :math:`age_{ij}`: age of the i-th subject at his j-th visit. * :math:`\epsilon_{ij}`: residual Gaussian noise (independent between visits) .. warning:: This model must be fitted on one feature only (univariate model). TODO? add some covariates in this very simple model. Parameters ---------- name : :obj:`str` The model's name. **kwargs Model hyperparameters: * with_random_slope_age : :obj:`bool` (default ``True``). Attributes ---------- name : :obj:`str` The model's name. is_initialized : :obj:`bool` ``True`` if the model is initialized, ``False`` otherwise. with_random_slope_age : :obj:`bool` (default ``True``) Has the LME a random slope for subject's age? Otherwise it only has a random intercept per subject. features : :obj:`list` of :obj:`str` List of the model features. .. warning:: LME has only one feature. dimension : :obj:`int` Will always be 1 (univariate). parameters : :obj:`dict` Contains the model parameters. In particular: * ``ages_mean`` : :obj:`float` Mean of ages (for normalization). * ``ages_std`` : :obj:`float` Std-dev of ages (for normalization). * ``fe_params`` : :class:`np.ndarray` of :obj:`float` Fixed effects. * ``cov_re`` : :class:`np.ndarray` Variance-covariance matrix of random-effects. * ``cov_re_unscaled_inv`` : :class:`np.ndarray` Inverse of unscaled (= divided by variance of noise) variance-covariance matrix of random-effects. This matrix is used for personalization to new subjects. * ``noise_std`` : :obj:`float` Std-dev of Gaussian noise. * ``bse_fe``, ``bse_re`` : :class:`np.ndarray` of :obj:`float` Standard errors on fixed-effects and random-effects respectively (not used in ``Leaspy``). See Also -------- :class:`~leaspy.algo.others.lme_fit.LMEFitAlgorithm` :class:`~leaspy.algo.others.lme_personalize.LMEPersonalizeAlgorithm` """ def __init__( self, name: str, with_random_slope_age: Optional[bool] = True, **kwargs ): super().__init__(name, **kwargs) self.with_random_slope_age = with_random_slope_age self.dimension = 1 @property def hyperparameters(self) -> DictParamsTorch: """Dictionary of values for model hyperparameters.""" return {}
[docs] def compute_individual_trajectory( self, timepoints: list[float], individual_parameters: IndividualParameters, ) -> torch.Tensor: """Compute scores values at the given time-point(s) given a subject's individual parameters. Parameters ---------- timepoints : array-like of ages (not normalized) Timepoints to compute individual trajectory at. individual_parameters : :obj:`dict` Individual parameters: * random_intercept * random_slope_age (if ``with_random_slope_age == True``) Returns ------- :class:`torch.Tensor` of :obj:`float` : The individual trajectories. The shape of the tensor is ``(n_individuals == 1, n_tpts == len(timepoints), n_features == 1)``. """ # normalize ages (np.ndarray of float, 1D) ages_norm = ( np.array(timepoints).reshape(-1) - self.parameters["ages_mean"] ) / self.parameters["ages_std"] # design matrix (same for fixed and random effects) X = sm.add_constant(ages_norm, prepend=True, has_constant="add") # assert 'random_intercept' in individual_parameters if not self.with_random_slope_age: # no random slope on ages (fixed effect only) re_params = np.array([individual_parameters["random_intercept"].item(), 0]) else: # assert 'random_slope_age' in individual_parameters re_params = np.array( [ individual_parameters["random_intercept"].item(), individual_parameters["random_slope_age"].item(), ] ) y = X @ (self.parameters["fe_params"] + re_params) return torch.tensor(y, dtype=torch.float32).reshape((1, -1, 1))