十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

保险风险预测实战:SVM与神经网络建模全流程解析

保险风险预测实战:SVM与神经网络建模全流程解析 简介面向保险行业风险预测场景的机器学习实战资源聚焦神经网络与SVM两种算法在保诚公司真实数据上的完整建模流程。资料从数据预处理切入覆盖缺失值清洗、标准化、特征选择、模型训练、参数调优以及模型评估混淆矩阵、精确率、召回率、F1分数等关键环节代码分段清晰、注释完整适合有一定Python基础、希望将机器学习落地到保险风控或金融数据分析场景的读者也可作为相关课程设计与项目复现的参考。压缩包共7个文件包含3个Python脚本、3个CSV数据文件和1个说明文档整体大小37.1MB目录结构简洁便于按“数据处理—模型实现—结果分析”的顺序对照学习。目前已有43人学习使用。借助该资源读者既能掌握神经网络处理非线性关系、SVM寻找最优超平面实现分类的核心原理也能基于真实保险数据开展预测实验理解高风险客户识别、定价策略优化等实际应用价值。1. 保险风险预测先看清保诚数据在预测什么打开这份以保诚Prudential竞赛数据为背景的保险风险预测项目第一件需要澄清的事反而和模型无关目标列 Response 是一个 1 到 8 的有序整数约 130 列混合类型特征训练集规模在 6 万行上下。很多人会下意识把它当成疾病分类或 NLP 文本任务实际上它是典型的单表监督学习——用客户填写的问卷变量推理人身险风险等级本质是回归加有序分类的混合问题。神经网络和 SVM 的组合在这里并不是噱头。SVM 负责在高维空间中找最大间隔分界神经网络负责自动组合非线性风险因子两者放在一起的价值在于对照面对同样的噪声数据谁会过拟合、谁会漏掉趋势一比便知。下面按特征工程、基线模型、SVM 与神经网络、评估落地的顺序展开新手能跟着步骤复现熟手可以在参数和坑位上得到确认。2. 保险风险预测的数据处理与特征工程2.1 先做目标定义和数据分层这类项目的第一个坑不是缺失值而是把 Response 当成普通整数回归直接丢进模型。Response 的取值虽然是 1 到 8但相邻等级之间的业务差异并不等距从等级 3 到 4 的风险跃迁和从等级 7 到 8 的跃迁在保费定价上含义完全不同。所以在特征工程之前先要把目标变量的性质定下来。我一般会按“先回归、后分桶”的路线处理训练阶段用 MSE 做损失评估阶段同时看 MAE 和 ±1 命中率。这样既保留等级之间的顺序信息又不会被个别极端等级带偏。定下目标之后立刻做分层切分保证每个风险等级在训练集和验证集中的比例一致。from sklearn.model_selection import train_test_split import pandas as pd train pd.read_csv(data/train.csv) trn, val train_test_split( train, test_size0.2, random_state42, stratifytrain[Response], # 按风险等级比例分层 ) print(trn.shape, val.shape) print(trn[Response].value_counts(normalizeTrue).sort_index())代码里的stratifytrain[Response]是关键参数。如果忽略它随机切分很可能让某些出现次数少的等级比如等级 8在验证集中只剩几十条模型评估结果会剧烈抖动。random_state42用来固定随机种子保证每次运行切分结果一致这是机器学习项目里可复现的第一步。跑完这段代码应该能看到训练集和验证集的 Response 分布几乎相同。2.2 特征清洗与特征变换的优先级保诚这份数据的特征命名带有明显的业务分组痕迹Product_Info_*是产品信息Ins_Age和BMI是被保人基础属性Employment_Info_*是职业相关变量Insurance_History_*是历史承保记录。这种分组本身就是特征工程的线索。特征分组常见处理方式理由类别型如 Product_Info_2有序编码或 one-hot树模型可直接吃有序编码SVM 和 MLP 需要再标准化数值型缺失中位数填充相比均值更抗离群点保险数据里长尾明显偏态分布如 BMIlog1p 变换把右偏分布拉正SVM 对尺度敏感高基数类别先按频次合并再编码避免 one-hot 后维度爆炸填充的顺序比填充的方法更重要。先切分、再在训练集上计算填充值、最后把填充值应用到验证集这是防止数据泄漏的底线。很多项目直接在完整数据上做fillna(train.median())验证集的信息提前进入了训练集交叉验证分数会虚高。num_cols train.select_dtypes(include[float64, int64]).columns.difference( [Response] ).tolist() # 先只在训练集上计算中位数 medians trn[num_cols].median() # 再应用到训练集和验证集 trn[num_cols] trn[num_cols].fillna(medians) val[num_cols] val[num_cols].fillna(medians) print(剩余缺失:, val[num_cols].isna().sum().sum())这段代码的逻辑是先select_dtypes筛出数值列排除目标列然后计算训练集每列的中位数最后用同一个medians填充两份数据。验证集填充用的必须是训练集统计量不能用验证集自己的中位数。类别型特征的处理要更小心。OrdinalEncoder的handle_unknownuse_encoded_value可以处理验证集中出现训练集没见过的类别from sklearn.preprocessing import OrdinalEncoder cat_cols [Product_Info_2, Employment_Info_5] oe OrdinalEncoder(handle_unknownuse_encoded_value, unknown_value-1) trn[cat_cols] oe.fit_transform(trn[cat_cols]) val[cat_cols] oe.transform(val[cat_cols])fit_transform只用在训练集transform用在验证集未知类别被编码成 -1。这样做比pd.factorize稳妥后者在验证集上会重新编号导致同一个类别在训练集和验证集里编码不同模型直接失效。提示特征工程的产物应该保存成文件或 pickle 对象。后续调参、重新训练都要复用同一套预处理逻辑手写两遍很容易出现训练和推理不一致。3. 用逻辑回归和 LightGBM 建立保险风险预测基线3.1 为什么神经网络之前必须先有基线一份号称“基于神经网络和 SVM”的机器学习项目最容易犯的错误是一上来就调神经网络。神经网络在这类表格数据上并没有天然优势反而更容易被缺失值、离群点和类别编码问题带偏。正确的顺序是先跑通一个简单可靠的基线再上复杂模型。逻辑回归是最合适的第一个基线它假设特征与目标之间存在线性关系训练快、可解释性强还能直接输出概率。在保诚这个数据集上逻辑回归配合标准化后的数值特征通常能拿到一个不算差的 RMSE。LightGBM 则是第二个基线它擅长捕捉特征间的非线性交互训练速度远快于神经网络适合用来做特征重要性的粗筛。import lightgbm as lgb from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_squared_error # 先用逻辑回归这里用线性回归替代目标为连续值时更直接 lin LinearRegression() lin.fit(trn[cat_cols num_cols], trn[Response]) pred_lin lin.predict(val[cat_cols num_cols]) # 再用 LightGBM 做非线性基线 model_lgb lgb.LGBMRegressor( n_estimators300, learning_rate0.05, num_leaves31, min_child_samples20, subsample0.8, colsample_bytree0.8, random_state42, ) model_lgb.fit( trn[cat_cols num_cols], trn[Response], eval_set[(val[cat_cols num_cols], val[Response])], callbacks[lgb.early_stopping(50), lgb.log_evaluation(50)], ) pred_lgb model_lgb.predict(val[cat_cols num_cols]) print(线性回归 RMSE:, mean_squared_error(val[Response], pred_lin, squaredFalse)) print(LightGBM RMSE:, mean_squared_error(val[Response], pred_lgb, squaredFalse))这段代码里early_stopping(50)表示如果验证集分数连续 50 轮不下降就停止训练log_evaluation(50)表示每 50 轮打印一次日志。subsample和colsample_bytree分别是行采样和列采样比例小于 1.0 可以抑制过拟合。num_leaves控制树的复杂度31 是 LightGBM 的推荐起点太大容易过拟合太小欠拟合。3.2 基线结果怎么读、怎么用把两个基线跑完先别急着上神经网络。先看差距如果 LightGBM 比线性回归好很多说明数据里确实存在非线性关系神经网络有发挥空间如果两者差不多说明特征工程还没做到位加模型复杂度意义不大。基线模型验证集 RMSE特点线性回归约 1.52快速给出下限适合检查特征是否有预测力LightGBM约 1.35非线性基线特征重要性可用于筛选SVRRBF 核待测对标准化敏感小样本表现稳定MLP待测潜力最大但调参成本最高上面的数值是示例量级实际结果会随特征工程细节浮动。关键是看相对关系。LightGBM 训练完成后可以用model_lgb.feature_importances_拿到特征重要性排序把排在最后 20% 的特征剔除后再训练一次往往能小幅提升 SVM 和神经网络的训练速度与泛化能力。提示基线模型的结果要记录成表格包含时间戳、特征列表、随机种子和验证集分数。后面每次调参都跟这个基线比而不是跟记忆中的最好分数比。4. SVM 与神经网络保险风险预测的两种走法4.1 SVM 在中小样本上的稳定性优势SVM 的核心思想是在特征空间中找一个最大间隔的分界面。对于保险风险预测这种维度在几十到一百出头、样本量在几万行的场景SVM 的 RBF 核函数能把特征映射到高维空间拟合非线性关系同时靠间隔最大化和支持向量来控制过拟合。但 SVM 有两个致命弱点必须先处理对特征尺度敏感、训练时间随样本量超线性增长。特征尺度问题用MinMaxScaler解决把每个特征压缩到 0 到 1 之间训练时间问题通常用两种手段绕开——用SVR配合较小的C值或者对训练集做随机子采样。from sklearn.svm import SVR from sklearn.preprocessing import MinMaxScaler from sklearn.pipeline import make_pipeline feat_cols cat_cols num_cols pipeline_svm make_pipeline( MinMaxScaler(), SVR( kernelrbf, C50.0, epsilon0.5, gammascale, cache_size500, ), ) pipeline_svm.fit(trn[feat_cols], trn[Response]) pred_svm pipeline_svm.predict(val[feat_cols]) print(SVR RMSE:, mean_squared_error(val[Response], pred_svm, squaredFalse))代码里的C是误分类惩罚系数越大越容易过拟合保险数据噪声多一般从 10 到 200 之间搜索。epsilon是回归 Tube 的宽度它决定了预测值与真实值之间多大的误差可以不计入损失0.5 意味着允许半级误差比较符合保险评级精度的业务预期。gammascale让模型根据特征数量自动计算核函数的宽度是sklearn的默认行为也是调参的好起点。SVR 调参时最值得注意的是顺序永远先固定gamma再搜C最后微调epsilon。三个参数同时网格搜索维度太高几万行数据上会慢到无法接受。SVR 参数作用推荐搜索范围C误分类惩罚越大越拟合训练集10, 50, 100, 200epsilon不敏感带宽度越大预测越平滑0.3, 0.5, 1.0gammaRBF 核宽度控制单点影响范围scale, 0.01, 0.14.2 用 PyTorch 搭一个保险风险预测 MLP神经网络在处理表格数据时需要更多工程设计。保险问卷特征之间的交互往往是稀疏的——比如年龄和 BMI 的组合对风险的影响不是简单的相加关系。MLP 通过隐藏层的非线性激活函数自动构造这些组合特征。下面代码用 PyTorch 实现一个三层 MLP。第一层把 130 个左右的特征压缩到 64 维第二层降到 32 维最后输出一个连续值。网络规模刻意保持克制因为 6 万行数据的表达能力有限层数太深要么过拟合要么训练不稳定。import torch import torch.nn as nn class RiskNet(nn.Module): def __init__(self, in_dim): super().__init__() self.net nn.Sequential( nn.Linear(in_dim, 64), nn.ReLU(), nn.Dropout(0.2), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 1), ) def forward(self, x): return self.net(x).squeeze(-1) model_nn RiskNet(len(feat_cols)) opt torch.optim.Adam(model_nn.parameters(), lr1e-3) criterion nn.MSELoss() Xtr torch.tensor(trn[feat_cols].to_numpy(), dtypetorch.float32) ytr torch.tensor(trn[Response].to_numpy(), dtypetorch.float32) Xval torch.tensor(val[feat_cols].to_numpy(), dtypetorch.float32) for epoch in range(50): model_nn.train() opt.zero_grad() loss criterion(model_nn(Xtr), ytr) loss.backward() opt.step() if epoch % 10 0: model_nn.eval() with torch.no_grad(): val_loss criterion(model_nn(Xval), val[Response].to_numpy()) print(fepoch {epoch}: train {loss.item():.4f} val {val_loss.item():.4f})关键点在Dropout(0.2)它随机丢弃 20% 的神经元输出相当于每次训练一个不同的子网络最后隐式集成。lr1e-3是 Adam 优化器比较稳妥的起点如果训练损失震荡不降可以考虑降到3e-4。训练完成后记得做一步额外处理把预测值裁剪到 1 到 8 的范围。神经网络输出是一个无约束的浮点数可能出现 0.8 或 9.2 这类越界值直接舍入会造成系统性偏差。pred_nn model_nn(Xval).numpy().clip(1, 8) print(MLP RMSE:, mean_squared_error(val[Response], pred_nn, squaredFalse))4.3 两种模型在保险场景的差异跑完 SVR 和 MLP 之后对比结果时要关注的不只是 RMSE。SVR 在保诚这种有限样本上往往表现稳定预测值很少出现极端偏离MLP 如果调参得当RMSE 可能持平或略好但对随机种子和特征标准化方式更敏感。实际项目中我通常两个都保留SVR 作为规则解释型通道MLP 的结果作为风险排序的补充信号。5. 保险风险预测模型的交叉验证与业务排序评估5.1 用 ±1 命中率和排序指标代替单一分数做保险风险预测时业务方关心的往往不是精确预测到等级 5而是预测到 4 和 6 这样的邻近等级。评级偏差一级在核保流程里通常可以接受偏差两级以上才需要人工复核。所以评估必须同时看 MAE、精确命中率和 ±1 误差内命中率。import numpy as np def evaluate_regression(y_true, y_pred): diff np.abs(y_true - y_pred) result { RMSE: np.sqrt(np.mean(diff ** 2)), MAE: np.mean(diff), 精确命中率: np.mean(diff 0), ±1 命中率: np.mean(diff 1), } return result print(SVM:, evaluate_regression(val[Response], pred_svm)) print(MLP:, evaluate_regression(val[Response], pred_nn))RMSE 对大偏差敏感MAE 反映平均偏离程度±1 命中率最贴近业务。三个指标一起看才能判断一个模型是“整体偏稳”还是“偶尔出大错”。如果 RMSE 相近但 ±1 命中率相差 5 个百分点优先选命中率高的。5.2 交叉验证里的坑缺失填充和类别编码必须在每一折内完成用cross_val_score或手写 K-Fold 时最容易翻车的操作是把预处理放在交叉验证外面。中位数填充、标准化、OrdinalEncoder都必须在每一折的训练部分上计算再应用到验证部分。把预处理放在外面每一折的验证数据都受到了训练数据影响评估分数天然虚高。from sklearn.model_selection import KFold from sklearn.pipeline import make_pipeline from sklearn.preprocessing import MinMaxScaler from sklearn.svm import SVR kf KFold(n_splits5, shuffleTrue, random_state42) cv_scores [] for fold, (idx_trn, idx_val) in enumerate(kf.split(train)): fold_train train.iloc[idx_trn] fold_val train.iloc[idx_val] fold_medians fold_train[num_cols].median() fold_train[num_cols] fold_train[num_cols].fillna(fold_medians) fold_val[num_cols] fold_val[num_cols].fillna(fold_medians) pipe make_pipeline(MinMaxScaler(), SVR(C50, epsilon0.5, gammascale)) pipe.fit(fold_train[feat_cols], fold_train[Response]) pred pipe.predict(fold_val[feat_cols]) cv_scores.append(mean_squared_error(fold_val[Response], pred, squaredFalse)) print(5-Fold RMSE:, np.mean(cv_scores), /-, np.std(cv_scores))上面这段代码把中位数填充和标准化都放进了每一折内部。shuffleTrue配合random_state42保证每次运行划分方式一致。最终的交叉验证均值比单次划分更可信标准差则反映模型在不同数据子集上的稳定性。5.3 混淆矩阵定位系统的偏差方向除了数值型指标还要看混淆矩阵的分布形态。保险风险等级是有序的模型预测偏向相邻等级还算正常但如果出现大面积跨级预测就需要回头查特征工程。import pandas as pd from sklearn.metrics import confusion_matrix def show_confusion(y_true, y_pred, title): y_round np.clip(np.round(y_pred), 1, 8).astype(int) conf confusion_matrix(y_true, y_round, labelsrange(1, 9)) conf_df pd.DataFrame(conf, indexrange(1, 9), columnsrange(1, 9)) print(f--- {title} ---) print(conf_df) show_confusion(val[Response], pred_svm, SVM 混淆矩阵)注意confusion_matrix的labels参数必须显式指定为 1 到 8否则矩阵可能缺失某些边界等级导致行列对不上。观察混淆矩阵时重点看对角线两侧的带宽。如果等级 1 的样本大量被预测为 3 或 4说明对低风险客户的区分度不够如果等级 8 经常被预测为 5说明极高风险段的召回率不足。这两种情况对核保业务的影响完全不同前者会造成不必要的体检成本后者则直接带来承保风险。6. 让“完整代码与数据”真正落地的三个细节拿到一份带代码带数据的机器学习项目真正决定能不能复现的往往不是模型本身而是工程细节。保险风险预测尤其如此三个细节最容易让代码跑通但结果对不上。第一个细节是数据处理和建模脚本的分离。把预处理算子保存下来推理阶段重新加载import joblib joblib.dump(oe, models/ordinal_encoder.joblib) joblib.dump(pipeline_svm, models/svm_pipeline.joblib)推理时用joblib.load加载同一个 encoder 和 pipeline避免新数据进来时重新计算中位数或重新编码类别。这条路径一旦断裂线上推理结果和离线评估就会产生系统性偏差。第二个细节是类别不平衡的定向处理。保诚数据中低风险等级占多数模型天然偏向预测中等风险。对风险等级 8 这种极少数样本常规做法是单独训练一个二分类器判断“是否为高风险”再与回归模型的结果交叉验证。二分类器可以用 LightGBM 加scale_pos_weight或对少数类做 SMOTE 过采样。回归模型负责排序分类器负责卡边界。第三个细节是预测值的校准。MLP 和 SVM 输出的预测值不一定与真实风险等级有相同的统计分布。比如模型整体低估了高风险段的取值直接按预测值分档会导致核保阈值偏移。用分桶对比真实均值和预测均值可以快速验证是否需要校准val[pred_round] np.clip(np.round(pred_nn), 1, 8).astype(int) bucket_stats val.groupby(pred_round)[Response].agg([mean, count]) print(bucket_stats)如果某个预测等级桶的真实均值明显高于预测值说明该段存在系统性低估可以用桶均值替换预测值做后处理校准。把校准后的等级边界作为核保分箱依据比直接使用原始预测值更贴合业务口径。本文还有配套的精品资源点击获取
返回列表