数学建模实战:基于随机森林的企业信贷风险量化模型构建与评估
1. 从“小贷公司信贷决策”到数学建模实战一个问题的多维度拆解2020年高教社杯全国大学生数学建模竞赛的C题题目是“中小微企业的信贷决策”。这个题目一出来当时就在我们参赛圈里引起了不小的讨论。它不像一些纯理论推导题那么“阳春白雪”而是直接把一个非常现实、非常接地气的金融问题抛给了我们这些学生银行或者说题目里的小额信贷公司该怎么决定给不给一家企业贷款给多少利息怎么定这背后是数学、统计学、计算机科学和金融知识的交叉应用。很多同学看到“信贷”、“企业”这些词第一反应可能是去翻金融学的教材找什么资产负债率、流动比率。但数学建模的魅力就在于它要求你从一堆看似杂乱的数据里自己定义问题自己寻找特征自己构建模型最后给出一个量化的、可解释的决策方案。今天我就以这道题的第一问为核心结合我们当时的解题思路、踩过的坑以及事后复盘的一些更优解法来一次彻底的拆解。无论你是正在备赛的同学还是对数据分析和数学建模感兴趣的朋友相信这篇超过5000字的“事后诸葛亮”式复盘能给你带来比标准答案更多的东西。第一问的具体内容是根据附件1中123家有信贷记录企业的相关数据建立信贷风险量化模型并给出附件2中302家无信贷记录企业的信贷风险等级A、B、C、D以及是否放贷的建议。简单说就是用一个有标签的数据集123家企业我们知道它们过往的信贷表现去训练一个模型然后给一个更大的无标签数据集302家企业打上风险标签。这本质上是一个有监督的分类问题。但难点在于数据是什么特征怎么提取标签怎么定义用什么模型评价标准是什么这一连串的问题才是建模的核心。接下来我将抛开简单的代码罗列深入每个环节的“为什么”和“怎么做”。2. 数据理解与特征工程模型大厦的地基拿到数据附件1和附件2第一步绝对不是急着跑模型。花在数据理解上的时间最终都会在模型效果上回报你。附件1给出了123家企业的“进项发票信息”和“销项发票信息”以及它们是否违约的信誉记录。附件2是302家企业的进销项发票信息但没有信誉记录。这就是我们全部的家当。2.1 原始数据字段解读与业务逻辑映射进项发票和销项发票是增值税发票的两种类型这本身就是非常重要的业务知识。进项发票企业采购原材料、服务时收到的发票代表企业的成本投入。销项发票企业销售产品、服务时开出的发票代表企业的收入产出。每一张发票都包含丰富的信息发票号码、开票日期、销方单位谁卖的、购方单位谁买的、金额、税额。其中金额不含税和税额是最核心的数值字段。价税合计就是金额税额。那么如何从这些流水账般的发票记录里提炼出能表征企业“信贷风险”的特征呢这就需要我们基于财务和业务常识进行特征构造。2.2 核心特征构造思路与计算实例我们的目标是评估企业的经营稳定性和盈利能力这些都是影响还款能力的关键。以下是我们构造的一些核心特征及其计算逻辑经营规模与活跃度特征总销售收入统计销项发票的“金额”总和。这是企业规模的直接体现。总采购成本统计进项发票的“金额”总和。发票张数进/销企业开票和收票的频率反映业务活跃度。合作商户数进/销统计唯一的销方或购方数量。合作方越多可能业务越分散风险也可能更复杂。盈利能力与效率特征这是重点毛利率这是一个极其关键的指标。粗略估算为(总销售收入 - 总采购成本) / 总销售收入。它反映了企业的盈利空间。毛利率持续低下或为负的企业风险自然高。月均/季度均收入将总销售收入按时间跨度题目数据大约3年平摊得到月均收入比总收入更能反映稳定的现金流能力。收入波动率计算每月销售收入的方差或变异系数。波动越剧烈说明经营越不稳定风险越高。财务健康度与风险特征进销项匹配度一个容易被忽略但很重要的点。健康的企业采购进项和销售销项在时间和品类上应有较强的相关性。我们可以计算例如“月度采购额与销售额的相关系数”。长期采购远大于销售囤货或销售远大于采购无本生意都可能有问题。大额发票占比统计金额超过某个阈值如全年平均单笔金额的2倍的发票张数占比。过度依赖少数几笔大额交易风险集中。负金额发票检查是否存在红字发票退货、折让频繁的退货可能暗示产品或销售有问题。时间序列行为特征趋势特征利用销项发票的日期和金额可以拟合一个简单的线性趋势看销售收入是在增长、停滞还是下滑。斜率即为趋势强度。季节性分析月度收入是否有明显的季节性规律。有规律可循的经营比杂乱无章的要好。实际操作中的坑直接对原始发票表进行groupby企业代码和日期如到月的聚合计算时一定要注意数据的完整性。有些企业可能在某个月份没有进项或销项这会导致时间序列出现缺失值。我们需要先构建一个完整的时间索引比如所有月份的列表然后用0或插值法填充缺失的月份否则计算波动率和趋势时会失真。注意特征工程不是一蹴而就的。我们通常会先构造一个庞大的特征池可能几十个然后通过相关性分析、重要性排序等方法进行筛选避免维度灾难和过拟合。3. 标签定义与模型选型如何教会模型识别风险附件1给了“信誉评级”和“是否违约”但题目要求我们对无标签数据输出A、B、C、D四个风险等级。所以我们首先需要利用附件1自己定义一个训练用的标签。3.1 从原始信誉记录到四分类标签的映射策略附件1中企业的信誉记录是文字描述比如“信誉良好无违约记录”、“信誉一般有少量短期违约”等。我们需要将其量化为A、B、C、D。这里没有标准答案体现了建模的开放性。一种合理的映射方式是A级低风险信誉良好无违约记录。B级中低风险信誉一般基本守信用可能有极短期违约但已解决。C级中高风险信誉较差有违约记录。D级高风险严重失信多次违约或违约金额大。关键在于一致性。你必须制定一个清晰、可重复的规则将每一条文字描述对应到四个等级上并记录下这个映射表。这是后续模型学习的“真理”。3.2 模型选择为什么是它而不是它这是一个典型的多分类问题。可供选择的模型很多逻辑回归简单、可解释性强但前提是特征与逻辑几率存在线性关系对于复杂非线性关系拟合能力弱。决策树/随机森林这是我们当时的主力模型。为什么因为它对特征的非线性关系、交互作用捕捉得很好不需要复杂的特征标准化对缺失值也不敏感还能给出特征重要性排序帮助我们理解哪些因素比如毛利率、波动率对风险判断影响最大。随机森林通过集成多棵树有效避免了单棵决策树的过拟合问题。梯度提升树如XGBoost、LightGBM。性能通常优于随机森林训练速度也快但参数调优更复杂可解释性稍弱于随机森林。支持向量机在小样本、高维数据上表现好但数据量稍大时训练慢且对参数和核函数选择敏感。神经网络理论上拟合能力最强但对于我们这种可能只有百来个样本、几十个特征的数据集极易过拟合且是“黑箱”可解释性差。我们的选择逻辑在样本量有限123个训练样本、特征多为数值型且可能存在复杂关系的场景下随机森林在稳健性、准确率和可解释性上取得了最好的平衡。它的“特征重要性”输出本身就能作为我们分析报告的一部分向“信贷经理”解释我们的判断依据。3.3 模型训练与评估避免“自欺欺人”直接用全部123个数据训练然后就去预测302个数据这是不严谨的。我们需要评估模型的泛化能力。训练集-测试集划分将123个有标签数据随机划分为训练集如80%和测试集如20%。注意保持类别比例使用分层抽样因为违约企业C、D级可能本身占比较少。评估指标对于多分类问题不能只看准确率。如果数据中A级企业占90%模型全预测为A也有90%准确率但这毫无意义。我们必须看混淆矩阵清晰看到每个类别被分对和分错的情况。精确率、召回率、F1-Score针对每一个类别尤其是风险高的C、D类计算这些指标。我们希望模型对高风险企业有很高的召回率尽可能不漏掉坏人即使这会牺牲一些精确率把一些好人误判为坏人。宏平均F1对所有类别的F1取平均能综合反映模型在各类别上的表现。交叉验证由于数据少采用K折交叉验证如5折或10折能更稳健地估计模型性能。即把训练集分成K份轮流用其中K-1份训练1份验证最后取平均性能。踩坑实录我们第一次就栽在了评估上。只做了简单划分没做分层导致测试集中高风险样本极少模型在测试集上“表现优异”但一用就发现对高风险企业完全不敏感。后来改用分层K折交叉验证才得到了可靠的性能估计。4. 风险等级预测与放贷策略从数学输出到业务决策模型训练好后我们对附件2的302家企业进行特征提取流程和附件1完全一致然后输入模型得到每个企业属于A、B、C、D四个等级的概率。4.1 处理预测概率与最终定级随机森林会输出一个概率向量如[0.7, 0.2, 0.08, 0.02]分别代表属于A、B、C、D的概率。最简单的定级规则是argmax即取概率最大的类别作为最终等级。但这里可以引入业务考量。例如如果模型判断一个企业属于D级的概率虽然只有0.25但属于C级的概率有0.4A和B很低那么综合来看它至少也是C级高风险。我们可以设定规则若D级概率超过阈值如0.2直接定为D若C级概率超过阈值如0.3定为C否则再看A、B。这样能更敏感地捕捉风险。4.2 是否放贷的建议建立决策规则题目要求给出是否放贷的建议。这不仅仅是模型输出的直接转换。我们需要制定一个与风险等级挂钩的决策规则。例如A级建议放贷。可给予优惠利率和较高额度。B级建议放贷。但需加强贷后检查利率适中。C级建议谨慎放贷。需提供额外担保或抵押缩短贷款期限提高利率。D级建议拒绝放贷。这个规则需要我们在论文中明确陈述它体现了数学模型与业务实践的结合。4.3 模型稳定性与敏感性分析一个好的模型不仅要准还要稳。我们做了以下分析特征重要性分析查看随机森林输出的特征重要性排序。在我们当时的模型中毛利率、收入波动率和月均收入稳居前三。这完全符合金融常识盈利差、收入不稳的企业风险高。这增强了我们模型的可信度。扰动测试对某个企业的关键特征如毛利率进行微小扰动如上下浮动5%观察其风险等级是否发生变化。如果等级轻易改变说明模型在该点附近决策边界不稳定需要警惕。对于边界样本的审视找出那些被模型预测为B/C或C/D边界的企业即两类概率很接近。这些企业是风险判断的模糊地带在业务中应该交由信审专家进行人工复核。我们的模型很好地起到了“初筛”和“辅助决策”的作用而不是完全替代人工。5. 代码实现关键环节与避坑指南这里不会粘贴全部代码但会指出几个关键环节的实现要点和容易出错的地方。5.1 数据读取与预处理import pandas as pd import numpy as np # 读取数据注意编码 df_invoice_sale pd.read_excel(附件1销项发票信息.xlsx, dtype{发票号码: str}) df_invoice_purchase pd.read_excel(附件1进项发票信息.xlsx, dtype{发票号码: str}) df_credit pd.read_excel(附件1企业信誉记录.xlsx) # 包含企业代号和信誉评级 # 合并进销项数据为每个企业计算特征 # 这是一个典型的groupby操作 def calculate_features(df_invoice, enterprise_col企业代号, amount_col金额, date_col开票日期): df_invoice[date_col] pd.to_datetime(df_invoice[date_col]) df_invoice[year_month] df_invoice[date_col].dt.to_period(M) # 转换为年月周期 features {} # 按企业分组 grouped df_invoice.groupby(enterprise_col) features[总金额] grouped[amount_col].sum() features[发票张数] grouped.size() features[合作方数量] grouped[销方单位代号].nunique() # 注意进项和销项的字段名不同 # 计算月度时间序列特征 monthly_amount df_invoice.groupby([enterprise_col, year_month])[amount_col].sum().unstack(fill_value0) features[月均金额] monthly_amount.mean(axis1) features[月金额标准差] monthly_amount.std(axis1) features[月金额变异系数] features[月金额标准差] / (features[月均金额] 1e-5) # 防止除零 # ... 计算其他特征 return pd.DataFrame(features) # 分别计算进项和销项特征 purchase_features calculate_features(df_invoice_purchase, enterprise_col企业代号, amount_col金额, date_col开票日期) sale_features calculate_features(df_invoice_sale, enterprise_col企业代号, amount_col金额, date_col开票日期) # 合并特征并计算衍生特征如毛利率 all_features purchase_features.join(sale_features, howouter, rsuffix_sale, lsuffix_purchase).fillna(0) all_features[毛利率] (all_features[总金额_sale] - all_features[总金额_purchase]) / (all_features[总金额_sale] 1e-5)避坑点1数据类型发票号码、企业代号这类ID字段读取时应指定为字符串dtypestr防止长数字被科学计数法或截断。避坑点2缺失值处理howouter合并后有些企业可能只有进项或只有销项会产生NaN。我们用fillna(0)填充但需在报告中说明对于只有单边交易的企业其毛利率等指标可能失真需特别关注。避坑点3周期转换使用dt.to_period(M)比dt.strftime(%Y-%m)更适合进行时间序列聚合计算。5.2 模型训练与评估示例from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import StratifiedKFold, cross_val_predict from sklearn.metrics import classification_report, confusion_matrix # 假设 X_train 是特征矩阵 y_train 是映射好的A/B/C/D标签 rf_model RandomForestClassifier(n_estimators200, max_depth10, min_samples_split5, class_weightbalanced, # 处理类别不平衡 random_state42) # 使用分层K折交叉验证进行评估 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) y_pred cross_val_predict(rf_model, X_train, y_train, cvskf, methodpredict) y_pred_proba cross_val_predict(rf_model, X_train, y_train, cvskf, methodpredict_proba) print(交叉验证分类报告) print(classification_report(y_train, y_pred, target_names[A, B, C, D])) # 特征重要性 rf_model.fit(X_train, y_train) # 用全部训练数据再拟合一次用于查看特征重要性 importances rf_model.feature_importances_ feature_names X_train.columns # 将特征重要性排序并可视化或打印避坑点4类别不平衡使用class_weightbalanced参数让模型在训练时更关注少数类高风险企业这是提升召回率的关键。避坑点5随机种子设置random_state确保结果可复现这对竞赛和论文至关重要。避坑点6交叉验证的应用cross_val_predict可以方便地得到每一折样本的预测结果用于整体评估避免信息泄露。5.3 对无标签数据的预测与输出# 对附件2数据做同样的特征工程得到 X_test_unknown # 使用训练好的完整模型进行预测 final_model RandomForestClassifier(n_estimators200, max_depth10, min_samples_split5, class_weightbalanced, random_state42) final_model.fit(X_train, y_train) # X_train, y_train 是附件1的全部数据 test_pred_labels final_model.predict(X_test_unknown) test_pred_proba final_model.predict_proba(X_test_unknown) # 生成结果DataFrame result_df pd.DataFrame({ 企业代号: unknown_enterprise_codes, # 附件2的企业代号列表 信贷风险等级: test_pred_labels, 是否放贷建议: [是 if x in [A, B] else 否 for x in test_pred_labels] # 根据之前定义的规则 }) # 可以额外保存概率用于分析边界案例 for i, class_name in enumerate(final_model.classes_): result_df[f{class_name}_概率] test_pred_proba[:, i] result_df.to_excel(C题第一问预测结果.xlsx, indexFalse)整个过程从数据清洗、特征构造、模型选择调优到最终决策是一个完整的闭环。数学建模竞赛考察的正是这种将实际问题抽象化、量化并利用工具解决的综合能力。第一问作为开端奠定了整个题目的基调。它告诉我们面对一个现实问题没有现成的公式可以套你需要自己定义变量、建立关系、寻找最优解。这份经历远比学会用一个随机森林模型来得珍贵。