健康保险费率计算:从数学模型到Matlab代码实战
2026/9/23 4:58:02 网站建设 项目流程

1. 项目缘起:从业务痛点到一个数学问题

最近在帮一个做健康险精算的朋友梳理他们的费率计算模型,发现一个挺有意思的现象:很多从业者,包括一些有经验的精算师,在构建费率模型时,往往更依赖经验数据和现成的精算软件,对于模型底层的数学逻辑和实现细节,反而有些“黑箱”操作。这带来的问题是,当业务场景变化(比如推出一个全新的险种,或者目标人群特征发生显著偏移)时,原有的模型可能不再适用,而调整起来又无从下手,因为不知道“黑箱”里具体是怎么算的。

这让我想起了当年参加数学建模竞赛的经历。那时候,我们面对一个陌生的实际问题,第一步就是把它抽象成一个清晰的数学模型,然后用编程工具(比如Matlab)去求解、验证。这个过程,本质上和构建一个稳健的费率计算模型是相通的。所以,我决定把这次帮忙的过程整理出来,抛开复杂的精算软件界面,回归到最本质的数学建模和代码实现上,手把手地带大家走一遍“健康保险费率计算”的完整实战流程。

这篇文章的目标很明确:如果你是一名对保险精算感兴趣的数据分析师、初入行的精算师,或者任何想用数学工具解决实际定价问题的朋友,你都能跟着这篇文章,理解健康保险费率计算的核心数学模型,并且获得一套可以直接运行、修改的Matlab代码。我们不止步于“是什么”,更要深挖“为什么这么建模”以及“代码如何一步步实现它”。

2. 健康保险费率计算的核心数学模型拆解

在开始敲代码之前,我们必须把业务问题“翻译”成数学语言。健康保险费率计算的核心目标,是确定一个合理的保费(Premium),使得保险公司收取的保费足以覆盖未来的理赔成本(Claim Cost),并包含必要的费用(Expenses)和合理的利润(Profit Margin)。

一个最基础、也最经典的费率计算模型可以表述为:

纯保费 × (1 + 费用附加率) × (1 + 风险附加率) = 毛保费

其中,纯保费的计算是整个模型的心脏,它直接对应着未来的预期理赔成本。而纯保费的计算,又依赖于几个关键因素:发病率/死亡率、保险金额、贴现率。下面我们来逐一拆解。

2.1 构建纯保费计算模型:从生命表到现金流贴现

纯保费,简单说就是未来理赔支出的期望现值。对于一年期健康险(比如医疗险、重疾险),我们通常采用损失模型;对于长期险(如终身重疾险、长期护理险),则需要用到多期模型,并引入生存概率和贴现。

这里我们以一个简化的一年期重大疾病保险为例进行建模。假设我们为一位年龄为 (x) 岁的男性被保险人定价,保额为 (S) 元。

核心变量定义:

  • (q_x):年龄 (x) 岁的人在一年内罹患约定重大疾病的概率(发病率)。这个数据来源于行业标准的疾病发生率表,比如《中国人身保险业重大疾病经验发生率表》。
  • (v):贴现因子,(v = \frac{1}{1+i}),其中 (i) 是年贴现率。因为保费是年初收取,而理赔可能发生在年中或年末,我们需要将未来的理赔支出贴现到保单起始时点。
  • (E):预期理赔金额。在一年期产品中,通常假设一旦发生约定重疾,即赔付全部保额 (S)。

那么,对于这张保单,保险公司的理赔支出是一个随机变量:

  • 有概率 (q_x) 支出 (S) 元(年末)。
  • 有概率 (1 - q_x) 支出 (0) 元。

因此,预期理赔支出的现值(即纯保费 (P_{pure}))为: [ P_{pure} = q_x \times S \times v ]

