The Projection Space#
The by-policy projection of the Term_US_S model; holds all formulas.
The Space is parameterized by point_id, so Projection[1] is an ItemSpace
projecting model point 1:
>>> Projection[1].result_cf() # the anchor cell
>>> Projection.point_id = 2 # or switch the default
Input data
Inputs are external files: plain CSVs living in the model folder’s parent
directory, products/term_life/, read at run time rather than stored inside the
model. The model folder therefore holds nothing but formulas — no _data/, no
IOSpec, no embedded values — so a diff of the model shows logic changes only, and an
input can be edited or swapped without rewriting the model. This follows
annuallife.TradLife_A; contrast basiclife.BasicTerm_S, which keeps its inputs
inside the model through modelx’s IOSpec machinery.
The consequence worth knowing: the model is not portable on its own. Copying the
Term_US_S folder without its parent’s CSVs produces a model that reads and then
fails on first evaluation. A test asserts this by round-tripping the model together
with its inputs.
input_dir() resolves the directory from _model.path.parent at run time, so
the model works wherever the repository is checked out. Each table has a filename
Reference and a reader Cells:
Reference |
Cells |
File |
|---|---|---|
model_point_file |
data.model_point_table() |
model_point_table.csv |
premium_rates_file |
data.premium_rates() |
premium_rates.csv |
mort_table_file |
data.mort_table() |
mort_table.csv |
class_factor_file |
data.class_factor_table() |
class_factor_table.csv |
shock_lapse_file |
data.shock_lapse_table() |
shock_lapse_table.csv |
To swap in a licensed mortality basis, replace mort_table.csv with a same-schema
file, or point mort_table_file at a different name. No formula changes.
Naming
Cells names follow lifelib’s basiclife.BasicTerm_S wherever that model has an
analogue — pols_* for policy counts, plural nouns for cash flows, *_rate for
rates, *_pp for per-policy amounts — and, for the monthly grid, the
duration_mth / duration / policy_year triple that savings.CashValue_SE
and the UL family in this library use. The technical notes use compact actuarial
symbols instead.
The time index
t counts policy months, 0-based, the same clock basiclife.BasicTerm_S
runs on: t = 0 is the issue month, period t runs from time t to time
t + 1, pols_if(t) is the count at time t (so pols_if(0) ==
pols_if_init()), and the frame is t = 0 .. proj_len() - 1 with
proj_len() = 12 * (95 - age_at_entry()) months to expiry at attained age 95.
Everything contractual about this product is nevertheless on an annual cycle —
the guaranteed premium schedule, the ART renewals, the shock lapse at the level-period
end — so the policy year is derived and used as a lookup key throughout:
duration(t) = t // 12 is the completed policy years at the start of month t,
policy_year(t) = duration(t) + 1 is the contractual 1-based label, and the attained
age is age(t) = age_at_entry() + duration(t). The frame is indexed by t; the
policy year is never indexed by.
The two-speed structure that follows is the library’s convention, asserted by
tests/test_model_conventions.py: mort_rate(t) and lapse_rate(t) are the
annual rates of the policy year containing month t — the vectors the technical
notes tabulate — and mort_rate_mth(t) and lapse_rate_mth(t) are the monthly
rates actually applied, 1 - (1 - q)^(1/12). The mapping is:
Notes symbol |
Cells |
Meaning |
|---|---|---|
x |
age_at_entry |
Issue age (ANB) |
x + dur |
age(t) |
Attained age in month t |
n |
policy_term |
Level period in years |
F |
sum_assured |
Face amount |
t = 0..12(95-x)-1 |
proj_len |
Number of months; frame ends at proj_len - 1 |
dur(t) |
duration(t) |
Completed policy years at month t |
duration_mth(t) |
Completed policy months at month t |
|
dur(t) + 1 |
policy_year(t) |
Policy year, the 1-based contractual label |
l(t) |
pols_if(t) |
In-force at start of month t (time t) |
(l(0)) |
pols_if_init |
In-force at issue |
d(t) |
pols_death(t) |
Deaths in month t |
s(t) |
pols_surv(t) |
Survivors to end of month t |
x(t) |
pols_lapse(t) |
Lapses at end of month t |
c(t) |
pols_conv(t) |
Conversions at end of month t |
(none) |
pols_maturity(t) |
Expiries at attained age 95 |
q(t) |
mort_rate(t) |
Annual mortality, all factors applied |
q_m(t) |
mort_rate_mth(t) |
The monthly rate actually applied |
(q_base) |
mort_rate_base(t) |
Base table rate before factors |
w(t) |
lapse_rate(t) |
Annual lapse rate, incl. the shock |
w_m(t) |
lapse_rate_mth(t) |
The monthly rate actually applied |
w(n-1) |
shock_lapse_rate |
Shock lapse, in full at month 12n - 1 |
cv(t) |
conv_rate(t) |
Annual conversion rate |
cv_m(t) |
conv_rate_mth(t) |
The monthly rate actually applied |
M(d) |
plt_mort_factor(d) |
PLT mortality deterioration, d = policy_year - n |
M(1) |
plt_mort_factor_init |
M(1) actually used |
(M(1) rule) |
plt_mort_factor_init_formula |
The notes’ formula for M(1) |
J |
jump_ratio |
AP(12n)/AP(12n - 1), fee included |
AP(t) |
premium_pp_ann(t) |
Guaranteed annualized premium, policy year of t |
(modal) |
premium_pp(t) |
Premium per policy actually due in month t |
(modal factor) |
modal_factor |
Modal factor of the model point’s premium mode |
(modal cycle) |
prem_cycle |
Months between instalments |
(due month) |
prem_due(t) |
Whether a modal premium falls due in month t |
G(t) |
premiums(t) |
Premium income |
K(t) |
commissions(t) |
Commission |
k(t) |
comm_rate(t) |
Commission rate |
X(t) |
premium_taxes(t) |
Premium tax |
E(t) |
expenses(t) |
Acquisition + maintenance |
DC(t) |
claims(t) |
Death claims |
CV(t) |
conv_credits(t) |
Conversion credit outflow |
NetCF(t) |
net_cf(t) |
Net cash flow |
phase(t) |
phase(t) |
LEVEL / PLT / EXPIRED |
conv_elig(t) |
conv_elig(t) |
Conversion eligibility |
(none) |
result_cf_annual() |
result_cf() summed into policy years |
Three notes on the mapping. The notes write deaths as d(t) while also using d
as the post-level-term duration index in M(d); the pols_death / plt_mort_factor
split removes that collision. The notes’ x(t) (lapses) and X(t) (premium tax)
differ only by case, which pols_lapse and premium_taxes separate. And
pols_maturity has no symbol in the notes at all — see below.
The shock lapse is not spread
Every ordinary decrement is converted to a monthly rate at 1 - (1 - q)^(1/12), so
twelve months of it compound back to exactly the annual rate the notes tabulate. The
shock lapse is the one exception the notes make explicitly: it is not spread, but
applied in full at the end of the final level-period month, t = 12n - 1,
immediately before the first ART premium falls due at t = 12n. Its policy year’s
annual rate w(n-1) is the shock, so lapse_rate_mth() returns zero in the
other eleven months of that year and the shock itself in the last one.
That is what makes the two grids reconcile: because the ordinary monthly rates
compound to the annual ones and the shock falls at a year boundary, the in-force
at every policy anniversary is identical to the annual-step model’s
— pols_if(12k) here equals pols_if(k) there, to floating-point. The cash flows
are not identical and are not meant to be: claims now fall at the end of the month of
death rather than the end of the policy year, maintenance expense accrues monthly, and
a modal premium is collected when it is contractually due. Those timing differences
are the reason for the monthly grid. result_cf_annual() sums the frame into
policy years so the two can be laid side by side.
Premium mode
premium_pp(t) is the premium actually collected in month t: the model point’s
premium_mode picks the modal factor (modal_factor()) and the payment months
(prem_cycle()) — annual in month 0 of each policy year, semi-annual every sixth
month, quarterly every third, monthly every month — and the annualized guaranteed
premium AP(t) the notes tabulate is premium_pp_ann(). Modal loading is
inside the factor (twelve monthly payments come to 0.99996 of the annual premium, six
two semi-annual instalments to 1.04 of it) exactly as the specimen prints it [S6]. The
jump ratio and the conversion credit are both on AP, which is what keeps them
mode-independent; commission is a rate keyed by policy year applied to the premium
actually collected, so a modal payer earns it in instalments too.
pols_maturity
The notes give the roll-forward as l(t+1) = l(t)(1-q_m)(1-cv_m)(1-w_m) and,
separately, the rule l(t) = 0 for x + duration(t) >= 95. Those do not reconcile in
the final period of the frame, t = proj_len() - 1: its survivors neither die, lapse
nor convert — their coverage simply runs out. Without a term for that, the roll-forward
appears to lose lives with no cause. pols_maturity(t) names it, zero in every period
but the last, so that
pols_if(t) - pols_if(t+1) = pols_death(t) + pols_lapse(t) + pols_conv(t) + pols_maturity(t)
holds for every t in the frame. It is bookkeeping determined by the notes’ own
rules, not an added assumption. The name follows BasicTerm_S.pols_maturity.
Cells Descriptions#
The premium mode of the selected model point: A, SA, Q or M [S6].
- proj_len()[source]#
The number of policy months projected: coverage ends at attained age 95.
12 * (95 - age_at_entry()). The frame ist = 0, 1, ..., proj_len() - 1;proj_len()is its exclusive end and the row count ofresult_cf().
- duration_mth(t)[source]#
Completed policy months at the start of month t.
titself, since every model point in this product is projected from issue. The cells exists so the monthly vocabulary reads the same here as in the UL family andsavings.CashValue_SE, where an in-force point can open mid-policy.
- duration(t)[source]#
Completed policy years at the start of month t,
duration_mth(t) // 12.0 throughout the first policy year. Every contractual schedule in this product is annual, so this is what the attained age and the premium schedule are read at.
- policy_year(t)[source]#
The contractual policy year containing month t,
duration(t) + 1.A 1-based label derived from
t, never indexed by: the guaranteed premium schedule inpremium_rates.csvis keyed by it, and so are the annual decrement vectors.
- age(t)[source]#
The attained age (ANB) in month t:
age_at_entry() + duration(t).Age changes on the policy anniversary rather than on the birthday, which is the ANB convention the whole model is built on [std].
- modal_factor()[source]#
The model point’s modal factor: the fraction of AP collected per instalment [S6].
Annual 1.0, semi-annual 0.52, quarterly 0.27, monthly 0.08333 — the specimen’s own scale, so the modal loading is inside the factor. Each is its own Reference, so a carrier’s own scale drops in without a formula change.
- prem_cycle()[source]#
Months between premium instalments: A 12, SA 6, Q 3, M 1.
Arithmetic of the mode rather than an assumption, so it is written here rather than held in a Reference.
- prem_due(t)[source]#
Whether a modal premium falls due at the beginning of month t.
Payments start at issue and repeat on the mode’s own cycle: annual in month 0 of each policy year, semi-annual every sixth month, quarterly every third, monthly every month.
AP(t): the guaranteed annualized gross premium per policy, fee included.
The notes’
AP, looked up inpremium_rates.csvatpolicy_year(t). It is the base for the jump ratio and the conversion credit, neither of which should move with the premium mode;premium_pp()is what is actually collected in month t, and it is that instalment — notAP— thatcommissions()andpremium_taxes()are charged on.
The premium per policy actually due at the beginning of month t.
modal_factor() * AP(t)in a payment month, zero otherwise.
- jump_ratio()[source]#
Initial premium jump ratio, fee included: the first ART annualized premium over the last level one,
premium_pp_ann(12n) / premium_pp_ann(12n - 1)(policy years n+1 and n).
- plt_mort_factor_init_formula()[source]#
The notes’ rule for M(1): min(8.0, 1 + 0.55*(J-1)) [std].
Returns 3.4514 for the anchor cell, where the worked example uses 3.50. Not used unless the model point leaves plt_mort_factor_override blank.
- plt_mort_factor_init()[source]#
M(1) actually used: the model point’s override if given, else the formula.
- plt_mort_factor(d)[source]#
Post-level-term mortality deterioration at PLT duration d; grades to 2.00 [std].
dis the notes’ PLT duration in years,d = policy_year(t) - n:d = 1in the first post-level-term policy year (monthst = 12n .. 12n + 11). It is not the frame indext.
- mort_rate(t)[source]#
The annual mortality rate of the policy year containing month t.
Base x class factor x PLT deterioration. The PLT duration is
d = policy_year(t) - n, so the multiplier applies fromt = 12n(d = 1) on.mort_rate_mth()is the rate actually applied in the month.
- mort_rate_mth(t)[source]#
q_m(t): the monthly mortality rate applied in month t [std].
1 - (1 - q)^(1/12)on the policy year’s annual rate, so twelve months of it compound back to exactly that rate — the notes’ own conversion.
- lapse_rate(t)[source]#
The annual lapse rate of the policy year containing month t [std].
The notes’
wvector, keyed byduration(t). Level period (duration = 0 .. n-1): 6% in policy year 1, 5% in year 2, 4% in years 3..n-2, 6% anticipatory in year n-1, and the shock in year n. Post-level term: 30%, 15%, then 10% by PLT durationd = policy_year(t) - n.lapse_rate_mth()is the rate actually applied in the month, and it treats the shock year specially — see there.
- lapse_rate_mth(t)[source]#
w_m(t): the lapse rate applied at the end of month t [std].
1 - (1 - w)^(1/12)on the policy year’s annual rate in every month except those of the final level-period year. The notes are explicit that the shock is not spread: that year’s annual rate is the shock, so it falls in full at the end of the final level-period month,t = 12n - 1— immediately before the first ART premium is due att = 12n— and the other eleven months of the year carry no ordinary lapse at all.
- conv_rate(t)[source]#
The annual conversion rate of the policy year containing month t [std].
Zero outside the eligibility window;
conv_rate_finalin the last eligible policy year,conv_rate_basebefore it. Both ship at 0, as the worked example requires.
- conv_rate_mth(t)[source]#
cv_m(t): the monthly conversion rate applied at the end of month t [std].
1 - (1 - cv)^(1/12)on the policy year’s annual rate, the same conversion the other voluntary decrement takes.
- pols_if(t)[source]#
Number of policies in-force at the start of month t, i.e. at time t.
pols_if(0) == pols_if_init(); zero fromt = proj_len()on, when coverage has expired at attained age 95.
- pols_surv(t)[source]#
Number of policies surviving to the end of month t, before voluntary decrements.
- pols_maturity(t)[source]#
Number of policies whose coverage ends at attained age 95.
Non-zero only in the final period of the frame,
t = proj_len() - 1. Not a decrement - the contract runs out - but needed for the in-force roll-forward to close; see the Space docstring.
- comm_rate(t)[source]#
Commission rate [std]: 80% in policy year 1 (
duration(t) == 0), 5% to the end of the level period, 2% after. It is a rate on the premium collected, so a modal payer pays it in instalments too.
- inflation_factor(t)[source]#
The expense inflation factor in month t,
(1 + inflation_rate) ** (t / 12).Compounded on the monthly grid rather than stepped at anniversaries: on an annual model the factor can only move once a year, and the monthly refinement is one of the things the finer grid buys.
Premium income in month t, at the beginning of the month.
Premium tax in month t [std].
- expenses(t)[source]#
Acquisition (
t = 0only) and inflating maintenance expenses in month t [std].One twelfth of the annual maintenance charge accrues each month, so a policy that runs a full year carries the same charge as it did on the annual grid — but it is now borne by the in-force of each month rather than of the anniversary, which is what makes a decrementing block cost less.
- claims(t)[source]#
Death claims incurred in month t, paid at the end of the month.
Face amount only: the notes’ simplification (i) — the pro-rata unearned-premium refund and the due-unpaid-premium deduction on death [S6] are not modelled. On the monthly grid the item is bounded by one modal premium, so it is immaterial by construction for a monthly payer and at most one annual premium on the deceased cohort for an annual one.
- conv_credits(t)[source]#
Conversion credit outflow: one annualized premium per conversion, after the first policy year (
duration(t) >= 1). The credit is contractual and stated on the annual premium [S6], so it does not move with the premium mode.
- result_cf_annual()[source]#
result_cf()summed into policy years, indexed bypolicy_year.Every cash flow column is the total of its twelve months;
pols_ifis the count at the start of the policy year,pols_if(12 * (policy_year - 1)), which is the number the annual-step model carried on the same row.On the shipped annual-mode points the two grids agree exactly on five columns —
pols_if,premiums,commissions,premium_taxesandconv_credits— because an annual premium is collected on the anniversary and weighted by the anniversary in force under either grid, and the other three are rates on it. They do not agree onclaimsorexpenses, which now fall where they happen rather than at the anniversary, and so not onnet_cf. That gap is the point of the finer grid — see the Space docstring.