简介:这份资源是2024年美国大学生数学建模竞赛ICM Problem E的完整参赛作品,围绕财产保险可持续性这一议题,用Python构建了从数据预处理到建模预测的全流程方案。内容适合数学建模初学者、进阶学习者以及需要完成课程设计、大作业或毕业设计的学生参考,也可作为保险精算与风险评估方向的入门实践案例。压缩包共43个文件,约18.43MB,包含7个Python脚本、3个Excel数据表、16张PNG与7张SVG图表、2份PDF文档及LaTeX源码等,覆盖灰色预测、层次分析、模糊综合评价、灰色关联与支持向量机等多类模型实现。资源中附有ROC曲线、PCA降维对比、混淆矩阵、灵敏度分析等可视化结果,以及可复用的svm_model.pkl模型文件,便于读者理解建模思路、复现实验流程并迁移到同类赛题。目前已有111人学习下载,适合希望系统掌握美赛建模方法与Python实现技巧的读者。
1. 财产保险可持续性建模:从一道美赛题到可复现的 Python 方案
2024年美国大学生数学建模竞赛的这道题,核心是给财产保险的可持续性建一套能算、能调、能解释的模型。很多人第一反应是去搜优秀论文,但真正动手时卡在三个地方:极端天气导致的巨灾赔付怎么量化、保险定价里的风险因子怎么选、Python 里用什么库把这三者串起来。这篇笔记不聊论文怎么写,只讲一个一线工程师会怎么把这道题拆成可运行的代码:从数据构造、风险建模到可持续性指标计算,每一步都给参数和踩坑记录。适合正在准备数学建模竞赛的学生,也适合想用 Python 做保险精算原型的从业者。读完你能拿到一套能跑通的骨架,改改参数就能套到类似题目上。
2. 财产保险可持续性的建模骨架:三个模块与数据从哪来
2.1 可持续性到底在算什么:赔付率、费率与资本充足
财产保险的可持续性,落到公式上就是三个量的平衡:赔付率(Loss Ratio)、费率充足度(Rate Adequacy)和资本缓冲(Capital Buffer)。赔付率是赔款除以保费,费率充足度是实际费率与目标费率的比值,资本缓冲是可用资本除以风险资本。这三个指标任何一个长期失衡,保险产品就不可持续。美赛题里通常会给一个地区多年的气象数据和保单数据,要求你评估在气候变化下这套保险还能不能做下去。
我一般会把问题拆成三层:第一层是风险层,用历史灾害数据拟合极端事件的频率和强度分布;第二层是定价层,把风险层的输出映射成纯保费,再加上费用和利润附加;第三层是可持续层,用蒙特卡洛模拟未来多年的赔付和资本变化,看破产概率。这三层在 Python 里分别对应 scipy.stats 做分布拟合、numpy 做定价计算、numpy 加循环做模拟。不要一上来就写模拟,先把前两层的数据结构定清楚,不然后面改参数会非常痛苦。
提示:美赛题给的数据往往不完整,气象数据可能只有站点日值,保单数据可能只有年度汇总。常见做法是用公开的 NOAA 或 ERA5 再分析数据补气象,用题里给的保单结构反推暴露度。
2.2 数据构造:用 Python 生成可复现的模拟数据集
真实竞赛数据往往有缺失,我习惯先构造一份可控的模拟数据来验证模型逻辑,再替换成真实数据。下面这段代码生成一个地区 20 年的财产暴露度、灾害事件和赔付记录。关键参数是暴露度增长率、灾害频率和赔付分布的形状参数。
import numpy as np import pandas as pd np.random.seed(42) # 固定随机种子,保证结果可复现 years = np.arange(2004, 2024) n_years = len(years) # 暴露度:初始 1000 亿,每年增长 3% exposure = 1000 * (1.03 ** np.arange(n_years)) # 灾害频率:泊松分布,均值从 2 次/年线性增到 4 次/年 freq_lambda = np.linspace(2, 4, n_years) n_events = np.random.poisson(freq_lambda) # 每次事件的赔付:对数正态分布,均值随暴露度缩放 records = [] for i, y in enumerate(years): for e in range(n_events[i]): # 对数正态参数:mu 随年份略增,sigma 固定 mu = np.log(exposure[i] * 0.01) + 0.1 * i sigma = 0.8 loss = np.random.lognormal(mu, sigma) records.append({'year': y, 'event_id': e, 'loss': loss}) df = pd.DataFrame(records) print(df.groupby('year')['loss'].sum().head())这段代码的逻辑是:暴露度按复利增长,灾害次数用泊松分布模拟,单次赔付用对数正态分布。参数0.01是基准损失率,0.1 * i让损失随年份缓慢上升,模拟气候变化影响。sigma=0.8控制赔付的尾部厚度,值越大极端事件越多。跑完你会得到一个按年汇总的赔付序列,后面定价和模拟都用它。
注意:随机种子必须固定,否则每次跑出来的可持续性指标都不一样,论文里没法复现。我见过有人忘了设种子,结果答辩时被问为什么两次结果差 20%。
2.3 风险层建模:拟合频率与强度的分布
拿到赔付记录后,第一步是拟合频率分布和强度分布。频率用泊松或负二项,强度用对数正态或帕累托。Python 里用 scipy.stats 的 fit 方法可以快速得到参数,但要注意拟合前先做描述性统计,看看有没有明显的离群值。
from scipy import stats # 频率拟合:每年事件数 annual_counts = df.groupby('year').size().values mu_freq = annual_counts.mean() print(f"泊松均值: {mu_freq:.2f}") # 强度拟合:单次赔付的对数正态参数 losses = df['loss'].values shape, loc, scale = stats.lognorm.fit(losses, floc=0) print(f"对数正态 shape={shape:.3f}, scale={scale:.3f}") # 拟合优度检验:KS 检验 ks_stat, p_value = stats.kstest(losses, 'lognorm', args=(shape, 0, scale)) print(f"KS 统计量={ks_stat:.4f}, p值={p_value:.4f}")floc=0强制位置参数为零,因为赔付不可能是负数。shape就是对数正态的 sigma,scale是 exp(mu)。KS 检验的 p 值大于 0.05 说明拟合可以接受。如果 p 值很小,说明尾部拟合不好,常见做法是换帕累托分布或者用极值理论里的广义帕累托分布拟合超阈值部分。这一步的参数直接决定后面纯保费的大小,所以不要跳过拟合优度检验。
3. 定价与可持续性模拟:把参数变成可调的函数
3.1 纯保费计算:从期望赔付到风险附加
纯保费等于期望赔付加上风险附加。期望赔付用频率乘以强度期望,风险附加常用方差原理或标准差原理。下面这个函数把前面的拟合参数封装起来,输入暴露度和年份,输出纯保费。
def pure_premium(exposure, mu_freq, shape, scale, loading=0.3): """ exposure: 暴露度(保额) mu_freq: 年灾害频率均值 shape, scale: 对数正态参数 loading: 风险附加系数,默认 0.3 """ expected_loss = mu_freq * scale * np.exp(shape**2 / 2) # 方差原理的风险附加 var_loss = mu_freq * (scale**2) * (np.exp(shape**2) - 1) * np.exp(shape**2) risk_load = loading * np.sqrt(var_loss) return expected_loss + risk_load # 计算 2024 年的纯保费 premium_2024 = pure_premium(exposure[-1], mu_freq, shape, scale) print(f"2024 年纯保费: {premium_2024:.2f}")loading=0.3是风险附加系数,实际定价里这个值由资本成本和目标偿付能力决定。expected_loss是对数正态的期望公式,var_loss是方差公式。这两个公式在精算教材里都有,但很多人写代码时会把方差公式里的 exp 项漏掉,导致风险附加偏小。跑完这个函数,你得到的是一个基准纯保费,后面模拟里可以按年份调整暴露度和频率参数。
3.2 蒙特卡洛模拟:未来 30 年破产概率怎么算
可持续性的核心指标是破产概率,即资本金不足以覆盖赔付的概率。用蒙特卡洛模拟未来 30 年,每年抽样灾害次数和赔付额,扣掉保费收入,看资本金什么时候跌破零。
def simulate_ruin(initial_capital, premium, mu_freq, shape, scale, n_years=30, n_sims=10000, expense_ratio=0.25): """ initial_capital: 初始资本金 premium: 年保费收入 expense_ratio: 费用率,从保费里扣除 """ ruin_count = 0 for _ in range(n_sims): capital = initial_capital for y in range(n_years): n_events = np.random.poisson(mu_freq) losses = np.random.lognormal( np.log(scale), shape, n_events ).sum() net_premium = premium * (1 - expense_ratio) capital += net_premium - losses if capital < 0: ruin_count += 1 break return ruin_count / n_sims # 假设初始资本 500 亿,年保费 80 亿 ruin_prob = simulate_ruin(500, 80, mu_freq, shape, scale) print(f"30 年破产概率: {ruin_prob:.4f}")n_sims=10000是模拟次数,次数越多结果越稳定,但计算时间线性增长。expense_ratio=0.25是费用率,包括运营成本和佣金。内层循环每年抽样一次灾害次数和赔付总额,更新资本金。如果资本金跌破零就记一次破产并跳出。这个函数跑 10000 次大概需要几十秒,如果嫌慢可以把 n_sims 降到 5000,但破产概率的置信区间会变宽。
提示:破产概率对初始资本和保费非常敏感。我一般会先跑一个基准场景,然后做敏感性分析,看破产概率随保费变化的曲线,找到使破产概率低于 1% 的最低保费。
3.3 可持续性指标:把破产概率翻译成业务语言
破产概率是统计语言,业务方更关心的是「保费要涨多少才能可持续」。我一般会算两个衍生指标:一是可持续保费,即让破产概率等于目标值(比如 1%)的保费;二是保费缺口,即当前保费与可持续保费的差值。
from scipy.optimize import brentq def target_ruin(premium, target=0.01): return simulate_ruin(500, premium, mu_freq, shape, scale) - target # 二分法找可持续保费 sustainable_premium = brentq(target_ruin, 50, 200) print(f"可持续保费: {sustainable_premium:.2f}") print(f"保费缺口: {sustainable_premium - 80:.2f}")brentq是 scipy 里的二分法求根函数,在 50 到 200 之间找使破产概率等于 1% 的保费。这个函数每次调用都会跑一遍模拟,所以总计算量是模拟次数乘以迭代次数。如果嫌慢,可以先用粗网格搜索缩小范围,再用二分法精修。跑完你会得到一个具体的保费数字,这个数字就是论文里最有说服力的结论之一。
4. 避坑与排查:建模时最容易翻车的五个地方
4.1 分布拟合不检验,尾部风险被低估
现象:模拟出来的破产概率很低,但实际历史数据里出现过极端赔付。原因:直接用对数正态拟合全部数据,尾部被平均掉了。解决:用极值理论拟合超阈值部分,或者用帕累托分布替换对数正态。我一般会画 QQ 图,看尾部偏离程度。
4.2 暴露度增长和灾害频率增长混在一起
现象:纯保费逐年上升,但分不清是暴露度涨了还是灾害变多了。原因:两个参数都随时间变化,没有做分离。解决:固定一个参数做敏感性分析,或者用回归模型分别估计趋势。常见做法是把暴露度标准化,只看单位暴露度的损失率变化。
4.3 蒙特卡洛模拟次数太少,结果不稳定
现象:每次跑出来的破产概率差好几个百分点。原因:n_sims 太小,抽样误差大。解决:至少跑 10000 次,或者用方差缩减技术。我习惯先跑 1000 次看大概范围,再跑 10000 次出最终结果。
4.4 忘了扣费用和再保险
现象:保费收入全用来赔付,资本金永远不降。原因:没有扣费用率和再保险分出。解决:在模拟里加 expense_ratio 和再保险层。再保险可以用一个简单的分层函数模拟,比如赔付超过自留额的部分分给再保人。
4.5 随机种子没固定,论文无法复现
现象:答辩时评委让你现场跑一遍,结果和论文里不一样。原因:没有设 np.random.seed。解决:在代码开头固定种子,并在论文里注明。如果用了多个随机过程,每个都要设独立的种子。
5. 进阶技巧:用敏感性分析找到可持续性的临界点
敏感性分析是这道题的加分项。我一般会做三组:保费对破产概率的敏感性、初始资本对破产概率的敏感性、灾害频率对可持续保费的影响。下面这段代码用循环生成敏感性曲线,比手动改参数快得多。
import matplotlib.pyplot as plt premiums = np.linspace(60, 120, 13) ruin_probs = [simulate_ruin(500, p, mu_freq, shape, scale, n_sims=2000) for p in premiums] plt.plot(premiums, ruin_probs, marker='o') plt.axhline(0.01, color='r', linestyle='--', label='目标 1%') plt.xlabel('年保费(亿)') plt.ylabel('30 年破产概率') plt.legend() plt.savefig('sensitivity.png', dpi=150)n_sims=2000是为了加快敏感性分析的速度,最终结论再用 10000 次跑。axhline画出 1% 的目标线,曲线与目标线的交点就是可持续保费。这张图放在论文里比一张表格直观得多。我还会把灾害频率的均值从 2 调到 6,看可持续保费怎么变,通常频率每增加 1 次,保费要涨 15% 到 25%。
最后一个习惯:每次改完参数,先跑一遍基准场景确认没报错,再跑敏感性分析。我踩过的最大坑是改了一个参数忘了同步改另一个,结果整晚的模拟都白跑了。希望帮到你。
本文还有配套的精品资源,点击获取