The Projection Space#

The by-policy projection of the Pflege_DE_S model.

The Space is parameterized by point_id, so Projection[1] is an ItemSpace projecting model point 1:

>>> Projection[1].result_cf()          # the worked example's anchor cell
>>> Projection.point_id = 5            # or switch the default

t counts policy months, 0-based: t = 0 is the month of issue and age(t) = age_at_entry + t // 12, so the attained age steps at the policy anniversary. The frame starts at duration_mth_init() — 0 for new business, the elapsed duration for an in-force model point — and runs to proj_len() - 1, where proj_len() = 12 * (omega_age() - age_at_entry()) is the number of projected months, the exclusive end of range(duration_mth_init(), proj_len()), and depends on the entry age and the terminal age alone. There is no maturity, no survival benefit and no tail state: the contract runs for life, and the closure is carried by the decrements.

Input data

Inputs are external files: plain CSVs living in the model folder’s parent directory, products/pflegerentenversicherung/, 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 Pflege_DE_S folder without its parent’s CSVs produces a model that reads and then fails on first evaluation.

Each table has a filename Reference and a reader Cells, both on Data, reached here through the data Reference:

Reference

Cells

File

model_point_file

data.model_point_table()

model_point_table.csv

benefit_scale_file

data.benefit_scale_table()

benefit_scale_table.csv

mort_table_file

data.mort_table()

mort_table.csv

incidence_file

data.incidence_table()

incidence_table.csv

care_file

data.care_table()

care_table.csv

lapse_file

data.lapse_table()

lapse_table.csv

surrender_file

data.surrender_table()

surrender_table.csv

expense_file

data.expense_table()

expense_table.csv

basis_file

data.basis_table()

basis_table.csv

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 annual rates and *_rate_mth for monthly ones, *_pp for per-policy amounts, claims(t, kind) with an uppercase kind string, pols_if_at(t, timing) for the end-of-period read. The technical notes use compact actuarial symbols instead. The mapping is:

Notes symbol

Cells

Meaning

(none)

model_point()

The selected model point row

n = 12(omega - entry)

proj_len()

Number of projected months

d0

duration_mth_init()

Month the frame opens at

t

duration_mth(t)

Months since issue

y(t) = t // 12 + 1

policy_year(t)

Versicherungsjahr

x(t)

age(t)

Attained age in month t

R

rente_mth()

Vereinbarte Pflegerente, PG5

pi_g

benefit_pct(g)

Leistungsstaffel percentage

(none)

waiver_flag(g)

Is an annuity payable at g?

P

premium_mth_pp()

Level monthly gross Beitrag

(none)

prem_net_level_pp()

Net level premium, A / U

m

prem_mode_months()

1, 3, 6, 12, or 0 for single

q_A(x)

mort_rate(t)

Active annual death rate

(none)

mort_rate_mth(t)

The same rate, monthly

mu_A(t)

mort_force(t)

Active force of mortality

q_g(x)

mort_rate_care(t, g)

Annual death rate at grade g

mu_g(t)

mort_force_care(t, g)

mort_mult(g) x mu_A(t)

i(x)

inc_rate(t)

Annual incidence into care

iota(t)

inc_force(t)

Force of incidence, 0 in the Wartezeit

s_g

inc_share(g)

Grade first entered, entry mix

(table)

det_rate(g)

Annual deterioration rate

delta_g(t)

det_force(t, g)

Force of deterioration

(table, damped)

rec_rate(t, g)

Annual recovery rate

rho_g(t)

rec_force(t, g)

Force of recovery

(table)

lapse_rate(t)

Annual lapse, active state

w(t)

lapse_rate_mth(t)

Monthly lapse probability

(allocation)

p_act_stay/death/care(t)

Active-state month transitions

(allocation)

p_pg_stay/death/worse/better

Grade-g month transitions

l_A(t)

pols_act(t)

Active at the start of month t

W_{g,z}(t)

pols_karenz(t, g, z)

In the Karenz ledger

l_g(t)

pols_pg(t, g)

In a paying Pflegegrad

E_g(t)

esc_pg(t, g)

Escalation-weighted l_g

(none)

pols_care(t)

Everyone in care

l(t)

pols_if(t)

In force at the start of t

l(t+1)

pols_if_at(t, timing)

BEG / END

(none)

pols_in_term(t)

In force inside the term

(none)

pols_waived(t)

Beitragsbefreiung population

(none)

pols_prem(t)

Units actually paying

(none)

pols_entry(t, g)

Entrants into grade g

(none)

pols_grad(t, g)

Karenz graduations into g

(none)

pols_reactiv(t)

Reaktivierung to the active state

pols_death(t)

pols_death(t)

Deaths from every state

pols_lapse(t)

pols_lapse(t)

Surrenders, active state only

(none)

pols_dead_cum(t)

Cumulative deaths

(none)

pols_lapse_cum(t)

Cumulative surrenders

(none)

premium_due(t)

Is an instalment due?

(none)

premium_pp(t)

The instalment charged

(none)

cum_prem_max_pp(t)

Premium payable to date

(none)

prem_units_at(t)

The same, in units of P

premiums(t)

premiums(t)

Beitrag income

(none)

rkw_pp(t)

Rueckkaufswert per policy

(none)

brg_pp(t)

Beitragsrueckgewaehr per policy

claims_annuity, _lapse,

claims(t, kind)

Benefit outgo by kind

_death

alpha x Beitragssumme

acq_expense_pp()

Acquisition charge at t = 0

(none)

beitragssumme()

The Zillmerung base

(1 + f)^(t/12)

expense_infl_factor(t)

Expense inflation factor

expenses(t)

expenses(t)

Acquisition + administration

c x annuity payments

claim_expenses(t)

Per-annuity-payment cost

net_cf(t)

net_cf(t)

Net cash flow, income positive

-net_cf(t)

liability_cf(t)

The same stream, outgo positive

i

rechnungszins()

Technical rate, pricing only

v**t

disc_factor(t)

(1 + i)^(-t/12)

(first order)

tar_mort_rate(t)

Blended, margined active q

(first order)

tar_mort_rate_care(t, g)

Blended, margined in-care q

(first order)

tar_inc_rate(t)

Blended, margined incidence

(first order)

tar_det_rate(t, g)

Margined deterioration

(first order)

tar_rec_rate(t, g)

Margined recovery

(first order)

tar_p_act(t), tar_p_pg(t, g)

Tariff month transitions

(first order)

tar_pols_act(t)

Tariff active ledger

(first order)

tar_pols_karenz(t, g, z)

Tariff Karenz ledger

(first order)

tar_pols_grad(t, g)

Tariff Karenz graduations

(first order)

tar_pols_pg(t, g)

Tariff paying ledger

(first order)

tar_esc_pg(t, g)

Tariff escalation ledger

(first order)

tar_pols_if(t)

Tariff in force

(first order)

tar_pols_prem(t)

Tariff premium-paying units

(first order)

tar_pols_death(t)

Tariff deaths

A

epv_benefits()

EPV of the Pflegerente

U

epv_prem_units()

EPV of premium, in units of P

G

epv_admin()

EPV of per-policy admin

C

epv_claim_expense()

EPV of claims cost

Four names needed care.

pols_if(t) is the count at the start of month t and is the weight on that same result_cf() row’s cash flows, which is the library-wide convention; the end-of-period count is pols_if_at() with "END" and never pols_if() at t + 1 written into a cash-flow row. Breaking that is silent: the exposure column becomes the correct series shifted one period while every cash flow beside it stays right.

pols_pg(t, g) and esc_pg(t, g) are the same population counted two ways. The first is a head count; the second weights each life by its own escalation factor since its annuity began. With leistungsdynamik = 0 they are identical at every t and g, which check_esc_ledger() asserts. The annuity is weighted on esc_pg and never on pols_pg, because using the head count would silently drop the escalation on the model points that carry one.

