Source code for actuarialmath.fractional

"""Fractional age assumptions- Computes survival and mortality functions between integer ages.

MIT License. Copyright (c) 2022-2023 Terence Lim
"""

import math
from actuarialmath import Lifetime

[docs]class Fractional(Lifetime): """Compute survival functions at fractional ages and durations Args: udd : select UDD (True, default) or CFM (False) between integer ages """ def __init__(self, udd: bool = True, **kwargs): super().__init__(**kwargs) self.udd_ = udd # # Define actuarial forms of survival functions at fractional ages #
[docs] def l_r(self, x: int, s: int = 0, r: float = 0.) -> float: """Number of lives at fractional age: l_[x]+s+r Args: x : age of selection s : years after selection r : fractional year after selection """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" assert r >= 0, "r must be non-negative" s += math.floor(r) # interpolate lives between consecutive integer ages r -= math.floor(r) if self.isclose(r): return self.l_x(x, s=s) if self.isclose(r, 1.0): return self.l_x(x, s=s+1) if self.udd_: return self.l_x(x, s=s)*(1-r) + self.l_x(x, s=s+1)*r else: return self.l_x(x, s=s)**(1-r) * self.l_x(x, s=s+1)**r
[docs] def p_r(self, x: int, s: int = 0, r: float = 0., t: float = 1.) -> float: """Probability of survival from and through fractional age: t_p_[x]+s+r Args: x : age of selection s : years after selection r : fractional year after selection t : fractional number of years survived Examples: >>> life = Fractional(udd=False).set_survival(l=lambda x,t: 50-x-t) >>> print(life.p_r(47, r=0.), life.p_r(47, r=0.5), life.p_r(47, r=1.)) """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" assert r >= 0, "r must be non-negative" assert t >= 0, "t must be non-negative" r_floor = math.floor(r) s += r_floor r -= r_floor if 0. <= r + t <= 1.: if self.udd_: return 1 - self.q_r(x, s=s, r=r, t=t) else: # Constant force shortcut within int age return self.p_x(x, s=s)**t # does not depend on r return self.l_r(x, s=s, r=r+t) / self.l_r(x, s=s, r=r)
[docs] def q_r(self, x: int, s: int = 0, r: float = 0., t: float = 1., u: float = 0.) -> float: """Deferred mortality rate within fractional ages: u|t_q_[x]+s+r Args: x : age of selection s : years after selection r : fractional year after selection u : fractional number of years survived, then t : death within next fractional years t Examples: >>> life = Fractional(udd=False).set_survival(l=lambda x,t: 50-x-t) >>> print(life.q_r(x, r=0.5)) """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" assert r >= 0, "r must be non-negative" assert t >= 0, "t must be non-negative" assert t >= 0, "t must be non-negative" r_floor = math.floor(r) s += r_floor r -= r_floor if 0 <= r + t + u <= 1: if u > 0: # Die within u|t_q == Die in t+u but not in u return self.q_r(x, s=s, r=r, t=u+t) - self.q_r(x, s=s, r=r, t=u) if self.udd_: # UDD shortcut within integer age return (t * self.q_x(x, s=s)) / (1. - r * self.q_x(x, s=s)) else: return 1 - self.p_r(x, s=s, r=r, t=t) return ((self.l_r(x, s=s, r=r+u) - self.l_r(x, s=s, r=r+u+t)) / self.l_r(x, s=s, r=r))
[docs] def mu_r(self, x: int, s: int = 0, r: float = 0.) -> float: """Force of mortality at fractional age: mu_[x]+s+r Args: x : age of selection s : years after selection r : fractional year after selection """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" assert r >= 0, "r must be non-negative" r_floor = math.floor(r) s += r_floor r -= r_floor if self.isclose(r): return self.mu_x(x, s=s) if self.udd_: # UDD shortcut return self.q_x(x, s=s) / (1. - r*self.q_x(x, s=s)) else: # Constant force shortcut return -math.log(max(0.000001, self.p_x(x, s=s)))
[docs] def f_r(self, x: int, s: int = 0, r: float = 0., t: float = 0.0) -> float: """Lifetime density function at fractional age: f_[x]+s+r (t) Args: x : age of selection s : years after selection r : fractional year after selection t : death at fractional year t """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" assert r >= 0, "r must be non-negative" assert t >= 0, "t must be non-negative" if 0. <= r + t <= 1.: # shortcuts available within integer ages if self.udd_: # UDD shortcut: constant q_x return self.q_x(x, s=s) # does not depend fractional age or duration else: # Constant force shortcut: if self.isclose(t): return self.f_x(x=x, s=s) mu = -math.log(max(0.00001, self.p_x(x, s=s))) return math.exp(-mu*t) * mu # does not depend on fractional age else: # else survive to integer age, times density at fractional age r_floor = math.floor(r) r -= r_floor # s.t. r < 1 s += r_floor # while maintaining x+s+r unchanged t_floor = math.floor(r + t) u = t_floor - r # s.t. u + r is integer t = t + r - t_floor # s.t. t < 1 return self.p_r(x, s=s, r=r, t=u) * self.f_r(x, s=s+u+r, t=t)
# # Define fractional age pure endowment function #
[docs] def E_r(self, x: int, s: int = 0, r: float = 0., t: float = 1.) -> float: """Pure endowment at fractional age: t_E_[x]+s+r Args: x : age of selection s : years after selection r : fractional year after selection t : limited at fractional year t """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" assert r >= 0, "r must be non-negative" assert t >= 0, "t must be non-negative" return self.p_r(x, s=s, r=r, t=t) * self.interest.v_t(t)
# # Define fractional age expectations of future lifetime #
[docs] def e_r(self, x: int, s: int = 0, t: float = Lifetime.WHOLE) -> float: """Temporary expected future lifetime at fractional age: e_[x]+s:t Args: x : age of selection s : years after selection t : fractional year limit of expected future lifetime """ assert x >= 0, "x must be non-negative" assert s >= 0, "s must be non-negative" if t == 0: return 0 # shortcuts for complete expectation elif t < 0: if self.udd_: # UDD case return self.e_x(x=x, s=s, t=t, curtate=True) + 0.5 else: # Constant Force: compute as temporary through maxage return self.e_r(x=x, s=s, t=self.max_term(x+s, t)) # shortcuts between integer ages elif t <= 1: if self.udd_: # UDD case if t == 1: # shortcut for UDD 1-year limited expectation return 1. - self.q_x(x=x, s=s)*(1/2) else: # shortcut for fractional limited expectation return self.q_r(x=x, s=s, t=t)*(t/2) + self.p_r(x, s=s, t=t)*t else: # Constant Force case mu = -math.log(max(0.00001, self.p_x(x=x, s=s))) # constant mu return (1. - math.exp(-mu*t)) / mu # shortcut # t > 1: apply one-year recursion formula else: return (self.e_r(x=x, s=s, t=1) + (self.p_x(x=x, s=s) * self.e_r(x=x, s=s+1, t=t-1)))
# # Approximation of curtate and complete lifetimes #
[docs] @staticmethod def e_approximate(e_complete: float = None, e_curtate: float = None, variance: bool = False) -> float: """Convert between curtate and complete expectations assuming UDD shortcut Args: e_complete : complete expected lifetime e_curtate : or curtate expected lifetime variance : to approximate mean (False) or variance (True) Returns: approximate complete or curtate expectation assuming UDD Examples: >>> print(Fractional.e_approximate(e_complete=15)) >>> print(Fractional.e_approximate(e_curtate=15)) """ if e_complete is not None: assert e_curtate is None, "one of e and e_curtate must be None" return e_complete - (1/12 if variance else 0.5) elif e_curtate is not None: return e_curtate + (1/12 if variance else 0.5) else: raise Exception("Provide a value for either e_complete or e_curtate")
if __name__ == "__main__": print(Fractional.e_approximate(e_complete=15)) # output e_curtate print(Fractional.e_approximate(e_curtate=15)) # output e_complete x = 45 life = Fractional(udd=False).set_survival(l=lambda x,t: 50-x-t) print(life.q_r(x, r=0.), life.q_r(x, r=0.5), life.q_r(x, r=1.)) life = Fractional(udd=True).set_survival(l=lambda x,t: 50-x-t) print(life.q_r(x, r=0.), life.q_r(x, r=0.5), life.q_r(x, r=1.))