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实现高性能分组。
案例场景:分组计算准备金
假设有一份保单数据,包含险种、年龄、保额和已缴年限。需要按险种分组,分别用不同公式计算理论准备金。此时可用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完成分组迭代,并在性能与可读性之间取得平衡。这是后续构建准备金评估、现金流预测和经验分析模型的基础能力。