pols_waived(t) is the Beitragsbefreiung population, and it is not everyone in care. A life in the Karenz ledger pays, because no annuity is yet payable and the waiver runs with the annuity; a life at Pflegegrad 1 pays on the delib_std grid, where benefit_pct(1) = 0, and is waived on the bahr grid, where it is 10 %. That is why the split is driven by waiver_flag() rather than by membership of the care ledger, and why pols_prem(t) is not monotone: a Herabstufung out of the insured grades revives the premium.

mort_rate(t) is the active-life rate. In-care mortality is not tabulated at all: mort_force_care() is mort_mult(g) times the active force, so mort_rate_care() is 1 - exp(-mort_mult(g) mu_A). The multiple is on the force and not on the rate, which matters at the oldest ages where the rate saturates.

The monthly step: constant forces, proportional allocation

Every shipped rate is annual and every transition inside a month is computed from forces held constant over the month, the competing transitions sharing one survival probability in proportion to their forces. Writing q for an annual rate, the force is mu = -ln(1 - q); with forces mu_1 ... mu_k out of a state,

p_stay = exp(-(mu_1 + … + mu_k) / 12) p_j = (mu_j / sum mu) (1 - p_stay)

so p_stay + sum p_j = 1 exactly, by construction, which is what p_act_stay() … p_pg_better() publish and what makes check_states() an identity rather than an approximation. Adding monthly rates instead, or applying q/12, gives different answers wherever the forces are large — which on this product is exactly where the money is.

The limiting-age convention needs one number. mort_rate is forced to 1.0 at omega_age() - 1, whose force is infinite; it is capped at -ln(1e-12) = 27.63 so the proportional allocation stays finite, and the incidence, deterioration and recovery forces are set to zero there so that every exit at the limiting age is death. What survives to omega_age is then 1e-12 of the age-109 cohort, which on the anchor cell is of the order of 1e-17 of the original policy.

The pricing engine, and why it is a separate ledger

The library publishes undiscounted cash flows. The Beitrag, however, is a priced quantity, so the model carries a second, self-contained actuarial-value engine — the tar_* cells — whose only output is premium_mth_pp(). That engine discounts; the projection does not.

Where premium_mth is positive on the model point, that is the premium and the engine is not consulted. Where it is 0.0, P is struck by equivalence on the first-order (erster Ordnung) bases: every rate multiplied by its prudence margin, the sexes blended at unisex_mix_male because sex may not enter a German premium, and no lapse at all. The first-order basis carrying no lapse is both German practice and what keeps the model acyclic — a pricing quantity must not depend on a behavioural assumption that depends on the path that depends on the premium.

Everything on the benefit side that scales with P — the Beitragsrückgewähr and the Zillmerung allowance — is linear in P, so the equivalence

P U = A + P D1 + P a1 + beta P U + G + C

solves in closed form, P = (A + G + C) / (U (1 - beta) - D1 - a1), and premium_mth_pp() multiplies that by the Risikozuschlag, which loads the gross premium and never the benefit. check_prem_equiv() then re-assembles both legs month by month from the tariff ledgers rather than from the closed form, so substituting a best-estimate rate into one leg, or dropping the Zillmerung term, makes it fail.

Modules that are off in the base run

Five constructions are implemented and switched off through the model point, so the base run reproduces the worked example while the machinery stays visible and testable: the Wartezeit (wartezeit_months = 0), the Karenzzeit (karenz_months = 0, in which case the ledger is empty and a life graduates in the month it enters), the Leistungsdynamik (leistungsdynamik = 0, in which case esc_pg == pols_pg), the Beitragsrückgewähr (beitragsrueckgewaehr = False, in which case claims_death is structurally zero) and the Stornoabzug (stornoabzug_rate = 0). Model points 7, 8, 9 and 10 switch them on one at a time.

Four further constructions are described in the technical notes and not implemented, each for a stated reason: no Überschussbeteiligung in any application form, the surplus chassis belonging to products/kapitallebensversicherung/; no Beitragsdynamik, whose acceptance rate is a behavioural assumption with nothing behind it; no Beitragsfreistellung, so every voluntary exit is a surrender; and no § 163 VVG re-rating, which is a management action conditional on emerging experience rather than a projected assumption.

Sign convention

net_cf() is income positive — Beitrag in, annuity, surrender value, death benefit and expenses out — which is the notes’ own orientation and the library-wide sign. liability_cf() publishes the same stream outgo-positive, liability_cf(t) = -net_cf(t) exactly, so a best-estimate liability is sum v(t) liability_cf(t) over whatever discount curve the valuation layer supplies. Both are columns of result_cf(), so the identity is verifiable in the frame rather than only in prose.

The shape to expect on the anchor cell is a large new-business strain at t = 0 — the 25 ‰ Zillmerung allowance is charged in full there — then thirty-odd years of positive monthly margins as the level Beitrag runs far above the risk premium, then a long negative tail from the seventies onward as the incidence curve overtakes it. That crossing is the Deckungskapital’s peak, and it is the whole economic content of an ageing reserve carried on a life-assurance chassis.

Cells Descriptions#

model_point()[source]#

The selected model point as a Series.

policy_id()[source]#

The policy identifier of the selected model point, PFL-0000NN.

sex()[source]#

The insured’s sex, M or F. A projection input that must not reach the price.

Sex may not enter the premium of a contract concluded from 21 December 2012, so premium_mth_pp() blends the two bases at unisex_mix_male and never reads this cells, while mort_rate() and inc_rate() read nothing else. The tension is sharper on this product than on any other in the library: women have materially higher incidence and materially longer care durations, so the unisex premium embeds a cross-subsidy whose size depends on the sex mix actually written — and that mix is endogenous to the price. Model points 1 and 2 are the same contract on the two sexes: equal premium_mth_pp(), unequal projected annuity.

age_at_entry()[source]#

The age last birthday at issue.

It drives proj_len(), every rate lookup through age(), and the Beitragssumme the Zillmerung allowance is struck on. The observed German entry band is of the order of 18 to 65, and purchase clusters much later than the band permits — predominantly between 45 and 60, when the buyer has seen a parent go into care. Model points 13 and 14 sit at the two ends.

duration_mth_init()[source]#

Complete months elapsed at the projection start; 0 for new business.

The frame opens at this t, which is a product fact rather than a house convention: an in-force model point opens at the duration the policy has already run. It does not change proj_len(), which is fixed by the entry age and the terminal age alone, so a point opening at t = 240 simply publishes a shorter frame ending at the same index. Reading it as a shift of the horizon is a listed pitfall.

status_init()[source]#

The state at the projection start: aktiv or pg1 … pg5.

An aktiv point seeds pols_act(); a point in claim seeds pols_pg() at its grade with the Karenzzeit already served, because a life in payment at the valuation date has served whatever deferral its contract carried. Model point 12 is the in-claim case.

rente_mth()[source]#

R: the vereinbarte Pflegerente at Pflegegrad 5, in euro per month.

The scaling constant of the whole benefit schedule: every grade pays benefit_pct(g) x rente_mth(). The market sells 1 000 € to 1 500 € a month, sized against the residual a Pflegeheim resident funds from savings after the statutory contribution and an average pension; the shipped 1 000 € is the round number at the lower end of that band and is a [std] choice, not an observation.

staffel_id()[source]#

The key into benefit_scale_table.csv naming this policy’s Leistungsstaffel.

