公司动态
R语言非参数模型在车险费率厘定中的实战应用
1. 从精算师的“经验法则”到数据驱动的费率厘定在传统保险精算领域费率厘定Rate Making很大程度上依赖于精算师的经验和一系列参数化假设。我们通常会假设索赔频率服从泊松分布索赔强度服从伽马分布然后通过广义线性模型GLM来拟合这些参数最终得到一个看起来“干净”且易于解释的费率表。这种方法统治了行业数十年它的优势在于结构清晰、结果可解释并且能与精算准备金评估等后续环节无缝衔接。然而随着数据量的爆炸式增长和业务场景的日益复杂这种“强假设”模型的局限性也越来越明显现实世界的数据关系往往是非线性的、交互的甚至是我们尚未完全理解的。强行用一个预设的数学公式去套用就像试图用一把直尺去测量蜿蜒的海岸线结果必然是粗糙且有偏差的。这就是为什么近年来像局部回归Loess、广义相加模型GAM和样条回归Spline Regression这类非参数或半参数模型开始在精算数据分析中崭露头角。它们不预先假定响应变量比如索赔成本与预测变量比如年龄、车辆功率之间具体的函数形式而是让数据自己“说话”去揭示其中可能存在的复杂关系。对于一名一线数据分析师或精算建模人员来说掌握这些工具意味着你能从数据中挖掘出更精细的风险模式识别出那些被GLM平滑掉的“尖峰”或“拐点”从而制定出更公平、更精准、更具竞争力的保险费率。在R语言生态中我们有极其强大的工具链来实践这些想法。mgcv包是拟合GAM的瑞士军刀splines包提供了构建样条的基础函数而loess函数则是进行局部加权回归的利器。本文将从一个实战精算师的角度手把手带你走过使用这三种非参数模型进行车险费率厘定的完整流程。我不会只停留在函数调用和结果展示上而是会深入探讨每种方法背后的核心思想、在保险场景下的适用边界、模型诊断的关键以及最终如何将“黑盒”般的拟合结果转化为业务部门能看懂、核保部门敢使用的“风险评分”或“费率系数”。你会发现从参数化思维切换到非参数思维不仅是技术的升级更是对风险本质认知的一次深化。2. 数据准备与探索性分析看见数据本身的形状在构建任何模型之前透彻地了解你的数据是第一步对于非参数模型尤其如此。因为模型本身不做强假设那么数据中的模式、异常和关系就必须通过我们的眼睛和工具先被揭示出来。假设我们手头有一份车险保单数据包含以下关键字段age驾驶员年龄、vehicle_power车辆功率如马力、exposure风险暴露期以年为单位、claim_count索赔次数、claim_amount索赔总金额。我们的目标费率指标是纯风险保费Pure Premium即claim_amount / exposure或者分别对索赔频率claim_count / exposure和索赔强度claim_amount / claim_count当claim_count0时进行建模。首先让我们加载必要的R包并查看数据的基本情况。除了基础的tidyverse我们重点关注可视化工具ggplot2和后续建模要用的mgcv。# 加载核心包 library(tidyverse) library(mgcv) library(splines) library(patchwork) # 用于组合图形 # 假设数据已加载为 data.frame insurance_data # 查看数据结构与摘要 glimpse(insurance_data) summary(insurance_data) # 计算关键指标 insurance_data - insurance_data %% mutate(pure_premium claim_amount / exposure, claim_freq claim_count / exposure) %% filter(exposure 0) # 排除暴露期为0的记录接下来是探索性分析的重头戏可视化。对于费率厘定我们最关心的是风险因子如年龄与响应变量如纯风险保费之间的关系形状。# 1. 年龄与纯风险保费的散点图与平滑趋势 p_age - ggplot(insurance_data, aes(x age, y pure_premium)) geom_point(alpha 0.2, size 0.5) # 半透明点避免过度绘制 geom_smooth(method loess, span 0.5, se TRUE, color blue) # 局部回归平滑线 labs(title 驾驶员年龄 vs. 纯风险保费, x 年龄, y 纯风险保费) theme_minimal() # 2. 车辆功率与纯风险保费的关系 p_power - ggplot(insurance_data, aes(x vehicle_power, y pure_premium)) geom_point(alpha 0.2, size 0.5) geom_smooth(method gam, formula y ~ s(x, bs cr), color red) # GAM平滑线 labs(title 车辆功率 vs. 纯风险保费, x 车辆功率 (马力), y ) theme_minimal() # 并排显示 p_age p_power注意在保险数据中纯风险保费的分布通常是极度右偏的大部分保单没有索赔纯风险保费为0少数大额索赔会拉高整体均值。直接绘制原始值的散点图可能被这些极端值主导。一个更稳健的做法是绘制分位数的平滑曲线或者对响应变量进行适当的变换如对数变换后再观察趋势。但在这个初步探索阶段我们的目标是定性观察是否存在明显的非线性模式。从上述图形中你可能会发现年轻驾驶员的风险并非线性下降而是在某个年龄段如25岁出现一个风险峰值车辆功率与风险的关系可能也不是简单的线性增长在低功率区间增长平缓超过某个阈值后风险急剧上升。这些直观发现正是我们选择非参数模型而非线性模型的直接动因。3. 局部回归Loess捕捉数据中的局部模式局部加权回归Loess是一种非常直观的非参数方法。它的核心思想很简单为了预测某一点的值我们只使用该点“附近”的数据并给这些邻近点赋予不同的权重通常距离越近权重越高然后在这个局部窗口内拟合一个低阶多项式通常是一阶或二阶。这个“附近”的范围由一个称为span的参数控制它决定了用于局部拟合的数据比例。在R中我们可以直接使用stats包中的loess()函数。在费率厘定中Loess非常适合用于单变量风险分析和初步的趋势探测因为它能快速给出一个平滑的曲线让我们看到风险因子与保费之间可能存在的任何复杂关系。# 使用loess拟合年龄对纯风险保费的影响 loess_model_age - loess(pure_premium ~ age, data insurance_data, span 0.3, # 较小的span捕捉更局部的波动较大的span更平滑 degree 2, # 局部拟合多项式的阶数1为线性2为二次 family symmetric, # 对异常值更稳健的拟合方式 control loess.control(surface direct)) # 生成预测值用于绘图 pred_data - data.frame(age seq(min(insurance_data$age), max(insurance_data$age), length.out 200)) pred_data$pure_premium_pred - predict(loess_model_age, newdata pred_data) # 绘制拟合曲线 ggplot() geom_point(data insurance_data, aes(x age, y pure_premium), alpha 0.1, size 0.5) geom_line(data pred_data, aes(x age, y pure_premium_pred), color darkgreen, size 1.2) labs(title Loess拟合年龄与纯风险保费的非线性关系, subtitle paste(Span , 0.3, Degree , 2), x 驾驶员年龄, y 纯风险保费) theme_minimal()关键参数解读与选择经验span这是Loess中最重要的调参项。span越小曲线越能捕捉细节波动但也越容易受到噪声影响而过拟合span越大曲线越平滑但可能忽略掉重要的局部特征。我的经验是可以从0.3到0.7之间尝试并通过交叉验证来选择。在保险数据中由于数据噪声大通常倾向于使用稍大的span如0.4-0.5来获得更稳健的趋势估计。degree局部多项式的阶数。1阶线性计算更快更平滑2阶二次能捕捉拐点。对于风险曲线我们常预期存在“U型”或“倒U型”如年轻和年老驾驶员风险高因此degree2通常是更好的起点。family默认为gaussian假设误差服从高斯分布。如果数据中有很多极端值保险数据常有使用familysymmetric能提供更稳健的拟合因为它降低了异常点的影响。Loess在费率厘定中的局限性 尽管Loess非常直观好用但它有两个主要缺点限制了其在最终费率模型中的应用。第一它是纯粹的非参数方法结果是一个“曲线”难以像GLM那样输出一个简洁的“系数表”供核保系统使用。第二它难以处理多个预测变量。虽然可以拓展到多维但“维数灾难”会导致在数据稀疏区域估计极不可靠。因此Loess更多扮演的是探索性工具和基准模型的角色。我们可以用它来揭示非线性模式然后用更结构化的模型如GAM去正式建模。4. 广义相加模型GAM结构化非参数建模的利器如果说Loess是灵活自由的“手绘”那么广义相加模型GAM就是既保持艺术性又具备工程严谨性的“电脑制图”。GAM是GLM的扩展它将线性预测子中的部分或全部线性项替换为预测变量的平滑函数。其基本形式可以表示为g(μ) β_0 f_1(x_1) f_2(x_2) ... f_p(x_p)其中g()是链接函数如对数链接μ是响应变量的期望值如索赔频率f_j()是各个预测变量的平滑函数。在R中mgcv包是拟合GAM的绝对主力它通过惩罚回归样条来估计这些平滑函数自动在拟合优度和平滑度之间进行权衡。让我们构建一个针对车险索赔频率的GAM模型。假设我们怀疑年龄和车辆功率的影响都是非线性的。# 使用mgcv拟合GAM模型。假设索赔次数服从泊松分布以暴露期作为偏移量。 # 这是保险建模中对于索赔频率的标准设置。 gam_model_freq - gam(claim_count ~ s(age, bs cr, k 10) s(vehicle_power, bs cr, k 10) offset(log(exposure)), data insurance_data, family poisson(link log), method REML) # 使用REML进行平滑参数估计通常更稳定 # 查看模型摘要 summary(gam_model_freq)模型摘要会输出大量信息我们需要重点关注以下几点平滑项显著性查看s(age)和s(vehicle_power)的近似p值。如果p值很小如0.05表明该变量的非线性效应是显著的不能用简单的线性项替代。解释偏差Deviance explained指标类似于线性回归中的R²告诉我们模型解释了响应变量多少比例的变化。GCV/UBRE/REML分数这是模型选择的标准之一分数越低通常表示模型拟合越好同时兼顾了复杂度。有效自由度edf每个平滑函数的edf代表了其非线性的“复杂程度”。edf1意味着该函数实质上是线性的edf越大表示曲线越弯曲复杂。如果edf非常接近1你可以考虑用线性项替换它来简化模型。模型诊断与可视化 拟合模型后必须进行诊断。mgcv提供了强大的绘图函数来可视化平滑效应。# 绘制各个平滑项的效果图 par(mfrow c(1, 2)) plot(gam_model_freq, select 1, shade TRUE, shade.col lightblue, rug TRUE, main 年龄对索赔频率的影响, xlab 年龄, ylab s(age)) plot(gam_model_freq, select 2, shade TRUE, shade.col lightcoral, rug TRUE, main 车辆功率对索赔频率的影响, xlab 车辆功率, ylab s(vehicle_power))这些图显示了在保持其他变量不变的情况下单个变量对线性预测子的贡献在链接函数尺度上。图中的阴影区域表示95%的置信区间。你可以清晰地看到风险随年龄或功率变化的“形状”。例如年龄曲线可能显示25岁左右有一个明显的风险峰然后平稳下降到70岁后又略有上升。车辆功率曲线可能在低功率时平缓超过150马力后风险陡增。将GAM结果转化为费率系数 这是将模型落地到业务系统的关键一步。GAM拟合的是平滑函数但我们的费率表需要的是离散的系数。通常的做法是选择一个基准水平例如年龄40岁车辆功率100马力。计算在其他年龄和功率水平下相对于基准水平的相对风险。将这个连续的风险函数离散化成分组系数。# 1. 创建包含所有年龄和功率组合的预测网格 pred_grid - expand.grid(age seq(18, 80, by 1), vehicle_power seq(50, 300, by 10), exposure 1) # 暴露期设为1方便计算 # 2. 使用模型进行预测在响应变量尺度即索赔频率 pred_grid$pred_freq - predict(gam_model_freq, newdata pred_grid, type response) # 3. 设定基准点并计算相对风险 base_age - 40 base_power - 100 base_freq - predict(gam_model_freq, newdata data.frame(age base_age, vehicle_power base_power, exposure 1), type response) pred_grid$rel_risk - pred_grid$pred_freq / as.numeric(base_freq) # 4. 离散化例如将年龄每5岁分组计算组内平均相对风险 age_group_coef - pred_grid %% mutate(age_group cut(age, breaks seq(18, 80, by 5), include.lowest TRUE, right FALSE)) %% group_by(age_group) %% summarise(avg_rel_risk mean(rel_risk, na.rm TRUE)) %% mutate(coef log(avg_rel_risk)) # 通常费率系数取对数形式便于相加 print(age_group_coef)这样我们就得到了一个可以与现有GLM系数表并存的、基于GAM风险形状的离散费率系数。核保和定价部门可以基于此进行调整。5. 样条回归平衡灵活性与可解释性样条回归可以看作是GAM的“组件”或一种更手动的实现方式。其核心思想是用一组分段多项式函数样条基函数来逼近复杂的非线性关系。通过在数据范围内设置一系列节点Knots并确保在节点处连接的多项式足够平滑通常要求函数值、一阶导数甚至二阶导数连续我们就能构造出一条非常灵活且光滑的曲线。在R中splines包提供了bs()B样条和ns()自然样条等函数来生成样条基。我们可以将其直接用在lm()或glm()中。相比于GAM的“全自动”样条回归给了建模者更多的控制权特别是对节点位置和数量的选择。library(splines) # 使用自然样条ns拟合年龄对纯风险保费的影响 # 自然样条在边界处约束为线性可以避免外推时的疯狂行为这在保险中很重要。 spline_model - glm(pure_premium ~ ns(age, df 5) vehicle_power, data insurance_data, family Gamma(link log), # 假设纯风险保费服从伽马分布 weights exposure) # 以暴露期作为权重 # 查看模型系数 - 注意这里看到的是样条基的系数不是直接的风险曲线 summary(spline_model) # 为了得到可解释的风险曲线我们需要进行预测绘图 plot_data - data.frame(age seq(18, 80, length.out 200), vehicle_power median(insurance_data$vehicle_power)) # 固定车辆功率为中位数 plot_data$pred - predict(spline_model, newdata plot_data, type response) ggplot(plot_data, aes(x age, y pred)) geom_line(size 1.2, color purple) labs(title 自然样条回归拟合的风险曲线, subtitle 车辆功率固定在中位数, x 驾驶员年龄, y 预测纯风险保费) theme_minimal()样条回归的关键决策与经验样条类型选择B样条非常灵活是mgcv中许多平滑器的基础。但在数据边界外的行为可能不稳定。自然样条在边界节点外强制为线性外推更保守。在保险场景下我们经常需要对年轻如18岁或年长如80岁的极端年龄进行定价自然样条的线性外推通常比多项式外推更合理、更安全。因此我强烈建议在费率模型中使用自然样条。自由度df或节点选择df参数间接决定了内部节点的数量df 节点数 样条阶数。df越大曲线越灵活df越小曲线越平滑。一个经验法则是先从较小的df如4或5开始然后根据残差分析或交叉验证结果逐步增加。也可以使用mgcv的自动平滑参数选择但手动设定df能提供更直接的控制。节点的位置通常默认基于分位数这在实际中效果不错。但在风险变化剧烈的区域如年轻驾驶员区间可以尝试手动增加节点以提高拟合精度。样条回归 vs. GAM样条回归更透明、可控。你可以明确知道用了多少个节点、什么类型的样条。模型结果可以无缝嵌入到传统的GLM框架中解释。适合当你对风险曲线的形状有初步假设或者需要与现有GLM系统高度兼容时使用。GAM通过mgcv更自动化、更强大。它能自动选择平滑度处理更复杂的交互效应如te(age, vehicle_power)并且内置了更先进的诊断和选择工具。适合探索性建模和处理复杂的高维非线性关系。在实际项目中我常常将两者结合先用GAM进行探索发现重要的非线性变量和大致形状然后为了生产系统的稳定性和可解释性用自然样条在GLM框架中重新拟合一个简化但形状近似的模型。6. 模型比较、验证与生产化考量当我们拥有了Loess趋势线、GAM模型和样条回归模型后如何选择如何确保模型不会过拟合又如何将最终的模型投入生产1. 模型比较与选择我们不能只依赖训练集上的拟合优度。一个在训练集上弯曲复杂的模型可能在未见数据上表现糟糕。因此交叉验证是金标准。# 简化的交叉验证示例将数据分为训练集和测试集 set.seed(123) train_indices - sample(1:nrow(insurance_data), size floor(0.8 * nrow(insurance_data))) train_data - insurance_data[train_indices, ] test_data - insurance_data[-train_indices, ] # 在训练集上拟合多个模型 # 模型1: 简单GLM线性 glm_linear - glm(claim_count ~ age vehicle_power offset(log(exposure)), data train_data, family poisson) # 模型2: GAM gam_model - gam(claim_count ~ s(age) s(vehicle_power) offset(log(exposure)), data train_data, family poisson, method REML) # 模型3: 样条GLM spline_glm - glm(claim_count ~ ns(age, df5) ns(vehicle_power, df5) offset(log(exposure)), data train_data, family poisson) # 在测试集上预测并计算偏差或RMSE test_preds - data.frame( actual test_data$claim_count / test_data$exposure, glm predict(glm_linear, newdata test_data, type response) / test_data$exposure, gam predict(gam_model, newdata test_data, type response) / test_data$exposure, spline predict(spline_glm, newdata test_data, type response) / test_data$exposure ) # 计算均方根误差RMSE library(Metrics) rmse_results - sapply(test_preds[, c(glm, gam, spline)], function(pred) rmse(test_preds$actual, pred)) print(rmse_results)通常GAM和样条模型的测试集误差会低于线性GLM。如果GAM和样条模型表现接近我会优先选择样条GLM因为它结构更简单与现有精算基础设施兼容性更好。2. 稳定性与可解释性验证对于费率模型稳定性至关重要。我们需要检查时间外样本验证用过去一年的数据训练预测下一年的数据看模型性能是否稳定。系数稳定性将数据按时间或随机分块分别拟合模型比较关键风险因子如年龄曲线的形状是否一致。如果波动很大说明模型可能过拟合或数据不足以支持复杂的非线性。业务可解释性将拟合出的风险曲线展示给业务专家老核保员。他们基于经验的直觉是宝贵的验证工具。如果模型显示“25岁驾驶员风险比40岁低”而业务经验强烈反对就需要深入检查数据或模型设定。3. 生产化落地从连续曲线到离散系数表如前所述精算系统和核保规则引擎通常需要离散的费率系数。除了之前提到的分组平均法还有更精细的做法在关键风险转折点附近细分例如在GAM拟合的年龄曲线上如果发现25岁是风险峰值那么18-24岁和25-30岁的分组就应该更细而不是简单的每5岁一组。平滑处理系数离散化后的系数可能出现不合理的“跳跃”。可以对这些离散系数再进行一次简单的局部平滑或单调性调整例如确保车险中年龄系数在大部分区间是单调下降的使其更符合业务常识。创建“模型评分卡”将连续变量如年龄、功率通过样条或GAM模型转换成一个“风险评分”这个评分可以作为新的特征输入到最终的定价GLM中。这样既保留了非线性信息又保持了最终模型的线性可加结构便于管理和解释。7. 实战中的陷阱与高级技巧在实际操作中仅仅跑通模型是远远不够的。下面分享几个我踩过坑才总结出的经验。陷阱一忽略暴露期与权重的处理保险数据中每张保单的暴露期exposure不同。在建模索赔次数泊松分布时必须将log(exposure)作为偏移量offset纳入模型。在建模索赔强度或纯风险保费伽马分布或Tweedie分布时则需要将exposure作为权重weights参数引入。如果忽略这一点你的模型本质上是在对保单数量而非风险单位进行拟合结果会产生严重偏差。在gam()和glm()中务必正确设置offset和weights参数。陷阱二过度追求拟合优度导致过拟合非参数模型非常灵活很容易把数据中的噪声也当成信号拟合进去。mgcv的默认平滑参数选择methodREML或methodGCV.Cp通常能很好地防止过拟合但仍需警惕。检查plot(gam_model)中的置信区间如果曲线剧烈波动而置信区间非常宽说明该处的数据支撑不足拟合结果不可信。此时可以通过增加k参数基函数维度的上限但用gamma参数1增加平滑惩罚或者直接换用更简单的模型。技巧一使用Tweedie分布直接建模纯风险保费传统的“频率-强度”两步建模法有时略显繁琐。Tweedie分布是一个强大的替代方案它能用一个模型同时刻画零索赔和正索赔的连续分布非常适合直接建模纯风险保费。mgcv支持Tweedie分布。# 使用Tweedie分布直接拟合纯风险保费权重为暴露期 gam_tweedie - gam(pure_premium ~ s(age) s(vehicle_power), data insurance_data, family tw(link log), weights exposure, method REML)技巧二引入交互效应——当年龄和车型风险并非独立年轻驾驶员开大马力车风险是不是会激增这种效应可以通过GAM中的张量积平滑te()来捕捉。gam_interaction - gam(claim_count ~ te(age, vehicle_power, k c(5, 5)) offset(log(exposure)), data insurance_data, family poisson, method REML) # 可视化交互效应 vis.gam(gam_interaction, view c(age, vehicle_power), plot.type contour, too.far 0.1, main 年龄与车辆功率的交互风险曲面)这张等高线图能清晰展示风险在“年龄-功率”二维平面上的分布可能揭示出在传统单变量分析中无法发现的“高风险组合”。技巧三处理分类变量与连续变量的混合费率模型中必然有分类变量如车型、地区。在GAM中你可以轻松地将平滑项和线性项或因子项混合。# 假设有分类变量 region insurance_data$region - as.factor(insurance_data$region) gam_mixed - gam(claim_count ~ s(age) s(vehicle_power) region offset(log(exposure)), data insurance_data, family poisson, method REML)模型会自动估计每个地区的相对风险系数同时考虑年龄和功率的非线性影响。这比单独为每个地区拟合一个模型要稳健得多。从局部回归的初步洞察到广义相加模型的自动化拟合与诊断再到样条回归在手控性与可解释性之间的平衡非参数模型为我们打开了一扇更精准理解风险的大门。它们不再是统计学课本上的抽象概念而是精算师和数据科学家手中实实在在的、能产生商业价值的工具。真正的挑战不在于代码实现而在于如何将这种数据驱动的、可能略显“黑盒”的洞察转化为稳定、可靠、且能被业务逻辑所接受的定价因子。这需要反复的验证、谨慎的离散化以及不间断的与业务专家的沟通。我的体会是最好的费率模型永远是统计学严谨性与业务直觉之间那个优雅的平衡点。下一次当你面对一份新的保单数据时不妨先画几条Loess曲线看看也许那些隐藏在数字背后的、弯曲的风险故事正等待着被你的模型讲述出来。