# modelx: pseudo-python
# This file is part of a modelx model.
# It can be imported as a Python module, but functions defined herein
# are model formulas and may not be executable as standard Python.
"""Input data shared by every by-policy projection.
The nine input CSVs are read here, **once per model**, and referenced from
:mod:`~.Pension_KR_S.Projection` as ``data``. :mod:`~.Pension_KR_S.Projection` is
parameterized by ``point_id``, so each ``Projection[N]`` is a separate ItemSpace with its
own cells cache; if the readers lived there, every model point would re-read every file.
Holding them in an unparameterized Space reads each file once no matter how many policies
are projected.
Inputs are **external files**: plain CSVs in the model folder's parent directory,
``products/pension_savings/``, rather than data 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. 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
``Pension_KR_S`` folder without its parent's CSVs produces a model that reads and then
fails on first evaluation.
:func:`input_dir` resolves the directory from ``_model.path.parent`` at run time, so the
model works wherever the repository is checked out. Each table has a filename Reference
and a reader Cells:
====================== ============================== ==========================
Reference Cells File
====================== ============================== ==========================
model_point_file model_point_table() model_point_table.csv
mort_table_file mort_table() mort_table.csv
mort_anchor_file mort_anchor_table() mort_anchor_table.csv
lapse_table_file lapse_table() lapse_table.csv
decl_rate_file decl_rate_table() decl_rate_table.csv
guar_rate_file guar_rate_table() guar_rate_table.csv
pricing_table_file pricing_table() pricing_table.csv
expense_table_file expense_table() expense_table.csv
tax_table_file tax_table() tax_table.csv
====================== ============================== ==========================
.. rubric:: The mortality table is a construction, not a copy
**No Korean industry mortality table is public.** 경험생명표 (*gyeongheom saengmyeongpyo*,
the experience life table), currently the 제10회 applied to new business from 2024-04, is
produced by 보험개발원 (Korea Insurance Development Institute) under the statutory office
of 보험요율산출기관, 보험업법 제176조, which carries **no publication obligation**. What is
released is the summary — 평균수명 남 86.3세 / 여 90.7세 and 65세 기대여명 남 23.7년 / 여
27.1년 on the 제10회 — and not the rates.
So :func:`mort_table` reads a **[std] construction** on the *annuitant* basis, and it must
not be shared with ``WholeLife_KR_S``: one table is loaded for survival and the other for
death, and using either for both is wrong in a known direction.
The construction has three parts, all recorded in :func:`mort_anchor_table` and asserted by
``Projection.check_mort_law``:
* a Makeham law ``mu(x) = A + B c**y`` with ``y = max(0, x - setback)``,
``q(x) = 1 - exp(-mu(x))``, fitted jointly to the six annuitant rates two carriers publish
in their 상품요약서 — 「연금사망률」 and 「개인연금사망률」 at ages 40 to 80 — and to the
annuity factors the one published annuitisation illustration implies, at **two** interest
bases;
* the female table is the male law **set back four years**, the whole-year setback closest to
the published 65세 기대여명 gap of 3.4 years between the sexes; it does not reproduce that
gap — the fitted tables differ by 3.66 years at 65;
* a second vintage, ``annuitant_revised``, is the issue vintage times 0.85 — a one-step
lightening of the order the 제9회 → 제10회 revision produced — which is what the
연금사망률 ratchet clause in every retrieved 약관 switches to when it is in the money.
**The fit honours the annuity factors and misses the rates, and the file says so rather than
smoothing it away.** The two published life-annuity implied factors are recovered to four
significant figures (23.7004 against 23.7000 and 31.1799 against 31.1768, inside the rounding
of the published 만원 amounts). The published *rates* are not: the fitted male rates run
about 1.30 times the published anchor at 70 and 0.72 times it at 80, and the female rates,
being a setback rather than an independent fit, run 1.9 to 2.8 times the published female
rates over ages 50 to 70. A three-parameter law cannot honour both, and the annuity factor is
what the payout phase depends on. Every row of both files carries a ``provenance`` column
saying which part of the construction it came from, and that the file is not a copy of a
보험개발원 table.
To swap in a company or filed basis, replace ``mort_table.csv`` with a same-schema file, or
point ``mort_table_file`` at a different name, then clear the cache. No formula changes:
the lookup already carries the table name and the sex. ``Projection.check_mort_law`` will
then report the shipped rates as no longer equal to the stated law, which is the correct
answer once the table is a real one.
"""
from modelx.serialize.jsonvalues import *
_formula = None
_bases = []
_allow_none = None
_spaces = []
# ---------------------------------------------------------------------------
# Cells
[docs]
def model_point_table():
"""The model point table, read from *model_point_table.csv*.
Indexed by ``point_id``. Point 1 is the technical notes' worked-example anchor cell:
male, 보험나이 40 at issue, 기본보험료 ₩500,000 a month for twenty years to 60, annuity
from 65 as a 종신연금형 with a ten-year guarantee.
"""
return pd.read_csv( # noqa: F821
input_dir() / model_point_file, index_col="point_id") # noqa: F821
[docs]
def mort_table():
"""The [std] annuitant mortality rates by table, sex and age, from *mort_table.csv*.
Two vintages live in the file under the ``table`` key: ``annuitant_issue``, the basis
filed in the 산출방법서 at 가입, and ``annuitant_revised``, the lighter table a
경험생명표 revision would produce. Neither is a copy of a 보험개발원 table; both are
constructions on the Makeham law recorded in :func:`mort_anchor_table`. See the Space
docstring.
"""
return pd.read_csv( # noqa: F821
input_dir() / mort_table_file, # noqa: F821
index_col=["table", "sex", "age"]).sort_index()
[docs]
def mort_anchor_table():
"""The law parameters, the terminal age and the published anchor rates, by table and sex.
Read from *mort_anchor_table.csv*, indexed by ``table``, ``sex`` and ``item``. The
``law_a`` / ``law_b`` / ``law_c`` / ``age_setback`` / ``improve_factor`` rows are what
``Projection.check_mort_law`` re-derives the shipped rates from; ``omega_age`` is the
terminal age; and the ``pub_q_*`` rows are the six annuitant rates two carriers publish
in their statutory product summaries — the only Korean annuitant mortality figures in
this library's input set, quoted rather than transcribed from a table nobody may read.
"""
return pd.read_csv( # noqa: F821
input_dir() / mort_anchor_file, # noqa: F821
index_col=["table", "sex", "item"]).sort_index()
[docs]
def lapse_table():
"""The [std] 해지 (surrender and lapse) rates by basis, segment and duration.
Read from *lapse_table.csv*. Two bases: ``pension``, the product's own vector, and
``savings``, the steeper comparison vector of a non-qualified savings contract, carried
so the two can be run side by side. Three segments in each: ``premium_paying`` carries
a duration curve keyed by ``from_year``, the first **0-based** policy year index ``t``
it applies from — ``from_year = 0`` is the first projected year — ``paid_up`` the single
rate applying between 납입완료 and 연금개시, and ``in_payment`` the zero that must apply
once the annuity has started.
"""
return pd.read_csv( # noqa: F821
input_dir() / lapse_table_file, # noqa: F821
index_col=["basis", "segment", "from_year"]).sort_index()
[docs]
def decl_rate_table():
"""The 공시이율 (declared crediting rate) scenarios, from *decl_rate_table.csv*.
Indexed by ``scenario`` and ``from_year`` — the **0-based** policy year index ``t`` the
step applies from — so a scenario is a step function of policy year rather than a
scalar. ``base`` is the composite's level 2.15%; ``floor`` drives
the declared rate below the guarantee at every duration, which is what the second column
of a published illustration shows; ``hybrid`` is the one retrieved design that pays a
fixed 3.5% for five years before reverting.
"""
return pd.read_csv( # noqa: F821
input_dir() / decl_rate_file, # noqa: F821
index_col=["scenario", "from_year"]).sort_index()
[docs]
def guar_rate_table():
"""The 최저보증이율 ladder, from *guar_rate_table.csv*, keyed by elapsed policy years.
``from_year`` is the 0-based policy year index ``t`` the step applies from, so the first
band is keyed ``0``.
The floor steps **down** with duration — 1.25% to five years, 1.00% to ten, 0.50%
after — which is the opposite of intuition and matters: the guarantee is strongest
exactly where the fund is smallest. A 금리연동형보험 must set one at all, under
감독규정 제7-60조제10호.
"""
return pd.read_csv( # noqa: F821
input_dir() / guar_rate_file, index_col="from_year") # noqa: F821
[docs]
def pricing_table():
"""The pricing, charge and module basis, one row per item, from *pricing_table.csv*.
The two published charge percentages and their periods, the annuity-phase charge, the
100.1% minimum-fund ratio, the payment and annuity frequencies, the 표준해약공제액
coefficients of 별표 14, the best-estimate mortality factor and the loan, holiday and
dividend parameters. Every row carries its source tag or its **[std]** rationale in the
``provenance`` column.
"""
return pd.read_csv( # noqa: F821
input_dir() / pricing_table_file, index_col="item") # noqa: F821
[docs]
def expense_table():
"""The best-estimate cash expense and commission levels, from *expense_table.csv*.
These are **cash flows**. They are entirely separate from the 계약체결비용 and
계약관리비용 of :func:`pricing_table`, which are contractual loadings deducted inside
the 계약자적립액; mixing the two double-counts expense in one direction and destroys the
fund calibration in the other. The commission rows are zero because the composite
follows a direct-channel product whose published 모집수수료율 is 0.00% in every year.
"""
return pd.read_csv( # noqa: F821
input_dir() / expense_table_file, index_col="item") # noqa: F821
[docs]
def tax_table():
"""The 연금저축 tax parameters, from *tax_table.csv*, one row per item.
The 세액공제 rates and cap, the ₩18,000,000 contribution ceiling, the 16.5%
기타소득세 on a 연금외수령, the 연금소득 withholding bands including the 3.3%
종신계약 rate in force from 2026-01-01, the ₩15,000,000 aggregation threshold and the
three limbs of the statutory 연금수령 test. **None of these is an insurer cash flow**
and none of them enters :func:`~.Pension_KR_S.Projection.net_cf`; they are carried
because they are what drives the lapse and annuitisation-election assumptions.
"""
return pd.read_csv( # noqa: F821
input_dir() / tax_table_file, index_col="item") # noqa: F821
# ---------------------------------------------------------------------------
# References
model_point_file = "model_point_table.csv"
mort_table_file = "mort_table.csv"
mort_anchor_file = "mort_anchor_table.csv"
lapse_table_file = "lapse_table.csv"
decl_rate_file = "decl_rate_table.csv"
guar_rate_file = "guar_rate_table.csv"
pricing_table_file = "pricing_table.csv"
expense_table_file = "expense_table.csv"
tax_table_file = "tax_table.csv"
pd = ("Module", "pandas")