这只是一个最基础的、单一人群、单一责任的计算。现实中,我们需要考虑更多维度:

  1. 多年龄与性别:不同年龄、性别的发病率 (q) 差异巨大。模型需要能处理一个年龄向量(如 20-60岁)和性别参数。
  2. 多责任与等待期:产品可能包含多种轻症、中症、重症责任,每种责任的发病率和赔付比例不同。此外,通常设有等待期(如90天),等待期内出险可能不予赔付或仅退还保费,这需要在模型中通过调整发病率来实现。
  3. 费用与风险附加:运营需要成本(佣金、核保、行政等),记为费用附加率 (\alpha);同时为了应对实际经验可能比预期更差的波动,需要增加风险附加率 (\beta)。
  4. 毛保费计算:考虑上述附加后,单张保单的毛保费 (P_{gross}) 为: [ P_{gross} = P_{pure} \times (1 + \alpha) \times (1 + \beta) ]

我们的Matlab代码,就是要将这个数学模型,特别是能处理多年龄、多性别的纯保费计算部分,完整地实现出来。

2.2 模型关键假设与数据来源探讨

任何模型都是建立在假设之上的,清楚假设的局限性比模型本身更重要。

  • 发病率假设((q_x)):这是模型最核心的输入,其准确性直接决定定价的成败。我们使用的是行业公开的经验生命表/疾病表,它代表的是历史人群的平均经验。在实际公司定价中,精算师会根据本公司过往的理赔数据、再保险公司的建议、对未来医疗技术进步和疾病谱变化的判断,对这个基准表进行修正(如乘以一个系数),形成公司的“定价发生率表”。在我们的实战代码中,我们会预留这个调整接口。
  • 贴现率假设((i)):贴现率反映了资金的时间价值和对未来投资回报的预期。对于一年期产品,由于期限短,贴现影响较小,但仍需考虑。通常参考公司资产端的预期投资收益率或国债收益率来确定。一个微小的变动,对于长期险产品保费的影响会是巨大的。
  • 费用附加假设((\alpha)):需要根据公司的运营模式进行精细拆分,如初年费用(高佣金)、续年费用、固定费用、变动费用等。在简化模型中,我们用一个综合比率来代表。
  • 均匀分布假设:在基础模型中,我们通常假设理赔发生在年末。更精确的做法是假设理赔在一年内均匀发生,那么贴现因子需要调整,例如使用 (v^{0.5})。我们会在代码中展示这种更精确的处理方式。

注意:模型假设的合理性需要反复验证。在交付业务部门使用前,必须进行大量的情景测试(Scenario Testing)和敏感性分析(Sensitivity Analysis),观察关键假设(如发病率、贴现率)变动±10%、±20%时,最终保费和公司利润的波动情况。这是模型从“能用”到“可靠”的关键一步。

3. Matlab实战:从零搭建费率计算程序

理论清晰后,我们进入最激动人心的环节:用Matlab将模型“代码化”。我们将遵循“数据准备 -> 核心计算 -> 结果整合”的流程来构建程序。

3.1 数据准备与参数初始化模块

首先,我们需要准备基础数据。在真实环境中,这些数据可能来自数据库或Excel文件。这里我们在代码中直接定义。

%% 健康保险费率计算模型 - 参数初始化模块 clear; clc; close all; % 1. 基础参数 S = 100000; % 保险金额,单位:元 i = 0.03; % 年贴现率(假设为3%) v = 1 / (1 + i); % 年贴现因子 alpha = 0.25; % 综合费用附加率(25%) beta = 0.05; % 风险附加率(5%) % 2. 目标人群年龄范围 ages = 20:1:60; % 计算20岁到60岁,每个整数年龄 num_ages = length(ages); % 3. 发病率数据(示例数据,非真实值!) % 这里用一个简单的指数增长函数来模拟发病率随年龄上升的趋势。 % 在实际应用中,此处应替换为从文件读取的真实疾病发生率表。 % 格式:第一列为年龄,第二列为男性发病率,第三列为女性发病率。 % 示例:假设基准发病率在25岁为0.0005,每增加一岁增长8%。 base_q_male = 0.0005 * (1.08 .^ (ages - 25)); base_q_female = 0.0003 * (1.08 .^ (ages - 25)); % 假设女性发病率较低 % 将发病率封装成结构体,方便管理 incidence_data.male = base_q_male; incidence_data.female = base_q_female; incidence_data.ages = ages; % 4. 性别标识 (1-男, 2-女) genders = {'Male', 'Female'};

