资讯动态

健康险理赔金额预测:从线性回归基线到业务可用的实战闭环

发布时间:2026/10/14 12:00:44 来源:尧图企业网站定制
简介本资源是一套面向数据科学初学者与保险科技从业者的健康保险理赔支出预测实战项目基于Kaggle真实医疗费用数据集聚焦于用机器学习建模预测个人医疗报销金额。项目完整覆盖数据探索、特征可视化分析含性别、吸烟状态、BMI、地区等多维度与费用的关联图谱、多种回归模型对比线性回归、SVR、Lasso、Ridge等及模型持久化部署适合用于课程设计、竞赛备赛或业务场景复现。压缩包共30个文件含7个核心Python脚本训练/测试/可视化/模型比较、11个已训练模型pkl文件、10张特征分析png图表及1个原始CSV数据集整体仅1.25MB轻量易上手。目前已有781人学习下载提供开箱即用的端到端流程从数据加载、分布诊断、相关性热力图与swarmplot可视化到多模型拟合与性能评估所有代码模块解耦清晰、注释完备便于理解特征工程逻辑与回归建模实践细节。1. 为什么健康保险理赔金额预测不是“拟合一条直线”那么简单从线性回归起步却必须直面医疗费用的长尾分布、诊断编码的稀疏性与时间窗口的业务约束你手上有某保险公司脱敏后的健康险理赔数据集包含被保人年龄、性别、职业类别、既往病史标签、就诊医院等级、ICD-10诊断编码最多3个、用药数量、住院天数、手术标志以及最终的理赔支出金额单位元。你用sklearn.linear_model.LinearRegression跑完训练R²达到0.82——看起来很美。但上线后发现对单次理赔超5万元的案例平均绝对误差高达2.3万元而占总量12%的“零理赔”样本在模型输出里全被预测为“287元”。这不是模型不准是问题没被正确定义。本篇不讲“如何调参”而是带你用Python从零复现一个能落地到精算初筛、核赔辅助、准备金预提场景的真实理赔支出预测流程它始于线性回归这个可解释基线但必须主动处理医疗费用的右偏分布、诊断编码的高维稀疏、特征间隐含的时间依赖如慢性病连续两年出险并把可视化分析直接嵌入特征工程决策闭环。适合刚接触保险科技的数据工程师、想补足业务语义的算法新人以及需要向业务方说清“为什么预测值是这个数”的精算支持岗。全文所有代码、数据结构、参数阈值均来自某实验室模拟项目X的实操沉淀非教学玩具。2. 构建可复现的理赔预测最小闭环从原始CSV加载到线性回归基线模型输出2.1 数据加载与业务驱动的字段清洗为什么“住院天数0”不等于“未住院”真实理赔数据中“住院天数”字段常出现0、负数或极大异常值如9999。但业务规则明确门诊理赔住院天数必为0而“住院天数0”也可能对应急诊留观实际产生床位费。若直接删除或填充均值会扭曲门诊/住院费用结构。我们采用三级清洗策略import pandas as pd import numpy as np # 假设原始数据已加载为df_raw df df_raw.copy() # 步骤1识别逻辑矛盾门诊但有手术标志 outpatient_with_surgery (df[visit_type] outpatient) (df[surgery_flag] 1) df.loc[outpatient_with_surgery, surgery_flag] 0 # 业务确认门诊手术在该系统中归类为特殊门诊surgery_flag应为0 # 步骤2住院天数业务校验基于医保结算规则 # 规则三甲医院住院天数30天需人工复核此处设为异常阈值 df.loc[df[hospital_level] tertiary , days_hospital_max] 30 df.loc[df[hospital_level] secondary, days_hospital_max] 21 df.loc[df[hospital_level] primary , days_hospital_max] 14 # 将超阈值住院天数标记为NaN后续作为缺失特征参与建模比粗暴删除保留更多信息 mask_abnormal_days df[days_hospital] df[days_hospital_max] df.loc[mask_abnormal_days, days_hospital] np.nan # 步骤3费用字段强校验理赔金额必须≥0且不能为浮点精度错误值如0.001 df df[(df[claim_amount] 0) (df[claim_amount] ! 0.001)]提示这里days_hospital_max不是固定常量而是根据hospital_level动态计算的业务阈值。这比全局设30天更符合医疗支付实际——基层医院收治重症患者能力有限住院周期天然更短。后续特征工程中这个“是否超限”会衍生为二值特征is_days_exceed_limit成为模型判断风险的重要依据。2.2 诊断编码ICD-10的稀疏特征工程从3个文本码到128维稠密向量原始数据中diag_code_1,diag_code_2,diag_code_3是字符串型ICD-10编码如E11.9、I25.10。直接One-Hot会产生上万维稀疏矩阵且无法表达编码语义相似性如E11.9糖尿病与E10.91型糖尿病应比E11.9与S06.0脑震荡更接近。我们采用分层嵌入策略from sklearn.feature_extraction.text import TfidfVectorizer import re # 步骤1统一预处理移除小数点只保留字母数字主干因ICD-10小数点后为细分此处先降维 def clean_icd(code): if pd.isna(code): return return re.sub(r\., , str(code).split()[0]) # 取首段移除小数点 df[diag_clean_1] df[diag_code_1].apply(clean_icd) df[diag_clean_2] df[diag_code_2].apply(clean_icd) df[diag_clean_3] df[diag_code_3].apply(clean_icd) # 步骤2构建诊断组合文本顺序敏感主诊断在前 df[diag_combo] df[diag_clean_1] df[diag_clean_2] df[diag_clean_3] # 步骤3TF-IDF向量化ngram_range(1,2)捕获单码和双码组合 vectorizer TfidfVectorizer( max_features128, # 严格限制维度避免过拟合 ngram_range(1, 2), # 捕获单诊断码E11和组合码E11 I25 stop_wordsNone, # ICD码无停用词 norml2 # L2归一化使向量长度可比 ) diag_tfidf vectorizer.fit_transform(df[diag_combo]).toarray() # shape: (n_samples, 128) # 步骤4将TF-IDF结果转为DataFrame列名带前缀便于追溯 diag_df pd.DataFrame( diag_tfidf, columns[fdiag_tfidf_{i} for i in range(128)], indexdf.index )参数说明max_features128是经某实验室模拟项目X验证的平衡点——低于64维损失语义信息高于256维在小样本10万条下显著增加噪声。ngram_range(1,2)关键在于捕捉“糖尿病并发冠心病”这类高频共病模式其TF-IDF权重会自然高于孤立诊断。此步骤输出的diag_df将与后续数值特征拼接构成模型输入X。2.3 线性回归基线模型不是终点而是诊断业务问题的起点我们不追求最高R²而要一个系数可解读、残差可归因的基线。使用sklearn标准流程但关键在目标变量处理与评估指标选择from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error import matplotlib.pyplot as plt # 特征拼接示例仅展示核心特征实际项目中需加入更多 X pd.concat([ df[[age, gender_encoded, days_hospital, med_count]], diag_df, pd.get_dummies(df[hospital_level], prefixhosp) ], axis1) y df[claim_amount] # 关键对y进行对数变换缓解长尾分布影响使线性假设更合理 y_log np.log1p(y) # log1p log(1y)避免y0时log(0)报错 # 划分数据集注意时间序列数据应按时间切分此处为简化用随机切分但实际项目必须用time-based split X_train, X_test, y_train_log, y_test_log train_test_split( X, y_log, test_size0.2, random_state42 ) # 训练线性回归 lr LinearRegression() lr.fit(X_train, y_train_log) # 预测注意预测后需expm1反变换回原尺度 y_pred_log lr.predict(X_test) y_pred np.expm1(y_pred_log) y_test np.expm1(y_test_log) # 评估优先看MAE业务更关心绝对误差而非R² mae mean_absolute_error(y_test, y_pred) rmse np.sqrt(mean_squared_error(y_test, y_pred)) print(f基线模型 MAE: ¥{mae:.0f} | RMSE: ¥{rmse:.0f}) # 绘制预测vs真实散点图重点观察高金额区域 plt.figure(figsize(8,6)) plt.scatter(y_test, y_pred, alpha0.3, s1) plt.plot([0, y_test.max()], [0, y_test.max()], r--, lw2) plt.xlabel(真实理赔金额元) plt.ylabel(预测理赔金额元) plt.title(线性回归基线预测vs真实对数变换后) plt.show()逻辑说明np.log1p(y)和np.expm1()是处理右偏分布的标准手法它让模型更关注相对误差而非绝对误差。散点图中若高金额区域5万点明显偏离对角线说明线性假设在此区间失效——这正是我们下一步要解决的业务问题而非技术缺陷。3. 特征可视化分析不是画图交差而是用统计图表驱动特征工程决策3.1 赔付金额分布与分箱策略为什么必须放弃“均值填充”改用“分位数分箱”首先绘制理赔金额直方图与累积分布函数CDFimport seaborn as sns plt.figure(figsize(12,4)) # 子图1金额直方图对数x轴 plt.subplot(1,2,1) sns.histplot(df[claim_amount], bins100, kdeFalse) plt.xscale(log) plt.xlabel(理赔金额对数尺度) plt.title(理赔金额分布典型长尾) # 子图2CDF图 plt.subplot(1,2,2) sorted_amounts np.sort(df[claim_amount]) cdf np.arange(1, len(sorted_amounts)1) / len(sorted_amounts) plt.plot(sorted_amounts, cdf, marker., linestylenone) plt.xscale(log) plt.xlabel(理赔金额对数尺度) plt.ylabel(累积概率) plt.title(累积分布函数CDF) plt.axhline(y0.9, colorr, linestyle--, label90%分位数) plt.axhline(y0.95, colorg, linestyle--, label95%分位数) plt.legend() plt.tight_layout() plt.show()关键洞察CDF图显示90%的理赔金额≤8,200元但95%分位数跃升至21,500元而最大值达1,200,000元。这意味着若用均值¥15,800填充住院天数缺失值会严重高估低额理赔群体更合理的做法是对days_hospital缺失样本按其claim_amount所处分位数区间取该区间内days_hospital的中位数填充。这就是业务驱动的分箱填充——代码实现见3.2节。3.2 诊断编码与金额的关联热力图发现隐藏的“高赔付组合”单纯看单个诊断码的平均赔付无意义如S06.0脑震荡平均¥12,000但样本仅3例。我们用条件频率热力图揭示组合规律# 提取主诊断diag_code_1和次诊断diag_code_2过滤掉空值 df_diag df.dropna(subset[diag_code_1, diag_code_2]).copy() df_diag[diag1_clean] df_diag[diag_code_1].apply(clean_icd) df_diag[diag2_clean] df_diag[diag_code_2].apply(clean_icd) # 计算每组诊断组合的样本量与平均赔付 pivot_table df_diag.groupby([diag1_clean, diag2_clean])[claim_amount].agg([count, mean]).reset_index() # 筛选高频组合count≥50并重塑为热力图矩阵 pivot_heatmap pivot_table[pivot_table[count] 50].pivot( indexdiag1_clean, columnsdiag2_clean, valuesmean ).fillna(0) # 绘制热力图仅显示top20主诊断和top20次诊断 plt.figure(figsize(12,10)) sns.heatmap( pivot_heatmap.iloc[:20, :20], annotTrue, fmt.0f, cmapYlOrRd, cbar_kws{label: 平均理赔金额元} ) plt.title(高频诊断组合平均赔付热力图Top20×Top20) plt.xlabel(次诊断编码) plt.ylabel(主诊断编码) plt.show()业务价值图中若发现E11(糖尿病)与I70(动脉硬化)组合的平均赔付¥42,800显著高于单独E11(¥8,500)则应在特征工程中显式构造交互特征is_diab_athero_combo (diag1_cleanE11) (diag2_cleanI70)。这种由可视化直接催生的特征比盲目添加多项式特征更可靠。3.3 年龄-赔付关系的分段拟合拒绝“线性假设”拥抱业务常识年龄与理赔金额的关系绝非单调递增。老年人65岁慢性病多但单次费用未必最高中年人45-60岁可能因重疾产生高额赔付。我们用分段线性回归验证from sklearn.pipeline import Pipeline from sklearn.preprocessing import PolynomialFeatures # 按年龄分三段青年45、中年45-60、老年60 df[age_group] pd.cut(df[age], bins[0, 45, 60, 100], labels[young, middle, elder]) # 对每段拟合独立线性模型 fig, axes plt.subplots(1, 3, figsize(15,4)) for i, (group, data) in enumerate(df.groupby(age_group)): # 移除金额为0的样本零理赔 data_nonzero data[data[claim_amount] 0] if len(data_nonzero) 50: continue # 用PolynomialFeatures生成age的一次项即线性 poly PolynomialFeatures(degree1, include_biasFalse) X_age poly.fit_transform(data_nonzero[[age]]) y_amt data_nonzero[claim_amount] lr_seg LinearRegression() lr_seg.fit(X_age, y_amt) # 预测平滑曲线 age_grid np.linspace(data_nonzero[age].min(), data_nonzero[age].max(), 100).reshape(-1,1) X_grid poly.transform(age_grid) y_pred_grid lr_seg.predict(X_grid) axes[i].scatter(data_nonzero[age], data_nonzero[claim_amount], alpha0.3, s2) axes[i].plot(age_grid, y_pred_grid, r-, lw2) axes[i].set_title(f{group}组斜率{lr_seg.coef_[0]:.1f}) axes[i].set_xlabel(年龄) axes[i].set_ylabel(理赔金额) plt.tight_layout() plt.show()参数说明PolynomialFeatures(degree1)看似多余实则是为后续扩展预留——若某段显示明显非线性可将degree改为2。当前结果若显示“中年组”斜率显著为正如320元/岁而“老年组”斜率为负如-85元/岁则应在特征中加入age_middle和age_elder两个分段变量而非单一age。4. 线性回归的三大避坑指南那些让精算师当场皱眉的“正确操作”4.1 现象模型在训练集R²0.85测试集R²骤降至0.42原因未处理多重共线性。days_hospital与med_count用药数量高度相关Pearson r0.78且二者均与claim_amount正相关。线性回归系数会剧烈震荡导致泛化失败。解决计算VIF方差膨胀因子剔除VIF5的特征。代码如下from statsmodels.stats.outliers_influence import variance_inflation_factor def calculate_vif(X): vif_data pd.DataFrame() vif_data[feature] X.columns vif_data[VIF] [variance_inflation_factor(X.values, i) for i in range(len(X.columns))] return vif_data vif_df calculate_vif(X_train.select_dtypes(include[np.number])) print(vif_df.sort_values(VIF, ascendingFalse).head(10)) # 若med_count的VIF8.2则删除该列改用med_category低/中/高用药量分箱特征4.2 现象预测结果中出现大量负数如-1200元原因对数变换后未做反变换截断。np.expm1(y_pred_log)当y_pred_log为较大负数时expm1结果趋近于-1再乘以金额单位即得负值。解决预测后强制截断为0并记录截断比例y_pred np.expm1(y_pred_log) y_pred_clipped np.clip(y_pred, 0, None) # 所有负值设为0 clip_ratio np.mean(y_pred 0) print(f负值截断比例{clip_ratio:.2%}属正常因对数变换在极低金额区不稳定)4.3 现象某医院等级如tertiary的系数为-15000业务方质疑“三甲医院理赔反而更少”原因未设置参考类别reference category。pd.get_dummies默认将首列作为基准若hospital_level排序为[primary, secondary, tertiary]则primary为基准tertiary系数-15000表示相比基层医院三甲医院在其他特征相同时预测金额低¥15,000——这显然违背常识。解决显式指定参考类别并用drop_firstFalse保留全部哑变量再手动减去基准# 正确做法以tertiary为参考 hosp_dummies pd.get_dummies(df[hospital_level], prefixhosp, drop_firstFalse) # 手动移除tertiary列其余列系数即相对于tertiary的差值 hosp_dummies hosp_dummies.drop(hosp_tertiary, axis1) # 此时hosp_secondary系数为正表示二级医院比三级医院平均多赔X元4.4 现象特征重要性排序中diag_tfidf_0权重最高但业务方完全看不懂这个编号代表什么原因TF-IDF向量缺乏可解释性。diag_tfidf_0可能是某个稀有编码的权重对整体预测贡献微弱但因标准化而数值大。解决改用SHAP值分析特征贡献并映射回原始诊断import shap explainer shap.LinearExplainer(lr, X_train) shap_values explainer.shap_values(X_test.iloc[:100]) # 计算前100样本 # 获取TF-IDF特征对应的原始诊断需保存vectorizer.vocabulary_ vocab_inv {v:k for k,v in vectorizer.vocabulary_.items()} top_tfidf_idx np.argsort(np.abs(shap_values).mean(0))[-5:][::-1] # top5 TF-IDF特征索引 for idx in top_tfidf_idx: if idx 128: orig_diag vocab_inv.get(idx, funknown_{idx}) print(fTF-IDF特征{idx} → 原始诊断: {orig_diag})5. 从线性回归到业务可用三个必须落地的进阶技巧5.1 用残差分析定位模型失效场景不是修模型是修数据定义线性回归的残差真实值-预测值是业务问题的晴雨表。我们按残差大小分组统计各组的业务特征分布# 计算残差原尺度 residuals y_test - y_pred # 定义高残差组|residual| 2*MAE即绝对误差超平均两倍 mae_threshold 2 * mae high_residual_mask np.abs(residuals) mae_threshold # 统计高残差组 vs 全体的诊断分布差异 high_res_df df.loc[X_test.index[high_residual_mask]] all_df df.loc[X_test.index] # 计算各诊断码在高残差组的占比提升率 diag_freq_high high_res_df[diag_code_1].value_counts(normalizeTrue) diag_freq_all all_df[diag_code_1].value_counts(normalizeTrue) enrichment (diag_freq_high / diag_freq_all).sort_values(ascendingFalse) print(高残差组显著富集的诊断码提升率3倍) print(enrichment[enrichment 3].head(10))实战价值若输出显示C91(白血病)的富集率8.2说明模型对血液肿瘤理赔严重低估。此时不应立刻换模型而应检查C91样本是否集中在某家医院→ 检查该医院编码是否被误标为primaryC91是否常伴多个次诊断→ 当前TF-IDF只用前3个诊断而C91平均有4.2个诊断是否遗漏了靶向药费用字段→ 业务确认该数据源未包含特药清单。这种残差驱动的数据诊断比调参更能提升业务效果。5.2 构建“可信度分数”让业务方知道何时该信模型何时该人工复核单纯给一个预测值不够需附带不确定性估计。我们用Bootstrap采样线性回归置信区间生成每个预测的可信度from sklearn.utils import resample def bootstrap_prediction_interval(X_sample, y_sample, X_pred, n_bootstrap100, alpha0.1): 对单个样本X_pred通过Bootstrap估计预测值的90%置信区间 preds [] for _ in range(n_bootstrap): X_boot, y_boot resample(X_sample, y_sample, random_state_) lr_boot LinearRegression() lr_boot.fit(X_boot, np.log1p(y_boot)) pred_log lr_boot.predict(X_pred.reshape(1,-1)) preds.append(np.expm1(pred_log)[0]) lower np.percentile(preds, (alpha/2)*100) upper np.percentile(preds, (1-alpha/2)*100) width upper - lower # 可信度分数区间越窄越可信归一化到0-1 confidence_score 1 / (1 width / np.mean(preds)) if np.mean(preds) 0 else 0 return lower, upper, confidence_score # 示例对测试集前5个样本计算 for i in range(5): x_single X_test.iloc[i:i1].values y_single y_test.iloc[i] lower, upper, conf bootstrap_prediction_interval( X_train.values, y_train, x_single[0] ) print(f样本{i1}: 真实{y_single:.0f}, 预测{y_pred[i]:.0f}, f90%CI[{lower:.0f},{upper:.0f}], 可信度{conf:.2f})参数说明n_bootstrap100在精度与速度间平衡alpha0.1对应90%置信水平。业务规则可设定可信度0.3的预测自动触发人工复核流程。这比固定阈值如“5万必审”更智能——它承认模型在不同样本上的不确定性差异。5.3 模型版本与业务指标的绑定让每次迭代都有业务意义最后一步也是最容易被忽略的将模型性能指标与精算KPI对齐。例如准备金计提要求“95%的预测误差在±15%以内”。我们编写验证函数def validate_against_actuarial_kpi(y_true, y_pred, threshold_pct0.15, coverage_target0.95): 验证模型是否满足精算KPIcoverage_target比例的样本相对误差≤threshold_pct abs_relative_error np.abs(y_true - y_pred) / (y_true 1) # 1防除零 coverage np.mean(abs_relative_error threshold_pct) print(f精算KPI验证) print(f- 要求{coverage_target*100:.0f}%样本相对误差≤{threshold_pct*100:.0f}%) print(f- 实际{coverage*100:.1f}%样本达标) print(f- 达标缺口{max(0, coverage_target - coverage)*100:.1f}%) # 返回未达标样本索引供业务分析 failed_mask abs_relative_error threshold_pct return np.where(failed_mask)[0] # 调用验证 failed_indices validate_against_actuarial_kpi(y_test, y_pred) # 输出前10个未达标样本的业务字段供精算师人工排查 print(\n典型未达标样本真实值预测值相对误差) for idx in failed_indices[:10]: true_val y_test.iloc[idx] pred_val y_pred[idx] err abs(true_val - pred_val) / (true_val 1) print(fID{X_test.index[idx]}: ¥{true_val:.0f} → ¥{pred_val:.0f} (err{err:.1%}))我的血泪经验在某跨平台系统中我们曾因未做此项验证导致模型上线后准备金计提偏差超2.3%触发监管问询。从此我养成了铁律任何模型交付前必须用业务KPI函数跑通且输出未达标样本清单。这比所有AUC、F1都更能体现技术价值——它让算法工程师真正站在精算师的立场思考问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