银行风控部门拿到一份贷款数据:2000笔借款,回收率分布图在1.0的位置突然冒出一根尖刺——全是全额还款的客户。标准贝塔回归(Beta Regression)在这里直接失效,因为它假设数据严格落在0到1之间,不包括端点。这篇文章用PyMC手搓一个"一膨胀贝塔"模型,把离散的全额还款和连续的部分还款塞进同一个似然函数里。
为什么标准工具会在这里翻车
先看清数据长什么样。2000笔合成贷款中,约30%全额还款(repayment=1.0),剩下70%分散在0到1之间。画成密度图:主体是一条平滑的贝塔曲线,右侧边缘却粘着一坨点质量。
传统方案两头不讨好。逻辑回归只认二元标签——还了还是没还,把"还了六成"和"还了九成"当成一回事。标准贝塔回归更尴尬,它的支撑集是开区间(0,1),碰到确切的1.0直接报错或数值爆炸。
金融风控的真实需求是同时回答两个问题:这人全额还款的概率有多大?如果没全额还,预期能收回多少比例?两个答案都得从同一批数据里抠出来。
一膨胀贝塔:把两个分布缝在一起
模型的核心想法不复杂。每一笔贷款的回收率Y服从混合分布:以概率π取确定值1,以概率(1-π)从贝塔分布中抽样。π本身又是借款人特征的函数——信用分越高、贷款价值比越低,全额还款概率越大。
用代码语言说,似然函数是分段定义的:
当y=1时:log p(y) = log(π)
当0
贝塔分布的参数α、β也不是固定的,由均值θ和精度φ推导:α=θφ,β=(1-θ)φ。θ同样回归于借款人特征,φ作为全局精度参数。
最终模型要估计三套东西:π的回归系数(5个,含截距)、θ的回归系数(5个,含截距)、精度φ(1个)。11个参数从2000个观测值里同时反推,靠的就是PyMC的自动微分变分推断(ADVI)或NUTS采样器。
手搓似然:从pm.Potential到逐行实现
PyMC没有内置"一膨胀贝塔",但给了pm.Potential这个后门——直接把对数似然塞进图模型。作者选择更硬核的路线:用pytensor.tensor手写了贝塔对数密度,再拼成完整似然。
关键代码块拆解如下。首先处理全额还款的示踪变量:
obs_full = pt.eq(repayment_data, 1.0) # 布尔张量,标记哪些观测是1.0
然后计算两个分支的对数概率。全额还款分支简单:就是logit_pi经过logistic变换后的对数。部分还款分支需要完整的贝塔对数密度,包括伽马函数和边界处理。
最后把两段缝起来:pt.switch(obs_full, log_prob_full, log_prob_partial)。这个switch操作在计算图层面完成,保证梯度能正常回传。
模型跑下来,2000个观测、11个参数,NUTS采样2000次 tune=1000,在普通笔记本上几分钟收玫。arviz的诊断图显示R-hat全部接近1,没有明显的路径依赖问题。
参数恢复:合成数据的诚实测试
用合成数据的好处是知道"正确答案"。作者设定的真实参数:π的截距0.5、信用分系数0.8、贷款价值比系数-0.6;θ的截距0.3、信用分系数0.5、贷款价值比系数-0.3;精度φ=5.0。
后验均值与真实值的偏差控制在0.1个标准差以内。信用分对全额还款概率的拉动效应(0.8)被准确捕捉,贷款价值比的负面冲击(-0.6)也没有被稀释。部分还款分支的参数同样稳健恢复。
这个诚实测试验证了两件事:模型设定没有识别问题,PyMC的采样器对这种分段似然处理得当。对于真实银行数据,只要把合成数据换成实际特征,同一套代码直接可用。
从贷款回收推广到更广泛的"零一膨胀"问题
一膨胀贝塔的镜像版本是零膨胀——比如医疗支出数据里大量零消费混杂着正数支出。同样的分段似然技巧,把π换成零点的概率质量即可。
更一般的框架是Tweedie分布或复合泊松-伽马模型,但那些假设特定的指数族形式。一膨胀贝塔的优势在于灵活:π和θ可以各自绑定不同的特征集,甚至可以引入层次结构处理多产品、多地域的贷款组合。
PyMC的自定义似然能力在这里是决定性因素。相比Stan需要手写C++,或JAGS受限于内置分布,PyMC的pytensor后端允许纯Python级别的计算图操作,同时保持自动微分和GPU加速的潜力。
这篇文章的完整代码已打包成可交互Notebook,点击原文徽章可直接在浏览器里跑。对于正在处理边界点质量问题的量化风控团队,这是一份可以直接落地的技术参考。
2000笔贷款、11个参数、两段式似然——这个数字组合或许会成为你下一个风控模型的起点。
热门跟贴