1. 项目概述:当数学建模遇上金融风控
去年带学生打MathorCup,A题“信用评分卡优化”一出来,我们团队就意识到,这绝不是一个简单的套模型题。它本质上是一个典型的、在强约束下的资源分配与决策优化问题。题目要求我们基于给定的客户数据,构建并优化信用评分卡,核心目标是最大化银行的利润(或最小化风险损失),同时必须满足一系列现实业务规则,比如通过率、坏账率、风险敞口等限制。这听起来像是机器学习问题,但当你真正开始构建目标函数和约束条件时,你会发现,它的最优解往往藏在线性规划(Linear Programming, LP)及其扩展形式(如混合整数线性规划,MILP)的求解器里。
为什么是线性规划?因为评分卡的“优化”动作,无论是调整评分阈值、决定授信额度,还是组合不同的风控策略,其决策变量(如是否给某个分数段的客户放款)与最终的目标(利润)和约束(通过率)之间,在题目设定的框架下,通常可以表达为线性关系。你的目标函数(总利润)是各客户群利润的线性加权和,你的约束(总通过人数、总坏账金额)也是线性不等式。这就把一个复杂的金融风控问题,转化成了一个标准的、可求解的数学优化问题。
网络上热词里提到的“分支定界/分支切割算法求解MILP”,正是我们当时求解核心模型的关键。因为有些决策变量必须是整数(比如,选择哪几张评分卡,或者某个策略是否启用),这就构成了混合整数线性规划。直接求解MILP是NP-Hard问题,而分支定界(Branch-and-Bound)框架是求解它的主流精确算法。简单来说,它通过不断“分支”将问题分解为更小的子问题,并“定界”快速排除那些不可能包含最优解的分支,从而在可接受的时间内找到全局最优解或近似最优解。
所以,这个项目的核心路径非常清晰:将信用评分卡的业务优化问题,严谨地建模成一个线性规划(或混合整数线性规划)模型,然后利用高效的优化求解器(如Gurobi, CPLEX,或开源的OR-Tools、SCIP)进行计算,最终得到一整套可执行的、最优的信贷决策方案。下面,我就把这个从业务理解到模型求解的完整链条拆开,结合我们踩过的坑和实战心得,给大家捋清楚。
2. 问题拆解:从风控业务到数学模型
拿到题目和数据,千万别急着套代码。第一步,也是最关键的一步,是把模糊的业务需求翻译成精确的数学语言。这决定了你的模型是否合理,求解是否可行。
2.1 核心决策变量定义
决策变量是你的“操作手柄”。在信用评分卡优化中,通常有两种定义方式,对应不同的优化粒度:
方式一:基于客户分箱的决策这是最直观的方式。信用评分卡通常会将客户根据分数划分为若干个风险等级(例如,A、B、C、D、F五个等级)。那么,你的决策变量可以是:
x_A, x_B, ..., x_F:连续变量,表示对每个分数段客户的通过率(0到1之间)。例如,x_A = 0.95表示A级客户95%通过审批。- 或者,
y_A, y_B, ..., y_F:整数变量(0或1),表示是否对某个分数段采取“通过”策略。这常用于策略组合优化。
方式二:基于评分卡阈值的决策更精细的优化是直接调整评分卡的审批阈值(Cut-off Score)。例如,原阈值是600分,高于600的通过。优化后可能变为610分。这时,决策变量可以是阈值S本身。但这样目标函数和约束关于S通常是非线性的(因为客户分布是离散的),需要做一些线性化处理或将其转化为第一种方式。
在我们的解题中,采用了第一种方式,因为它能更自然地与线性规划结合。我们将客户按初始评分分成了20个组(bin),决策变量x_i表示第i组客户的放款比例。
2.2 目标函数构建:利润最大化
银行的最终目的是盈利。因此,目标函数通常是期望利润最大化。对于每一组客户i,我们需要计算:
- 单客期望收入:贷款利息收入。假设贷款额度为
L_i,利率为r,则收入为L_i * r。 - 单客期望损失:即坏账损失。这需要用到题目提供的违约概率(PD)和违约损失率(LGD)。期望损失 =
L_i * PD_i * LGD_i。 - 单客期望利润:
利润_i = L_i * r - L_i * PD_i * LGD_i - 运营成本。其中运营成本可能是一个固定值或比例。
那么,对于该组所有客户,总期望利润就是:总利润 = Σ (客户数_i * x_i * 利润_i)。 这个Σ (客户数_i * x_i * 利润_i)就是一个关于决策变量x_i的线性函数,完美符合线性规划的要求。
注意:这里的
PD_i和LGD_i需要从题目数据中估计。通常,同一分数段内的客户具有相似的PD。我们可以用该分数段历史违约客户的占比来估算PD,LGD有时题目会直接给出或假设一个固定值(如45%)。这是建模的第一个关键假设,需要明确说明。
2.3 约束条件梳理:业务的紧箍咒
光追求利润不行,银行经营有严格的风险控制和合规要求。这些就构成了模型的约束条件,同样是线性的。
总体通过率约束:银行可能希望控制整体业务规模或通过率在一个范围内。
总体通过客户数 = Σ (客户数_i * x_i)- 约束可能为:
总体通过率下限 <= (总体通过客户数 / 总客户数) <= 总体通过率上限
坏账率约束:控制整体风险水平。
总坏账金额 = Σ (客户数_i * x_i * L_i * PD_i * LGD_i)总放贷金额 = Σ (客户数_i * x_i * L_i)- 约束为:
(总坏账金额 / 总放贷金额) <= 坏账率上限。注意,这是一个比值约束,但可以通过变形转化为线性约束:总坏账金额 <= 坏账率上限 * 总放贷金额。
高风险客户限制:对评分最低的几组客户(如F级),可能严格限制其通过率甚至禁止通过。
x_F <= 0.05或x_F = 0
预算或资本约束:总放贷金额不能超过可用资金总额
B。Σ (客户数_i * x_i * L_i) <= B
决策变量自身约束:
0 <= x_i <= 1,对于某些需要整数决策的,则y_i ∈ {0, 1}。
将这些目标函数和约束条件用数学公式写出来,一个完整的信用评分卡优化线性规划模型就诞生了。它看起来就像这样:
Maximize: Σ_i (N_i * x_i * P_i) Subject to: Σ_i (N_i * x_i) / N_total >= P_min Σ_i (N_i * x_i * L_i * PD_i * LGD_i) <= LR_max * Σ_i (N_i * x_i * L_i) Σ_i (N_i * x_i * L_i) <= B 0 <= x_i <= 1, for all i (可能还有 x_j <= C_j 对于特定高风险组j)其中,N_i是i组客户数,P_i是单客利润。
3. 模型求解:算法选择与实现细节
模型建好了,怎么解?对于纯线性规划(LP),有成熟的单纯形法(Simplex)和内点法(Interior-Point)可以高效求解全局最优解。但我们的模型往往包含整数变量(比如必须选择3张评分卡中的2张),这就变成了MILP,需要更专门的算法。
3.1 求解器:你的计算引擎
不要试图自己从头实现单纯形法或分支定界算法,那是科研人员的工作。我们应该站在巨人的肩膀上,使用成熟的优化求解器:
商业求解器(性能最强):
- Gurobi:学术界和工业界公认的标杆,对学术免费,速度和稳定性极佳。MathorCup这类比赛通常允许使用。
- CPLEX:IBM的老牌产品,同样非常强大。
- 它们都提供了Python、Java、C++等接口,集成非常方便。
开源求解器(免费可选):
- OR-Tools (Google):谷歌推出的优化工具套件,内置了多个求解器,对MILP支持良好,文档丰富,是比赛中的热门选择。
- SCIP:目前最强大的非商业开源混合整数规划求解器之一。
- PuLP (Python):一个建模库,可以调用多种后端求解器(包括CBC、GLPK等)。
我们的选择是:Python + PuLP + Gurobi。PuLP提供了非常直观的建模语法,而Gurobi作为后端求解器保证了求解效率。对于无法获得Gurobi许可的情况,PuLP默认的CBC求解器也能应付中小规模问题。
3.2 求解过程与代码框架
下面是一个高度简化的、基于PuLP的核心代码框架,展示了如何将上述数学模型“翻译”成代码:
import pulp import pandas as pd # 1. 读取和处理数据 data = pd.read_csv('customer_data.csv') # 假设数据已按评分分组并计算好所需字段 # data 中包含列:bin, customer_count, avg_loan, pd, lgd, interest_rate, operation_cost # 2. 创建问题实例 # 最大化问题 prob = pulp.LpProblem('Credit_Scorecard_Optimization', pulp.LpMaximize) # 3. 定义决策变量 # 为每个分箱创建一个连续变量,代表放款比例,范围在[0, 1] x = pulp.LpVariable.dicts('x', data['bin'].tolist(), lowBound=0, upBound=1) # 4. 构建目标函数 # 计算每个分箱的单客期望利润 data['profit_per_customer'] = data['avg_loan'] * data['interest_rate'] - \ data['avg_loan'] * data['pd'] * data['lgd'] - \ data['operation_cost'] # 总利润 = sum(客户数 * 放款比例 * 单客利润) prob += pulp.lpSum([data.loc[i, 'customer_count'] * x[data.loc[i, 'bin']] * data.loc[i, 'profit_per_customer'] for i in data.index]) # 5. 添加约束条件 # 5.1 总体通过率约束 (例如,不低于30%) total_customers = data['customer_count'].sum() prob += pulp.lpSum([data.loc[i, 'customer_count'] * x[data.loc[i, 'bin']] for i in data.index]) >= 0.3 * total_customers # 5.2 坏账率约束 (例如,不超过5%) # 总坏账金额 total_bad_debt = pulp.lpSum([data.loc[i, 'customer_count'] * x[data.loc[i, 'bin']] * \ data.loc[i, 'avg_loan'] * data.loc[i, 'pd'] * data.loc[i, 'lgd'] for i in data.index]) # 总放贷金额 total_loan = pulp.lpSum([data.loc[i, 'customer_count'] * x[data.loc[i, 'bin']] * data.loc[i, 'avg_loan'] for i in data.index]) prob += total_bad_debt <= 0.05 * total_loan # 5.3 高风险组约束 (例如,最低分箱通过率不超过1%) lowest_bin = data['bin'].min() # 假设bin是数值或可排序 prob += x[lowest_bin] <= 0.01 # 6. 求解问题 # 使用CBC求解器(开源),如果安装了Gurobi,可以替换为 pulp.GUROBI() prob.solve(pulp.PULP_CBC_CMD(msg=False)) # 7. 输出结果 print(f"求解状态: {pulp.LpStatus[prob.status]}") print(f"最大化总利润: {pulp.value(prob.objective):.2f}") for b in data['bin']: if x[b].varValue > 1e-5: # 忽略接近零的值 print(f"分箱 {b}: 建议放款比例 = {x[b].varValue:.3f}")这段代码清晰地展示了从建模到求解的流水线。关键在于第4步和第5步,如何正确地将业务逻辑转化为线性表达式。
3.3 关于“分支定界”算法
当我们引入整数变量时(例如y_i ∈ {0,1}表示是否对第i组客户采用高利率策略),PuLP和Gurobi在调用prob.solve()时,会自动启用分支定界算法来求解这个MILP问题。你不需要手动实现它,但理解其原理对调试和解释结果很有帮助:
- 松弛:首先忽略整数约束,求解对应的线性规划松弛问题(LP Relaxation)。这会得到一个目标值上界(对于最大化问题)。
- 分支:如果松弛解中某个整数变量
y_i的值是小数(比如0.7),就创建两个新的子问题:一个强制y_i = 0,另一个强制y_i = 1。这就像一棵树的分叉。 - 定界与剪枝:求解每个子问题的松弛解。如果某个子问题的解比当前已知的整数解还差,或者它的松弛解都无法超过当前最优整数解,那么这个分支就可以被“剪掉”(不再探索),因为它不可能产生更好的整数解。
- 迭代:不断分支、求解、定界、剪枝,直到找到满足整数要求的、且被证明是最优或接近最优的解。
在求解日志中,你可能会看到“Gap”在逐渐缩小,这个Gap就是当前最优整数解与全局上界之间的差距,是衡量求解进度和精度的关键指标。
4. 方案优化与策略分析
得到一组最优的x_i(放款比例)只是第一步。更重要的是如何解读这个结果,并形成可执行的业务策略。
4.1 结果解读与策略生成
求解器输出的x_i可能是一些小数,如x_A=0.95, x_B=0.8, x_C=0.3, x_D=0.01, x_F=0.0。这直接翻译成业务策略:
- A级客户:优质客户,给予95%的通过率,几乎全部放行。
- B级客户:次优客户,通过率80%,可以适当收紧。
- C级客户:中等风险客户,通过率30%,需要严格筛选。
- D级客户:高风险客户,仅1%通过,近乎拒绝。
- F级客户:极高风险客户,完全拒绝。
基于此,可以制定差异化审批策略:
- 自动通过:评分高于A阈值的,系统自动通过。
- 人工复审:评分在B和C区间的,转入人工审批流程,结合其他信息综合判断。
- 自动拒绝:评分低于D阈值的,系统自动拒绝。
4.2 敏感性分析与“What-If”场景
线性规划的一个巨大优势是便于进行敏感性分析(Sensitivity Analysis)。这能回答业务非常关心的问题:“如果某个条件变化了,我的最优策略和最大利润会怎样变?”
- 约束右端值变化的影响:例如,如果监管要求的坏账率上限从5%收紧到4%,我的最大利润会下降多少?这个信息在求解报告中称为“影子价格(Shadow Price)”或“对偶价格(Dual Price)”。它量化了放松或收紧一单位约束所带来的边际利润变化。这对于资源分配和谈判极具价值。
- 目标函数系数变化的影响:如果某个客户群的利率(
r)或违约率(PD)预估发生了变化,当前的最优解是否依然最优?求解器提供的“目标系数允许增减范围”可以告诉你答案。
在比赛中,进行深入的敏感性分析,并据此提出动态策略调整建议,是论文的重要加分项。例如,你可以指出:“在当前市场环境下,坏账率约束是限制利润的主要瓶颈,其影子价格为X。这意味着,如果银行能通过提升催收能力将坏账损失降低1个百分点,理论上可增加Y百万元利润。”
4.3 模型扩展:多评分卡与策略组合
原题可能更复杂,比如提供多张基础评分卡(如“收益优先型”、“风险规避型”),要求你从中选择几张进行组合,并对不同客户群体应用不同的评分卡。这就引入了0-1整数变量。
- 定义变量
z_k ∈ {0,1}:是否选用第k张评分卡。 - 定义变量
y_{i,k} ∈ {0,1}:是否对第i组客户使用第k张评分卡。 - 需要添加约束:
Σ_k z_k <= K(最多选K张卡),且y_{i,k} <= z_k(只有被选中的卡才能被使用)。 - 目标函数变为:
Max Σ_i Σ_k (客户数_i * y_{i,k} * 利润_{i,k})。
这个模型明显更复杂,变量更多,求解时间更长。但建模思路一脉相承,只是约束条件更丰富了。这时,求解器的性能差异就会体现出来。
5. 实战心得与避坑指南
最后,分享一些在实战中总结出来的、教科书上不会写的经验。
5.1 数据预处理是地基
模型再漂亮,数据不准全白搭。
- 缺失值处理:对于关键的PD、LGD字段,如果缺失,不能简单删除或填0。需要根据业务逻辑,用同组均值、中位数,或通过简单模型(如基于其他特征的回归)进行插补,并说明处理方法。
- 异常值处理:对于明显不符合逻辑的极端值(如贷款额度为负),要查明原因,是数据错误还是特殊业务(如冲正交易)。通常需要与业务方确认,或采用盖帽法(Capping)进行处理。
- 变量转换:线性规划要求线性关系。如果原始变量与目标之间可能存在非线性,可以考虑分段线性化或引入辅助变量。但在信用评分中,经过分箱处理后,组内用线性近似通常是可接受的。
5.2 模型假设必须清晰
所有模型都是对现实的简化,必须明确你的假设。
- 独立性假设:我们假设不同客户之间的违约是独立的。现实中可能存在系统性风险导致违约相关,但题目数据通常不包含这类信息。
- 参数稳定性假设:我们使用历史数据估计的PD、LGD来预测未来,假设这些参数在未来一段时间内是稳定的。
- 明确写下这些假设,并在论文的“模型评价与推广”部分讨论这些假设不成立时的影响,这体现了建模的严谨性。
5.3 求解效率与规模平衡
- 问题规模:客户分箱数(决策变量数)不宜过多也不宜过少。过多(如超过1000个)可能导致求解变慢;过少(如少于10个)则策略过于粗糙,失去优化意义。通常20-50个分箱是一个合理的范围。
- 整数变量带来的计算挑战:MILP的求解时间随整数变量数量指数级增长。如果模型中有大量0-1变量,求解可能非常耗时。可以尝试:
- 先求解LP松弛问题,观察哪些变量在松弛解中已经是0或1,将其固定。
- 设置合理的求解时间限制(Time Limit)和最优间隙(MIP Gap)。比如,设置“在1小时内找到Gap小于1%的解即可”,这在商业应用中很常见。
- 使用启发式方法(如贪心算法)先找到一个较好的可行解,作为求解器的初始解(Warm Start),可以大幅加速求解过程。
5.4 结果验证与业务合理性检查
求解器给出的“数学最优解”未必是“业务可行解”。
- 检查极端值:是否出现了某个分箱通过率为99.9%,而相邻分箱为0%这种跳跃过大的情况?这可能在数学上最优,但业务上难以解释(风险是连续的)。可以考虑添加平滑性约束,如
|x_i - x_{i+1}| <= δ。 - 进行压力测试:用另一份验证集(Out-of-Sample)数据,按照优化后的策略模拟运行,计算实际的关键指标(利润、坏账率),看是否与模型预测相符。这是检验模型泛化能力的金标准。
- 与基准策略对比:一定要设置一个基准策略,例如“所有分数高于600的客户全部通过”。清晰地展示你的优化策略相比基准策略,在利润、风险等指标上提升了多少百分比,用数据说话。
信用评分卡优化是一个完美的交叉领域课题,它要求你既懂金融风控的业务逻辑,又能熟练运用运筹优化的数学工具。通过线性规划建模,我们可以将复杂的业务决策问题转化为清晰的计算问题,从而找到在既定规则下的“最优解”。这个过程本身,就是数据驱动决策的核心体现。希望这份从实战中总结的指南,能帮你下次面对类似问题时,思路更清晰,下手更有力。记住,建模的关键不在于用了多复杂的算法,而在于你是否准确地把业务问题“翻译”成了数学语言。