代码解读与心得

  • clear; clc; close all;是Matlab脚本的好习惯,清空工作区、命令窗口和图形窗口,避免旧数据干扰。
  • 将参数集中定义在开头,而不是散落在代码中,极大提高了可维护性。未来要调整贴现率或费用率,只需修改这一个地方。
  • 发病率数据是模型的核心驱动。这里用函数生成模拟数据是为了代码的完整性。实战中,你必须将base_q_malebase_q_female的赋值部分,替换为从load()xlsread()等函数读取真实数据表的代码。一个常见的做法是将发病率表存为incidence_table.csv,然后读取。
  • 使用结构体incidence_data来组织相关数据,比使用多个独立的变量更清晰,也便于作为参数传递给函数。

3.2 核心计算函数实现

接下来,我们编写一个独立的函数来计算单个年龄、性别的纯保费和毛保费。这样做的好处是逻辑清晰,易于单元测试和复用。

%% 核心计算函数:calculatePremium function [pure_premium, gross_premium] = calculatePremium(q, S, v, alpha, beta, assumption) % 计算指定发病率下的纯保费和毛保费 % 输入: % q: 重大疾病发病率 % S: 保险金额 % v: 贴现因子 % alpha: 费用附加率 % beta: 风险附加率 % assumption: 理赔发生时间假设,'year_end'(年末)或 'mid_year'(年中) % 输出: % pure_premium: 纯保费 % gross_premium: 毛保费 % 根据不同的理赔时间假设,计算理赔支付的现值因子 switch lower(assumption) case 'year_end' discount_factor = v; % 年末理赔,贴现一年 case 'mid_year' discount_factor = sqrt(v); % 年中理赔,近似贴现半年 v^0.5 otherwise error('Invalid assumption. Use ''year_end'' or ''mid_year''.'); end % 计算纯保费:期望理赔现值 pure_premium = q * S * discount_factor; % 计算毛保费:考虑费用和风险附加 gross_premium = pure_premium * (1 + alpha) * (1 + beta); end

代码解读与心得

  • 这个函数封装了最核心的数学公式。输入参数明确,输出结果清晰。
  • 引入了assumption参数来处理“理赔发生时间”这个精算细节。这是从“基础模型”走向“精细模型”的一小步,体现了建模的严谨性。在实际中,还可能考虑更复杂的理赔分布模式。
  • 使用switch语句来处理不同假设,使代码更易扩展。如果未来需要增加“均匀分布”假设,只需在此添加一个case
  • error函数用于处理非法输入,这是编写健壮程序的好习惯,能快速定位问题。

3.3 主程序:循环计算与结果可视化

有了核心函数和基础数据,我们就可以对目标人群的每一个年龄进行计算了。