delib_std is 0 / 30 / 50 / 75 / 100 %; bahr is the statutory 10 / 20 / 30 / 40 / 100 % minimum grid of § 127 SGB XI. The difference is not only a level: on bahr Pflegegrad 1 is insured, so a grade-1 life is waived there and pays on delib_std.

prem_end_age()[source]#

The attained age at which the Beitrag ceases; 110 means lifelong.

Three forms are sold: lifelong payment until death or claim, payment to a fixed age — typically 65 or 85 — and a single Einmalbeitrag. A shortened Beitragszahlungsdauer raises the level premium and the reserve and best matches the buyer’s earning life; model point 4 carries it. Once the term has ended the contract is paid up: no premium, no lapse, and the cover runs on.

prem_mode()[source]#

The instalment frequency: monthly, quarterly, half_yearly, annual or single.

prem_mode_months()[source]#

m: the number of months one instalment covers; 0 for the Einmalbeitrag.

1, 3, 6 or 12, and 0 for single, which is the sentinel the premium cells read as “one payment at t = 0 and nothing thereafter”.

The model charges no separate Ratenzahlungszuschlag: the instalment loading is folded into admin_prem_pct, which is a percentage of the premium collected and so is invariant to the mode. The consequence is worth stating because a user will read it the wrong way round — in this model annual mode prices very slightly below monthly, through the timing of the discounting alone, which is the opposite sign to a real German tariff.

premium_mth()[source]#

The contractual monthly Beitrag from the model point; 0.0 is a sentinel.

0.0 does not mean a free contract. It means derive the premium by equivalence, and premium_mth_pp() then runs the first-order pricing engine. A positive value is taken as the contractual premium and the engine is not consulted — which is how an in-force model point carries the premium it was actually sold at.

rating_factor()[source]#

The Risikozuschlag multiplier on the gross premium; 1.00 at standard rates.

A Risikozuschlag buys the same annuity at a higher price, so it scales the premium and never the benefit, and claims() is invariant to it. No German Risikozuschlag scale for this product was established; model point 13 carries 1.50 as a [std] illustration.

wartezeit_months()[source]#

The Wartezeit from inception, in months; 0 in the base run.

Care beginning inside it is not covered, so inc_force() is zero while t < wartezeit_months(). The underwritten mainstream product usually has none — the Gesundheitsprüfung does the screening the Wartezeit does in the subsidised Pflege-Bahr product, where the statutory maximum is five years. The pairing is near deterministic: no underwriting implies a long Wartezeit, underwriting implies none. Model point 7 carries 36 months.

karenz_months()[source]#

K: the Karenzzeit from onset, in months; 0 in the base run.

A different device from the Wartezeit and routinely confused with it: the Wartezeit runs from inception and denies cover, the Karenzzeit runs from onset and defers an admitted claim. It is a clock per onset, not a gate on the aggregate, which is why pols_karenz() carries a ledger dimension for it rather than a shift on the benefit. Where it is positive, a material share of new claimants die inside the deferral — mortality is highest immediately after onset — so it removes disproportionately more claims than its length suggests. Model point 7 carries six months.

leistungsdynamik()[source]#

d: the annual escalation of the annuity in payment; 0.0 in the base run.

Not to be confused with a Beitragsdynamik, which raises premium and cover before claim and is not modelled at all. The economic case for this one is that the Eigenanteil a resident pays rises continuously while the statutory benefit is uprated episodically, so a level annuity loses ground throughout a spell. Its cost is much smaller than it looks — the annuity is paid to a population with heavily elevated mortality, so the escalation compounds over three to five years, not fifteen. Model point 8 carries 2 % a year.

beitragsrueckgewaehr()[source]#

Whether the Beitragsrückgewähr death benefit is on; False in the base run.

Switched on, the contract returns the premiums payable to date on death at any time, which converts a pure biometric risk cover into a savings-bearing one: the reserve needed to fund it is close to the accumulated premium itself, and the option roughly doubles the premium for the same annuity. The base run leaves it off because the savings roll-forward is demonstrated far better by products/kapitallebensversicherung/; model point 9 turns it on to show how large the effect is.

The implemented form is the gross one — no offset for annuity already paid. The market’s more common form nets the annuity off, but that netting is floored at zero per life, and these ledgers are aggregates, so netting at the aggregate level would let a life that received a large annuity subsidise one that received none. The consequence — the option overstates the death benefit relative to the market-standard form — is stated rather than hidden.

stornoabzug_rate()[source]#

The deduction from the Rückkaufswert, as a fraction; 0.0 in the base run.

Admissible only if agreed, appropriate and quantified in the contract, and a deduction for unamortised acquisition costs is expressly ineffective. No Stornoabzug for any German Pflegerenten tariff was established, so the base run ships zero — a non-zero deduction requires a contractual quantification this corpus cannot supply — and model point 10 carries 5 % as a [std] illustration.

Named ``stornoabzug_rate`` and not ``stornoabzug``. It is a fraction, and the library gives every rate the *_rate suffix; RV_DE_S, FRV_DE_S and Riester_DE_S all spell this rate the same way. Bare stornoabzug is FRV_DE_S’s euro amount retained from surrenders and one of its result_cf() columns — a different quantity, which is why the two do not share a name.

pols_if_init()[source]#

The policy count at the frame’s first t; 1.0 on every shipped model point.

result_cf()’s first pols_if value equals this exactly, which is the library-wide assertion that pols_if is a start-of-period count: no decrement has been applied when a period opens.

omega_age()[source]#

The terminal age, 110, from basis_table.csv.

A [std] modelling choice rather than a table fact — the DAV tables run higher — and it costs nothing material: an active female life aged 45 survives to 110 with probability of the order of 1e-4 on the shipped basis. What it buys is a closed system: mort_rate() is forced to 1.0 at omega_age() - 1, so the decrements account for the whole cohort and check_states() closes exactly instead of leaving a truncation residue.

rechnungszins()[source]#

i: the technical interest rate, 1,00 % a year, from basis_table.csv.

The Höchstrechnungszins of the DeckRV for new business from 1 January 2025, which attaches to the cohort at issue and is then locked for the life of the contract. This is the one place a discount rate appears in the model, and it is used only by the pricing engine: the projection publishes undiscounted cash flows. It is also the single most leveraged pricing assumption here, because a Pflegerente’s benefits fall on average some thirty-five years after issue.

proj_len()[source]#

n: the number of projected months, 12 (omega_age - age_at_entry).

The exclusive end of the frame, counted from t = 0: the frame is range(duration_mth_init(), proj_len()), the last projected index is proj_len() - 1 and result_cf().index[-1] == proj_len() - 1 whether the frame is 0-based or opens partway through, which is the library’s reading and is asserted for every model point. On the anchor cell — entry age 45, omega_age 110 — it is 780, so the frame runs t = 0 … 779, 780 monthly rows, attained ages 45 to 109.

It depends on the entry age and the terminal age alone, not on duration_mth_init(): a point opening at duration_mth_init() = d0 publishes proj_len() - d0 rows and still ends at its own proj_len() - 1, because duration_mth_init shortens the frame at the front, never at the back. That last index is the point’s own — proj_len() varies with the entry age — not the anchor’s 779.

duration_mth(t)[source]#

Complete months elapsed since issue at the start of month t, which is t.

Published as its own cells because it is the quantity the Wartezeit is measured in, and because t is an index while this is a duration: on a model point opening at duration_mth_init() = 240 the frame’s first row is already 240 months old.

policy_year(t)[source]#

y(t): the Versicherungsjahr, t // 12 + 1, 1-based.

The key into lapse_table.csv and surrender_table.csv, both of which are tabulated to year 40 with year 40’s value applying to every later year.

age(t)[source]#

x(t): the attained age, age_at_entry() + t // 12.

