1.4 담보별 산출방법#
이 챕터에서 배우는 것
사망·진단·입원 세 보장의 보험금이 모두 발생(incidence)으로 계산되고, 식은
inforce × 발생률 × benefit로 셋 다 똑같다는 것“한 번 vs 반복”을 가르는 건 식이 아니라 1.3에서 본 그 축 — 그 발생 사건이 너를 어느 풀에서 빼내는가: 계약 전체(사망) / 자기 미진단 풀 (진단) / 없음(입원)
사망의 탈퇴는 별도의 사망탈퇴율이, 진단의 탈퇴는 같은 진단 발생률이 맡는 비대칭
is_diagnosisflag의 진짜 의미 — “한 번만 지급되는가”가 아니라 “이 보장이 자기만의 미진단 풀을 갖고 있는가”손계산 / 엔진 출력 /
gmm.trace의undiagnosed트리가 어떻게 서로를 검증하는가
각 CalculationMethod 값이 calculation_methods.csv 에 적는 라벨 이라면 —
DEATH 라고 적는 것 vs DIAGNOSIS 라고 적는 것의 차이는 어디서 정해지나?
그 선택은 CalculationMethod 결정 가이드 의 자리이고,
본 챕터는 엔진이 그 산출방법을 받아서 무엇을 다르게 하는가 입니다.
이 챕터의 모든 예제는 동일한 toy 설정 — 월 사망/발생/진단율 1%, 보험금 12,000, 3 개월 — 위에서 산출방법만 바꿔 결과를 비교합니다. 한 페이지에 세 모드의 차이가 명료하게 드러나도록 의도된 셋업입니다.
발생은 같고, 빠지는 풀이 다르다#
세 보장 모두 보험금은 **발생(incidence)**으로 계산합니다 — 매월
inforce × 발생률 × benefit. 식은 셋이 똑같습니다. “한 번이냐 반복이냐”를
가르는 건 식이 아니라, 1.3에서 본 그 축 — 그 발생 사건이 너를 어느 풀에서
빼내는가:
보장 |
발생 사건 |
빠지는 풀 |
결과 |
|---|---|---|---|
사망 (DEATH) |
사망 |
계약 전체 |
한 번 |
진단 (DIAGNOSIS) |
진단 |
그 담보의 |
한 번 |
입원 (MORBIDITY) |
입원 |
없음 (축소되는 풀 없음) |
반복 |
flowchart TB
DEATH["사망 (DEATH)<br/>계약 전체 풀에서 탈퇴 · 한 번"]
DIAG["진단 (DIAGNOSIS)<br/>자기 미진단 풀에서 탈퇴 · 한 번"]
MORB["입원 (MORBIDITY)<br/>어느 풀도 안 빠짐 · 반복"]
classDef stock fill:#eaf1f8,stroke:#547fa6,color:#17344e
classDef outflow fill:#f9eeee,stroke:#b96d6d,color:#552626
classDef step fill:#f7f2e8,stroke:#b38a45,color:#493617
class DEATH step
class DIAG stock
class MORB outflow
엔진 구현으로는 청구 알고리즘이 둘뿐입니다 — 자기 미진단 풀을 가진 진단(B)과, 그렇지 않은 나머지(A):
알고리즘 |
보장 |
청구 식 |
|---|---|---|
(A) |
사망, 입원 |
매월 |
(B) |
진단 |
매월 |
is_diagnosis flag 가 정확히 이 (A)/(B) 분기 결정자입니다. False → 식 (A),
True → 식 (B). 한 가지 주의 — 사망의 “한 번”은 알고리즘 (A) 안이 아니라
별도의 사망탈퇴율이 계약 풀을 줄이는 데서 나옵니다. 그래서 사망과 입원이
같은 (A)인데도 결과가 정반대입니다.
아래 세 절이 같은 toy 데이터로 그 차이를 사망 → 진단 → 입원 순으로 보여줍니다.
사망 — 계약 전체 풀에서 탈퇴#
월 사망률 1% 의 단일 사망보장. 사망 사건은 사람을 계약 전체 inforce
에서 빼냅니다 — 그 탈퇴를 맡는 건 별도의 사망탈퇴율(mortality_annual)
이고, 사망보험금 발생률 자체는 어떤 풀도 줄이지 않습니다(1.3). 이 toy 에서는
두 입력에 같은 1% 를 주어, 발생률로 청구가 매월 일어나도 같은 율로 계약 풀이
줄어 누적이 “한 번”으로 수렴하는 걸 봅니다.
import fastcashflow as fcf
# 사망률 -- 월 사망률 1% 의 연 환산
death_rate = 1 - (1 - 0.01) ** 12
# 해지율 -- 해지 없음
lapse_rate = 0.0
basis = fcf.Basis(
mortality_annual = death_rate, # 보유계약 사망률 (위 death_rate)
lapse_annual = lapse_rate, # 해지율 (해지 없음)
discount_annual = 0.0, # 연 할인율 0 (검증 단순화)
ra_confidence = 0.75, # 위험조정 신뢰수준 75%
mortality_cv = 0.0, # 사망률 변동계수 0 (RA = 0 강제)
coverages = (fcf.CoverageRate("DEATH", death_rate),), # 사망 보장 1 종 (청구 rate = 같은 death_rate)
)
mp = fcf.ModelPoints.single(
issue_age = 40, # 가입연령 40세
sex = 0, # 성별 (0=남, 1=여)
benefits = {"DEATH": 12_000}, # DEATH 보장의 보험금 12,000
premium = 0, # 월납 보험료 0 (보험료 cash flow 무시)
term_months = 3, # 보험기간 3개월
calculation_methods = {
"DEATH": fcf.CalculationMethod.DEATH, # 코드 → 산출방법 매핑
},
)
r = fcf.gmm.measure(mp, basis)
print(f"inforce : {r.cashflows.inforce[0, :3]}") # 보유계약 trajectory
print(f"mortality_cf : {r.cashflows.mortality_cf[0, :3]}") # 사망보험금 cash flow
print(f"BEL[0] : {float(r.bel[0]):.2f}") # 최선추정부채
print(f"cumulative 3m : {float(r.cashflows.mortality_cf[0, :3].sum()):.2f}")
출력:
inforce : [1. 0.99 0.9801]
mortality_cf : [120. 118.8 117.612]
BEL[0] : 356.41
cumulative 3m : 356.41
손계산:
t |
|
rate × benefit |
claim |
|---|---|---|---|
0 |
1.0000 |
0.01 × 12,000 |
120.00 |
1 |
0.9900 |
0.01 × 12,000 |
118.80 |
2 |
0.9801 |
0.01 × 12,000 |
117.61 |
핵심: 청구가 매월 발생하는데도 누적이 ~356 으로 수렴 — inforce 가
줄어드니까 (죽은 사람은 다음 달 inforce에 없으니까). “한 번만” 은 청구 식의 특성이 아니라
같은 율로 inforce 가 감쇠한다 는 calibration 의 결과.
진단 — 자기 미진단 풀에서 탈퇴#
진단도 사망처럼 “한 번”입니다 — 단, 빠지는 풀이 다릅니다. 진단 사건은 사람을
계약(inforce)에서 빼내지 않습니다. 그 사람은 계약에 남아 보험료를 내고
다른 보장도 그대로 작동합니다(1.3의 사건 표 — 진단은 탈퇴 ‘아니요’). 대신
빠지는 건 그 담보만의 미진단(undiagnosed) 풀 — 한 번 진단받은 사람은
그 풀에서 빠져, 다음 달에 또 같은 진단을 받지 않습니다.
그래서 엔진은 보장마다 자기 undiagnosed 풀을 두고, 진단 발생률로
청구하면서 동시에 그 풀을 (1 - 진단발생률) 로 축소합니다. 사망에서 별도의
사망탈퇴율이 계약 풀을 줄이던 자리를, 진단에서는 같은 진단 발생률이 자기
미진단 풀을 줄이는 것 — 탈퇴와 발생이 한 입력에 묶입니다.
import fastcashflow as fcf
# 진단 발생률 -- 월 1% 진단율의 연 환산
cancer_rate = 1 - (1 - 0.01) ** 12
basis = fcf.Basis(
mortality_annual = 0.0, # 보유계약 사망률 0 (감쇠 안 함)
lapse_annual = 0.0, # 해지율 0 (해지 없음)
discount_annual = 0.0, # 연 할인율 0 (검증 단순화)
ra_confidence = 0.75, # 위험조정 신뢰수준 75%
mortality_cv = 0.0, # 사망률 변동계수 0 (RA = 0 강제)
coverages = (fcf.CoverageRate("CANCER", cancer_rate),), # 암진단 보장 1 종 (청구 rate = cancer_rate)
)
mp = fcf.ModelPoints.single(
issue_age = 40, # 가입연령 40세
sex = 0, # 성별 (0=남, 1=여)
benefits = {"CANCER": 12_000}, # CANCER 보장의 진단 일시금 12,000
premium = 0, # 월납 보험료 0 (보험료 cash flow 무시)
term_months = 3, # 보험기간 3개월
calculation_methods = {
"CANCER": fcf.CalculationMethod.DIAGNOSIS, # 코드 → 산출방법 매핑
},
)
r = fcf.gmm.measure(mp, basis)
print(f"inforce : {r.cashflows.inforce[0, :3]}")
print(f"morbidity_cf : {r.cashflows.morbidity_cf[0, :3]}") # 진단 cash flow
print(f"BEL[0] : {float(r.bel[0]):.2f}")
print(f"cumulative 3m : {float(r.cashflows.morbidity_cf[0, :3].sum()):.2f}")
출력:
inforce : [1. 1. 1.]
morbidity_cf : [120. 118.8 117.612]
BEL[0] : 356.41
cumulative 3m : 356.41
손계산:
t |
|
|
rate × benefit |
claim |
|---|---|---|---|---|
0 |
1.0000 |
1.0000 |
0.01 × 12,000 |
120.00 |
1 |
1.0000 |
0.9900 |
0.01 × 12,000 |
118.80 |
2 |
1.0000 |
0.9801 |
0.01 × 12,000 |
117.61 |
핵심: inforce 는 안 줄어드는데 undiagnosed 가 별도 풀로 매월 1%
씩 감쇠. 청구 시계열이 사망과 정확히 같음 — 누적 ~356 으로 수렴. 빠지는
풀만 다를 뿐(계약 전체 vs 자기 미진단 풀), “한 번”이 되는 원리는 같습니다.
입원 — 어느 풀에서도 안 빠짐, 그래서 반복#
입원은 사건이 발생하지만 사람을 어느 풀에서도 빼내지 않습니다 —
계약(inforce)도, 자기 하위 풀도 줄지 않습니다(1.3의 사건 표 — 입원은 탈퇴
‘아니요’이고, 진단 같은 자기 미진단 풀도 없음). 빠지는 풀이 없으니 살아있는
한 매월 새로 청구됩니다.
toy 로 확인하려면 입원 발생률만 두고, 보유계약 사망률(mortality_annual)
= 0 으로 두어 사망 / 해지에 의한 inforce 감쇠까지 없앱니다 — 그러면 어떤
풀도 어떤 식으로도 감쇠하지 않는 순수한 반복이 드러납니다.
import fastcashflow as fcf
# 입원 발생률 -- 월 1% 발생률의 연 환산
inpatient_rate = 1 - (1 - 0.01) ** 12
# 산출기초
basis = fcf.Basis(
mortality_annual = 0.0, # 보유계약 사망률 0 (감쇠 안 함)
lapse_annual = 0.0, # 해지율 0 (해지 없음)
discount_annual = 0.0, # 연 할인율 0 (검증 단순화)
ra_confidence = 0.75, # 위험조정 신뢰수준 75%
mortality_cv = 0.0, # 사망률 변동계수 0 (RA = 0 강제)
coverages = (fcf.CoverageRate("INPATIENT", inpatient_rate),),
)
# 모델 포인트 (계약 하나)
mp = fcf.ModelPoints.single(
issue_age = 40, # 가입연령 40세
sex = 0, # 성별 (0=남, 1=여)
benefits = {"INPATIENT": 12_000}, # INPATIENT 보장의 입원 1건당 12,000
premium = 0, # 월납 보험료 0 (보험료 cash flow 무시)
term_months = 3, # 보험기간 3개월
calculation_methods = {
"INPATIENT": fcf.CalculationMethod.MORBIDITY, # 코드 → 산출방법 매핑
},
)
r = fcf.gmm.measure(mp, basis)
print(f"inforce : {r.cashflows.inforce[0, :3]}")
print(f"morbidity_cf : {r.cashflows.morbidity_cf[0, :3]}") # 입원 cash flow
print(f"BEL[0] : {float(r.bel[0]):.2f}")
print(f"cumulative 3m : {float(r.cashflows.morbidity_cf[0, :3].sum()):.2f}")
출력:
inforce : [1. 1. 1.]
morbidity_cf : [120. 120. 120.]
BEL[0] : 360.00
cumulative 3m : 360.00
손계산:
t |
|
rate × benefit |
claim |
|---|---|---|---|
0 |
1.0000 |
0.01 × 12,000 |
120.00 |
1 |
1.0000 |
0.01 × 12,000 |
120.00 |
2 |
1.0000 |
0.01 × 12,000 |
120.00 |
핵심: 같은 식 inforce × 발생률 × benefit 인데, 빠지는 풀이 없어
매월 120 이 새로 발생. “반복” 도 식의 특성이 아니라 입원 발생률이 어떤
풀도 줄이지 않는다 의 결과.
세 모드를 가른 건 단 하나 — 발생 사건이 어느 풀에서 사람을 빼내는가. 사망은 계약 전체 풀(별도 사망탈퇴율로 축소), 진단은 자기 미진단 풀(진단 발생률로 축소), 입원은 아무 풀도 아님. 청구 식은 셋 다 같고, 이 한 가지가 시계열의 모양을 완전히 바꿉니다.
세 모드 나란히#
보장 |
|
별도 풀 |
청구 시계열 |
누적 |
|---|---|---|---|---|
사망 (DEATH) |
|
없음 |
|
356.41 |
진단 (DIAGNOSIS) |
|
|
|
356.41 |
입원 (MORBIDITY) |
|
없음 |
|
360.00 |
사망 과 진단 의 청구 시계열이 글자 그대로 동일 함에도 메커니즘은 다르고 —
사망 의 “한 번” 은 계약 전체 inforce 가, 진단 의 “한 번” 은 자기
undiagnosed 풀이 각각 표현. 입원 은 어느 풀도 감쇠 안 하니 “반복” 이
자동입니다.
is_diagnosis flag 의 진짜 의미#
import fastcashflow as fcf
# 위 DIAGNOSIS (CANCER) 예제 재구성 (바로 앞 INPATIENT 예제가 mp / basis 를 덮었으므로)
basis = fcf.Basis(
mortality_annual = 0.0,
lapse_annual = 0.0,
discount_annual = 0.0,
ra_confidence = 0.75,
mortality_cv = 0.0,
coverages = (fcf.CoverageRate("CANCER", cancer_rate),),
)
mp = fcf.ModelPoints.single(
issue_age = 40,
sex = 0,
benefits = {"CANCER": 12_000},
premium = 0,
term_months = 3,
calculation_methods = {"CANCER": fcf.CalculationMethod.DIAGNOSIS},
)
fcf.gmm.trace(0, mp, basis) # 위 DIAGNOSIS (CANCER) 예제의 mp / basis
trace 의 Coverages 노드에는 매 보장마다 is_diagnosis flag 가 표시됩니다:
+- Coverages (rate-driven, n=1)
| `- 'CANCER' method=DIAGNOSIS risk=1 is_diagnosis=True rate -> <callable>
is_diagnosis 의 진짜 의미는 “한 번만 지급되는가” 가 아닙니다. 정확한
의미는:
“이 보장이 자기만의 별도 풀 을 갖고 있는가”
보장 종류 |
|
풀 자리 |
|---|---|---|
DEATH |
False |
공유 |
MORBIDITY |
False |
공유 |
DIAGNOSIS |
True |
per-coverage |
DEATH 와 MORBIDITY 가 둘 다 is_diagnosis=False 인 건 kernel 분기로 보면
같은 알고리즘 (A) 을 쓴다는 뜻. “DEATH 와 MORBIDITY 가 의미적으로 한 묶음”
이 아닙니다.
함정 — mortality_annual ↔ DEATH coverage rate 의 미스매치#
위 사망 예제는 같은 death_rate 을 두 자리에 넣었습니다:
basis = fcf.Basis(
mortality_annual = death_rate, # 자리 1
coverages = (fcf.CoverageRate("DEATH", death_rate),) # 자리 2
)
엔진 안에서 이 두 자리는 별개 양:
자리 |
역할 |
|---|---|
|
|
|
사망 보장의 청구 rate |
손계산은 보통 두 양이 같다고 가정하고 결과 숫자 (356.41 등) 를 도출. 한 자리만 override 하면 두 양이 silent 어긋나 결과가 손계산과 안 맞습니다.
예: mortality_annual = 0 으로 두고 DEATH coverage rate 만 1% 로 두면:
asmp_buggy = fcf.Basis(
mortality_annual = 0.0, # 감쇠 0
coverages = (fcf.CoverageRate("DEATH", death_rate),) # 청구만 1%
)
→ DEATH 보장이 MORBIDITY 처럼 작동 (in-force 안 줄어드는데 청구는 매월 1% 발생). in-force가 안 줄어 같은 사람들에게 매월 사망보험금이 다시 지급됨. 누적 360 (위 MORBIDITY 결과와 같음) — 손계산의 356 과 어긋남.
이 함정의 방어 패턴은 정기보험 와 본 챕터의
모든 예제처럼 death_rate 한 변수를 만들어 두 자리에 넘기는
방식. 한 자리만 바꾸려 해도 두 자리가 한 변수를 공유하니 silent 어긋남이
구조적으로 차단됩니다.
gmm.trace 의 Rates 노드는 두 자리 (mortality_annual / DEATH(an)) 의 값을
동일 행에 한꺼번에 표시 하므로 어긋났을 때 한눈에 보입니다.
인접 레시피#
CalculationMethod 결정 가이드 — 담보별 산출방법의 매 코드를 어느 산출방법으로 등록할지. 본 챕터는 그 산출방법이 엔진 안에서 어떻게 다르게 동작하는지.
정기보험 — DEATH 만 사용하는 가장 단순한 사례.
사망 + 단순 진단 일시금 (작성 예정) — DEATH 와 DIAGNOSIS 의 결합. 본 챕터의 두 메커니즘을 한 contract 에 동시 사용.
검증 패턴 — gmm.trace — 본 챕터의 trace 출력을 어떻게 사용하는지의 전반.