Implementation Notes#
Status: Draft, 2026-09-03. The product this implements is specified in
product-spec.md and the projection is derived in
technical-notes.md; the sources for both are in
sources.md. The worked example of the notes is read off this model
rather than the other way round. Every parameter value below is one of theirs, and where
this document adds one it says so and tags it std.
This is a mechanics demonstration, not a pricing or reserving result. The contractual mechanics are sourced clause by clause, and to a precision no other product in this repository reaches — the benefit definition of a Korean 실손 contract is the supervisor’s 표준약관 at 시행세칙 별표 15 and not a carrier document [S1] REG-R23 REG-R25 — but every quantitative input is a standardization. The published 장기손해보험 참조순보험요율 of 보험개발원, the statutory rate bureau REG-R4, does not cover 실손의료보험 at all R20 REG-R61; the 산출방법서 where the 예정위험률 and 예정사업비율 live is a 기초서류 that is filed and never published REG-R2; and 제10회 경험생명표 releases only summary statistics REG-R33. There is no public Korean indemnity-medical morbidity or severity basis and no public Korean mortality table. Replace the assumption tables with company data before drawing any conclusion from the output.
Medical_KR_S is a monthly, by-policy projection of gross best-estimate liability cash
flows for 4세대 실손의료보험 (silson uiryo boheom, fourth-generation indemnity medical
insurance). It stands alone in krlib: it inherits nothing and nothing states a delta
against it, because it is the only contract in this repository whose benefit is a
reimbursement of an incurred cost — the 실손해 branch of 보험업감독규정 제7-63조제1항제2호
REG-R17 R19 — rather than a stated sum. There is no 보험가입금액 that determines a
claim here, only an annual limit that caps one.
Two Spaces, the house layout. Data is unparameterized and holds eight cells: seven CSV
readers and input_dir(). Projection carries 120 cells and 47 References and is
parameterized by point_id, so every Projection[N] is a separate ItemSpace with its own
cells cache.
Run it#
python products/indemnity_medical/run.py # anchor cell, point_id = 1
python products/indemnity_medical/run.py 8 # the cell where the public cap binds
run.py prints the model point, the first thirteen months of the cash flow statement, the
ten policy-year totals, the renewal and experience-rating ledger, the undiscounted totals
and the ten check_* identities. Everything it prints is ASCII, so the output lands on a
Windows console under any code page: amounts are labelled KRW, and both priced units are
romanized — geubyeo for 급여 (covered by 국민건강보험) and bigeubyeo for 비급여. Real
output, with the three wide frames elided — they are in
technical-notes.md in full — and with four header lines continued
onto an indented second line so that this page stays inside its column width; the text is
otherwise what run.py prints, character for character:
Medical_KR_S - silson uiryo boheom (4th-generation indemnity medical), monthly grid
model point 1: anchor 40M all covers full limit - M40 (man nai, age last birthday),
inside National Health Insurance
annual limit = KRW 50,000,000 per bojang jongmok per-visit cap = KRW 200,000
boninbudam sanghanaek = KRW 3,260,000 (decile 6)
first-year premium = KRW 11,982.00/month, split geubyeo 4,792.80 / bigeubyeo 7,189.20
retention: geubyeo 20% inpatient, bigeubyeo 30% inpatient;
projection = 120 months, 10 policy years to age 49
modules: bigeubyeo rider = True 3-dae bigeubyeo = True yoyul sangdaedo = True
musago halin = True
suspension rate = 0.00% cost-trend multiplier = 1.00 utilisation multiplier = 1.00
[ result_cf() t = 0..12; the ten policy-year totals; result_prem() by policy year ]
Undiscounted totals over the projection (KRW)
premiums 1,558,165.43
claims 1,201,095.17
expenses 109,071.58
claim_expenses 36,032.86
commissions 93,489.93
net_cf 118,475.90
loss ratio 0.7708
Roll-forward and contractual identities
[ the ten check_* cells, each True; they are tabulated under Tests below ]
Three lines to the same thing:
import modelx as mx
model = mx.read_model("products/indemnity_medical/Medical_KR_S")
model.Projection[1].result_cf() # the worked example's anchor cell
model.Projection[1].result_prem() # the renewal and 요율 상대도 ledger, by policy year
model.Projection[1].result_pols() # the five decrements
Projection takes a point_id; Projection[1] is the worked-example anchor cell.
result_cf() returns a DataFrame indexed by policy month t, one column per cash flow
line; result_prem() is the frame a reader needs to follow the experience-rating loop,
because it is indexed by policy year, which is the clock that mechanism runs on.
model.Projection.doc carries the notes’ symbols mapped to the cells names and states the
age basis; model.Data.doc says what each input file is and, for the mortality table, what
it is not.
Two clocks, and the model keeps them apart in its naming#
Cash flows run on the policy month and take t. Everything the contract itself does —
every limit, every counter, the co-payment cap, the annual aggregate, the renewal, the
experience-rating window — runs on the policy year and takes y. The year is the
「계약일로부터 매1년 단위로 도래하는 계약해당일 전일까지의 기간」 of [S1 제5조제2항], and
every limit resets on it.
policy_year(t) = t // 12 + 1 is the only bridge between the two, and the annual claim is
spread evenly across the twelve months of its year std: nothing published gives a
within-year seasonality, and on this product the month is a presentation grid for an annual
quantity rather than a unit of account. It shows in the printed statement — within a policy
year every per-policy amount is constant and every row is the year’s first row scaled by
pols_if(t).
A cells that takes y never takes t and vice versa, which is the whole of the
convention. claims_ge_in_pp(y) is a policy-year amount per surviving policy;
claims(t, "GE_IN") is the month’s cash flow weighted by pols_if(t). Mixing them is the
easiest way to project a year’s claim twelve times.
t is 0-based: the first projected month is t = 0, the frame is
range(proj_len()), and proj_len() is the number of projected policy months — the
exclusive end of the frame, the library-wide meaning of proj_len — 120 on every
shipped model point, so result_cf() has 120 rows indexed t = 0 … 119. The policy year
y is the contractual 1-based label t // 12 + 1, derived from t and never used to
index the frame; result_prem() is indexed by that label, 1 … 10.
CSV time keys. No input file is keyed by the model’s t. lapse_table.csv is keyed
policy_year 1 … 10, a contractual 1-based label read through policy_year(t), so its
values are unchanged; mort_table.csv is keyed (sex, age) and read at age(t);
utilisation_table.csv is keyed (sex, age_start) and read at util_band(y);
severity_table.csv (stream, point), claim_shape_table.csv bucket,
oop_ceiling_table.csv decile and model_point_table.csv point_id carry no time
axis at all, and model_point_table.csv holds no duration or in-force column. Nothing in
the inputs moved with the frame.
The horizon is two 재가입 cycles, and it is stated rather than contractual#
Ten policy years — two five-year 보장내용 변경주기 [S1 제23조]
REG-R17 제7-63조제2항제6호나목 — or the run to max_cover_age = 100 on 보험나이 if that
comes first, which on the shipped model points it does not.
The distinction is the point, and it is why reentry_cycles = 2 is a Reference rather than
a hard-coded 120. At the fifth 계약해당일 a 4세대 contract’s benefit terms are replaced by
whatever the supervisor is then prescribing — on the real calendar, 5세대, whose 비급여 특약
is split 중증 / 비중증 and which excludes 근골격계 이학요법치료, 체외충격파치료 and 주사료
outright [S2] R5 R6. Nothing past the first 재가입 is a projection of this
contract’s terms. The model assumes re-entry on unchanged terms, twice, and says so; set
reentry_cycles = 1 for a projection that stops where the specified contract does.
pols_maturity(t) carries the survivors out at the horizon. Nothing is paid — a
순수보장성 contract has no maturity benefit and there is no claims(t, "MATURITY") limb —
but without the count the in-force roll-forward would appear to lose lives with no cause in
the final month. What ends there is the stated horizon, not the contract.
The reimbursement machinery, in the order it must be applied#
This is the part of the model that is the product, and the order is not optional. Applying these five reductions in any other sequence produces a number that is not a claim under this contract.
1. 본인부담상한제 first, as an exclusion from covered loss — oop_trunc(y). The NHIS
refunds a member’s annual 본인일부부담금 above an income-graded ceiling R10 REG-R53, and
the 표준약관 excludes anything so refundable from cover twice over, at 제5조제3항 and at
제4조제3항제1호 [S1]. The 급여 half of the claim is therefore bounded above at roughly
0.80 × 본인부담상한액 — ₩720,000 for a 1분위 insured on the 2026 scale against ₩6,744,000
for a 10분위 one, a nine-fold spread driven by income and nothing else. It is applied
inside paid_out_per_visit, on the incurred cost, so it changes where the deductible
bites; applied to the finished claim it is wrong on the kinked outpatient limb.
2. The co-payment and the deductible, per event — claims_ge_in_pp, claims_ge_out_pp,
claims_np_in_pp, claims_np_out_pp and the shared paid_out_per_visit. Inpatient
reimbursement is a flat percentage: 80% 급여 [S1 기본형 제3조], 70% 비급여
[S1 특별약관 제3조], 40% on either where 국민건강보험 entitlement does not apply
[S1 제3조제3항제1호]. Outpatient reimbursement is the cost less
max(flat floor, 자기부담률 × cost), then capped per visit at ₩200,000 [S1 제5조제5항];
the floor is ₩10,000 clinic-tier and ₩20,000 hospital-tier on the 급여 side
[S1 기본형 제3조 표1] and a flat ₩30,000 at every provider on the 비급여 side
[S1 특별약관 표1], which is why the 급여 side needs a provider mix (clinic_share) and the
비급여 side does not.
The deductible is applied to the cost distribution, never to its mean, and that is the
single most important implementation decision in the model. The rule is a flat amount below
a crossing point and a percentage above it: at the clinic tier ₩10,000 until ₩50,000 of
covered cost and 20% above it, so a ₩10,000 visit pays nothing, a ₩50,000 visit pays
₩40,000, and the ₩200,000 per-visit cap binds at ₩250,000. E[f(X)] ≠ f(E[X]) for a kinked
f: on the 비급여 통원 stream the mean gives ₩104,545 against the distribution’s ₩76,970,
+35.83%. severity_table.csv exists for exactly this reason, and the error hides — the
급여 통원 blend at clinic_share = 0.63 is 1.05% wrong while its tiers are +6.36% / −17.01%.
3. The ₩2,000,000 annual inpatient co-payment cap, on what remains — the top_up term
of claims_ge_in_pp [S1 제5조제4항] REG-R17 제7-63조제2항제2호. Applied second, and to
the retention that survives the truncation, because both reliefs run in the same direction
and applying them in parallel double-counts.
4. The 3대비급여 sub-limits, which displace the main limit for their three classes —
claims_physio_pp, claims_inject_pp, claims_mri_pp, summed in claims_np_three_pp.
도수·체외충격파·증식치료 share one 50-act counter and ₩3,500,000; 주사료 has its own 50 acts
and ₩2,500,000; MRI has ₩3,000,000 and no counter [S1 특별약관 제3조(3) 표1]. These caps
replace the ₩50,000,000 aggregate for those three classes rather than sitting inside it,
so np_limit_factor(y) is applied to claims_np_main_pp(y) and never to
claims_np_three_pp(y). Where 3대비급여형 is not held those treatments are simply uncovered
and do not fall back into the main limit — model point 6 is that election.
Two gates inside this limb are worth naming. Cover beyond the first ten physical-therapy
acts is conditional on a documented clinical re-assessment every ten acts [S1] R2 — the
only benefit in krlib gated on a clinical review rather than on a definition — which a
projection can represent only as a continuation probability at the boundary,
physio_cont_prob = 0.60 std. And non-covered injections of 항암제, 항생제 and
희귀의약품 leave the ₩2,500,000 sub-limit for the main 비급여 limit
[S1 특별약관 제3조(3)제2항]: inject_carve_share = 0.25 std draws that boundary, the
carved payment is added in claims_np_out_pp and removed from acts_inject_eff(y), and
since non-covered injections were 18.5% of all 2024 claims R8 it is a first-order
calibration question rather than a detail.
5. The annual aggregate, per 보장종목 and per policy year — ge_limit_factor(y),
np_limit_factor(y). 상해 and 질병 carry separate ₩50,000,000 limits on each of the two
parts [S1 제5조], so the raw claim is split by share_injury = 0.15 std and each part
capped — a whole-contract annual exposure of ₩100,000,000 in the ordinary reading.
What a deterministic projection can and cannot say about the limits#
E[min(X, Λ)] ≠ min(E[X], Λ). A projection that applies a limit to an expectation
understates the limit’s bite by ignoring dispersion. On every shipped model point the
₩2,000,000 inpatient cap, the three 3대비급여 money limits, the two 50-act counters, the
100-visit cap and the ₩50,000,000 annual limits do not bind: the expected annual claim
of a single cell is two orders of magnitude below them, and the supervisor’s own tail figure
is that 0.005% of insureds took more than ₩50,000,000 in 2019 R1.
The machinery is implemented anyway, for two reasons: it binds under any seriatim or
stochastic run, which is where the dispersion an expected-value grid averages away lives;
and check_annual_limits() proves it is wired, which is a different claim from proving
it is exercised. Do not delete a limit because it reads slack.
The one limit that does bind on a shipped point is the 본인부담상한제 truncation, on model
point 8 — a high-utilisation cell (util_mult = 10) on the lowest 본인부담상한액 decile —
where oop_trunc(y) runs from 0.801828 in policy year 1 to 0.602441 in year 10,
with a visible step at the utilisation band change in year 6. On the anchor it is
identically 1.0, the incurred 급여 본인부담금 of ₩84,805.89 sitting a factor of 38.4 below
the ₩3,260,000 ceiling, so the error a wrong ordering would make is invisible. That is why
model point 8 exists.
The experience-rating loop is the model’s reason to exist#
The renewal premium of the 비급여 rider is a function of the individual policyholder’s own prior-year non-covered claim amount [S1 특별약관 제6조제3항] R2 R3: a feedback loop from claims to premium, inside a single policy, on an annual clock, modelled rather than described, which is the whole reason this product is in the library.
The band is memoryless, so there is no chain to carry: 「보험금 지급(사고) 이력이
1년마다 초기화됩니다」 R2. band_share(y, b) reads claims_np_rated_pp(y − 1) and nothing
earlier, so the band distribution at renewal y is simply the distribution of the annual
rated claim in year y − 1 — claim_shape_table.csv rescaled by shape_rel(k) to that
year’s mean and read against the fixed money thresholds ₩1,000,000 / ₩1,500,000 /
₩3,000,000 [S1 특별약관 제6조제3항]. Because the thresholds are fixed money and the claim
level trends, contracts migrate into the surcharge bands year by year with nothing in the
model changing. That migration is the loop. On the anchor the 3단계 buckets empty by
policy year 5 and the 5단계 contribution to the surcharge pool rises from 0.012 at
commencement to 0.040 by policy year 7.
The discount is solved, not set. The wording defines it as the solution to a
revenue-neutrality constraint — 「상대도 적용 전·후의 총 보험료 수준이 일치하도록」 [S1] —
so reld_solved(y) = (1 − Σ_{b≥2} w_b r_b) / w_1. The shape table is calibrated so that at
the anchor cell’s first-year claim level the band mix reproduces the published commencement
distribution 72.9 / 25.3 / 0.8 / 0.7 / 0.3 R12 exactly, which makes the solved
discount come out at 0.957477 — the specification’s 0.9575, a 4.25% discount — as a
result rather than as an input. Hard-coding the illustration’s 0.95 instead gives
reld_avg = 0.9945494, a 0.55% leak out of a scheme the wording requires be self-financing,
growing as the band mix moves. check_relativity_neutral() is the identity that proves it.
A std cap of 5% sits under the discount, reld_disc_cap = 0.05, from the two
published values — 「5% 내외」 at launch R1 and −5% 잠정 at commencement R3. It is slack
at commencement and binds from policy year 5 on the anchor cell; from there the scheme stops
being revenue-neutral and the average relativity rises above 1 — reld_avg reaching
1.0025494 at year 5 and 1.0095494 from year 7, and 1.329009 on the high-utilisation
model point 8. That is the loop reaching the aggregate, and it is a feature of the
design rather than of this implementation.
Two things run alongside and are different animals. The 무사고 할인 takes 10% off the
whole office premium after two consecutive claim-free years R1 [S3], where the
relativity has a one-year lookback and touches only the rider:
noclaim_share(y) = band_share(y − 1, 1) × band_share(y, 1) = 0.729012² = 0.5314584961 on
the anchor, the two years treated as independent std. A one-year lookback would hand
the discount to policyholders who had earned one clean year and cost 2.09% of the year-3
office premium. And the three-year deferral, reld_start_year = 4, reflects the
2024-07-01 commencement three years after launch R3: the anchor cell’s first three
renewals are a plain attained-age re-rate, which is what 1세대 through 3세대 were.
reld_exempt_share = 0.15 std strikes 산정특례 conditions and all claims of an
insured graded 장기요양 1·2등급 out of the rating count and never out of the benefit
[S1 특별약관 제6조제3항] REG-R54: claims_np_pp(1) = 62,978.2477 is paid in full while
claims_np_rated_pp(1) = 53,531.5106 is what the band is read against. Applying the 15% to
the claim would silently delete a fifteenth of the cover. This exemption is the only direct
statutory cross-reference between this model and LTC_KR_S.
The renewal recursion, and one order of operations#
base(y) = base(y-1) × (1 + a) × (1 + b(y)), a = 0.04, |b(y)| ≤ 0.25
The 표준약관’s own renewal illustration labels its basis increment
「기초율 증가분 = 전년도 기준보험료 × 25%」, which reads as additive — but 3,640 is 25% of
14,000 × 1.04 = 14,560 and not of 14,000: the corridor applies to the age-adjusted
prior premium [S1 제30조]. Reproducing the illustration’s printed row
14,000 → 18,200 → 23,660 → 30,758 → 39,985 → 51,980 requires it. Getting it wrong costs 4%
of the corridor every year and compounds — prem_np_base(10) = 20,625.8505 on the correct
recursion against 20,096.9929 on the additive misreading, 2.564% low.
b(y) is not an input. Each priced unit is re-rated at its own claim trend, clipped
to the corridor — basis_incr_ge(y) at med_trend_ge = 0.010 and basis_incr_np(y) at
med_trend_np = 0.081, the 2024 growth rates of the statutory co-payment and of non-covered
spend R9 REG-R41. That std re-rating rule is what keeps each unit’s loss ratio
stable unless the corridor clips it, and it is why the two units are re-rated separately:
the corridor binds per 위험구분단위 and not on the portfolio average
[S1 제30조제2항] REG-R17 제7-63조제2항제3호, which is what check_renewal_corridor() tests
— each unit, against the age-adjusted prior premium.
The relativity is applied only to the rider, in prem_gross_mth(y) and never to
prem_np_base(y): 「비급여 특약 보험료만 할증되며 보험료 전체가 할증되는 것은
아닙니다」 R2. Applying reld_avg(y) to the whole office premium gives a year-5 gross of
₩16,440.5468 against the correct ₩16,426.4626.
A finding worth recording. The corridor and the age loading compose multiplicatively, so
the wording admits a re-rate of 1.25 × 1.04 = 1.30 a year. A claim trend below 30% is
therefore fully recoverable and the corridor does not bind economically even where it
bites arithmetically: model point 10 carries trend_mult = 4.5, its 비급여 re-rate is
clipped from 36.45% to 25%, and its loss ratio still rises only from 0.5333 to 0.5913 over
ten years. The ±25% corridor is a much weaker constraint than it looks.
Renewal decline is its own decrement, and not a lapse#
renewal_decline_rate = 0.01 a year std, acting only in the twelfth month of each
policy year, pols_renewal_decline(t) non-zero only where (t + 1) mod 12 = 0. It is a
separate decrement from pols_lapse because it is a separate act: a lapse is a missed
premium and a decline is the exercise of a contractual option at a contractual date, on a
contract the insurer cannot exit [S5] [S3]. On a one-year renewable product the second is
what the product is exposed to, and it is the larger of the two voluntary exits at every
annual boundary — 0.0089881183 against 0.0079263524 at t = 11, and 0.0083529502 against
0.0043181413 at t = 23.
Folding it into lapse_rate_mth(t) makes the annual boundary invisible, and
check_pols_roll_fwd() balances either way, so the roll-forward will not catch it. On a
contract whose whole architecture is annual, that boundary is what the model exists to show.
The five decrements are applied in the order mortality, lapse, suspension, renewal decline
std; nothing published fixes it, and at these rates the ordering is worth less than a
basis point a year.
Three absences that are product facts#
Each of these is a missing cells, so nothing in the output points at it; the test module asserts the name list instead.
No death benefit. On death from a non-covered cause the contract pays the 계약자적립액 and the 미경과보험료, and on a one-year pure protection contract the 계약자적립액 is nil REG-R17 제7-63조제1항제1호 REG-R25 제22조. Mortality is therefore a pure liability-releasing decrement, there is no
claims_death, and the direction of prudence is inverted with it: an over-statement of mortality is anti-conservative.No surrender value. 「이 상품은 1년만기 순수보장성 상품으로 해약환급금이 발생하지 않습니다」 [S3]. There is no
cv_pp, noclaims_lapsecolumn, no 보험계약대출 and no 보험료 자동대출납입 — so there is nothing to break the fall on a missed premium, which is the reason the first-year lapse rate is what it is. The 표준해약공제액 cap of [별표 14] has nothing to bite on either REG-R20 REG-R19.No waiting period. Unusual among Korean health products, and a direct consequence of the indemnity form: there is no lump sum to anti-select against, so no 면책기간 and no 감액기간 of the kind
Cancer_KR_Scarries.
A fourth absence is easy to misread as a bug. There is no acquisition strain:
commissions(t) and expenses(t) are level rates on every premium and t = 0 is
positive at ₩205.4984, because on a rolling one-year renewable the acquisition/renewal
distinction has no content after year one. A reader who expects the sister libraries’
month-0 trough will look for a bug that is not there.
Modules that are off in the base run#
Each is implemented, off on the anchor so the base run reproduces the worked example, and exercised somewhere in the shipped table.
Module |
Off position |
Switched on |
|---|---|---|
개인실손 중지·재개 — suspension while a 단체실손 is in force, mandatory as a facility under 감독규정 제7-63조제2항제7호 REG-R17 R16 |
|
Model point 9 at 3% a year. Resumption is not modelled: the contract that resumes is a different projection, so this is carried as a decrement and not as a state |
The 40% branch, where 국민건강보험 entitlement does not apply [S1 제3조제3항제1호] [S1 특별약관 제3조제8항] |
|
Model point 10: retention rises to |
요율 상대도 [S1 특별약관 제6조] |
|
On on eight points. Off on 5 and 9, where the contract is a plain attained-age renewable — which is what 1세대 through 3세대 were |
무사고 할인 R1 [S3] |
|
On on eight points; off on 5 and 9 beside the relativity |
3대비급여형 [S1 특별약관 제3조(3)] |
|
Model point 6 is the not-held election |
The 비급여 특약 itself [S1 특별약관] |
|
Model point 5 is 급여-only, at |
trend_mult and util_mult are stress multipliers rather than modules — 1.0 on the anchor,
4.5 on point 10 (cost trend, which runs the corridor into its clip) and 10.0 on point 8
(utilisation, which is what makes the public truncation bind).
Named and not modelled#
The behavioural response to the experience rating. The supervisor’s own worked example has a policyholder cutting his claims by 93% in response to a surcharge, saving ₩300,000 of premium and ₩2,700,000 of out-of-pocket cost in one year R2. The contract is designed to change the insured’s behaviour; this model projects the premium’s response to claims and not the claims’ response to premium. A stated limitation, not an oversight.
The frequency half of the claim distribution.
claim_shape_table.csvtrends its amounts and holds its zero-claim mass fixed at 0.729012, so the 1단계 share is constant while the size of a claim grows. In reality claiming frequency rises with age too.재가입 into a different generation — which is what will actually happen at the fifth anniversary R7 — and with it 계약전환, the 계약재매입 and 선택형 할인 schemes of the 2026 reform R5, 노후실손 / 유병력자실손 R17, 단체실손, 해외여행 실손, and the resumption half of 중지·재개 R16.
Any measurement basis. No 책임준비금 REG-R3 REG-R8, no CSM REG-R60, no risk adjustment, no K-ICS 요구자본 REG-R13, no 해약환급금준비금 REG-R11, no policyholder tax REG-R57. On a one-year indemnity contract the 잔여보장요소 is at most one year’s unearned premium and the 발생사고요소 is the material item, and the 해약환급금준비금 has nothing to bite on because there is no surrender value at all.
Inputs are external files#
Seven CSVs in products/indemnity_medical/, beside run.py, read at run time. The model
folder holds __init__.py, _system.json and its two Space folders and nothing else — no
_data/, no IOSpec, no embedded values — so a diff of the model shows logic changes only.
This is the annuallife.TradLife_A layout; contrast basiclife.BasicTerm_S, which keeps
its inputs inside the model. The consequence worth knowing: the model is not portable on
its own — copy Medical_KR_S without its parent’s CSVs and it reads cleanly, then fails
on first evaluation.
File |
Reference |
Reader |
Contents |
|---|---|---|---|
|
|
|
ten policy configurations, indexed by |
|
|
|
std Makeham construction, sex × 만나이 0–110, 222 rows |
|
|
|
std annual lapse by policy year 1–10, last row applying onward |
|
|
|
std annual claim frequencies and mean stay, sex × five-year band, 36 rows |
|
|
|
std discrete cost distribution per event, eight streams, 33 points |
|
|
|
std ten-bucket distribution of the annual rated 비급여 claim |
|
|
|
본인부담상한제 ceiling by income decile, 2026 scale — transcribed R10 |
Read once, in Data#
Projection is parameterized by point_id, so readers placed there would re-read every
file for every policy. They live in the unparameterized Data Space instead, and
test_inputs_are_read_once_not_once_per_model_point asserts the property against the file
set registered in kr_registry.INPUT_FILES. input_dir() returns _model.path.parent,
resolved at run time and never hard-coded, so the model works from any checkout.
Every assumption CSV carries a provenance column and every cell in it begins with a
citation tag. model_point_table.csv is the only exemption, a model point being a
configuration rather than an assumption.
To swap in a company basis, replace the CSVs with same-schema files, or point the *_file
References at different names, and clear the cache. No formula changes.
Six of the seven tables are constructions, and that is a finding#
It is worth being exact about why, because it is a positive result about what Korea publishes rather than a list of documents this pass failed to fetch. The published 장기손해보험 참조순보험요율 covers 일반상해, 교통상해, 질병 사망률, 후유장해, 입원율, 암 발생률, 비용손해, 재물손해 and 배상책임 — 실손의료보험 is not among them R20 REG-R61 — and the 산출방법서 is never published REG-R2. What the supervisor does publish, annually and in quantity, is aggregate experience R7 R8 R12 REG-R44, and every table below is anchored on that.
mort_table.csv, and the prudence that runs backwards. A std Makeham
construction, q(x) = 1 − exp(−(A + B·c^x)) above age 15 with a log-linear child schedule
below it, fitted to the four summary statistics 국가데이터처 publishes REG-R38. It is
not a transcription: 제10회 경험생명표 is not published in full REG-R33 and the
single-year 완전생명표 qx tables live behind KOSIS and were not downloaded REG-R39. Male
q(40) = 0.00132019. As noted above, on this product an over-statement of mortality is
anti-conservative — the reverse of every other model in the library — so the usual instinct
to load the table is wrong here.
lapse_table.csv. 0.100 falling to 0.020 over policy years 1–10, the last row
applying onward. No 실손-specific persistency table is published anywhere. The ultimate is
anchored on the only 실손-specific figure there is — the 1–3세대 in-force block fell 3.3%
in 2025 R7 — which blends lapse, death and conversion, so 2.0% is what is left once
mortality and the renewal decline are taken out. The first-year rate is set against an
unverified 장기손해보험 13회차 유지율 of about 86%, from a news summary.
utilisation_table.csv, and how its level was solved. The age-40 male row is not chosen;
it is solved from three published quantities, and the solve is what makes the anchor cell
reproduce the market rather than resemble it:
the premium is the published 2021 new-business anchor, ₩11,982 a month for a 40세 남자 on a 10-carrier 손해보험 average R1 — and male 40 is the 기준연령 요건 of 감독규정 제1-2조제2호 REG-R9, so it is the prescribed disclosure cell as well;
the loss ratios are the published 4세대 2022 첫 반기 figures — 급여 97.5%, 비급여 73.0%, combined 82.8% R12 — applied to that premium on the 40/60 split R2, giving targets of ₩56,075.8 and ₩62,977.4 of first-year claim;
the inpatient share of the 비급여 claim is set at 0.30 std, which is what fixes the admission frequency, since one admission drives both halves of the claim.
Three scalars — one on admissions, one on 급여 outpatient visits, one on everything 비급여 — are then solved so that the model’s own year-1 arithmetic hits both targets: the model produces ₩56,076.22 and ₩62,978.25, a first-year combined loss ratio of 0.8280091. The age curves and the sex factors around the solved row are std shapes following the NHIS coverage ratios by age band R9 REG-R41, and there is deliberately no maternity bump in the female rows, because pregnancy and childbirth are excluded from 4세대 cover.
One thing must be said plainly here. 보험개발원 does publish a 질병입원율 grid by age
and sex on the same display REG-R61. It is a 장기손해보험 fixed-benefit hospitalization
incidence on a 참조순보험요율 (net premium) footing, not a count of admissions producing a
payable indemnity claim after a 20% co-payment, and a reference net rate is not a best
estimate. It is therefore available as an external anchor for the age slope of
adm_rate and is not used for the level; substituting it, and restating the level solve
against it, is the first thing a user with company data should try. 실손 위험률 itself
remains unpublished R20.
severity_table.csv. Eight discrete distributions — ge_in, ge_out, np_in,
np_room, np_out, physio, inject, mri — because a single mean is unusable against a
kinked deductible, as above. The dispersion being standardized over is real and published:
the 건강보험심사평가원 price survey found 도수치료 quoted anywhere between ₩5,000 and
₩600,000 across Seoul hospitals R2.
claim_shape_table.csv. Tabulated in KRW at the anchor’s first-year level and read as a
shape: shape_rel(k) divides each bucket by shape_mean() = 53,530.7924, so the same
distribution serves every model point and year and only the level comes from the model.
Bucket 0 carries the 72.9% no-claim mass, the six small buckets take their dispersion from
the six lowest bands of the published 4세대 claim-size distribution R12 — those bands’
shares normalised, and their midpoints scaled by a single factor onto the sub-₩1,000,000
range 2단계 occupies — and the three tail buckets sit inside the surcharge bands at the
published 0.8 / 0.7 / 0.3 shares R12. check_claim_shape() asserts both
normalisations — Σ share = 1 and
Σ share × rel = 1 — because a shape that does not integrate to its own mean makes
reld_solved() divide by the wrong denominator.
oop_ceiling_table.csv is the only transcription in the set — the 2026 본인부담상한제
scale, ₩900,000 at 1분위 to ₩8,430,000 at 10분위 R10 — and it is the single most important
interaction in the product, because it is where a public scheme reaches inside a private
contract and truncates half of its claim, differently by income.
Sign convention#
net_cf is income positive — premiums less every claim limb, less maintenance expense,
less claim handling expense, less commission — the library-wide sign and the notes’ own, so
unlike the whole life and payout annuity models there is no outgo-positive liability_cf
companion to publish: one stream, one sign, one name.
The shape to expect is unlike anything else in the library. The whole ledger collapses to
net_cf = 0.87 × premiums − 1.03 × claims, so the sign of any period is decided by one
number — the loss ratio, against the model’s own break-even of 0.844660. Year 1 runs at
0.828009, which is the calibration closing rather than an accident, so the product opens on
a margin of about 1.7% of premium with a claim already 98% of the way to break-even. The one
negative year is policy year 6, at −₩276.14, where the utilisation band steps at
attained age 45 while the premium takes its age loading smoothly.
The identity check_net_cf() closes#
One line, and it is the one a reader adding up the printed statement is checking:
net_cf = premiums − claims_ge_in − claims_ge_out − claims_np_in − claims_np_out
− claims_np_three − expenses − claim_expenses − commissions
check_net_cf_resid(t) computes it as net_cf(t) − (premiums(t) − claims(t) − expenses(t) − claim_expenses(t) − commissions(t)), with claims(t) summing over exactly the five kinds
result_cf() prints. Reading the benefit back out of the same claims(t, kind) machinery
the columns are built from is the point: it catches a benefit kind that exists in
claims(t, kind) but was never given a column, which would leave the statement silently
short of outgo the model is charging.
It is also why result_cf() publishes the five claims_* split columns and no aggregate
claims column — an aggregate beside its parts stops the columns summing to net_cf
without knowing which to skip, which is why claims is in the library’s RETIRED_COLUMNS
register. The claims(t, kind) cells stays, and claims(t) with no kind is the sum.
check_net_cf() closes to cash_tol = 1e-6 rather than to roll_fwd_tol = 1e-10, because
it re-reads won amounts through a chain of annual-to-monthly divisions; check_claim_shape()
has a third tolerance, shape_tol = 1e-9, closing a probability distribution and not an
amount.
Naming#
lower_snake_case throughout, reusing lifelib’s basiclife.BasicTerm_S vocabulary wherever
there is an analogue: pols_* for counts, plural nouns for cash flows, *_rate annual with
*_rate_mth monthly, *_pp per policy, claims(t, kind) with an uppercase kind, and
pols_if_at(t, timing) for the within-month reads. Quantities on the policy year take
y and never t.
The notes’ symbols, and where they live#
model.Projection.doc carries the full 80-row table; the load-bearing rows are these.
Notes symbol |
Cells |
Meaning |
|---|---|---|
|
|
가입나이 on 만나이, attained 만나이, |
|
|
first-year office premium; 비급여 share of it |
|
|
연간 보험가입금액 per 보장종목; 통원 1회당 한도 |
|
|
the three voluntary and involuntary monthly decrements |
|
|
in force at the start of month |
|
|
annual frequencies at the attained band |
|
|
자기부담률, 20% / 30%, or 60% off-scheme |
|
|
급여 본인부담금 incurred, the 상한액, the truncation |
|
|
the five per-policy annual claim limbs |
|
|
the rated 비급여 claim, exemptions removed |
|
|
the five-band 요율 상대도 mix and its factors |
|
|
before the cap, after the cap, the average applied |
|
|
the 4% age loading; the clipped basis increment |
|
|
the two 기준보험료 and the office premium |
|
|
net cash flow, income positive |
Four needed care, and the reasons are in the Projection docstring. oop_trunc(y) is a
factor on the incurred loss and not on the benefit, which makes its position in the
calculation order legible from its name. claims_np_rated_pp(y) is a different quantity
from claims_np_pp(y) and not a netting of it — one is paid, the other is rated.
reld_solved / reld_one are spelled apart because the gap between them is the discount
cap binding. And band_share(y, b) takes the year whose renewal it prices, reading the
claim of y − 1, so no caller has to remember the offset.
Names this product argued for in krlib’s cross-model review#
Three. Two of them retired a rival name into the shared RETIRED_NAMES register, so no
krlib model reintroduces it; the third is a structural argument about which limb a claim
belongs to and has no retired counterpart, though the claims subtotal column it depends
on being absent is in RETIRED_COLUMNS.
renewal_decline_rate, notrenew_rate. The proportion who decline a 갱신 is a decrement, and the shorter name read as its complement to half the models that tried it.Term_KR_Suses the same name for the same event on a ten-year cycle; this product uses it on a one-year cycle, which is where it matters most.claims_np_threeas a column and a limb of its own. The three 3대비급여 classes sit inside the 특별약관 but carry their own money and count limits instead of the ₩50,000,000 aggregate, so folding them intoclaims_np_inorclaims_np_outwould lose the distinction the contract makes and route them throughnp_limit_factor(y).pols_maturity, notpols_expiry. The count whose cover ends at the scheduled end, paid or not. This product pays nothing for it and publishes it anyway, which is the case the register exists to settle:claims(t, "MATURITY")would be the payment, and there is none.
One name was argued against. retain_rate_* holds the fraction the insured keeps
(0.20, 0.30, 0.60), not the fraction reimbursed, because 자기부담률 is the wording’s own
quantity and inverting it would put the model and the 표준약관 in different units at the
exact clause where they must agree.
Standardizations used#
Every row is std; the sourced contractual parameters are in
product-spec.md and technical-notes.md and are
not repeated. “Observed range” is what the retrieved documents actually bound, and several
bound nothing at all — which is said rather than papered over.
Parameter |
Value |
Rationale |
Observed range |
|---|---|---|---|
|
2 |
two 보장내용 변경주기, so the horizon is ten policy years; past the second 재가입 the terms projected are not the terms specified [S1 제23조] |
the cycle length itself is sourced at 5 years [S1] REG-R17; 2세대 wrote 15 [S4], and the number of cycles to project is a choice nothing bounds |
|
0.01 |
no published 실손 renewal-decline series exists; the residual of the 3.3% blended 1–3세대 in-force decay R7 once mortality and lapse are taken out |
none published for any Korean product |
lapse curve |
0.100 → 0.020 over ten years |
anchored on the same 3.3% blended decay R7; nothing to break the fall on a contract with no surrender value [S3] |
first year set against an unverified 장기손해보험 13회차 유지율 of about 86% |
utilisation level |
solved on three published quantities |
the anchor cell’s year-1 급여 and 비급여 claims are made to reproduce the published 4세대 2022 loss ratios R12 on the published 2021 premium anchor R1 |
the solve reproduces 97.5% / 73.0% / 82.8% exactly; the level itself is not otherwise bounded, 실손 위험률 being unpublished R20 |
inpatient share of the 비급여 claim |
0.30 |
needed to close the 비급여 solve, one admission driving both halves of the claim |
none published |
utilisation shape |
NHIS coverage ratios by age band |
age curves and sex factors follow the public coverage-ratio profile R9 REG-R41; no maternity bump, pregnancy being excluded from cover [S1] |
the 질병입원율 grid of REG-R61 is an external anchor for the slope and is deliberately not used for the level |
severity distributions |
eight discrete grids |
a kinked deductible makes |
the dispersion is real and published — 도수치료 ₩5,000 to ₩600,000 across Seoul hospitals R2 |
claim-shape zero mass |
0.729012, held constant |
the shape trends its amounts and not its frequency; a stated limitation, since claiming frequency rises with age too |
the 72.9% commencement share is sourced R12; the FSC’s own commencement mix R3 resolves only three bands and puts 1등급 at 62.1%; nothing published gives the trend of either |
|
0.60 |
the ten-act clinical re-assessment gate is the only benefit in |
none published; nothing bounds it |
|
0.25 |
the 항암제·항생제·희귀의약품 share leaving the ₩2,500,000 sub-limit [S1 특별약관 제3조(3)제2항] |
none published; non-covered injections were 18.5% of all 2024 claims R8, so the boundary is first-order |
|
0.15 |
splits the claim between 상해 and 질병 so each can be capped at its own ₩50,000,000 limit [S1 제5조] |
none published; binds nowhere on the shipped points |
|
0.010, 0.081 |
2024 growth of the statutory co-payment and of non-covered spend R9 REG-R41; the re-rating rule |
scheme outlay grew 4.3% on the same measure; 비급여 compounds at about twice the whole |
|
0.04 |
the only published age-slope datum in any retrieved document, derived: the 표준약관’s renewal illustration prints 나이증가분 that are each exactly 4.0% of the prior base [S1 제30조] |
stylised, at an unnamed age; a flat 4% is not a real age slope and over-recovers against the utilisation curve at these ages |
|
0.05 |
a floor under the solved discount, from the two published values 「5% 내외」 R1 and −5% 잠정 R3 |
those two figures are the range; the wording itself sets no cap |
|
0.15 |
the 산정특례 and 장기요양 1·2등급 exemptions from the rating count [S1 특별약관 제6조제3항] REG-R54, anchored on 암 and 뇌·심혈관 claims at 15.0% of 2025 claims R7 |
the 15.0% claim share is sourced; the exempt fraction of it is not |
|
4 |
the 2024-07-01 commencement, three years after the 2021-07-01 launch R3 R1 |
dates sourced; nothing to bound |
|
|
successive years’ claiming treated as independent; the 무사고 할인 has a two-year lookback R1 [S3] |
nothing published gives the persistence of claiming |
|
0.06, 0.07, 0.03 |
a split of the one published aggregate — 손해조사비 plus 사업비 of about 16.1% of 2025 premium R7; claim handling is charged on claims because 손해조사비 is claim-driven |
no 상품요약서 with a 사업비 disclosure was obtained for any generation; 감독규정 제4-32조제5항 caps first-year commission at one year’s premium and is nowhere near binding REG-R22 |
mortality construction |
Makeham above 15, log-linear child schedule below |
제10회 경험생명표 is not published in full REG-R33 and the KOSIS single-year |
male e₀ 80.585 against 80.8, e₆₅ 19.455 against 19.5; female 86.370 against 86.6 and 23.657 against 23.7 |
|
one factor a year |
a proportional truncation of an expectation is not the same as truncating each realisation, and the NHIS ceiling runs on the calendar year while every contractual limit runs on the policy year |
the ceiling table itself is transcribed R10; the deterministic representation is the standardization |
within-year claim spread |
uniform over twelve months |
no published seasonality; the contract’s own machinery is annual, so the month is a presentation grid |
none published |
decrement order |
mortality, lapse, suspension, renewal decline |
nothing published fixes it; worth less than a basis point a year at these rates |
none |
보험가입금액 election |
₩50,000,000 with ₩200,000 per visit on nine points, the ₩10,000,000 / ₩100,000 rung on point 7 |
the ceiling is sourced [S1 제5조]; which rung a carrier sells is a 사업방법서 matter |
a 5세대 menu offers ₩50m / ₩30m / ₩10m against ₩200k / ₩150k / ₩100k [S3] |
가입나이 envelope |
0–65 |
not published; a 사업방법서 matter REG-R2 |
0–49 on one 2세대 direct product [S4]; 노후·유병력자 families run to 90 R17 |
|
1e-10, 1e-6, 1e-9 |
one closes an identity between cells in a single expression, one re-reads won amounts through annual-to-monthly division, one closes a probability distribution |
all far below one won, or one part in 1e9 of a probability |
Two of these are the ones to replace first. inject_carve_share sits on 18.5% of the
national claim; and the utilisation level is a solve against a published loss ratio rather
than against experience, so it reproduces the market’s 2022 result exactly and carries none
of the 2023–2025 deterioration that ran the generation to 115.1% by 2025 R7 R8. That
gap is not a difference of view about the mechanics: it is the five-year grace on
rate-adequacy verification of 감독규정 제7-63조제2항제6호가목 R12 REG-R17, under which
4세대 first re-rated in 2025. The model re-rates from year 2; the real book could not.
Tests#
tests/test_model_conventions_kr.py applies the house style, parametrized over
kr_registry.MODELS rather than restated here: the two-Space layout, the external inputs
with no orphan CSV, the provenance column and citation tag on every assumption CSV row,
the docstrings and their required phrases, the age basis declared in the Projection
docstring (만나이 here, and the model says so because the contract prices on 보험나이 and
the two differ for half of all issue dates [S1 제21조] REG-R25), the result_cf() contract
— indexed by t, first column pols_if, a net_cf column, all names lower_snake_case, no
NaN, and the 0-based frame range(proj_len()): 120 rows, t = 0 … 119, last index
proj_len() - 1 — that every check_*() returns True on every shipped model point, and
the read → write → re-read round trip. The ten check_*() cells each take no argument and
return a real bool, with the signed residual under check_*_resid.
Check |
What it asserts, and what it would catch |
|---|---|
|
the five decrements account for every life leaving the in-force; catches a decrement dropped or double-counted |
|
the printed statement adds to |
|
the claim-shape distribution sums to 1 and has mean 1; catches a shape that makes |
|
the five 요율 상대도 bands partition the contracts in every year |
|
|
|
neither priced unit moves more than ±25% a year against the age-adjusted prior premium; catches the loading applied on the wrong side of the clip |
|
no money or count limit is exceeded — wiring, not exercise |
|
|
|
|
|
the three expense rates reconcile to the published 16% aggregate |
tests/test_indemnity_medical_kr.py is this model’s own suite, and it asserts the notes’
worked example hard-coded as a module-level table rather than pickled, so a reviewer can
check it against technical-notes.md by eye:
The anchor cell’s basis, value by value — every frequency and severity row the notes print,
shape_mean() = 53,530.7924, the tenshape_relmultiples, maleq(40) = 0.00132019andmort_rate_mth(0) = 0.0001100825,lapse_rate_mth(0) = 0.0087416110, and the ₩3,260,000 ceiling at decile 6.The policy-year-1 cash flow statement, all twelve printed rows to ₩0.0001 and
pols_ifto ten decimals, includingpols_if(12) = 0.8898237107, together with the rows where the product does something —t = 11, 12, 24, 36, 47, 48.The undiscounted totals: ₩1,558,165.4328 of premium, ₩1,201,095.1706 of claims across the five limbs, +₩118,475.9008 of net cash flow, a loss ratio of 0.770839, and the one negative policy year at −₩276.1354.
The renewal and experience-rating ledger row by row:
reld_solved(2) = 0.957477as a solved value,reld_one(y) = 0.95from year 5 where the cap binds,reld_avgreaching 1.0095494,noclaim_share = 0.5314584961, andprem_np_base(10) = 20,625.8505.Every entry in the notes’ Known modeling pitfalls list, one test each, named after the pitfall it protects — twenty-two of them, from multiplying a rate by the 보험가입금액 through the deductible on the mean, the ordering of the two 급여 reliefs, the corridor on the age-adjusted base, the relativity applied to the whole premium, the hard-coded 0.95, the one-year versus two-year lookback, the injection carve-out counted twice, the utilisation table read at the issue age,
lapse_rateused on the monthly grid, the renewal decline folded into lapse,reld_exempt_shareapplied to the benefit, theclaimssubtotal column, the invented acquisition strain, and the premium re-read in yeary > 1.Each optional module in both positions — the 비급여 rider, 3대비급여형, the relativity, the 무사고 할인, suspension at 0 and 3%, and
nhi_coveredin both branches.The structural product facts: that
claims_death,cv_ppandclaims_lapsedo not exist; thatpols_maturityis a count with no payment; thatt = 0is positive; thatoop_trunc(y) ≡ 1on the anchor and runs 0.801828 → 0.602441 on model point 8; that model point 10’s 비급여 basis increment is clipped to exactly 0.25; and that every assumption row carries a provenance tag.
python -m pytest tests -q