The age steps at the policy anniversary rather than on a birthday, which is the convention this monthly grid imposes; an implementation on real dates carries a fractional offset of at most one year [std].

benefit_pct(g)[source]#

pi_g: the Leistungsstaffel percentage at Pflegegrad g.

Read from benefit_scale_table.csv on this policy’s staffel_id. The annuity paid at grade g is benefit_pct(g) x rente_mth(), irrespective of the care setting: the same amount is payable at home and in a Pflegeheim, which is what makes the product a Summenversicherung and removes any need to prove where care is given.

The middle steps carry the cost. Time spent at each grade is very unequal — a person entering at grade 2 and deteriorating spends most of the spell at grades 2 and 3 and only the final months at grade 5 — so the time-weighted average percentage over a spell is far below 100 %, and two tariffs with the same top step and different middle steps differ by more than the headline suggests.

waiver_flag(g)[source]#

True when an annuity is payable at Pflegegrad g, and so the Beitrag waived.

Waiver and benefit run on one trigger: the premium is waived from the first month in which any annuity is payable and revives on exit from the paying grades. That is the market-standard design, and it is what lets check_waiver() reconcile the premium stream and the benefit stream against a single ledger.

The grade-1 case is the whole point of publishing this as its own cells. On delib_std benefit_pct(1) is zero, so a grade-1 life is in care, is counted in pols_if(), generates no annuity and keeps paying; on bahr the same life is waived. Wiring the waiver to membership of the care ledger instead gets both wrong.

mort_rate(t)[source]#

q_A(x): the annual active-life death rate at the attained age, by sex.

Read from mort_table.csv, a [std] Gompertz proxy — not DAV 2008 T and not the DAV 2008 P active-life table, neither of which is public or redistributed here. It is forced to 1.0 at omega_age() - 1, the limiting-age convention that closes the system.

This is the rate of an active life. In-care mortality is a multiple of its force and is mort_rate_care().

mort_rate_mth(t)[source]#

The monthly active-life death rate, 1 - (1 - mort_rate(t))**(1/12).

Published because it is the quantity a reader checks the monthly step against, and because the error it guards is dividing an annual rate by twelve: mort_rate(t)/12 is strictly below this rate wherever the annual rate is positive, so dividing by twelve understates the monthly decrement. Twelve of these rates compounded return the annual rate exactly, while twelve of them added overshoot it — the direction is the opposite of the intuition, which is why the pair is published. The projection itself does not use it — it works in forces, through mort_force() — but the two agree by construction, 1 - exp(-mort_force(t)/12) being this same number.

mort_force(t)[source]#

mu_A(t): the force of active-life mortality, -ln(1 - mort_rate(t)).

Held constant over the month. At the limiting age mort_rate is 1.0 and the force is infinite; it is capped at -ln(1e-12) = 27.63 so the proportional allocation of the month’s exits stays finite. What survives the year of age 109 is then 1e-12 of the cohort that entered it — of the order of 1e-17 of the original policy on the anchor cell — so the closure identity is exact to far beyond the tolerance.

mort_rate_care(t, g)[source]#

q_g(x): the annual death rate of a life in Pflegegrad g.

1 - exp(-mort_force_care(t, g)). Published so the single most load-bearing biometric statement in this product is visible as a number: the mortality of a Pflegebedürftiger is a large multiple of an active life’s at the same age, rising sharply with the grade.

Two consequences. The annuity in payment is short — three to five years, not the fifteen to twenty of a healthy-life pension at the same age — so pricing it on an annuity table built to be prudent about people living longer would be prudent in exactly the wrong direction and would materially overprice the benefit. And grade and mortality are correlated, so the highest-paying state is the shortest-lived: a model applying an average benefit percentage to a survival curve computed at an average mortality gets the wrong answer in a way no total will reveal.

mort_force_care(t, g)[source]#

mu_g(t): mort_mult(g) times the force of active mortality at the same age.

On the force and not on the rate, which is what makes the multiple mean the same thing at every age: 1.5 at grade 1 rising to 9.0 at grade 5, carrying the research file’s order of magnitude — two to three times an active life at grade 2, five to ten times at grade 5. On rates the ratio compresses towards 1 as mort_rate saturates at the oldest ages, which is an artefact of the rate scale and not a change in the basis.

inc_rate(t)[source]#

i(x): the annual rate at which an active life enters any Pflegegrad.

Read from incidence_table.csv by sex and attained age, a [std] exponential proxy capped at inc_cap. It is the rate of leaving the active state for care; the grade actually entered is drawn from inc_share(), because entry is not uniformly at the lowest grade — a stroke or a fracture enters directly at grade 3 or 4.

The Wartezeit is not applied here. It gates the force, in inc_force(), so that this cells stays the tariff-comparable table rate at every age.

inc_force(t)[source]#

iota(t): the force of incidence into care, zero inside the *Wartezeit*.

-ln(1 - inc_rate(t)) once t >= wartezeit_months(), and exactly zero before that: care beginning inside the Wartezeit is not covered at all. It is also zero at the limiting age, so every exit there is death and the allocation stays well defined.

inc_share(g)[source]#

s_g: the share of entrants into care whose first Pflegegrad is g.

Read from care_table.csv; sums to 1.00 over the five grades. It is deliberately not the stock distribution of Pflegebedürftige, which runs about 9 / 44 / 27 / 14 / 6 %: entrants skew lower than the stock, because deterioration moves people up the grades over a spell. Using the stock as the entry mix is a listed pitfall, and the model’s own stock share at grades 4 and 5 over a whole projection exceeds the entry share, which is the arithmetic statement of the same thing.

det_rate(g)[source]#

The annual rate of deterioration from Pflegegrad g to g + 1.

Read from care_table.csv; zero at grade 5, which has nowhere to go. Deterioration dominating recovery is one of the four properties a replacement table must preserve, and it is what makes the benefit grow over a spell.

det_force(t, g)[source]#

delta_g(t): the force of deterioration out of grade g; zero at grade 5.

Zero also at the limiting age, so every exit there is death.

rec_rate(t, g)[source]#

The annual rate of recovery from grade g, damped above rec_age_ref.

rec_rate(g) x exp(-rec_age_decay x max(0, age(t) - rec_age_ref)) with rec_age_ref = 75 and rec_age_decay = 0.10 [std]. The damping is what makes deterioration dominate recovery above 75 — property (b) of a replacement table — and it encodes the one thing about Reaktivierung that is not in doubt: it is real after acute events at younger ages and small at the ages where most claims arise.

Recovery from grade 1 leads to the active state, and a life that recovers starts paying its Beitrag again.

rec_force(t, g)[source]#

rho_g(t): the force of recovery out of grade g; zero at the limiting age.

lapse_rate(t)[source]#

The annual lapse rate from the active state in policy year y(t).

Read from lapse_table.csv, tabulated to year 40 with year 40’s rate applying to every later year, and zero once the premium term has ended: a paid-up contract has no premium-driven exit, and the model implements no Beitragsfreistellung election, so every voluntary exit is a surrender.

Lapse here is profitable to the insurer — an early lapser paid for years and never reached the risk period — which inverts the usual protection intuition and is why the first-order pricing basis deliberately carries none.

lapse_rate_mth(t)[source]#

w(t): the monthly lapse probability, 1 - (1 - lapse_rate(t))**(1/12).

Applied to the survivors of the insured decrements and of the reactivation inflow, so that a life cannot both die and lapse in the same month. Nothing in care lapses: a claimant with a waived premium has no premium to default on and a live annuity to forfeit.

p_act_stay(t)[source]#

The probability that an active life is still active at the end of month t.

exp(-(mu_A + iota)/12), the constant-force survival of the two competing decrements. With p_act_death() and p_act_care() it sums to exactly 1.

