Source code for relife.model.nested

from typing import Callable, Optional

import numpy as np
from numpy.typing import NDArray
from scipy.optimize import newton
from typing_extensions import override

from relife.model import LifetimeModel
from relife.quadratures import gauss_legendre
from relife.typing import ModelArgs


[docs] class AgeReplacementModel(LifetimeModel[NDArray[np.float64], *ModelArgs]): r""" Age replacement model. Lifetime model where the asset is replaced at age :math:`a_r`. Parameters ---------- baseline : LifetimeModel Underlying lifetime model. Notes ----- This is equivalent to the distribution of :math:`\min(X,a_r)` where :math:`X` is a baseline lifetime model and ar the age of replacement. """ def __init__(self, baseline: LifetimeModel[*ModelArgs]): super().__init__() self.baseline = baseline
[docs] def sf( self, time: NDArray[np.float64], ar: NDArray[np.float64], *args: *ModelArgs, ) -> NDArray[np.float64]: return np.where(time < ar, self.baseline.sf(time, *args), 0)
# TODO : correct formula ? if not, does AgeReplacementModel have to be LifetimeModel ?
[docs] def hf( self, time: NDArray[np.float64], ar: NDArray[np.float64], *args: *ModelArgs ) -> NDArray[np.float64]: return self.baseline.hf(time, *args)
# TODO : correct formula ? if not, does AgeReplacementModel have to be LifetimeModel ?
[docs] def chf( self, time: NDArray[np.float64], ar: NDArray[np.float64], *args: *ModelArgs ) -> NDArray[np.float64]: return self.baseline.chf(time, *args)
[docs] @override def isf( self, probability: NDArray[np.float64], ar: NDArray[np.float64], *args: *ModelArgs, ) -> NDArray[np.float64]: return np.minimum(self.baseline.isf(probability, *args), ar)
[docs] def pdf( self, time: NDArray[np.float64], ar: NDArray[np.float64], *args: *ModelArgs, ) -> NDArray[np.float64]: return np.where(time < ar, self.baseline.pdf(time, *args), 0)
[docs] @override def moment( self, n: int, ar: NDArray[np.float64], *args: *ModelArgs ) -> NDArray[np.float64]: return self.ls_integrate( lambda x: x**n, np.array(0.0), np.array(np.inf), ar, *args, )
[docs] @override def mrl( self, time: NDArray[np.float64], ar: NDArray[np.float64], *args: *ModelArgs ) -> NDArray[np.float64]: ub = np.array(np.inf) mask = time >= ar if np.any(mask): time, ub = np.broadcast_arrays(time, ub) time = np.ma.MaskedArray(time, mask) ub = np.ma.MaskedArray(ub, mask) mu = self.ls_integrate(lambda x: x - time, time, ub, ar, *args) / self.sf( time, ar, *args ) return np.ma.filled(mu, 0)
[docs] @override def ls_integrate( self, func: Callable[[NDArray[np.float64]], NDArray[np.float64]], a: NDArray[np.float64], b: NDArray[np.float64], ar: NDArray[np.float64], *args: *ModelArgs, ndim: int = 0, deg: int = 100, ) -> NDArray[np.float64]: ub = np.minimum(np.inf, ar) b = np.minimum(ub, b) a, b = np.atleast_2d(*np.broadcast_arrays(a, b)) args_2d = np.atleast_2d(*args) if isinstance(args_2d, np.ndarray): args_2d = (args_2d,) def integrand( x: NDArray[np.float64], *_: *tuple[NDArray[np.float64], ...] ) -> NDArray[np.float64]: return np.atleast_2d(func(x) * self.baseline.pdf(x, *_)) w = np.where(b == ar, func(ar) * self.baseline.sf(ar, *args_2d), 0) return gauss_legendre(integrand, a, b, *args_2d, ndim=2, deg=deg) + w
[docs] class LeftTruncatedModel(LifetimeModel[NDArray[np.float64], *ModelArgs]): r"""Left truncated model. Conditional distribution of the lifetime model for an asset having reach age :math:`a_0`. Parameters ---------- baseline : LifetimeModel Underlying lifetime model. """ def __init__(self, baseline: LifetimeModel[*ModelArgs]): super().__init__() self.baseline = baseline # TODO : correct formula ? if not, does LeftTruncatedModel have to be LifetimeModel ?
[docs] def sf( self, time: NDArray[np.float64], a0: NDArray[np.float64], *args: *ModelArgs ) -> NDArray[np.float64]: return super().sf(time, a0, *args)
# TODO : correct formula ? if not, does LeftTruncatedModel have to be LifetimeModel ?
[docs] def pdf( self, time: NDArray[np.float64], a0: NDArray[np.float64], *args: *ModelArgs ) -> NDArray[np.float64]: return super().pdf(time, a0, *args)
[docs] def isf( self, probability: NDArray[np.float64], a0: NDArray[np.float64], *args: *ModelArgs, ) -> NDArray[np.float64]: cumulative_hazard_rate = -np.log(probability) return self.ichf(cumulative_hazard_rate, a0, *args)
[docs] def chf( self, time: NDArray[np.float64], a0: NDArray[np.float64], *args: *ModelArgs, ) -> NDArray[np.float64]: return self.baseline.chf(a0 + time, *args) - self.baseline.chf(a0, *args)
[docs] def hf( self, time: NDArray[np.float64], a0: NDArray[np.float64], *args: *ModelArgs, ) -> NDArray[np.float64]: return self.baseline.hf(a0 + time, *args)
[docs] def ichf( self, cumulative_hazard_rate: NDArray[np.float64], a0: NDArray[np.float64], *args: *ModelArgs, ) -> NDArray[np.float64]: return ( self.baseline.ichf( cumulative_hazard_rate + self.baseline.chf(a0, *args), *args ) - a0 )
[docs] def rvs( self, a0: NDArray[np.float64], *args: *ModelArgs, size: Optional[int] = 1, seed: Optional[int] = None, ) -> NDArray[np.float64]: return self.baseline.rvs(*(a0, *args), size=size, seed=seed) + a0
[docs] class EquilibriumDistribution(LifetimeModel[*ModelArgs]): r"""Equilibrium distribution. The equilibirum distribution is the distrbution computed from a lifetime model that makes the associated delayed renewal process stationary. Parameters ---------- baseline : LifetimeModel Underlying lifetime model. References ---------- .. [1] Ross, S. M. (1996). Stochastic processes. New York: Wiley. """ def __init__(self, baseline: LifetimeModel[*ModelArgs]): super().__init__() self.baseline = baseline
[docs] def sf(self, time: NDArray[np.float64], *args: *ModelArgs) -> NDArray[np.float64]: return 1 - self.cdf(time, *args)
[docs] @override def cdf(self, time: NDArray[np.float64], *args: *ModelArgs) -> NDArray[np.float64]: args_2d = np.atleast_2d(*args) time_2d = np.atleast_2d(time) if isinstance(args_2d, np.ndarray): args_2d = (args_2d,) res = gauss_legendre( self.baseline.sf, 0, time_2d, *args_2d, ndim=2 ) / self.baseline.mean(*args_2d) # reshape 2d -> final_dim ndim = max(map(np.ndim, (time, *args)), default=0) if ndim < 2: res = np.squeeze(res) return res
[docs] def pdf(self, time: NDArray[np.float64], *args: *ModelArgs) -> NDArray[np.float64]: # self.baseline.mean can squeeze -> broadcast error (origin : ls_integrate output shape) mean = self.baseline.mean(*args) sf = self.baseline.sf(time, *args) if mean.ndim < sf.ndim: # if args is empty, sf can have more dim than mean if sf.ndim == 1: mean = np.reshape(mean, (-1,)) if sf.ndim == 2: mean = np.broadcast_to(mean, (sf.shape[0], -1)) return sf / mean
[docs] def hf(self, time: NDArray[np.float64], *args: *ModelArgs) -> NDArray[np.float64]: return 1 / self.baseline.mrl(time, *args)
[docs] def chf(self, time: NDArray[np.float64], *args: *ModelArgs) -> NDArray[np.float64]: return -np.log(self.sf(time, *args))
[docs] @override def isf( self, probability: NDArray[np.float64], *args: *ModelArgs ) -> NDArray[np.float64]: return newton( lambda x: self.sf(x, *args) - probability, self.baseline.isf(probability, *args), args=args, )
[docs] @override def ichf( self, cumulative_hazard_rate: NDArray[np.float64], *args: *ModelArgs ) -> NDArray[np.float64]: return self.isf(np.exp(-cumulative_hazard_rate), *args)