Implementation Notes#
Status: Draft, 2026-08-20. Built from technical-notes.md; the
product it implements is specified in product-spec.md. Every parameter
value here is one of theirs, and where the model carries something the notes do not settle,
it is named below rather than absorbed.
This is a mechanics demonstration, not a pricing or reserving result. What is sourced on this product is the contractual machinery: the either/or public-certification and company-basis trigger, the once-only care lump sum (kaigo ichijikin, 介護一時金) that does not terminate the contract, the survival-tested care annuity (kaigo nenkin, 介護年金) paid in advance with its ten-instalment cap and retroactive extinction, the 保険料払込免除 (hokenryō haraikomi menjo, premium waiver) firing one grade below the lump sum, and the outright nil surrender value (kaiyaku-henreikin, 解約返戻金) [S1] [S2] [S4] [S7] [S8] [S12]. Everything quantitative is a std standardization. No carrier publishes 予定発生率 (assumed incidence), assumed interest rate (yotei riritsu, 予定利率) or 予定死亡率 for this product, the regulator confirms there is nothing standard to publish for 第三分野 (dai-san-bun’ya, third-sector) business R10, and the 算出方法書 is a filed 基礎書類 that is not public REG-R2 — so the office premium is a model-point input, the mortality table is a constructed proxy, and the whole morbidity basis is built in public from the government’s certification rate (nintei-ritsu, 認定率) and grade composition R4 R5 REG-R30. Replace the assumption tables with company data before drawing any conclusion from the output.
LTC_JP_S is the modelx implementation of the notes: a monthly, single-model-point
projection of gross best-estimate liability cash flows for nursing-care insurance
(kaigo hoken, 介護保険) on the public-scheme-linked (kōteki kaigo hoken rendō-gata,
公的介護保険連動型) design.
The chassis. This product states its deltas against the jplib third-sector chassis,
whose model is Medical_JP_S and whose documents are
the medical product specification and
the medical technical notes. LTC_JP_S does not
inherit from that model in modelx — Projection._bases is empty and every formula here is
written out — so the relationship is documentary, not structural. What is inherited in
substance is the monthly grid, the timing conventions, the attained age (man-nenrei, 満年齢)
basis, the mortality construction from 第三分野標準生命表2018, the lapse table and the expense
structure; what is replaced outright is the benefit machinery. Medical_JP_S is
frequency × severity × limit with two day ledgers that never bind; LTC_JP_S is
incidence into an absorbing state with a payment counter that does. There is no d_pay,
no d_ben, no L1, no LA and no agg_days_* cells anywhere in this model.
Run it#
From the repository root:
python lifelib/libraries/jplib/products/nursing_care/run.py # anchor cell, point 1
python lifelib/libraries/jplib/products/nursing_care/run.py 5 # another model point
or, three lines to the same thing:
import modelx as mx
model = mx.read_model("lifelib/libraries/jplib/products/nursing_care/LTC_JP_S")
model.Projection[1].result_cf()
Projection takes a point_id; Projection[1] is the worked example’s anchor cell and
reproduces the notes’ four-row table, its month-by-month trace and its policy-year-1
aggregate to the precision the notes display. result_cf() is a tidy DataFrame indexed by
policy month t; result_pols() is its policy-count companion. The Projection docstring
holds the full mapping between the notes’ symbols and the cells names.
Four ledgers, nested rather than disjoint#
This is not the third-sector chassis’s frequency × severity × limit shape. It is incidence into an absorbing state, and it is carried as four ledgers:
Cells |
What it counts |
|---|---|
|
In force, not yet certified at |
|
Entered |
|
Entered |
|
Entered |
pols_if(t) = pols_act(t) + care_w(t) is the definition. The other two are marginal
first-entry counters riding on the same survival ledger, not compartments, and the fact
that makes them work is an ordering rather than a sum:
care_n(t) <= care_l(t) <= care_w(t) <= pols_if(t) at every t
check_nesting() asserts exactly that, and never a total. Adding the three together counts
the same lives three times, and losing the ordering lets a life start the annuity before its
lump sum has been paid — which the contract forbids, since an unpaid lump sum is paid
together with the first instalment [S1].
The ordering is preserved by the recursion rather than imposed on it, and that took the
entry pools to be different for the three grades. pols_entry_w(t) is drawn from
pols_act(t) alone, because a life already certified at the waiver grade cannot enter it
again. pols_entry_l(t) and pols_entry_n(t) are drawn from everyone in force who has
not yet reached that grade — pols_if(t) - care_l(t) and pols_if(t) - care_n(t) — which
includes lives already on waiver, because progression up the certification ladder is the
dominant route into the higher grades, not direct entry from health. Since the grade shares
fall as the grade rises, the monthly entry rates are ordered the same way and the nesting
follows.
認定率 is a prevalence, and the conversion has two terms#
What the government publishes is a point-in-time count of certified persons — 19.4% of 第1号被保険者 at 31 March 2024, and 50.8% of them at 要介護2以上 R4 R5 REG-R30. It is not a flow of new certifications, and multiplying it by a benefit amount as if it were an annual claim frequency is the notes’ first-listed pitfall. In an illness-death model with no recovery the conversion is an identity, not an approximation:
i_G(x) = prev_G'(x) / (1 - prev_G(x)) + prev_G(x) x (mu_C(x) - mu_H(x))
Three implementation decisions follow, and each is a way the arithmetic can be wrong while the code still runs.
prev_slope(t) is the analytic derivative of the fitted logistic, beta x prev x (1 - prev/prev_ceil), not a difference quotient: this term goes straight into the claim rate, so
a numerical derivative would put its noise there too.
The second term is not a refinement. A rising prevalence understates incidence, because
the certified population is simultaneously being drained by its own excess mortality. It
enters as prev_G x (k - 1) x mort_rate, where k = care_mort_mult = 2.75, so k is not
only a post-onset assumption: setting it to 1 — which is what “there is no impaired-life
table, so leave it out” amounts to — cuts lifetime lump-sum claims on the anchor cell by
about 31%. That coupling is the least obvious property of this model, and the test suite
runs the k = 1 case to hold the size of it.
The dimensional check that catches the whole family of errors: prev and prev_G are
dimensionless proportions, beta carries units of 1/year, and both terms are therefore
rates per year and may be added. A formula that adds a prevalence to a rate is the commonest
way to get this wrong. The conversion also rests on a stationary-population assumption
std — the cross-sectional 認定率 by age read as the path a cohort will follow — which the
notes state and the model inherits unchanged.
The annuity ledger: in advance, capped, and a partial product#
ann_count(t) sums the cohorts entering at t, t − 12, …, t − 12(n_A − 1), each
weighted by care_surv(s, t). Three things in that sentence are load-bearing.
The j = 0 term is pols_entry_n(t) itself — payment in advance, on the entry month
[S1] [S7]. Deferring the first instalment by a year removes roughly a tenth of the annuity
liability and misdates all of it.
care_surv(s, t) is a partial product of the monthly care-state survival factors from
s to t, never a ratio SC(t)/SC(s) of cumulative products. The care-state mortality
rate reaches 1 at the terminal age, so the cumulative product underflows to zero and the
ratio form divides by zero exactly where the tail of this liability lives.
pols_term(t) is the cohort taking its last permitted instalment. It is removed from
care_n, care_l, care_w and hence from pols_if — the contract’s retroactive
extinction on the tenth instalment [S1], expressed on a monthly grid. On the anchor cell
the cap binds: entrants take 4.62 instalments on average, 14.9% of them reach the tenth,
and removing the cap raises lifetime annuity cost by 11.4%. Do not carry the medical
chassis’s intuition across — there the 通算 aggregate limit reads zero forever.
check_ann_ledger() rebuilds the month’s instalments by scanning every month in the cap
window and keeping the annual anniversaries, instead of stepping back in twelves. A ledger
that paid the first instalment a year late, ran past the cap, or used the ratio form of
care_surv shows up in its residual rather than in a total, where it would be invisible.
The step at age 65, and where “180日” means two different things#
f_age(t) is the sub-65 specified-disease (tokutei shippei, 特定疾病) gate: 0.20 below 65
where the company-basis limb is written, 0.05 without it, 1.00 at 65 and over [S1] [S4]
[S12] R1 R3. Entry into 要介護2以上 jumps 6.1× between age 64 and age 65 on the shipped
basis. That step is a real feature of the product — a smooth curve through 65 misprices
every issue age in the lower half of the 40–79 issue range, which is most of it — and the
gate is the company-basis limb’s only effect in the model: the limb’s own adjudication
is out of scope [std scope].
“180日” names two mechanisms in this market and an implementation must not carry one of them
twice. An exclusion period (futanpo kikan, 不担保期間) or a 認知症診断責任開始期 means cover has not
started; the 180-day (90-day where dementia-defined) test inside the company-basis trigger
means the care state must have persisted. The composite has the second and not the first
on the care benefits, so waiting_mths() is zero on the anchor cell and claims run from
month 0. The one waiting period in the model belongs to the rider. Related timing trap, also
implemented: a 要介護認定 takes effect retroactively to the application date R1, so every
claim is dated at the month the trigger is met, not at notification.
Absences that are product facts#
Stated rather than left to inspection, and each asserted by a test so it cannot be added back by accident:
No day ledger of any kind. No
d_pay, nod_ben, noagg_days_*, no 日額. The unit of account is an annual annuity instalment, not a paid day, so the 支払限度日数 machinery of the third-sector chassis has no counterpart.No
cv_pp. There is no 解約返戻金 at any duration [S1] [S2] [S7] [S8].claims(t, "LAPSE")exists, returns zero and is published as a zero column, because a missing column would hide the product fact rather than state it.No
claims_death. The contract terminates on death with nothing payable [S1] [S11].No maturity. Cover and premiums are both whole of life (終身), so
proj_len()is set by the mortality table’s terminal age — 116 male, 118 female, read from the table rather than hard-coded — and there is nopolicy_term, no maturity benefit and no renewal.
Inputs are external files#
The model folder holds formulas only. Its five inputs are plain CSVs in this directory, read
once per model by the Data Space and reached from Projection through its data
Reference:
products/nursing_care/
model_point_table.csv <- inputs live here
mort_table.csv
lapse_table.csv
prevalence_table.csv
grade_share_table.csv
run.py
model.md product-spec.md technical-notes.md sources.md
LTC_JP_S/ <- formulas only
__init__.py _system.json
Data/__init__.py (reads the CSVs, once per model)
Projection/__init__.py (the by-policy projection)
File |
Contents |
Provenance |
|---|---|---|
|
Eight model points indexed by |
anchor cell std, the notes’ worked example |
|
Table mortality by sex and attained age, 40 to the terminal age, with a |
std log-linear graduation through eighteen anchor rates quoted from 第三分野標準生命表2018 REG-R18 REG-R20; not a copy of the published table REG-R21 |
|
Annual lapse by policy year, 9 / 7 / 6 / 5.5 / 5 / 4.5 / 3 % |
std durational shape, anchored to the 5.6% p.a. industry 解約・失効率 REG-R31 |
|
The two sourced 認定率 anchors and the three logistic parameters fitted through them |
|
|
Share of all certified persons at each grade or above |
This follows lifelib’s annuallife/TradLife_A, which keeps its input beside the model and
reads it at run time, rather than basiclife/BasicTerm_S, which stores its inputs inside
the model. Data.input_dir() resolves from _model.path.parent, so it works wherever the
repository is checked out. The trade-off: copy LTC_JP_S/ without the CSVs and it reads
fine, then fails on first evaluation. What it buys is that a diff of the model shows logic
changes only, and that a company basis drops in by replacing a same-schema file — no formula
changes. Both halves are covered by tests.
The mortality table is a construction, not a copy#
第三分野標準生命表2018 is published free, in full, at a stable public URL, so anyone can retrieve it
and check a rate — a real contrast with the restricted tables uklib has to work around.
But the publisher’s site terms prohibit reproduction REG-R21, so this library ships a
documented proxy anchored to a table you can go and read.
mort_table.csv is that proxy, and it is now the library-wide canonical one: the same
file, cell for cell and provenance string for provenance string, is shipped by every jplib
product that reads 第三分野標準生命表2018. Its construction has two parts.
Anchors. Eighteen rows — nine per sex — carry a rate quoted from the published table
REG-R18 REG-R20: ages 40, 60, 65, 75, 80, 85, 90, 115 and 116 for males, and 40, 60, 65,
70, 75, 80, 85, 90 and 118 for females. Each says so in its provenance column, marked
ANCHOR, and each is a quoted rate inside a std construction rather than a copy of
the table REG-R21. The male q60 = 0.00548 the worked example quotes is one of them.
Graduation. Between adjacent anchors the rate is log-linear in age,
q(x) = q(a) x ( q(b) / q(a) ) ** ( (x - a) / (b - a) ) for a <= x <= b
which reproduces every anchor exactly by construction — nothing sourced is disturbed by
the fitting — and is locally the Gompertz family, the family the published table itself
follows over this range. Above the last female anchor at 90 the female curve is continued at
the male age-90-to-115 log-slope and closed on the sourced female terminal rate. The shipped
file is restricted to ages 40 and over, which is the whole of this product’s issue-age
range, and q = 1 at the terminal ages 116 and 118.
The property that matters for this product is that the incidence identity reads the
mortality basis at every age — its second term is prev_G x (k - 1) x mort_rate(x) — so
every derived rate is an output of whichever mortality table is underneath it. Under the
canonical table six of the eight ages in the notes’ entry-rate ladder (60, 65, 75, 80, 85
and 90, male) now sit on a quoted rate rather than on a fitted one, and the other two
are interpolated between quoted rates. That is a strictly better position than the single-pin
graduation this product shipped before, which agreed with the source at exactly one age.
What remains std is the interpolation rule and the extension of the female curve above
90, both of which say so in the provenance column.
The prevalence tail above age 82 is the larger of the two, and it carries the claims.
The logistic in prevalence_table.csv is pinned to the two sourced 認定率 at ages 70 and 82
and to nothing above 82. It has three parameters and two anchors, so one degree of
freedom is unidentified, and the free one — prev_ceil = 0.95 std — is exactly the
one that sets the tail. prev(90) = 0.662 and prev(100) = 0.894 are therefore unsourced
extrapolations, not fitted values, and 40.2% of the anchor cell’s lifetime benefit
outgo (74.7% at issue age 79, model point 8) falls at attained age 83 or over. That is a
first-order model risk, not a detail of the fit: refitting the same anchors at
prev_ceil = 0.60 / 0.50 / 0.40 gives prev(90) = 0.517 / 0.459 / 0.388 and moves lifetime
benefit outgo by −4.2% / −5.8% / −6.7%, with the timing of the claims moving more than the
total. technical-notes.md sets this out under Key sensitivities and model risks, and a
test pins both tail rates and the extrapolated claim share so neither can drift into looking
sourced.
Modules that are off in the base run#
Each is implemented, switched off on the anchor cell so the base run reproduces the worked example, and exercised in its other position by a shipped model point.
Module |
Switch |
Off (anchor cell) |
On |
|---|---|---|---|
State-tested annuity and its recovery decrement |
|
|
point 5: |
Anti-selective lapse, loading incidence |
|
0.0 |
point 8: 0.30, where cumulative lapse has passed the 20% reference point |
認知症一時金特約 (ninchishō ichijikin tokuyaku), a dementia lump-sum rider (tokuyaku, 特約) |
|
false |
point 6: adds ¥47,673 of lifetime rider claims |
1-year 不担保期間 of the simplified-underwriting design |
|
false |
point 7: zeroes all three entry rates and every claim for |
Company-basis trigger limb |
|
true, so the sub-65 gate is 0.20 |
point 4: written off, dropping the gate to 0.05 |
The recovery rate and the state test must be tested together. rec_rate() returns zero
unless the annuity is state-tested, so with the rate at zero the switch is an exact no-op —
the test suite asserts that the state-tested run with rec_rate = 0 reproduces the
survival-tested run cash flow for cash flow. Recovery is deliberately scoped to the annuity
ledger [std scope]: a paid lump sum cannot be unpaid [S1], and a recovery decrement on
care_l would let the same life claim the once-only benefit twice.
Known limit of that switch: a recovered life keeps its 保険料払込免除 for life. rec_rate is
the rate of falling below the annuity grade G_N = 要介護3, not the rate of returning to
health, and it is applied to care_n alone. A life that recovers therefore leaves the
annuity ledger and may re-qualify on a new 介護年金支払基準日, but stays in care_w: it never
re-enters pols_act, never resumes premium and is never again exposed to lapse.
For the step the switch models this is correct — G_W = 要介護1 is two grades below G_N,
so a 要介護3 → 要介護2 downgrade stops a state-tested annuity and leaves the waiver running,
which is what the ladder says [S1] [S2] [S8]. What is not modelled is recovery below
G_W, the downgrade that would end the waiver and restore the premium. No retrieved source
publishes a transition matrix between grades, and a single rate cannot carry two thresholds,
so the model implements the threshold it can evidence and names the other as a gap. The
direction is known: under annuity_test = state with rec_rate > 0, premium income is a
lower bound and the waived band an upper bound — a lower bound in the exact sense that
pols_act(t), and so every premium, is identical to the rec_rate = 0 run while the
annuity ledger is materially smaller. The one route by which recovery reaches care_w is
the ten-payment cap, and it moves it upward: a recovered life takes no tenth instalment,
so pols_term(t) extinguishes fewer contracts. Nothing ever releases a life back into
pols_act. The base run is unaffected — rec_rate is zero there — and
test_a_recovered_life_keeps_its_waiver_for_life asserts every clause of this, so it is a
documented limit rather than a silent one.
Two further constructions are outside the model and named rather than hidden: the right to change base rates (基礎率変更権), whose exercise is not modelled although the parameterized incidence basis is precisely the capability the regime asks for R10 REG-R15; and reinstatement (fukkatsu, 復活), which belongs here as a new model point rather than as a negative lapse, because the 責任開始期 resets to the 復活日 [S1] [S2].
Sign convention#
The notes’ net_cf(t) is already income positive — premiums less claims, acquisition
and maintenance expense, the claim-handling expense and commission, each on its own line —
which is the library-wide sign of net_cf. So unlike the models whose notes
print the stream outgo-positive, there is no liability_cf companion column here: one
stream, one sign, one name. check_net_cf() re-adds the statement from exactly the columns
result_cf() publishes, so a cash flow that exists in net_cf but not in the statement, or
the reverse, fails a test rather than hiding in a total.
Naming#
Cells names follow lifelib’s basiclife/BasicTerm_S wherever it has an analogue: pols_*
for policy counts, plural nouns for cash flows, *_rate for annual rates with *_rate_mth
for their monthly counterparts, *_pp for per-policy amounts, claims(t, kind) with an
uppercase kind, and pols_if_at(t, timing) for the within-month reads. The notes use
compact actuarial symbols and the full mapping lives in the Projection docstring. Four
cases needed care:
Notes |
Cells |
Why |
|---|---|---|
|
|
|
|
|
It is a policy count, not a term of the contract — and the bare word in a life model reads as the policy term, which this whole-of-life product does not have |
|
|
The library rule is that a bare |
|
|
These are proportions, not rates; naming them |
Four further names carry library-wide rulings rather than this product’s own reasoning, and
each means the same thing in all nine jplib models:
Name |
Ruling |
|---|---|
|
|
|
|
|
The multiplier turning the shipped valuation table into the projection basis, spelled the same way in all nine models. It is not an actual-to-expected ratio, which is a different thing |
|
The name is the benefit’s form — a stream of instalments — not its contingency, and the library uses it for both kinds. Here it is a living benefit: the 介護年金 is paid while the insured survives in a certified care state at 要介護3以上, and death stops it. In |
|
|
Standardizations used#
Everything the notes mark std is carried unchanged: mort_be_factor = 1.25; the care-state
mortality multiple care_mort_mult = 2.75; the prevalence logistic (prev_ceil = 0.95,
prev_beta = 0.194069, prev_x_mid = 85.710591); the constant grade shares 0.715 / 0.508 /
0.340; the sub-65 gate 0.20, or 0.05 without the company-basis limb; rec_rate = 0; the
monthly conversion of every annual rate; uniform incidence within the policy year; the
processing order of incidence, then mortality, then lapse; the lapse table; ¥20,000
acquisition expense; ¥250 monthly maintenance inflating at 1.0% p.a.; ¥5,000 per claim
event; initial commission at 150% of annualized premium and renewal at 3.0% from policy year
2; and the office premium itself, ¥11,500 a month on the anchor cell.
Three standardizations are the model’s own, because the notes do not settle them, and all
three are declared in the Projection docstring as well as here:
The shape of the mortality construction — the log-linear interpolation rule between the quoted anchors, and the extension of the female curve above its last anchor at 90. The notes publish rates, not a table; the shape that joins them is the model’s own.
The rider’s incidence —
dementia_share = 0.35of the entry rate into the lump-sum grade, after a six-month 認知症診断責任開始期, with the MCI limb and the dementia limb carried as one event paying(1 + mci_fraction) x dementia_amountat the dementia date. No published dementia incidence basis appears in any retrieved source; the merge gets the amount right and dates the 10% MCI limb late.A cap at 1 on
mort_rateandmort_rate_care— a guard, not an assumption. On the shipped table2.75 qfirst exceeds 1 at male age 105 and female age 111, so the cap binds from there; without it the monthly conversion1 - (1 - q)^(1/12)returns a complex number at the terminal age.
The build corrected technical-notes.md where it and the model disagreed, and they now
agree. In three places the notes’ own arithmetic was wrong: the prevalence at age 60 and
every prevalence, slope and incidence figure derived from it (a relative error of about 6e-6
in the notes’ arithmetic), the corresponding digits in the three month traces, and the
description of mort_be_factor = 1.25 as the reciprocal of the midpoint of the sourced
70%–85% band, which it is not — 0.80 is a round value inside the band, and the value 1.25 is
unchanged.
The fourth restatement is the adoption of the library-wide canonical mortality table
described above, and it is the largest. Every derived figure in the notes that reads the
mortality basis was recomputed on it — not adjusted, but re-derived from the contractual
mechanics. Nothing sourced moved: the one rate the worked example quotes, male
q60 = 0.00548, is an anchor of the canonical table and is unchanged, so the worked
example’s four rows, all three month traces and the whole policy-year-1 aggregate are
unchanged to the last displayed digit. What moved is everything computed off the table at
ages other than 60:
the entry-rate ladder into 要介護2以上 at ages 64 to 90 — 0.000295 / 0.001795 / 0.004795 / 0.012413 / 0.030456 / 0.065003 / 0.115568, against 0.000296 / 0.001808 / 0.004861 / 0.012716 / 0.030609 / 0.062836 / 0.108219 before — with age 60 unchanged at 0.000134;
the gradients it quotes: 65-to-90 is now a factor of 64 (was 60) and 60-to-90 a factor of 862 (was 807);
the lifetime loss ratio, 34.7% (was 34.6%), with lapse off 53.5% (was 53.4%) and at half lapse 43.5% (was 43.3%);
the share of the block in force at age 85, 0.126 (was 0.123), and the premium exposure behind the two premium-scale ratios, 41.8% and 27.3% (were 41.5% and 27.2%);
the annuity ledger’s average instalment count and tenth-instalment share, 4.62 and 14.9% (were 4.66 and 15.1%);
the waiver’s yen cost, ¥92,738 and 5.53% of the in-force-weighted premium (was ¥91,922 and 5.52%);
four sensitivities —
k= 4’s annuity effect −8.3% (was −8.4%), the five-instalment cap −25.9% (was −26.1%),f_sub65= 1’s loss ratio 36.1% (was 35.9%) — and the two module totals, point 5’s annuity ¥360,416 (was ¥337,364) and point 6’s rider ¥47,673 (was ¥46,890);and the age at which the care-state mortality cap first binds, male 105 and female 111 (were 103 and 108).
Figures that were recomputed and found unchanged at the precision they are quoted were left
alone rather than restated: the 6.1× step at 65, the 30.1% waiver band at age 85, the 5.85%
premium overstatement, k = 1’s −31.0% and k = 4’s +12.4% on the lump sum, the grade
shares’ +7.5%, the cap’s +11.4%, point 5’s −12.4% recovery effect, and the crossover to
negative net_cf in policy month 228 at attained age 79. Every figure on both lists is
asserted by a test, so the restatement is held in place rather than trusted.
product-spec.md needed no change: it carries none of the derived figures, and every
contractual parameter in the notes is unchanged and still identical to its.
Tests#
tests/test_model_conventions_jp.py asserts the house style — the external-inputs layout,
the read-once property, the Data / Projection split, the docstring contract, the
result_cf column vocabulary, the no-argument check_* shape and the read-write-re-read
round trip. tests/test_nursing_care_jp.py asserts this product, in five blocks:
The worked example, hard-coded as module-level tables so a reviewer can check it against the notes by eye: every assumption value the notes list, the four-row table, all three month traces term by term, the policy-year-1 aggregate with its closing ledgers, and the entry-rate ladder by attained age — the one contract with the notes that binds at ages other than the pinned age 60.
Every entry in the notes’ Known modeling pitfalls list, one test each, named after the pitfall. Each is a way an implementation can look right and be wrong, so each test is written to fail if the pitfall were committed rather than to restate the right answer.
The identities: the in-force roll-forward naming every decrement, and all four
check_*cells with their per-tresiduals, on all eight model points.Each optional module in both positions — off in the base run and switched on. Where the switch is a model point column rather than a Space Reference, the test copies the model and its external inputs to a temporary directory and edits the copy, so the shipped tables are never written to.
The structural facts and the notes’ key sensitivities: the nesting, the binding cap, the in-advance annuity, the partial-product survival, the step at 65, the whole-of-life run-off, the absent day ledger, the zero lapse claim, the mortality cap, and the eight sensitivity figures the notes quote — so a change of basis that moved one of them would have to move the document too.
The mortality-table test is the one worth naming on its own: it asserts that every row is
marked std, that the eighteen ANCHOR rows carry exactly the rates the library quotes
from 第三分野標準生命表2018, and that between adjacent anchors the log of the rate is linear in
age, so the file can be read off in two lines and reproduces every sourced rate exactly.
That is what keeps the redistribution position checkable rather than merely stated — and it
is what makes the same table verifiably the same table in every product that ships it.
python -m pytest lifelib/libraries/jplib/tests/test_nursing_care_jp.py -q