p_act_death(t)[source]#

The probability that an active life dies during month t.

The death force’s share of the month’s exits: (mu_A / (mu_A + iota)) (1 - p_stay). Allocating the one survival probability in proportion to the forces is what makes the three active-state probabilities sum to 1 exactly; adding monthly rates instead, or dividing an annual rate by twelve, does not.

p_act_care(t)[source]#

The probability that an active life enters care during month t.

(iota / (mu_A + iota)) (1 - p_stay), and zero inside the Wartezeit, where the incidence force is zero. The grade entered is then split by inc_share().

p_pg_stay(t, g)[source]#

The probability that a life in Pflegegrad g is still there at month end.

exp(-(mu_g + delta_g + rho_g)/12). Three forces compete out of a paying state, not one — death, deterioration and recovery — which is the arithmetic form of the product’s central structural fact: the paying state has three exits and only death is absorbing. A model that lets it be exited only by death overstates the liability; one that treats every downgrade as a termination understates it.

p_pg_death(t, g)[source]#

The probability that a life in Pflegegrad g dies during month t.

p_pg_worse(t, g)[source]#

The probability of a Höherstufung from grade g to g + 1 in month t.

Zero at grade 5. The insured does not elect this: a Höherstufung is applied for and re-assessed by the Medizinischer Dienst or MEDICPROOF, so grade change is a biometric transition here rather than a claims-management outcome.

p_pg_better(t, g)[source]#

The probability of a Herabstufung from grade g to g - 1 in month t.

Out of grade 1 it leads to the active state — Reaktivierung — where the life starts paying its Beitrag again and becomes exposed to lapse once more. Out of the higher grades it moves the life down one step of the Leistungsstaffel, which reduces the annuity without ending it, and out of the lowest insured grade it stops the annuity and revives the premium. All three cases fall out of the same recursion; none of them is a claim decision.

pols_act(t)[source]#

l_A(t): lives in the active state at the start of month t.

Seeded at duration_mth_init() from status_init() and rolled forward as

l_A(t+1) = [ l_A(t) p_act_stay(t) + pols_reactiv(t) ] (1 - w(t))

— survivors of both insured decrements, plus the month’s Reaktivierungen, then exposed to lapse. Lapse acts after the insured decrements and after the reactivation inflow; the alternatives give different answers and this ordering is the one the notes’ processing order states.

pols_entry(t, g)[source]#

Lives entering Pflegegrad g from the active state during month t.

pols_act(t) x p_act_care(t) x inc_share(g). Entry is split across the grades because it is not uniformly at the lowest one, and the split is one of the four properties a replacement basis must preserve.

pols_karenz(t, g, z)[source]#

W_{g,z}(t): lives in Pflegegrad g whose Karenzzeit clock stands at z.

The ledger exists only when karenz_months() > 0; otherwise it is empty and a life graduates in the month it enters. Entrants join at z = 1 and advance one month at a time, subject to the same transitions as a served life — they die, deteriorate and recover exactly as if the annuity were running, they simply are not paid.

The clock is discarded on reactivation: the Karenzzeit runs from onset, so a recovered life who later relapses starts a new onset and a new clock. That is why recovery out of g = 1 leaves this ledger for the active state instead of moving to a lower g, and why pols_reactiv() reads across both ledgers.

A life in this ledger is in care, is counted in pols_if() and in pols_care(), receives no annuity, and pays its Beitrag, because the waiver runs with the annuity and not with the diagnosis.

pols_grad(t, g)[source]#

Lives graduating out of the Karenz ledger into Pflegegrad g in month t.

Where karenz_months() == 0 this is exactly pols_entry() — the degenerate case the base run runs in — and where it is positive it is the z = K cohort carried one more month through the same transitions.

The difference between the two is the whole cost of a Karenzzeit: summed over the projection, graduations fall strictly short of entries, and the shortfall is the deaths and recoveries recorded inside the deferral. Because mortality is highest immediately after onset, that shortfall is larger than the length of the deferral suggests — a deferred period on a population with elevated mortality removes disproportionately more claims than the same period would on a healthy population.

pols_reactiv(t)[source]#

Lives recovering out of Pflegegrad 1 to the active state during month t.

Read across both care ledgers — the paying one and the Karenz one — because a life still inside its deferred period can recover just as a paid one can. A reactivated life rejoins pols_act(), resumes paying its Beitrag and becomes exposed to lapse again in the same month, which is why the term appears inside pols_act()’s lapse bracket rather than beside it.

pols_pg(t, g)[source]#

l_g(t): lives in Pflegegrad g at the start of month t, Karenz served.

The ledger the annuity is paid on. Seeded at duration_mth_init() from status_init() — an in-claim model point opens with its whole cohort here, at its grade, with the deferral already served — and rolled forward as

l_g(t+1) = l_g(t) p_stay + l_{g-1}(t) p_worse + l_{g+1}(t) p_better + grad_g(t)

with the g = 1 recovery term flowing to the active state instead and the g = 5 deterioration term absent. Every one of those moves is internal to the in-force population, which is why check_pols_roll_fwd() sees only deaths and lapses.

esc_pg(t, g)[source]#

E_g(t): the escalation-weighted counterpart of pols_pg().

The same population, weighted by each life’s own escalation factor since its annuity began: the identical recursion with one extra factor of (1 + d)**(1/12) on the surviving weights, entrants joining at weight 1.

Carrying the escalation as a value ledger rather than as a duration-since-onset cohort dimension is what keeps the model O(n) instead of O(n squared); the price is that it reports only the aggregate escalation, which is all the cash flow needs. With leistungsdynamik = 0 the extra factor is 1 and this is the pols_pg() recursion exactly, so the two ledgers agree at every t and g — check_esc_ledger() asserts it.

The annuity is weighted on this ledger and never on pols_pg(). Using the head count would silently drop the escalation on every model point that carries one, and no total in the frame would look wrong.

pols_care(t)[source]#

Everyone in care at the start of month t: the Karenz and paying ledgers.

sum_g pols_pg(t, g) + sum_{g,z} pols_karenz(t, g, z). It is not the population receiving an annuity and not the population whose premium is waived: a life inside its Karenzzeit is here and is paid nothing, and a life at Pflegegrad 1 on the delib_std grid is here, is paid nothing and keeps paying.

pols_if(t)[source]#

l(t): policies in force at the start of month t, active or in care.

The weight on that same result_cf() row’s cash flows, which is the library-wide convention; end-of-period state goes through pols_if_at() and never through this cells read at t + 1. result_cf()’s first value equals pols_if_init() exactly, because no decrement has been applied when a period opens.

Every Pflegegrad transition is internal to this count. Lives leave it only by death or by surrender, which is what check_pols_roll_fwd() asserts.

pols_if_at(t, timing)[source]#

The in-force count at a point inside month t: "BEG" or "END".

"BEG" is pols_if() at t and "END" is pols_if() at t + 1. The cells exists so that end-of-period state has somewhere to live other than pols_if(), whose meaning is fixed to the start of the period across the whole library.

pols_in_term(t)[source]#

In-force units inside the premium-paying term: pols_if(t) while x(t) < prem_end_age.

Zero once the term has ended, at which point the contract is paid up: no premium, no waiver population and no lapse, with the cover running on to the terminal age.

pols_waived(t)[source]#

The Beitragsbefreiung population: in-term units in a paying Pflegegrad.

sum_{g : waiver_flag(g)} pols_pg(t, g), restricted to the premium term. Three consequences follow directly from the Leistungsstaffel, and each is a way to get the waiver wrong: a life in a Karenz ledger is not here, because no annuity is yet payable; a Pflegegrad 1 life is not here on delib_std and is here on bahr; and a life downgraded out of the insured grades leaves here and starts paying again.

