3. R编程进阶

在精算建模与保险数据分析中,数据往往具有“分组多、规则杂、迭代频繁”的特点。例如,不同险种需要套用不同的准备金公式,不同年龄段的死亡率需要分段插值,不同保单年度需要滚动计算现金流。此时,仅靠基础向量运算已不足以表达业务逻辑,必须掌握R的流程控制、函数封装与向量化迭代工具。本节围绕条件判断、循环、自定义函数、apply家族与高效数据处理五个层次展开,帮助你写出既正确又高效的R代码。

核心原理:控制流与向量化的分工

R是向量化语言,但并非所有逻辑都能天然向量化。条件判断解决“分支选择”,循环解决“重复执行”,自定义函数解决“逻辑复用”,apply家族解决“分组迭代”,高效数据处理则解决“规模与性能”。五者不是替代关系,而是协作关系。

  • 条件判断: if/else用于标量分支,ifelse()与dplyr::case_when()用于向量化分支。
  • 循环: for适合状态依赖的迭代,while适合条件驱动的迭代,但应尽量预分配内存。
  • 自定义函数:把“险种+参数+数据”封装为可复用单元,是精算模型工程化的基础。
  • apply家族: apply、lapply、sapply、tapply、mapply分别面向矩阵、列表、分组与多参数场景。
  • 高效数据处理:优先使用data.table或dplyr的向量化分组操作,避免逐行循环。

精算场景中的典型判断:ifelse(age < 18, "未成年", ifelse(age <= 60, "成年", "老年"))。但嵌套ifelse可读性差,推荐case_when()。

方法流程:从分支到分组的递进

下面以一个“保费折扣计算”场景串联各知识点。假设保险公司根据投保人年龄、历史理赔次数和渠道,计算折扣系数。

# 1. 自定义函数:计算单个保单折扣
calc_discount <- function(age, claims, channel) {
  base <- if (age < 25) 0.9 else if (age <= 55) 1.0 else 0.95
  claim_adj <- ifelse(claims == 0, 1.05, ifelse(claims <= 2, 1.0, 0.9))
  channel_adj <- switch(channel,
    "online" = 1.02,
    "agent" = 1.00,
    "broker" = 0.98,
    1.00
  )
  round(base * claim_adj * channel_adj, 3)
}

# 2. 向量化调用
df <- data.frame(
  age = c(23, 40, 62, 35),
  claims = c(0, 3, 1, 0),
  channel = c("online", "agent", "broker", "online")
)
df$discount <- mapply(calc_discount, df$age, df$claims, df$channel)

# 3. 分组迭代:按渠道计算平均折扣
tapply(df$discount, df$channel, mean)

# 4. 高效分组:data.table写法
library(data.table)
dt <- as.data.table(df)
dt[, .(avg_discount = mean(discount)), by = channel]

上述代码体现了从“单点函数”到“向量化调用”再到“分组聚合”的完整链路。关键点在于:mapply把多参数函数应用到多个向量,tapply按因子分组,data.table则用by实现高性能分组。

R编程进阶:五层能力协作模型 条件判断 if / ifelse / case_when 循环 for / while / 预分配 自定义函数 function / 默认参数 apply家族 lapply / sapply / mapply 高效处理 data.table / dplyr 精算建模任务 折扣计算 · 准备金分组 · 现金流滚动 输出:可复用、可审计、可扩展的R代码 函数封装 + 向量化迭代 + 分组聚合

案例场景:分组计算准备金

假设有一份保单数据,包含险种、年龄、保额和已缴年限。需要按险种分组,分别用不同公式计算理论准备金。此时可用split加lapply,也可直接用data.table的by。

# 按险种分组,分别计算准备金
reserve_calc <- function(sub_dt) {
  if (unique(sub_dt$type) == "term") {
    sub_dt[, reserve := premium * 0.6 * (1 - paid_years / 20)]
  } else {
    sub_dt[, reserve := premium * 0.8 * (1 - paid_years / 30)]
  }
  sub_dt
}

dt_list <- split(dt, by = "type")
result <- rbindlist(lapply(dt_list, reserve_calc))

这里split负责分组,lapply负责迭代,rbindlist负责合并。若数据量达到百万行,建议直接用dt[, reserve := ... , by = type],避免拆分与合并开销。

工具 适用场景 返回类型 精算示例
apply 矩阵按行/列 向量/矩阵 死亡率矩阵按年龄求均值
lapply 列表逐元素 列表 多险种分别建模
sapply 简化结果 向量/矩阵 批量计算保费
tapply 按因子分组 数组 按渠道统计赔付率
mapply 多参数并行 向量/列表 年龄+性别+保额计算费率

误区与复盘

误区一:用循环逐行计算。在R中,逐行for循环不仅慢,还容易因类型转换出错。应优先向量化或分组聚合。

误区二:滥用ifelse嵌套。三层以上嵌套可读性急剧下降,建议改用case_when()或switch()。

误区三:忽略函数参数校验。精算函数应检查年龄、保额、年限是否合法,避免NA或负值导致模型失真。

复盘建议:每写完一个自定义函数,问自己三个问题:能否向量化?能否分组复用?边界条件是否处理?这三点决定了代码能否从“能跑”升级为“可交付”。

掌握本节内容后,你应能根据业务规则选择合适的分支结构,用函数封装精算逻辑,用apply家族或data.table完成分组迭代,并在性能与可读性之间取得平衡。这是后续构建准备金评估、现金流预测和经验分析模型的基础能力。