"""Policy Values - Computes present value of future losses and reserves
MIT License. Copyright 2022-2023 Terence Lim
"""
import math
import numpy as np
import scipy
import matplotlib.pyplot as plt
from typing import Dict, Any
from actuarialmath import Premiums, Actuarial
[docs]class Contract(Actuarial):
"""Set and retrieve policy contract terms
Args:
premium : level premium amount
benefit : insurance death benefit amount
settlement_policy : settlement expense per policy
endowment : endowment benefit amount
initial_policy : first year total expense per policy
initial_premium : first year total premium per $ of gross premium
renewal_policy : renewal expense per policy
renewal_premium : renewal premium per $ of gross premium
discrete : annuity due (True) or continuous (False)
T : term of insurance
discrete : annuity due (True) or continuous (False)
"""
def __init__(self,
premium: float = 1,
initial_policy: float = 0,
initial_premium: float = 0,
renewal_policy: float = 0,
renewal_premium: float = 0,
settlement_policy: float = 0,
benefit: float = 1,
endowment: float = 0,
T: int = Premiums.WHOLE,
discrete: bool = True):
self.premium=premium
self.benefit=benefit
self.discrete=discrete
self.initial_policy=initial_policy
self.initial_premium=initial_premium
self.renewal_policy=renewal_policy
self.renewal_premium=renewal_premium
self.settlement_policy=settlement_policy
self.endowment=endowment
self.T = T
[docs] def set_contract(self, **terms) -> Any:
"""Update any existing policy contract terms
Args:
**kwargs : one or more contract policy terms, and its value to update
"""
for key, value in terms.items():
if hasattr(self, key):
setattr(self, key, value)
return self
@property
def premium_terms(self) -> Dict: # terms required by gross_premiums
"""Dict of terms required for calculating gross premiums
Examples:
>>> life = PolicyValues().set_interest(i=0.04)
>>> contract = Contract(premium=2.338, benefit=100, initial_premium=.1,
>>> renewal_premium=0.05)
>>> contract.premium = life.gross_premium(a=12, A=life.insurance_twin(a),
>>> **contract.premium_terms)
"""
return dict(benefit=self.benefit,
initial_policy=self.initial_policy,
initial_premium=self.initial_premium,
renewal_policy=self.renewal_policy,
renewal_premium=self.renewal_premium,
settlement_policy=self.settlement_policy)
[docs] def renewals(self, t: int = 0) -> "Contract":
"""Returns contract object with initial terms set to renewal terms
Args:
t : number of years after initial
"""
return Contract(benefit=self.benefit,
renewal_policy=self.renewal_policy,
renewal_premium=self.renewal_premium,
initial_policy=self.renewal_policy,
initial_premium=self.renewal_premium,
settlement_policy=self.settlement_policy,
discrete=self.discrete,
premium=self.premium,
endowment=self.endowment,
T=self.T - t)
@property
def renewal_profit(self) -> float:
"""Renewal dollar profit (premium less renewal expenses)"""
# premium less renewal per premium and expense"""
return ((self.premium * (1 - self.renewal_premium)) - self.renewal_policy)
@property
def initial_cost(self) -> float:
"""Total initial cost (net of renewal component of expenses and premiums)"""
return ((self.initial_policy - self.renewal_policy) +
(self.premium * (self.initial_premium - self.renewal_premium)))
@property
def claims_cost(self) -> float:
"""Total claims cost (death benefit + settlement expense)"""
return self.benefit + self.settlement_policy
[docs]class PolicyValues(Premiums):
"""Compute net and gross future losses and policy values"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
#
# Net Future Loss shortcuts for WL and Endowment Insurance
#
[docs] def net_future_loss(self, A: float, A1: float, b: int = 1) -> float:
"""Shortcut for net policy value with WL or Endowment Insurance factors
Args:
A : insurance factor at age (x)
A1 : insurance factor at t years after x
b : benefit amount
"""
return b * (A1 - A) / (1 - A)
[docs] def net_variance_loss(self, A1: float, A2: float, A: float = 0,
b: int = 1) -> float:
"""Variance of net loss with WL or Endowment Insurance factors
Args:
A : insurance factor at age (x)
A1 : first moment of insurance factor at t years after x
A2 : insurance factor at double force of interest t years after x
b : benefit amount
"""
if not A:
A = A1 # assume t = 0 => A = A1
return b**2 * (A2 - A1**2) / (1 - A)**2
[docs] def net_policy_variance(self, x, s: int = 0, t: int = 0, b: int = 1,
n: int = Premiums.WHOLE, endowment: int = 0,
discrete: bool = True) -> float:
"""Variance of net future loss for WL or Endowment Insurance only
Args:
x : age of selection
s : years after selection
n : term of life insurance
t : years after issue to compute policy variance
b : benefit amount
endowment : endowment amount
discrete : annuity due (True) or continuous (False)
"""
if n < 0: # Whole Life
A2 = self.whole_life_insurance(x, s=s+t, moment=2,
discrete=discrete)
A1 = self.whole_life_insurance(x, s=s+t, discrete=discrete)
A = self.whole_life_insurance(x, s=s, discrete=discrete)
elif endowment == b: # Endowment Insurance
n = self.max_term(x=x+s+t, t=n)
A2 = self.endowment_insurance(x, s=s+t, t=n-t, moment=2,
discrete=discrete)
A1 = self.endowment_insurance(x, s=s+t, t=n-t, discrete=discrete)
A = self.endowment_insurance(x, s=s, t=n, discrete=discrete)
else:
raise Exception("Variances for WL and Endowment Ins only")
return self.net_variance_loss(A=A, A1=A1, A2=A2, b=b)
#
# Net Policy Value for special and WL/endowment insurance
#
[docs] def net_policy_value(self, x: int, s: int = 0, t: int = 0, b: int = 1,
n: int = Premiums.WHOLE, endowment: int = 0,
discrete: bool = True) -> float:
"""Net policy value assuming net premiums from equivalence principle: E[L_t]
Args:
x : age initially insured
s : years after selection
n : term of life insurance
t : years after issue to compute policy variance
b : benefit amount
endowment : endowment amount
discrete : discrete/annuity due (True) or continuous (False)
"""
if n < 0: # Shortcut available for Whole Life
A1 = self.whole_life_insurance(x, s=s+t, discrete=discrete)
A = self.whole_life_insurance(x, s=s, discrete=discrete)
elif endowment == b: # Shortcut available for (equal) Endowment Insurance
n = self.max_term(x=x+s+t, t=n)
A1 = self.endowment_insurance(x, s=s+t, t=n-t,
discrete=discrete)
A = self.endowment_insurance(x, s=s, t=n, discrete=discrete)
else: # Special Term or (unequal) Endowment insurance has no shortcut
n = self.max_term(x=x+s+t, t=n)
A1 = self.endowment_insurance(x, s=s+t, t=n-t, discrete=discrete,
b=b, endowment=endowment)
a1 = self.temporary_annuity(x, s=s+t, t=n-t, b=b,
discrete=discrete)
A = self.endowment_insurance(x, s=s, t=n, discrete=discrete,
b=b, endowment=endowment)
a = self.temporary_annuity(x, s=s, t=n, b=b, discrete=discrete)
return A1 - a1 * (A / a)
return self.net_future_loss(A=A, A1=A1, b=b) # apply shortcut
#
# Gross Future Loss shortcuts for WL and Endowment Insurance
#
[docs] def gross_future_loss(self, A: float | None = None,
a: float | None = None,
contract: Contract | None = None) -> float:
"""Shortcut for gross policy value with WL or Endowment Insurance factors
Args:
A : insurance factor at age (x)
a : annuity factor at age (x)
contract : policy contract terms and expenses
"""
contract = contract or Contract()
if a is None: # assume WL or Endowment Insurance for twin annuity
a = self.annuity_twin(A, discrete=contract.discrete)
elif A is None: # assume WL or Endowment Insurance for twin annuity
A = self.insurance_twin(a, discrete=contract.discrete)
return ((A * contract.claims_cost + contract.initial_cost) -
(a * contract.renewal_profit))
[docs] def gross_variance_loss(self, A1: float, A2: float = 0,
contract: Contract | None = None) -> float:
"""Variance of gross loss with WL or endowment insurance factors
Args:
A1 : insurance factor
A2 : insurance factor at double the force of interest
policy : policy terms and expenses
"""
contract = contract or Contract()
interest = self.interest.d if contract.discrete else self.interest.delta
return (((contract.renewal_profit / interest) + contract.benefit +
contract.settlement_policy)**2 * (A2 - A1**2))
[docs] def gross_policy_variance(self, x: int, s: int = 0, t: int = 0,
n: int = Premiums.WHOLE,
contract: Contract | None = None) -> float:
"""Variance of gross policy value for WL and Endowment Insurance only
Args:
x : age initially insured
s : years after selection
n : term of life insurance
t : years after issue to compute policy variance
contract : policy contract terms and expenses
"""
contract = contract or Contract()
if n < 0: # WL
A2 = self.whole_life_insurance(x, s=s+t, moment=2,
discrete=contract.discrete)
A1 = self.whole_life_insurance(x, s=s+t,
discrete=contract.discrete)
elif contract.endowment == contract.claims_cost: # Endowment
n = self.max_term(x=x+s+t, t=n)
A2 = self.endowment_insurance(x, s=s+t, t=n-t, moment=2,
discrete=contract.discrete)
A1 = self.endowment_insurance(x, s=s+t, t=n-t,
discrete=contract.discrete)
else:
raise Exception("Variance for WL or Endowment Ins only")
return self.gross_variance_loss(A1=A1, A2=A2, contract=contract)
#
# Gross Policy Value for special insurance
#
[docs] def gross_policy_value(self, x: int, s: int = 0, t: int = 0,
n: int = Premiums.WHOLE,
contract: Contract | None = None) -> float:
"""Gross policy values for insurance: t_V = E[L_t]
Args:
x : age initially insured
s : years after selection
t : number of years of premiums paid
n : term of insurance
contract : policy contract terms
"""
contract = contract or Contract()
if n < 0: # Whole life shortcut
A = self.whole_life_insurance(x, s=s+t,
discrete=contract.discrete)
elif contract.endowment == contract.claims_cost: # Endowment Ins shortcut
n = self.max_term(x=x+s+t, t=n)
A = self.endowment_insurance(x, s=s+t, t=n-t,
discrete=contract.discrete)
else: # Special term insurance
n = self.max_term(x=x+s+t, t=n)
A = self.term_insurance(x, s=s+t, t=n-t, discrete=contract.discrete)
a = self.temporary_annuity(x, s=s+t, t=n-t,
discrete=contract.discrete)
endowment = 0
if contract.endowment: # endowment not equal to claims cost
endowment = self.E_x(x, s=s+t, t=n-t) * contract.endowment
initial_cost = 0 if t else contract.initial_cost
return (A * contract.claims_cost + initial_cost + endowment -
(a * contract.renewal_profit))
contract = contract.renewals(t) if t else contract # ignore initial if t>0
return self.gross_future_loss(A=A, contract=contract)
#
# Future Loss random variable: L(T_x)
#
[docs] def L_from_t(self, t: float, contract: Contract | None = None) -> float:
"""PV of Loss L(t) at time of death t = T_x
Args:
t : year of death
contract : policy contract
"""
c = contract or Contract()
k = math.floor(t) if c.discrete else t
if c.T > 0 and k >= c.T: # if endowment insurance and t is beyond term
t = c.T
endowment = c.endowment * self.Z_from_t(t)
else:
endowment = 0
return ((c.claims_cost * self.Z_from_t(t, discrete=c.discrete))
+ endowment
+ c.initial_cost
- (c.renewal_profit * self.Y_from_t(t, discrete=c.discrete)))
[docs] def L_from_prob(self, x: int, prob: float,
contract: Contract | None = None) -> float:
"""Percentile of PV future loss r.v. L given probability
Args:
x : age
prob : probability threshold
contract : policy contract
"""
contract = contract or Contract()
t = self.Z_t(x, prob, discrete=contract.discrete)
return self.L_from_t(t, contract)
[docs] def L_to_t(self, L: float, contract: Contract | None = None) -> float:
"""Time of death T_x s.t. PV future loss is no more than L
Args:
L : PV of future loss
contract : policy contract terms and expenses
"""
c = contract or Contract()
T = Contract.solve(lambda t: self.L_from_t(t, c),
target=L,
grid=(0, self._MAXAGE),
mad=True)
if not c.discrete:
return T
return math.floor(T) if self.L_from_t(t=T, contract=c) <= L else math.ceil(T)
[docs] def L_to_prob(self, x: int, L: float,
contract: Contract = Contract()) -> float:
"""Probability such that PV of future loss r.v. is no more than L"
Args:
x : age selected
L : PV of future loss
contract : policy contract terms and expenses
"""
t = self.L_to_t(L, contract)
return self.S(x, 0, t)
[docs] def L_plot(self,
x: int,
s: int = 0,
stop: int = 0,
T: float | None = None,
contract: Contract | None = None,
ax: Any = None,
dual: bool = False,
title: str | None = None,
color: str = 'r',
alpha: float = 0.3)-> float | None:
"""Plot PV of future loss r.v. L vs time of death T_x
Args:
x : age selected
s : years after selection
stop : time to end plot
contract : policy contract terms and expenses
T : point in time to indicate probability and loss values
ax : figure object to plot in
title : title of plot
dual: whether to plot survival function on secondary axis
color : color to plot curve
alpha : transparency of plot area
"""
contract = contract or Contract()
if ax is None:
fig, ax = plt.subplots(1, 1)
K = 'K' if contract.discrete else 'T'
stop = stop or self._MAXAGE - (x + s)
step = 1 if contract.discrete else stop / 1000.
steps = np.arange(0, stop + step, step)
# plot PV loss values
y = [self.L_from_t(t, contract=contract) for t in steps]
ax.bar(steps, y, width=step, alpha=alpha, color=color)
ax.tick_params(axis='y', colors=color)
#ax.plot(steps, y, '.', c=color)
xmin, xmax = ax.get_xlim()
ymin, ymax = ax.get_ylim()
yjig = (ymax - ymin) / 50
xjig = (xmax - xmin) / 50
if dual:
p = [self.p_x(x=x, s=s, t=t) for t in steps]
bx = ax.twinx()
bx.step(steps, p, '-', c='g', alpha=alpha,
where='pre' if contract.discrete else 'post')
#bx.bar(steps, p, color='g', alpha=.2, width=step, align='edge')
bx.set_ylabel(f"$S({K})$", color='g')
bx.tick_params(axis='y', colors='g')
if T is not None:
# plot indicate(T*)
z = self.L_from_t(T, contract=contract)
label1, = ax.plot(T, z, c=color, marker='o', label=f"L({T:.2f})={z:.4f}")
ax.legend(handles=[label1], loc='lower left')
# indicate corresponding S(T*)
if dual:
prob = self.S(x, s, T)
label2, = bx.plot(T, prob, c='g', marker='o',
label=f"Pr[{K}>{T:.2f}]={prob:.4f}")
bx.legend(handles=[label2], loc='upper right')
ax.set_title(f"PV future loss at issue r.v. $_0L$")
ax.set_ylabel(f"$L({K}_x)$", color=color)
ax.set_xlabel(f"${K}_x$")
#plt.tight_layout()
return z
def _L_plot(self, x: int, s: int = 0, stop: int = 0,
T: float | None = None,
contract: Contract | None = None,
ax: Any = None,
title: str | None = None,
color='r') -> float:
"""Plot PV of future loss r.v. L vs time of death T_x
Args:
x : age selected
s : years after selection
stop : time to end plot
contract : policy contract terms and expenses
T : point in time to indicate probability and loss values
title : title of plot
color : color to plot curve
"""
contract = contract or Contract()
if ax is None:
fig, ax = plt.subplots(1, 1)
K = 'K' if contract.discrete else 'T'
stop = stop or self._MAXAGE - (x + s)
step = 1 if contract.discrete else stop / 1000.
steps = np.arange(0, stop + step, step)
# plot PV loss values
y = [self.L_from_t(t, contract=contract) for t in steps]
ax.plot(steps, y, '.', c=color)
xmin, xmax = ax.get_xlim()
ymin, ymax = ax.get_ylim()
yjig = (ymax - ymin) / 50
xjig = (xmax - xmin) / 50
if T is None:
ax.plot(steps, y, ".", color=color)
ax.hlines(0, xmin, xmax, colors='g', linestyles=':')
z = None
else:
# plot indicate(T*)
z = self.L_from_t(T, contract=contract)
ax.plot(T, z, c=color, marker='o')
ax.text(T, z + yjig, f"L*={z:.2f}", c=color)
# indicate given time of death T*
ax.vlines(T, ymin, z, colors='g', linestyles=':')
ax.text(T + xjig, ymin, f"{K}={T:.2f}", c='g')
# indicate corresponding S(T*)
p = self.S(x, s, T)
ax.hlines(z, T, xmax, colors='g', linestyles=':')
ax.text(xmax, z-yjig, f"Prob={p:.3f}", c='g', va='top', ha='right')
#ax.set_title(f"Pr[${K}_x$ >= {K}(Z*)] > {p:.3}")
ax.set_title(f"PV future loss r.v. $L({K}_{{{x if x else 'x'}}})$"
if title is None else title)
ax.set_ylabel(f"$L({K}_x)$", color=color)
ax.set_xlabel(f"${K}_x$")
plt.tight_layout()
return z
if __name__ == "__main__":
from actuarialmath.sult import SULT
from actuarialmath.policyvalues import Contract
life = SULT()
x = 20
P = life.net_premium(x=x)
contract = Contract(premium=P, discrete=True)
T = life.L_to_t(L=0, contract=contract) # breakeven T
print(T)
life.L_plot(x=x, T=T, contract=contract)
print("SOA Question 6.24: (E) 0.30")
life = PolicyValues().set_interest(delta=0.07)
x, A1 = 0, 0.30 # Policy for first insurance
P = life.premium_equivalence(A=A1, discrete=False) # Need its premium
contract = Contract(premium=P, discrete=False)
def fun(A2): # Solve for A2, given Var(Loss)
return life.gross_variance_loss(A1=A1, A2=A2, contract=contract)
A2 = life.solve(fun, target=0.18, grid=0.18)
contract = Contract(premium=0.06, discrete=False) # Solve second insurance
variance = life.gross_variance_loss(A1=A1, A2=A2, contract=contract)
print(variance)
print()
print("SOA Question 6.30: (A) 900")
life = PolicyValues().set_interest(i=0.04)
contract = Contract(premium=2.338, benefit=100, initial_premium=.1, renewal_premium=0.05)
var = life.gross_variance_loss(A1=life.insurance_twin(16.50),
A2=0.17, contract=contract)
print(var)
print()
print("SOA Question 7.32: (B) 1.4")
life = PolicyValues().set_interest(i=0.06)
contract = Contract(benefit=1, premium=0.1)
def fun(A2):
return life.gross_variance_loss(A1=0, A2=A2, contract=contract)
A2 = life.solve(fun, target=0.455, grid=0.455)
contract = Contract(benefit=2, premium=0.16)
var = life.gross_variance_loss(A1=0, A2=A2, contract=contract)
print(var)
print()
print("SOA Question 6.12: (E) 88900")
life = PolicyValues().set_interest(i=0.06)
a = 12
A = life.insurance_twin(a)
contract = Contract(benefit=1000, settlement_policy=20,
initial_policy=10, initial_premium=0.75,
renewal_policy=2, renewal_premium=0.1)
contract.premium = life.gross_premium(A=A, a=a, **contract.premium_terms)
print(A, contract.premium)
L = life.gross_variance_loss(A1=A, A2=0.14, contract=contract)
print(L)
print()
from actuarialmath.sult import SULT
print("SOA Question 6.6: (B) 0.79")
life = SULT()
P = life.net_premium(62, b=10000)
contract = Contract(premium=1.03*P, renewal_policy=5,
initial_policy=5, initial_premium=0.05, benefit=10000)
L = life.gross_policy_value(62, contract=contract)
var = life.gross_policy_variance(62, contract=contract)
prob = life.portfolio_cdf(mean=L, variance=var, value=40000, N=600)
print(prob, 0.79)
life.L_plot(62, contract=contract)
print()
print("SOA Question 7.6: (E) -25.4")
from actuarialmath.sult import SULT
life = SULT()
P = life.net_premium(45, b=2000)
contract = Contract(benefit=2000, initial_premium=.25, renewal_premium=.05,
initial_policy=2*1.5 + 30, renewal_policy=2*.5 + 10)
G = life.gross_premium(a=life.whole_life_annuity(45),
**contract.premium_terms)
gross = life.gross_policy_value(45, t=10,
contract=contract.set_contract(premium=G))
net = life.net_policy_value(45, t=10, b=2000)
V = gross - net
print(V, -25.4)
T = life.L_to_t(G, contract=contract)
print(G)