%% 主程序:批量计算与结果分析 % 初始化结果存储矩阵 % 行:不同年龄,列:1-纯保费(男), 2-毛保费(男), 3-纯保费(女), 4-毛保费(女) results = zeros(num_ages, 4); for idx = 1:num_ages current_age = ages(idx); % 获取当前年龄的发病率 q_male = incidence_data.male(idx); q_female = incidence_data.female(idx); % 计算男性保费 (假设理赔发生在年中,更符合实际) [pure_male, gross_male] = calculatePremium(q_male, S, v, alpha, beta, 'mid_year'); results(idx, 1) = pure_male; results(idx, 2) = gross_male; % 计算女性保费 [pure_female, gross_female] = calculatePremium(q_female, S, v, alpha, beta, 'mid_year'); results(idx, 3) = pure_female; results(idx, 4) = gross_female; end % 将结果整合成表格,便于查看和导出 result_table = table(ages', results(:,1), results(:,2), results(:,3), results(:,4), ... 'VariableNames', {'Age', 'Pure_Premium_Male', 'Gross_Premium_Male', ... 'Pure_Premium_Female', 'Gross_Premium_Female'}); disp('费率计算结果(前10行):'); disp(head(result_table, 10)); % 计算整体平均保费 avg_gross_male = mean(results(:,2)); avg_gross_female = mean(results(:,4)); fprintf('\n目标人群(20-60岁)平均毛保费:\n'); fprintf('男性:%.2f 元\n', avg_gross_male); fprintf('女性:%.2f 元\n', avg_gross_female);

代码解读与心得

  • 使用for循环遍历每个年龄。虽然Matlab在矩阵运算上更有优势,但在此处清晰的逻辑比微小的性能提升更重要。如果年龄数非常多,可以考虑向量化改造。
  • 将结果存储在矩阵results中,最后用table函数转换为更易读的表格形式。table是Matlab中处理表格式数据的利器,可以方便地排序、筛选和导出到Excel(writetable函数)。
  • dispfprintf用于在命令窗口输出结果,方便调试和快速查看。
  • 一个关键细节:我们在循环中调用calculatePremium函数时,统一使用了'mid_year'假设。这意味着我们认为理赔平均发生在保单年度的中点。这是一个比“年末假设”更贴近现实的常用简化处理。

3.4 结果可视化与敏感性分析

数字表格不够直观,我们需要用图形来展示费率随年龄、性别的变化趋势,并进行关键的敏感性分析。

%% 结果可视化 figure('Position', [100, 100, 1200, 500]); % 设置图形窗口大小 % 子图1:毛保费随年龄变化曲线 subplot(1, 2, 1); plot(ages, results(:,2), 'b-o', 'LineWidth', 1.5, 'MarkerSize', 5, 'DisplayName', '男性-毛保费'); hold on; plot(ages, results(:,4), 'r-s', 'LineWidth', 1.5, 'MarkerSize', 5, 'DisplayName', '女性-毛保费'); xlabel('年龄'); ylabel('毛保费 (元)'); title('健康险毛保费随年龄变化趋势 (保额10万)'); legend('Location', 'northwest'); grid on; hold off; % 子图2:纯保费与毛保费对比(以男性为例) subplot(1, 2, 2); plot(ages, results(:,1), 'k--', 'LineWidth', 1.5, 'DisplayName', '男性-纯保费'); hold on; plot(ages, results(:,2), 'b-', 'LineWidth', 1.5, 'DisplayName', '男性-毛保费'); xlabel('年龄'); ylabel('保费 (元)'); title('纯保费 vs. 毛保费 (男性)'); legend('Location', 'northwest'); grid on; hold off; %% 敏感性分析:贴现率(i)和发病率对保费的影响 % 我们分析当贴现率在2%~4%之间波动时,对40岁男性保费的影响 target_age = 40; target_idx = find(ages == target_age); q_target = incidence_data.male(target_idx); i_range = 0.02:0.0025:0.04; % 贴现率从2%到4% premium_sensitivity = zeros(length(i_range), 1); for i_idx = 1:length(i_range) i_current = i_range(i_idx); v_current = 1 / (1 + i_current); [~, gross_current] = calculatePremium(q_target, S, v_current, alpha, beta, 'mid_year'); premium_sensitivity(i_idx) = gross_current; end figure; plot(i_range*100, premium_sensitivity, 'm-^', 'LineWidth', 2, 'MarkerSize', 8); xlabel('贴现率 i (%)'); ylabel('毛保费 (元)'); title(sprintf('敏感性分析:贴现率对%d岁男性毛保费的影响', target_age)); grid on; fprintf('\n敏感性分析:贴现率从2%%上升到4%%,%d岁男性毛保费变化范围:%.2f 元 ~ %.2f 元\n', ... target_age, min(premium_sensitivity), max(premium_sensitivity));

代码解读与心得

  • 可视化是理解模型输出的关键。第一个子图清晰地展示了保费随年龄指数增长的趋势,以及性别差异。第二个子图揭示了附加费用(费用和风险)在总保费中的占比,这对于成本控制有指导意义。
  • 敏感性分析是精算模型的必备环节。这里我们演示了贴现率i变动对最终保费的影响。在实际工作中,你必须对每一个关键假设(发病率、费用率、死亡率等)都做类似的敏感性分析,并生成“龙卷风图”(Tornado Chart)来直观展示哪个假设对结果的影响最大。这能帮助管理者理解模型的风险点所在。
  • 在敏感性分析的循环中,我们固定了其他参数,只变动贴现率。这种“控制变量法”是分析多参数模型各因素影响的标准做法。

4. 模型优化与生产环境拓展

我们目前构建的是一个基础但完整的计算引擎。要将其用于接近生产环境的场景,还需要考虑以下拓展和优化:

4.1 引入多责任与等待期模型

真实的重疾险产品责任复杂。假设我们的产品责任如下:

  • 重症责任:100%保额,等待期90天后生效。
  • 中症责任:50%保额,等待期90天后生效。
  • 轻症责任:30%保额,等待期90天后生效。

同时,我们有三张对应的发生率表:q_serious,q_moderate,q_mild。等待期的处理,通常是将等待期内的发病率设为零(或极低的退还保费发生率)。

那么,纯保费的计算将变为各责任纯保费之和:

% 假设已获得各年龄的三种疾病发生率 pure_premium = 0; % 重症责任 pure_premium = pure_premium + q_serious(idx) * S * 1.00 * v_mid; % 中症责任 pure_premium = pure_premium + q_moderate(idx) * S * 0.50 * v_mid; % 轻症责任 pure_premium = pure_premium + q_mild(idx) * S * 0.30 * v_mid;

这要求我们的数据准备模块能加载和处理多张发生率表,核心计算函数也需要升级以支持多责任输入。

4.2 从单张保单到整体业务:现金流与利润测试

我们目前计算的是“单张保单”的保费。保险公司关心的是整个产品线、所有保单的聚合表现。这就需要引入现金流模型利润测试

  1. 现金流模型:模拟未来多年(如20年)的现金流情况。每年都有保费收入、理赔支出、费用支出、投资收益等。这需要我们将发病率从单年扩展到多年,并考虑退保率、续保率等因素。
  2. 利润测试:在现金流模型的基础上,计算关键利润指标,如新业务价值(VNB)、内含价值(EV)、偿付能力要求等。这通常需要在一个包含大量随机模拟(如蒙特卡洛模拟)的框架下进行,以评估在不同经济情景(投资收益率变化、发病率恶化等)下的盈利能力。

这部分代码会复杂得多,其核心是构建一个多维数组(例如cash_flow[policy_year, simulation_scenario])来存储每年的净现金流,然后进行贴现和汇总。这超出了本文基础篇的范围,但它是精算定价从“计算保费”迈向“评估业务”的必经之路。

4.3 代码工程化建议

当模型越来越复杂,代码就需要更好的组织。

  • 模块化:将数据加载、核心计算、结果输出、可视化、敏感性分析分别写成独立的函数或脚本文件(.m文件)。主脚本只负责调用和协调。这就像搭积木,每块功能清晰,易于调试和复用。
  • 使用面向对象编程(OOP):对于非常复杂的模型,可以考虑定义类。例如,定义一个InsuranceProduct类,其属性包括保额、责任列表、发生率表等,方法包括calculatePremiumrunProfitTest等。这能更好地封装数据和逻辑。
  • 数据接口:确保核心计算函数与数据源解耦。数据最好从外部文件(CSV, Excel)读入,而不是硬编码在脚本里。这样,当发生率表更新时,只需替换数据文件,而无需修改代码。
  • 版本控制:使用Git等工具管理代码版本。每次对模型假设或代码逻辑的重大修改,都应留有记录。这对于模型审计和回溯至关重要。

5. 常见问题与调试心得

在实现和运行上述模型的过程中,你几乎一定会遇到下面这些问题。这里分享一些我的排查思路。

问题一:计算出的保费为NaN(非数字)或Inf(无穷大)。

  • 排查思路:立即检查核心计算公式pure_premium = q * S * discount_factor
    1. 检查发病率q:是否有可能为负数?或者数据读取错误导致某些值为NaN?使用min(incidence_data.male)max(incidence_data.male)查看范围,用any(isnan(incidence_data.male))检查是否存在NaN。
    2. 检查贴现因子v:如果贴现率i为 -1(这不可能),会导致分母为零,v为无穷大。确保i是合理的正数(如0.02-0.05)。
    3. 检查保险金额S:确认是否为正常正数。
  • 心得:在编写循环或向量化计算前,先用一组确定的、合理的参数手动调用一次calculatePremium函数,确保基础公式正确。这是“单元测试”的思想。

问题二:保费随年龄变化的曲线看起来“不平滑”,有异常的跳点。

  • 排查思路:这几乎肯定是输入数据——发病率表——的问题。
    1. 可视化发病率曲线:立即画图plot(ages, incidence_data.male)。如果发病率曲线本身就有跳点,那么保费曲线有跳点是正常的,但这提示你需要审查数据来源的合理性。
    2. 检查数据源:是否是读取Excel或CSV时,某些行有格式错误、文本字符混入?使用Matlab的导入工具(Import Tool)仔细检查原始数据文件。
    3. 数据插值:有时原始发生率表只提供5岁一组的粗粒度数据(如25-29岁组用一个发生率)。你需要通过插值(如线性插值、样条插值)来获得每个整数年龄的发生率。interp1函数可以帮你完成这个工作。
  • 心得:“Garbage in, garbage out”。模型结果的质量直接取决于输入数据的质量。永远不要假设数据是完美的,可视化是检验数据的第一步。

问题三:敏感性分析结果与直觉不符,比如贴现率上升,保费反而上升?

  • 排查思路:回顾纯保费公式 ( P = q \times S \times v ),其中 ( v = 1/(1+i) )。贴现率i上升,贴现因子v下降,纯保费应该下降。如果出现相反情况,问题可能不在这个公式。
    1. 检查毛保费公式:确认是否是费用附加率alpha或风险附加率beta被错误地定义成了与i相关的变量。
    2. 检查计算顺序:在敏感性分析的循环中,是否不小心改变了其他变量(如S,q)的值?确保循环体内只变动你想要分析的那个参数。
    3. 打印中间变量:在循环内加入fprintf语句,打印出每次循环的i_current,v_current,pure_premium等值,一步步跟踪计算过程。
  • 心得:当模型行为与预期不符时,最有效的办法是“分解问题,逐步验证”。将复杂的计算链打断,检查每一个中间环节的输出。Matlab的交互式环境和实时变量查看功能是调试的利器。

通过这个从数学原理到Matlab代码实现的完整流程,我们不仅得到了一套可用的费率计算工具,更重要的是建立了一种用计算思维解决精算定价问题的方法论。模型可以不断复杂化以贴近现实,但核心依然是:清晰的定义、合理的假设、严谨的实现,以及不间断的验证。这套代码框架为你提供了一个坚实的起点,你可以在此基础上,加载真实数据、添加更多保险责任、甚至构建简单的现金流模型,逐步迈向更专业的精算分析。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询