pols_prem(t)[source]#

The units that actually pay a Beitrag: pols_in_term(t) - pols_waived(t).

Not monotone. It falls as lives die, lapse and enter paying grades, and it rises again whenever a Herabstufung takes a life out of the insured grades and revives its premium obligation. A model whose premium-paying count only ever falls has wired the waiver as an absorbing state, which is the single most common way to get this product’s premium stream wrong.

pols_death(t)[source]#

Deaths during month t, from every state.

The active state at p_act_death, and each Pflegegrad — in the paying ledger and in the Karenz ledger alike — at p_pg_death(t, g), which is grade-increasing because the mortality multiple is. Aggregating the care ledgers into one average death rate is the arithmetic form of forgetting that the highest-paying state is also the shortest-lived.

pols_lapse(t)[source]#

Surrenders during month t, from the active state only.

[ pols_act(t) p_act_stay(t) + pols_reactiv(t) ] w(t) — the survivors of the insured decrements together with the month’s reactivations, exposed to lapse after both. Nothing in care lapses: a claimant with a waived premium has no premium to default on and a live annuity to forfeit. A Pflegegrad 1 life on delib_std does still pay and could in principle surrender; the population is small and the model does not model it, which is a stated simplification rather than an oversight.

pols_dead_cum(t)[source]#

Cumulative deaths before the start of month t.

One of the two absorbing counts that, with the three live ledgers, partition the initial cohort at every t; see check_states().

pols_lapse_cum(t)[source]#

Cumulative surrenders before the start of month t.

premium_due(t)[source]#

Whether an instalment falls due at the start of month t.

t % prem_mode_months() == 0 inside the premium term, and for the Einmalbeitrag only at t = 0. The instalment months key off issue, not off the frame’s first row, so an in-force quarterly point opening at t = 240 still pays in the months the contract always paid in.

A waiver that begins between two due dates therefore takes effect at the next due date, which is the German convention for a Beitragsbefreiung on a fractionated contract; the alternative — refunding the unearned instalment — is a different and equally arguable rule the model does not implement.

premium_pp(t)[source]#

The instalment charged per paying policy at the start of month t.

premium_mth_pp() x prem_mode_months() on a due month and zero otherwise, so a quarterly contract pays three months’ worth three times a year rather than a twelfth of the annual amount every month. For the Einmalbeitrag it is the whole Einmalbeitrag, once, at t = 0.

cum_prem_max_pp(t)[source]#

The Beitrag payable to date per policy on an uninterrupted path.

A deterministic quantity, not a ledger: it is what a policy that never claimed, never lapsed and never died would have paid by the end of month t, and it is the base of both the Rückkaufswert and the Beitragsrückgewähr. It includes the instalment collected at the start of month t, because both benefits fall at the end of the month.

Computing it in closed form rather than by accumulating premiums() is what lets an in-force model point carry the right base at the frame’s first row: the premiums paid before the valuation date were paid, whether or not the model projected them.

premiums(t)[source]#

Beitrag income in month t: premium_pp(t) x pols_prem(t).

Collected in advance, at the start of the month, from the lives then inside the term and not waived. Income-positive, like every other term of net_cf().

rkw_pp(t)[source]#

The Rückkaufswert per surrendering policy at the end of month t.

rkw_prem_ratio(min(y(t), 40)) x cum_prem_max_pp(t) x (1 - stornoabzug_rate()) — the guaranteed surrender value as a fraction of premiums paid to date, which is the scale-free form a German contract states, less any contractual Stornoabzug.

It is near zero for the first several years and stays well below premiums paid for a long time, because the Zillmerung allowance is large and the risk premium in the early years is small. That is the honest thing to tell a buyer, and it is the strongest argument for the Beitragsrückgewähr option. Whether a pure-risk Pflegerente falls inside § 169 VVG at all is an open question this library states rather than assumes away; the table encodes the answer for a contract that does.

brg_pp(t)[source]#

The Beitragsrückgewähr per dying policy at the end of month t.

cum_prem_max_pp(t) where the option is on and 0.0 where it is not, so claims(t, "DEATH") is structurally zero in the base run. The gross form — no offset for annuity already paid — for the reason given at beitragsrueckgewaehr().

claims(t, kind=None)[source]#

Benefit outgo in month t by kind; the total when kind is omitted.

"ANNUITY" is the Pflegerente, R sum_g pi_g E_g(t), paid in advance on the escalation ledger. Two things about that weighting are load-bearing. It is esc_pg() and not pols_pg(), so an escalation is never silently dropped. And it is a grade-by-grade sum, never an average percentage on an average survival curve: grade and mortality are correlated, so the highest-paying state is the shortest-lived and the two are not interchangeable. The Karenz ledger contributes nothing — a life inside its deferred period is in care, is counted in force, pays its premium and receives no annuity.

"LAPSE" is the Rückkaufswert and "DEATH" the Beitragsrückgewähr, both at the end of the month on the lives that left during it.

beitragssumme()[source]#

The Beitragssumme the Zillmerung allowance is struck on.

premium_mth_pp() x 12 x (min(prem_end_age(), beitragssumme_cap_age) - age_at_entry()) and, for the Einmalbeitrag, the Einmalbeitrag itself.

A lifelong-premium contract has no finite Beitragssumme without a convention, and beitragssumme_cap_age = 85 [std] is that convention. It is a parameter, not a citation: what is cited is the 25 ‰ ceiling of DeckRV § 4 that the per-mille is set exactly at, cut from 40 ‰ by the LVRG from 1 January 2015.

acq_expense_pp()[source]#

The acquisition and distribution charge per policy, incurred once at t = 0.

acq_permille / 1000 x beitragssumme(), with the per-mille set exactly at the § 4 DeckRV Höchstzillmersatz so the ceiling binds visibly. The base is the Beitragssumme, not the annual premium — charging the per-mille on an annual premium understates it by a factor of the paying term and is a listed pitfall.

Because it falls at t = 0 only, an in-force model point never incurs it: its frame opens at duration_mth_init() > 0 and the cost was incurred before the valuation date. That is correct, and it is worth knowing before comparing an in-force point’s first row with a new-business point’s.

expense_infl_factor(t)[source]#

The expense inflation factor at month t: (1 + expense_infl)**(t/12).

Applied to the per-policy administration cost and to the per-annuity-payment claims cost, both of which are euro amounts quoted at t = 0 prices. Over the anchor cell’s sixty-five years it is a factor of about 2.6, which is enough for the per-policy line to decide the viability of a small contract on its own.

expenses(t)[source]#

Acquisition and administration expense in month t, at the start of the month.

Three components: the acquisition charge at t = 0 only; a per-policy administration cost on every policy in force, inflated; and a percentage of the Beitrag just collected, which is where the instalment loading the model does not charge separately lives. Claims-handling cost is not here — it is a per-event cost and is published as its own column, claim_expenses().

claim_expenses(t)[source]#

The claims-handling cost of month t, per annuity payment made.

claim_expense_pp x expense_infl_factor(t) x sum_{g : waiver_flag(g)} pols_pg(t, g), so it is weighted on the paying grades only: a Pflegegrad 1 life on delib_std generates none, and neither does a life inside its Karenzzeit.

The level is set low, and that is a product fact rather than an optimistic assumption: the Pflegegrad is determined by the Medizinischer Dienst or by MEDICPROOF and not by the insurer, so the Nachprüfung is a documentation exercise rather than the adversarial re-assessment that drives a Berufsunfähigkeitsrente’s claims cost.

net_cf(t)[source]#

The net liability cash flow of month t, income positive.

Beitrag less the Pflegerente, the Rückkaufswert, any Beitragsrückgewähr, the acquisition and administration expense and the claims-handling cost. The notes’ own sign, and the library-wide one.

