Source code for assetlife.lifetime_models._non_parametric_models

"""Nonparametric lifetime models."""

from typing import Any, Generic, Literal, NamedTuple, TypeVar, final

import numpy as np
from matplotlib.axes import Axes
from optype.numpy import Array1D

from assetlife.lifetime_models._base import _plot_probability_function


class NonParametricEstimation(NamedTuple):
    timeline: Array1D[np.float64]
    values: Array1D[np.float64]
    se: Array1D[np.float64]


KT = TypeVar("KT", bound=str)


class NonParametricLifetimeModel(Generic[KT]):
    _ci_bounds: tuple[float, float] = (0.0, 1.0)
    _estimations: dict[KT, NonParametricEstimation | None]

    def __init__(self) -> None:
        self._estimations = {}

    def plot(
        self,
        fname: KT,
        ax: Axes | None = None,
        **kwargs: Any,
    ) -> Axes | None:
        ci = kwargs.pop("ci", True)
        drawstyle = kwargs.get("drawstyle", "steps-post")
        estimations = self._estimations.get(fname, None)
        if estimations is not None:
            time, y, se = estimations
            if ci:
                return _plot_probability_function(
                    time,
                    y,
                    se=se,
                    ci_bounds=self._ci_bounds,
                    ax=ax,
                    drawstyle=drawstyle,
                    **kwargs,
                )
            return _plot_probability_function(
                time,
                y,
                ax=ax,
                drawstyle=drawstyle,
                **kwargs,
            )


[docs] @final class ECDF(NonParametricLifetimeModel[Literal["sf", "cdf"]]): """ Empirical Cumulative Distribution Function. Parameters ---------- time : ndarray Observed lifetime values. """ def __init__(self, time: Array1D[np.float64]) -> None: super().__init__() timeline, counts = np.unique(time, return_counts=True) timeline = np.insert(timeline, 0, 0) cdf = np.insert(np.cumsum(counts), 0, 0) / np.sum(counts) se = np.sqrt((1 - cdf) / len(time)) self._estimations["sf"] = NonParametricEstimation(timeline, 1 - cdf, se) self._estimations["cdf"] = NonParametricEstimation(timeline, cdf, se)
[docs] def sf(self) -> NonParametricEstimation | None: """ The estimation of the survival function. Returns ------- out : tuple of 3 1darray A tuple containing the timeline, the estimated values and the estimated standard errors. """ return self._estimations.get("sf", None)
[docs] def cdf(self) -> NonParametricEstimation | None: """ The estimation of the cumulative distribution function. Returns ------- out : tuple of 3 1darray A tuple containing the timeline, the estimated values and the estimated standard errors. """ return self._estimations.get("cdf", None)
[docs] @final class KaplanMeier(NonParametricLifetimeModel[Literal["sf"]]): r"""Kaplan-Meier estimator. Compute the non-parametric Kaplan-Meier estimator (also known as the product limit estimator) of the survival function from lifetime data. Parameters ---------- time : ndarray Observed lifetime values. event : ndarray of boolean values (1d), default is None Boolean indicators tagging lifetime values as right censored or complete. entry : ndarray of float (1d), default is None Left truncations applied to lifetime values. Notes ----- For a given time instant :math:`t` and :math:`n` total observations, this estimator is defined as: .. math:: \hat{S}(t) = \prod_{i: t_i \leq t} \left( 1 - \frac{d_i}{n_i}\right) where :math:`d_i` is the number of failures until :math:`t_i` and :math:`n_i` is the number of assets at risk just prior to :math:`t_i`. The variance estimation is obtained by: .. math:: \widehat{Var}[\hat{S}(t)] = \hat{S}(t)^2 \sum_{i: t_i \leq t} \frac{d_i}{n_i(n_i - d_i)} which is often referred to as Greenwood's formula. References ---------- .. [1] Lawless, J. F. (2011). Statistical models and methods for lifetime data. John Wiley & Sons. .. [2] Kaplan, E. L., & Meier, P. (1958). Nonparametric estimation from incomplete observations. Journal of the American statistical association, 53(282), 457-481. """ def __init__( self, time: Array1D[np.float64], event: Array1D[np.bool_] | None = None, entry: Array1D[np.float64] | None = None, ) -> None: super().__init__() if event is None: event = np.ones_like(time).astype(bool) if entry is None: entry = np.zeros_like(time) timeline = np.unique(time) n = ( (timeline <= time.reshape(-1, 1)) * (timeline >= entry.reshape(-1, 1)) ).sum(axis=0) d = ((time.reshape(-1, 1) == timeline) * event.reshape(-1, 1)).sum(axis=0) sf = (1 - d / n).cumprod() with np.errstate(divide="ignore"): var = (sf**2) * (d / (n * (n - d))).cumsum() var = np.where(n > d, var, 0) self._estimations["sf"] = NonParametricEstimation( np.insert(timeline, 0, 0), np.insert(sf, 0, 1), np.insert(np.sqrt(var), 0, 0), )
[docs] def sf(self) -> NonParametricEstimation | None: """ The estimation of the survival function. Returns ------- out : tuple of 3 1darray A tuple containing the timeline, the estimated values and the estimated standard errors. """ return self._estimations.get("sf", None)
[docs] @final class NelsonAalen(NonParametricLifetimeModel[Literal["chf"]]): r"""Nelson-Aalen estimator. Compute the non-parametric Nelson-Aalen estimator of the cumulative hazard function from lifetime data. Parameters ---------- time : ndarray Observed lifetime values. event : ndarray of boolean values (1d), default is None Boolean indicators tagging lifetime values as right censored or complete. entry : ndarray of float (1d), default is None Left truncations applied to lifetime values. Notes ----- For a given time instant :math:`t` and :math:`n` total observations, this estimator is defined as: .. math:: \hat{H}(t) = \sum_{i: t_i \leq t} \frac{d_i}{n_i} where :math:`d_i` is the number of failures until :math:`t_i` and :math:`n_i` is the number of assets at risk just prior to :math:`t_i`. The variance estimation is obtained by: .. math:: \widehat{Var}[\hat{H}(t)] = \sum_{i: t_i \leq t} \frac{d_i}{n_i^2} Note that the alternative survivor function estimate: .. math:: \tilde{S}(t) = \exp{(-\hat{H}(t))} is sometimes suggested for the continuous-time case. References ---------- .. [1] Lawless, J. F. (2011). Statistical models and methods for lifetime data. John Wiley & Sons. """ _ci_bounds: tuple[float, float] = (0.0, np.inf) def __init__( self, time: Array1D[np.float64], event: Array1D[np.bool_] | None = None, entry: Array1D[np.float64] | None = None, ) -> None: super().__init__() if event is None: event = np.ones_like(time).astype(bool) if entry is None: entry = np.zeros_like(time) timeline = np.unique(time) n = ( (timeline <= time.reshape(-1, 1)) * (timeline >= entry.reshape(-1, 1)) ).sum(axis=0) d = ((time.reshape(-1, 1) == timeline) * event.reshape(-1, 1)).sum(axis=0) chf = (d / n).cumsum() with np.errstate(divide="ignore"): var = (d / n**2).cumsum() self._estimations["chf"] = NonParametricEstimation( np.insert(timeline, 0, 0), np.insert(chf, 0, 0), np.insert(np.sqrt(var), 0, 0), )
[docs] def chf(self) -> NonParametricEstimation | None: """ The estimation of the cumulative hazard function. Returns ------- out : tuple of 3 1darray A tuple containing the timeline, the estimated values and the estimated standard errors. """ return self._estimations.get("chf", None)