3. Python 精算模型基础:寿险准备金计算模型
本章将使用 Python 构建一个简化的寿险准备金计算模型,重点在于理解精算模型的输入/输出规范、计算逻辑与 Serving 接口的对应关系。准备金是保险公司对保单未来赔付义务的当前估计,其计算是精算模型的核心场景之一。
3.1 准备金计算的基本原理
寿险准备金通常采用 未来法 计算:准备金 = 未来赔付支出现值 - 未来保费收入现值。对于一份 n 年期定期寿险,在保单第 t 年末的准备金可表示为:
V_t = A_{x+t : n-t|} - P * ä_{x+t : n-t|}
其中:
- A:死亡赔付的精算现值
- P:年缴净保费
- ä:期初付生存年金的精算现值
- x:投保年龄,n:保险期限,t:已过保单年度
关键假设:本模型使用静态死亡率表(如 CL1 生命表)和固定折现率(如 3.5%),不考虑退保、费用和分红,以聚焦核心计算流程。
3.2 模型输入规范
模型输入采用结构化字典格式,便于后续 API 序列化。输入参数分为保单特征、假设参数和计算控制三类:
| 参数类别 | 参数名 | 类型 | 说明 |
|---|---|---|---|
| 保单特征 | age | int | 投保年龄(18-65) |
| 保单特征 | term | int | 保险期限(年) |
| 保单特征 | sum_assured | float | 保额(元) |
| 假设参数 | interest_rate | float | 折现率(如 0.035) |
| 假设参数 | mortality_table | list[float] | 各年龄死亡率 qx 数组 |
| 计算控制 | valuation_year | int | 评估年度(保单年度 t) |
输入规范要点:所有数值参数使用 float 类型,年龄和期限使用 int,死亡率表使用 list[float]。这种规范直接映射到 JSON API 的请求体结构。
3.3 模型核心实现
以下 Python 代码实现了准备金计算的核心逻辑,包含精算现值计算和准备金聚合:
import numpy as np
def calculate_reserve(inputs: dict) -> dict:
"""
寿险准备金计算模型
:param inputs: 包含 age, term, sum_assured, interest_rate,
mortality_table, valuation_year 的字典
:return: 包含各项精算现值和准备金的字典
"""
age = inputs['age']
term = inputs['term']
SA = inputs['sum_assured']
i = inputs['interest_rate']
qx = inputs['mortality_table']
t = inputs['valuation_year']
v = 1 / (1 + i) # 折现因子
remaining = term - t # 剩余保险期限
# 提取剩余期限的死亡率(从 age+t 开始)
q = qx[age+t : age+t+remaining]
p = [1 - qk for qk in q]
# 计算死亡赔付精算现值 A
A = 0.0
for k in range(remaining):
prob_survive = np.prod(p[:k]) if k > 0 else 1.0
A += prob_survive * q[k] * (v ** (k + 0.5))
# 计算期初付生存年金精算现值 ä
a_ddot = 0.0
for k in range(remaining):
prob_survive = np.prod(p[:k]) if k > 0 else 1.0
a_ddot += prob_survive * (v ** k)
# 计算年缴净保费(均衡保费)
P_net = SA * A / a_ddot if a_ddot > 0 else 0.0
# 计算第 t 年末准备金
reserve = SA * A - P_net * a_ddot
return {
'A_x': round(A, 6),
'a_ddot_x': round(a_ddot, 6),
'P_net': round(P_net, 2),
'reserve': round(reserve, 2),
'valuation_year': t
}
3.4 模型输出规范
输出同样采用结构化字典,包含中间精算变量和最终准备金结果:
| 输出字段 | 类型 | 说明 |
|---|---|---|
| A_x | float | 死亡赔付精算现值(每元保额) |
| a_ddot_x | float | 期初付生存年金精算现值 |
| P_net | float | 年缴净保费(元) |
| reserve | float | 第 t 年末准备金(元) |
| valuation_year | int | 评估年度(回显) |
精度注意:精算现值保留 6 位小数,准备金和保费保留 2 位小数。API 返回时应统一使用 float 类型,避免 Decimal 序列化问题。
3.5 模型调用示例
以下示例演示如何使用模型计算一份 30 岁男性、保额 100 万、20 年期的定期寿险在第 5 年末的准备金:
# 构造输入
sample_input = {
'age': 30,
'term': 20,
'sum_assured': 1_000_000,
'interest_rate': 0.035,
'mortality_table': cl1_male_qx, # CL1 生命表男性死亡率数组
'valuation_year': 5
}
# 调用模型
result = calculate_reserve(sample_input)
print(f"评估年度: {result['valuation_year']}")
print(f"死亡精算现值(A): {result['A_x']}")
print(f"年金精算现值(ä): {result['a_ddot_x']}")
print(f"年缴净保费: {result['P_net']} 元")
print(f"第5年末准备金: {result['reserve']} 元")
输出结果示例:
评估年度: 5
死亡精算现值(A): 0.023456
年金精算现值(ä): 12.345678
年缴净保费: 1899.50 元
第5年末准备金: 45678.90 元
3.6 模型计算流程与 Serving 映射
下图展示了准备金模型的计算流程及其与 API Serving 层的对应关系:
3.7 模型与 API 的接口契约
准备金模型与 API Serving 层之间遵循明确的接口契约,确保模型可被独立部署和调用:
| 契约维度 | 规范要求 | 示例 |
|---|---|---|
| 输入格式 | JSON 对象,键名 snake_case | {"age": 30, "term": 20} |
| 输出格式 | JSON 对象,包含中间变量 | {"reserve": 45678.90} |
| 错误处理 | 返回错误码 + 描述信息 | {"error": "INVALID_AGE"} |
| 幂等性 | 相同输入必须返回相同输出 | 纯函数无状态 |
| 性能要求 | 单次计算 < 100ms | 批量计算可优化 |
模型 Serving 最佳实践:将精算模型封装为纯函数,不依赖全局状态。输入输出使用标准 Python 字典,便于直接序列化为 JSON。死亡率表等静态数据通过参数注入,而非硬编码在模型内部。
3.8 模型验证与测试
为确保模型正确性,需要针对边界条件和已知结果进行验证:
- 零时刻验证:t=0 时,准备金应等于未来法计算的初始值
- 满期验证:t=n 时,准备金应为 0
- 折现率验证:折现率为 0 时,精算现值退化为概率和
- 死亡率验证:死亡率为 0 时,准备金应为 0(无赔付风险)
def test_reserve_boundary():
"""边界条件测试"""
# 满期时准备金应为 0
result = calculate_reserve({
'age': 30, 'term': 10, 'sum_assured': 100000,
'interest_rate': 0.035,
'mortality_table': cl1_male_qx,
'valuation_year': 10
})
assert abs(result['reserve']) < 0.01, "满期准备金应为 0"
# 死亡率为 0 时准备金应为 0
zero_mort = [0.0] * 100
result = calculate_reserve({
'age': 30, 'term': 10, 'sum_assured': 100000,
'interest_rate': 0.035,
'mortality_table': zero_mort,
'valuation_year': 0
})
assert abs(result['reserve']) < 0.01, "无死亡风险时准备金应为 0"
print("所有边界测试通过")
通过以上步骤,我们构建了一个符合精算原理、输入输出规范、可被 API 直接调用的寿险准备金计算模型。下一阶段将基于此模型实现 RESTful API 服务。