The shape to expect on the anchor cell is a large strain at t = 0, where the 25 ‰ Zillmerung allowance is charged in full; then thirty-odd years of positive monthly margins, the level Beitrag running far above the risk premium; then a long negative tail from the seventies onward as the incidence curve overtakes it. That crossing is where the Deckungskapital peaks, and it is the whole economic content of an ageing reserve carried on a life-assurance chassis.

liability_cf(t)[source]#

The same stream as net_cf(), outgo positive: -net_cf(t) exactly.

The orientation a valuation layer consumes: a Solvency II best estimate is sum v(t) liability_cf(t) over the relevant risk-free term structure, plus a risk margin. Published as a column beside net_cf() so the sign convention is verifiable in the frame rather than only in prose.

disc_factor(t)[source]#

v**t: (1 + rechnungszins())**(-t/12), the monthly discount factor.

Used only by the pricing engine. The projection publishes undiscounted cash flows and this cells never touches them; it exists so that the equivalence the Beitrag is struck by is visible in the model rather than only in the notes.

tar_mort_rate(t)[source]#

The first-order active-life death rate: blended across the sexes, margined.

act_mort_margin x [ mix q_M(x) + (1 - mix) q_F(x) ], forced to 1.0 at the limiting age. Two separate things are happening and both are deliberate.

The blend is the unisex constraint: sex may not enter the premium of a contract concluded from 21 December 2012, so the price is struck on a mixed basis while the projection runs on the model point’s own sex. unisex_mix_male = 0.50 is a [std] assumption, and pricing a 50 / 50 mix while writing 60 / 40 is a named model risk — the mismatch is the whole cross-subsidy, and the mix is endogenous to the price.

The margin is prudence, and its direction forks by risk. For an active life, prudence means lower mortality — a life that survives is a life that can claim — so the margin is 0.90 and not 1.10. Only the direction is cited; no German Sicherheitszuschlag level for a Pflegetafel was established, and the responsible actuary sets it in practice.

tar_mort_rate_care(t, g)[source]#

The first-order in-care death rate at Pflegegrad g.

care_mort_margin x [ 1 - exp(-mort_mult(g) mu_blend) ], where mu_blend is the force of the unmargined blended active rate. Taking the multiple on the unmargined force is what keeps the two mortality margins from compounding: the active margin lengthens the pre-claim period and the in-care margin lengthens the annuity, and they are separate prudence statements about separate risks. care_mort_margin = 0.85 lowers in-care mortality, which lengthens the annuity — prudent in the direction that costs money here, and the opposite of what an annuity table would do.

tar_inc_rate(t)[source]#

The first-order incidence rate: blended across the sexes, margined, capped.

min(inc_margin x [ mix i_M(x) + (1 - mix) i_F(x) ], inc_cap). inc_margin = 1.25 is prudence in the obvious direction — more claims — and it is the margin the whole product’s basis risk sits behind: DAV 2008 P was built on the superseded Pflegestufen and the 2017 reform widened the insured population, and no margin is a substitute for that. The cap binds only at the very oldest ages, where the exponential proxy would otherwise exceed the survival it is applied to.

tar_det_rate(t, g)[source]#

The first-order deterioration rate: det_margin x det_rate(g), capped below 1.

det_margin = 1.15 moves lives faster into the higher-paying grades, which is prudence on a schedule whose top steps pay most.

tar_rec_rate(t, g)[source]#

The first-order recovery rate: rec_margin x rec_rate(t, g).

rec_margin = 0.80 produces fewer recoveries and so longer spells in payment, which is the prudent direction for a benefit that stops on recovery.

tar_p_act(t)[source]#

The first-order active-state month transitions, as (stay, death, care).

The same constant-force, proportional-allocation step as p_act_stay() and its companions, on the first-order rates and with the Wartezeit gate applied to the incidence force. Returned as one tuple rather than three cells because the tariff ledgers are an internal engine: they have no reader outside premium_mth_pp() and check_prem_equiv(), and splitting them would triple the pricing engine’s surface for no gain.

tar_p_pg(t, g)[source]#

The first-order grade-g month transitions, as (stay, death, worse, better).

The same allocation as p_pg_stay() and its companions, on the first-order rates. The four components sum to exactly 1 by construction, which is what makes the tariff ledgers a closed system and check_prem_equiv() a real identity.

tar_pols_act(t)[source]#

The first-order active ledger, from one policy at issue.

Seeded at t = 0 with 1.0 whatever the model point’s status is, because the premium is struck at issue on an active life — an in-force point that supplies its own premium never reaches this engine at all — and rolled forward with no lapse. The absence of lapse is both German first-order practice and what keeps the model acyclic: a pricing quantity must not depend on a behavioural assumption that depends on the path that depends on the premium.

tar_pols_karenz(t, g, z)[source]#

The first-order Karenz ledger, empty unless karenz_months() > 0.

Present so that the tariff basis prices the benefit the contract actually pays: a Karenzzeit removes a material share of claims, more than its length suggests, and omitting it from the pricing basis would load the premium with a benefit the contract does not provide.

tar_pols_grad(t, g)[source]#

First-order graduations into Pflegegrad g; the entrants themselves when K = 0.

tar_pols_pg(t, g)[source]#

The first-order paying ledger at Pflegegrad g.

tar_esc_pg(t, g)[source]#

The first-order escalation ledger, the one the tariff annuity is valued on.

Identical in construction to esc_pg(), so a contract sold with a Leistungsdynamik is priced with one. With the dynamic off it equals tar_pols_pg() at every t and g.

tar_pols_if(t)[source]#

The first-order in-force count: active, deferred and paying together.

tar_pols_prem(t)[source]#

The first-order premium-paying units: in-term, less the waived paying grades.

The waiver enters the price, not only the projection. On a level-premium contract issued at 45 and claiming at 82 it removes the remaining premium stream for the whole of the paying period — of the order of four years of premium, the same order as one year of benefit — and that cost sits inside the level premium. It is one of the reasons a Pflegerente is dearer than a Pflegetagegeld of nominally equal benefit.

tar_pols_death(t)[source]#

First-order deaths in month t, from every state.

Read only by the Beitragsrückgewähr leg of the equivalence: a death benefit written into a Pflegerente is a death cover and has to be priced as one.

epv_benefits()[source]#

A: the expected present value of the Pflegerente on the first-order basis.

sum_t v**t R sum_g pi_g tar_esc_pg(t, g), discounted at the Rechnungszins. On the anchor cell it is the whole of the benefit side: with no Beitragsrückgewähr and no survival benefit, this annuity is the only thing the contract ever pays.

epv_prem_units()[source]#

U: the expected present value of the premium stream in units of P.

sum_{t : premium_due(t)} v**t m tar_pols_prem(t). Dividing the benefit and expense values by this is what strikes the level premium, so it is the annuity factor of the equivalence and the quantity a reader recomputes to check the premium by hand.

epv_admin()[source]#

G: the expected present value of the per-policy administration cost, first order.

epv_claim_expense()[source]#

C: the expected present value of the per-annuity-payment claims cost, first order.

prem_net_level_pp()[source]#

The net level premium: epv_benefits() / epv_prem_units().

Benefits only — no expense loading, no Zillmerung, no Risikozuschlag. Published beside premium_mth_pp() because the gap between the two is the whole of the expense loading, and a reader who wants to know what the biometrics alone cost reads it here.

premium_mth_pp()[source]#

P: the level monthly gross Beitrag per policy.

Where premium_mth is positive on the model point, that is the premium and this cells returns it unchanged. Where it is 0.0, the premium is struck by equivalence on the first-order bases at the Rechnungszins:

