ARTICLE DETAIL

资讯详情

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

2020国赛C题复盘:中小微企业信贷决策建模与代码实现

2020国赛C题复盘:中小微企业信贷决策建模与代码实现 简介这份资源面向参加全国大学生数学建模竞赛的选手、指导教师以及对金融风控与数据分析感兴趣的学习者聚焦2020年C题「中小微企业信贷决策」的完整解题方案。压缩包共160个文件约248.35MB以xlsx数据表、txt说明、jpg与png图表、m脚本、csv数据集及docx论文为主另含pdf与嵌套rar覆盖数据、代码、论文与可视化全流程。论文部分系统阐述信贷风险评估建模思路涉及信息不对称、信用评估困难等背景并运用线性回归、决策树、随机过程等工具配合缺失值处理、异常值检测、特征工程与交叉验证完成模型选择与验证MATLAB源码则实现数据导入、清洗、建模、训练、测试与结果可视化便于读者对照复现。目前已有663人学习适合需要完整赛题方案、可运行脚本与排错参考的读者深入研读。1. 2020年国赛C题到底在考什么中小微企业信贷决策的建模骨架2020年全国大学生数学建模竞赛C题题目背景是银行对中小微企业放贷时如何在有限信贷额度下决定给谁贷、贷多少、利率定多少、期限多长同时把坏账风险压到可接受范围。这道题之所以被反复拿出来复盘是因为它同时踩中了三个真实业务痛点数据里既有企业规模、营收、利润这类结构化指标又有信誉评级、是否违约这类离散标签决策变量不是单一数值而是「贷不贷 贷多少 利率 期限」的组合目标函数既要最大化银行收益又要控制风险天然是一个带约束的多目标优化问题。如果你正在准备数学建模竞赛或者想拿这道题当练手项目补上「数据清洗 → 特征工程 → 风险评分 → 信贷组合优化」的完整链路这篇笔记就是按这个顺序拆的。我会把论文里常见的建模骨架、源代码里真正要写的几个模块、参数怎么定、哪里容易翻车一条条讲清楚。适合已经会 Python 基础语法、但还没独立跑通过一道完整信贷决策题的人也适合带队的指导老师拿去当讲评材料。2. 从附件数据到可用特征信贷决策题的数据预处理链路2.1 三个附件表的结构与合并逻辑这道题通常给三张表企业信息表含规模、营收、利润、信誉评级等、信贷记录表历史贷款金额、利率、期限、是否违约、以及可能的进项/销项发票表。真实比赛里最耗时的不是建模而是把这三张表按企业 ID 对齐。常见做法是先用企业信息表做主表左连接信贷记录再对发票表做聚合后合并。import pandas as pd import numpy as np # 读取三张原始表注意编码国赛附件常见 gbk info pd.read_csv(企业信息.csv, encodinggbk) credit pd.read_csv(信贷记录.csv, encodinggbk) invoice pd.read_csv(进销项发票.csv, encodinggbk) # 发票表按企业聚合成特征总金额、笔数、月均金额、金额标准差 inv_feat invoice.groupby(企业ID).agg( 发票总金额(金额, sum), 发票笔数(金额, count), 发票月均金额(金额, mean), 发票金额波动(金额, std) ).reset_index() # 主表左连接保证不丢企业 df info.merge(credit, on企业ID, howleft) \ .merge(inv_feat, on企业ID, howleft) # 缺失值处理数值列填 0 或中位数类别列填众数 num_cols df.select_dtypes(include[np.number]).columns df[num_cols] df[num_cols].fillna(df[num_cols].median()) df[信誉评级] df[信誉评级].fillna(df[信誉评级].mode()[0])这段代码的关键在howleft必须以企业信息表为准否则会丢掉没有信贷记录的企业而这类企业恰恰是决策里「要不要放贷」的重点对象。发票聚合用std而不是只求和是因为金额波动大的企业往往经营不稳定这个特征在后续风险评分里权重不低。参数上缺失值填充策略要分列讨论营收、利润这类连续变量用中位数比均值稳因为信贷数据里极端值多信誉评级这种有序类别填众数后还要做序数编码A1, B2, C3, D4不能直接 one-hot否则丢掉等级信息。2.2 特征工程把「能不能贷」翻译成数值原始字段直接丢进模型效果通常一般需要构造几类衍生特征。第一类是偿债能力比率利润/营收、营收/贷款额、发票总金额/贷款额。第二类是稳定性特征发票金额波动/发票月均金额这个比值越大说明经营越不稳。第三类是历史行为特征历史违约次数、历史贷款平均利率。# 偿债能力 df[利润率] df[利润] / df[营收].replace(0, np.nan) df[营收贷款比] df[营收] / df[贷款额].replace(0, np.nan) df[发票覆盖比] df[发票总金额] / df[贷款额].replace(0, np.nan) # 稳定性 df[经营波动率] df[发票金额波动] / df[发票月均金额].replace(0, np.nan) # 历史行为 df[历史违约次数] df.groupby(企业ID)[是否违约].transform(sum) # 无穷值和缺失再兜一次 df df.replace([np.inf, -np.inf], np.nan) df df.fillna(df.median(numeric_onlyTrue))replace(0, np.nan)这一步是血泪经验分母为 0 直接除会得到 inf后面标准化时整个特征列就废了。transform(sum)而不是agg是为了保持行数不变方便直接拼回原表。提示特征做完先做一次相关性检查利润率、营收贷款比、发票覆盖比三者相关性往往很高保留两个即可否则共线性会让逻辑回归的系数解释变得很难看。3. 风险评分模型逻辑回归、XGBoost 怎么选怎么调3.1 为什么信贷题首选逻辑回归做基线信贷决策题有一个隐藏要求模型要能解释。银行不可能接受一个「黑匣子」告诉你某企业不能贷却说不清原因。逻辑回归输出的是概率系数可以直接翻译成「某指标每上升一个单位违约几率变化多少」这在论文里好写在答辩时也好答。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.metrics import roc_auc_score, classification_report features [利润率, 营收贷款比, 发票覆盖比, 经营波动率, 历史违约次数] X df[features] y df[是否违约] X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy) scaler StandardScaler() X_train_s scaler.fit_transform(X_train) X_test_s scaler.transform(X_test) lr LogisticRegression(class_weightbalanced, max_iter1000, C1.0) lr.fit(X_train_s, y_train) prob lr.predict_proba(X_test_s)[:, 1] print(AUC:, roc_auc_score(y_test, prob)) print(classification_report(y_test, (prob 0.5).astype(int)))class_weightbalanced是必须的违约样本通常远少于正常样本不加这个参数模型会倾向于全预测「不违约」准确率看着高但毫无用处。C1.0是正则强度倒数C 越小正则越强如果特征多且样本少调到 0.1 到 0.5 之间能压住过拟合。stratifyy保证训练测试集里违约比例一致否则测试集可能一个违约样本都没有AUC 直接算不出来。3.2 XGBoost 做对比与特征重要性验证逻辑回归跑通后用 XGBoost 做一版对比主要目的不是替换而是看特征重要性排序是否一致。如果两个模型都认为「历史违约次数」最重要那这个结论就站得住。import xgboost as xgb dtrain xgb.DMatrix(X_train, labely_train) dtest xgb.DMatrix(X_test, labely_test) params { objective: binary:logistic, eval_metric: auc, max_depth: 4, eta: 0.1, subsample: 0.8, colsample_bytree: 0.8, scale_pos_weight: (y_train 0).sum() / (y_train 1).sum() } model xgb.train(params, dtrain, num_boost_round200, evals[(dtest, test)], early_stopping_rounds20) xgb.plot_importance(model, max_num_features10)max_depth4是信贷数据常用的深度再深容易记住噪声。scale_pos_weight等价于逻辑回归的 class_weight处理不平衡。early_stopping_rounds20防止无效迭代验证集 AUC 连续 20 轮不升就停。两个模型跑完如果 AUC 差距在 0.03 以内论文里就主推逻辑回归XGBoost 作为稳健性检验。这个取舍在答辩时是加分项因为说明你懂业务约束不是唯精度论。4. 信贷组合优化把评分变成「贷给谁、贷多少、利率多少」4.1 决策变量与目标函数的建模风险评分给出的是每个企业的违约概率 p_i接下来要解一个优化问题在总信贷额度约束下选择放贷企业集合、每家贷款额 x_i、利率 r_i、期限 t_i使银行期望收益最大。期望收益可以写成Σ [ (1 - p_i) · x_i · r_i · t_i - p_i · x_i · LGD ]其中 LGD 是违约损失率常见取 0.5 到 0.7。约束包括总贷款额不超过额度上限、单家贷款额在上下限之间、利率在监管区间内、以及风险敞口约束比如总违约期望损失不超过某个阈值。from scipy.optimize import minimize import numpy as np n len(df) p lr.predict_proba(scaler.transform(df[features]))[:, 1] # 违约概率 LGD 0.6 R_max 0.15 # 利率上限 R_min 0.04 # 利率下限 Total 1e7 # 总信贷额度 def neg_profit(x): # x 前 n 个是贷款额后 n 个是利率 amt x[:n] rate x[n:] profit np.sum((1 - p) * amt * rate - p * amt * LGD) return -profit cons [ {type: ineq, fun: lambda x: Total - np.sum(x[:n])}, # 总额约束 {type: ineq, fun: lambda x: 0.05 * Total - np.sum(p * x[:n] * LGD)} # 风险约束 ] bounds [(0, 0.1 * Total)] * n [(R_min, R_max)] * n x0 np.concatenate([np.full(n, Total / n * 0.5), np.full(n, 0.08)]) res minimize(neg_profit, x0, boundsbounds, constraintscons, methodSLSQP)SLSQP适合这种带边界和不等式约束的中小规模问题。x0初始值给总额的一半均分比全零收敛快。风险约束里0.05 * Total是允许的期望损失上限这个参数要根据银行风险偏好调调太小会导致很多企业贷不到调太大又失去风控意义。4.2 用遗传算法处理离散决策上面是连续松弛版实际业务里「贷不贷」是 0/1 决策。如果企业数量在几十家以内可以用遗传算法或模拟退火处理离散选择。import random def fitness(selected): # selected 是 0/1 列表 amt np.array([df.iloc[i][建议额度] if selected[i] else 0 for i in range(n)]) if amt.sum() Total: return -1e9 rate np.full(n, 0.08) profit np.sum((1 - p) * amt * rate - p * amt * LGD) return profit # 简单遗传算法框架 pop [[random.randint(0, 1) for _ in range(n)] for _ in range(50)] for gen in range(100): pop sorted(pop, keyfitness, reverseTrue)[:25] # 交叉变异略按标准 GA 流程补全遗传算法的关键是适应度函数里对超额度直接给极大负值让不可行解自然淘汰。种群规模 50、迭代 100 代是常见起点企业数超过 100 家时要把种群调大否则搜索不充分。5. 避坑与排查这道题最容易翻车的五个地方5.1 违约标签泄漏现象模型 AUC 高到 0.98 以上论文里看着很漂亮。原因特征里混入了「是否违约」直接相关的字段比如某些附件里「违约次数」和标签是同一来源。解决建模前把和标签同源的列全部剔除只保留贷款申请时点之前能拿到的信息。5.2 利率优化结果全是边界值现象优化出来的利率不是上限就是下限。原因目标函数对利率是线性的没有加入利率上升导致违约概率上升的反馈。解决把 p_i 写成利率的函数比如 p_i(r) p_i0 k·rk 取 0.5 到 1.5 之间这样最优利率会落在中间。5.3 数据标准化在训练测试集上分别做现象测试集 AUC 比交叉验证低很多。原因先对全量数据标准化再切分测试集信息泄漏到训练集。解决fit_transform只在训练集上做测试集用训练集的 scaler 做transform上面代码里已经这么写了。5.4 忽略企业规模分层现象模型对大企业预测准对小企业全预测不违约。原因小企业样本少且违约模式不同。解决按规模分层建模或者把规模作为交互项加入比如「利润率 × 是否小微」。5.5 优化问题无解时直接报错现象minimize返回失败论文里没结果。原因约束太紧可行域为空。解决把风险约束改成软约束加惩罚项或者先解一个松弛问题找到可行点再逐步收紧。6. 论文与代码怎么对齐让评阅人一眼看到复现路径最后一章讲一个具体技巧论文里的模型描述和源代码必须能一一对应。我一般会在论文每个公式后面标注对应的代码文件名和函数名比如「式(3) 对应optimize.py的neg_profit函数」。这样评阅人想验证时不用猜。再给一个验证方法把源代码里的随机种子固定跑三遍如果三次结果完全一致说明没有隐藏的随机性如果论文里写的结果和代码跑出来的对不上优先检查数据划分和标准化顺序。我自己的习惯是论文定稿前一定用一份干净的虚拟环境重跑一遍全流程把requirements.txt里的版本号锁死避免「在我电脑上能跑」的经典翻车。这道题的价值不在于拿奖而在于它是一条完整的「数据 → 模型 → 优化 → 决策」链路跑通一遍后面遇到任何信贷、风控、资源分配类问题都能套。希望帮到你。本文还有配套的精品资源点击获取
返回列表