"""
Survival Functions
Provides actuarial survival functions and calculations including
temporary and permanent life functions, actuarial present values, etc.
"""
import numpy as np
from numbers import Real
from typing import Optional
from .mortality_table import MortalityTable
[docs]
class SurvivalFunctions:
"""
A class for calculating various actuarial survival functions
and life contingencies based on mortality tables.
"""
[docs]
def __init__(self, mortality_table: MortalityTable, interest_rate: float):
"""
Initialize SurvivalFunctions with a mortality table and interest rate.
Args:
mortality_table: MortalityTable instance
interest_rate: Annual interest rate for discounting (e.g. 0.10 for 10%)
"""
if isinstance(interest_rate, bool) or not isinstance(interest_rate, Real):
raise TypeError("interest_rate must be a real number (e.g. 0.10 for 10%).")
if interest_rate <= -1:
raise ValueError("interest_rate must be greater than -1.0.")
self.mt = mortality_table
self.i = float(interest_rate)
self.v = 1 / (1 + self.i) # Discount factor
[docs]
def npx(self, x: int, n: int) -> float:
"""
Calculate n-year survival probability: npx
Args:
x: Age
n: Number of years
Returns:
Probability that (x) survives n years
"""
if n < 0:
return 0.0
if n == 0:
return 1.0
if x + n > self.mt.ages[-1]:
# Extrapolate using last available survival probability
last_age_idx = np.where(self.mt.ages <= x)[0]
if len(last_age_idx) == 0:
return 0.0
last_idx = last_age_idx[-1]
remaining_years = x + n - self.mt.ages[last_idx]
px_remaining = self.mt.px_values[last_idx] ** remaining_years
return self.mt.lx_values[last_idx] * px_remaining / self.mt.lx_values[np.where(self.mt.ages == x)[0][0]]
# Find indices for ages x to x+n
start_idx = np.where(self.mt.ages == x)[0]
end_idx = np.where(self.mt.ages == x + n)[0]
if len(start_idx) == 0 or len(end_idx) == 0:
return 0.0
start_idx = start_idx[0]
end_idx = end_idx[0]
# Calculate cumulative survival probability
survival_prob = np.prod(self.mt.px_values[start_idx:end_idx])
return float(survival_prob)
[docs]
def nqx(self, x: int, n: int) -> float:
"""
Calculate n-year mortality probability: nqx
Args:
x: Age
n: Number of years
Returns:
Probability that (x) dies within n years
"""
return 1 - self.npx(x, n)
[docs]
def tpx(self, x: int, t: float) -> float:
"""
Calculate t-year survival probability using linear interpolation.
Args:
x: Age
t: Fractional years
Returns:
Probability that (x) survives t years
"""
if t == 0:
return 1.0
elif t < 0:
return 0.0
# Integer and fractional parts
n = int(t)
frac = t - n
# Base survival probability for n years
n_year_survival = self.npx(x, n)
if n + x >= self.mt.ages[-1]:
# Extrapolate using constant force of mortality from last age
last_age = self.mt.ages[-1]
if x + n >= last_age:
remaining_years = t - (last_age - x)
q_last = self.mt.get_qx(last_age)
mu = -np.log(1 - q_last) # Force of mortality approximation
return n_year_survival * np.exp(-mu * remaining_years)
else:
return n_year_survival
# Linear interpolation for fractional year
q_next = self.mt.get_qx(x + n)
survival_frac = 1 - (frac * q_next)
return n_year_survival * survival_frac
[docs]
def annuity_due(self, x: int, n: Optional[int] = None) -> float:
"""
Calculate the actuarial present value of an annuity-due: äx or äx:n
Args:
x: Age
n: Term of annuity (None for whole life)
Returns:
Actuarial present value of annuity-due
"""
if n is None:
# Whole life annuity
annuity = 0.0
v_t = 1.0
for t in range(len(self.mt.ages) - np.where(self.mt.ages == x)[0][0]):
annuity += v_t * self.tpx(x, t)
v_t *= self.v
return annuity
else:
# Term annuity
annuity = 0.0
v_t = 1.0
for t in range(n):
annuity += v_t * self.tpx(x, t)
v_t *= self.v
return annuity
[docs]
def assurance(self, x: int, n: Optional[int] = None) -> float:
"""
Calculate the actuarial present value of a whole life assurance: Ax or Ax:n
Args:
x: Age
n: Term of assurance (None for whole life)
Returns:
Actuarial present value of assurance
"""
if n is None:
# Whole life assurance
assurance = 0.0
v_t = self.v
max_t = len(self.mt.ages) - np.where(self.mt.ages == x)[0][0] - 1
for t in range(1, max_t + 1):
# Simplified: probability of death in year t
q_xt = 1 - self.npx(x, t) / self.npx(x, t-1) if t > 1 else 1 - self.npx(x, 1)
assurance += v_t * min(q_xt, 1.0) # Cap at 1.0
v_t *= self.v
return float(assurance)
else:
# Term assurance
assurance = 0.0
v_t = self.v
for t in range(1, n + 1):
# Simplified: probability of death in year t
q_xt = 1 - self.npx(x, t) / self.npx(x, t-1) if t > 1 else 1 - self.npx(x, 1)
assurance += v_t * min(q_xt, 1.0) # Cap at 1.0
v_t *= self.v
return float(assurance)
[docs]
def net_single_premium(self, x: int, n: Optional[int] = None) -> float:
"""
Calculate net single premium for whole life or term assurance.
Args:
x: Age
n: Term (None for whole life)
Returns:
Net single premium
"""
return self.assurance(x, n)