P U = A + P D1 + P a1 + beta P U + G + C

where A is epv_benefits(), U epv_prem_units(), G epv_admin(), C epv_claim_expense(), beta the premium-related administration percentage, a1 the Zillmerung allowance in units of P and D1 the Beitragsrückgewähr value in units of P. Everything that scales with P is linear in it, so the equation solves in closed form,

P = (A + G + C) / [ U (1 - beta) - D1 - a1 ]

and for the Einmalbeitrag U = 1, so the same expression gives the Einmalbeitrag directly.

The Risikozuschlag multiplies the gross premium and never the benefit, so claims() is invariant to it. It is applied here, after the equivalence, which means the Beitragssumme an extra-risk contract’s Zillmerung is charged on is the rated premium’s — the amount the policyholder actually contracts to pay.

There is no published German rate card for this product to reproduce, so this is a computed quantity rather than a reproduced one, and the technical notes sanity-check its level against an argued band rather than against a citation.

prem_units_at(t)[source]#

The number of premium units of P payable to date on an uninterrupted path.

cum_prem_max_pp(t) / P written without P, so the Beitragsrückgewähr leg of the equivalence can be assembled before the premium it would otherwise depend on is known. Breaking that circularity is the only reason this cells exists.

check_net_cf_resid(t)[source]#

The cash flow statement’s residual in month t — the library’s first ruling.

net_cf(t) less its reconstruction from the statement’s own published parts: premiums - claims_annuity - claims_lapse - claims_death - expenses - claim_expenses, every one of them a column of result_cf(). Zero everywhere.

It is zero by construction while net_cf() subtracts claims(t), and that is the point: the residual re-derives the headline number from the three claims kinds separately rather than from their subtotal, so a benefit that stops being included in claims(t), or a column added to the frame without being subtracted, fails here instead of silently changing the answer. Every model in this library publishes this cells and its no-argument companion, so that no model’s headline number is the one quantity nothing checks.

check_net_cf()[source]#

True when the cash flow statement reconciles in every projected month.

No argument, one bool over all t, the library-wide shape; check_net_cf_resid() gives the signed residual of the month that failed.

check_pols_roll_fwd_resid(t)[source]#

The in-force roll-forward residual in month t; zero everywhere.

pols_if(t+1) - [ pols_if(t) - pols_death(t) - pols_lapse(t) ]. It says that lives leave the in-force population only by death or surrender, and that every Pflegegrad transition — entry into care, deterioration, Herabstufung, Reaktivierung, graduation out of the Karenz ledger — is internal to it.

That is a strong statement about a nine-state ledger and it is not trivially true: the three ledgers are rolled forward independently, and a life double-counted between them, or lost between the Karenz ledger and the paying one, or lapsed out of a paying grade, leaves a residual here. What it does not catch is a life leaving the system altogether, which is why check_states() is published beside it.

check_pols_roll_fwd()[source]#

True when the in-force roll-forward closes in every projected month.

check_states_resid(t)[source]#

The state-partition residual at the start of month t; zero everywhere.

pols_act + sum_{g,z} pols_karenz + sum_g pols_pg + pols_dead_cum + pols_lapse_cum less pols_if_init(). The three live ledgers and the two absorbing counts partition the initial cohort at every t, and the identity is assembled by direct summation over the ledgers, with no reference to the recursion that produced any of them.

That independence is what makes it more than the telescope of check_pols_roll_fwd(). It catches a wrong seeding of an in-force model point, a life counted in two grades at once, an entrant into care who never leaves the active ledger, and a Karenz cohort that graduates twice. Because mort_rate is forced to 1 at the limiting age, it also closes at the far end: the decrements account for the whole policy rather than leaving a truncation residue.

check_states()[source]#

True when the ledgers and the absorbed counts partition the cohort at every month.

check_waiver_resid(t)[source]#

The Beitragsbefreiung split residual in month t; zero everywhere.

pols_prem(t) + pols_waived(t) - pols_in_term(t). The waiver splits the in-term population: it neither loses a policy nor creates one, and a life that stops paying is a life that started being paid.

It is arithmetically trivial while pols_prem() is a difference, and it is published anyway because the failure it guards is not a slip in the subtraction but a disagreement about who belongs on each side — a Karenz life counted as waived, a grade-1 life waived on the delib_std grid, a waiver that never revives on a Herabstufung. Each of those changes pols_waived and pols_prem in ways that still sum correctly, so the check is read together with the tests that assert the membership itself.

check_waiver()[source]#

True when the waiver splits the in-term population in every projected month.

check_esc_ledger_resid(t)[source]#

The escalation-ledger residual in month t; zero everywhere.

With leistungsdynamik = 0 it is sum_g [ esc_pg(t, g) - pols_pg(t, g) ], which must be exactly zero: the two recursions are then identical and any difference is a coding divergence between them. With a positive dynamic it is the sum of the negative parts of the same differences, which must be zero because an escalated ledger can never fall below the head count it escalates.

The invariant matters because the annuity is weighted on esc_pg. If the escalation ledger drifted below the population — an entrant added at the wrong weight, a survivor escalated before rather than after the transition — the benefit would be understated and no total in the frame would look wrong.

check_esc_ledger()[source]#

True when the escalation ledger dominates the head count in every projected month.

check_prem_equiv_resid(t)[source]#

The discounted first-order imbalance of month t; its sum over t is zero.

v**t [ premium - benefit - expense ] on the tariff ledgers: the premium leg is P m tar_pols_prem(t) on due months, the benefit leg the Pflegerente valued on tar_esc_pg plus the Beitragsrückgewähr where the option is on, and the expense leg the Zillmerung allowance at t = 0, the per-policy administration, the premium-related administration and the claims cost.

It is not a tautology. The premium level comes from the closed form in premium_mth_pp(), but both legs here are re-assembled month by month from the ledgers, so substituting a best-estimate rate into one leg, dropping the Zillmerung term, forgetting the waiver in tar_pols_prem or valuing the annuity on tar_pols_pg instead of tar_esc_pg all make the sum miss zero.

Individual months are large and of both signs — the early ones strongly positive, the late ones strongly negative — so only the sum is an identity. Where the model point supplies its own Beitrag no equivalence was struck, and the residual is zero by construction: an equivalence that was never struck cannot be checked.

check_prem_equiv()[source]#

True when the gross premium closes the first-order equivalence.

The sum of check_prem_equiv_resid() over the whole tariff horizon, against a tolerance scaled by P U — the size of the premium leg — because the residual is a difference of two large discounted values and an absolute tolerance on it would be a statement about the contract’s size rather than about the equivalence.

result_cf()[source]#

Result table of cash flows, indexed by policy month t.

Contiguous from duration_mth_init() to proj_len() - 1. pols_if is the start-of-month count, which is the weight applied to every cash flow on the same row, and its first value equals pols_if_init() exactly. pols_act, pols_care and pols_prem split it three ways for a reader following the projection: active, in care — deferred and paying together — and actually paying a Beitrag.

claims_death is structurally zero unless beitragsrueckgewaehr is on, and claims_lapse is zero once the premium term has ended, both published rather than dropped so the product facts are stated instead of inferred. net_cf is premiums less the three claims columns, expenses and claim_expenses; liability_cf is its negative.

result_states()[source]#

Result table of the state ledgers, flows and rates, indexed by policy month t.

The frame a reader needs to follow the multi-state machinery behind result_cf(): the five paying grades, the Karenz ledger and the three flows between them, the two decrements out of the system, and the four annual rates the whole projection is built from. It is not part of the house contract and carries no check_*; mort_rate_care_pg5 is published in preference to all five grades’ because the grade-5 rate is the one that carries the product’s central biometric fact.