简介2020年数学建模C题企业信贷风险预测的完整解答文档面向备战数模竞赛、希望掌握机器学习风控建模的读者。文档围绕“量化分析—特征工程—模型构建—评估优化”主线展开先将企业信贷违约风险转化为可量化评分再从进项与销项有效发票数、作废发票数、负数发票次数及占比、交易金额与时长等维度系统挖掘特征推荐决策树、随机森林、XGBoost、LightGBM等模型输出违约概率以0.5为阈值判定违约并按概率排序划分五级或八级风险档位。文中还给出特征重要性排序、PDPBox可解释性分析、添加随机扰动验证鲁棒性以及多模型评价指标对比与耦合优化的进阶思路可直接用于论文撰写与答辩展示。资源为1个docx文件压缩包仅9KB内容紧凑实用。已有4018人学习下载适合赛前快速梳理建模流程、补全论文分析亮点。1. 一道让多数参赛队栽在数据上的建模题2020年C题到底在算什么2020年数学建模C题“中小微企业的信贷决策”表面是给银行做贷款审批实际是一场特征工程和可解释性的大考。很多队伍把精力花在堆模型上结果被评阅人一句话噎死——“你的特征怎么解释”这道题的核心不在于拟合精度而在于你能否从发票流水里读出企业的经营真相进项是采购、销项是收入、供求稳定是波动率、信誉评级是违约概率的先验。用错一个口径后面全盘皆输。本文从数据清洗、特征构建、决策模型到避坑验证按我实际打这类题的习惯拆给你看适合准备建模竞赛的新手也适合想优化信贷评分逻辑的分析师。2. 从docx赛题到干净数据集信贷数据的清洗逻辑与特征工程2.1 第一步不是建模是把附件读成可计算的DataFrame2020年C题附件里给的是企业流水和进销项发票数据常见格式是CSV或Excel但题面解答文档用docx包装。很多队伍拿到手就急着做回归结果变量读进来全是字符串、日期格式不统一、进项金额带负号。我一般会先用pandas把数据读进来做一次类型体检把每列dtype和缺失率打印出来再决定清洗策略。这个步骤虽然琐碎但能避免后面所有特征计算出现“哑巴错误”。import pandas as pd import numpy as np df pd.read_excel(附件1_企业流水.xlsx, sheet_nameSheet1, parse_dates[开票日期]) df.info() print(df.isnull().mean().sort_values(ascendingFalse).head(20))这段代码的逻辑是先用info()看整体列类型再用isnull().mean()看每列缺失率。如果缺失超过30%要么剔除该列要么用业务含义填充如果日期列没被解析成datetime后面的月度聚合会直接报错。参数说明parse_dates指定需要解析的日期列名sheet_name按实际Excel页签名称改。这里的关键在于——你不能默认数据是干净的尤其是从docx附件导出的数据经常带不可见字符、全角数字或Excel科学计数法残留下来的精度问题。清洗发票金额时最常见的坑是进项和销项混在一列靠一个“类型”字段区分但那个字段可能有大小写、空格或繁体变体。我会先统一去除空白、统一小写再单独构造进项金额和销项金额两列。注意汇票、退票、红字发票这些特殊记录直接影响后面销售收入和采购成本的准确性如果附件说明里有“作废”“负数”“红冲”标记第一步就过滤掉不然后面算出来的营收和成本全是错的。2.2 特征不是越多越好信贷场景要的是稳定性和趋势做过信贷评分的人都知道银行关心两个问题这个企业能不能还、愿不愿意还。“能不能还”看现金流规模和稳定性“愿不愿意还”看历史违约和信誉评级。2020年C题的4个附件数据里有企业基本信息信誉评级、是否违约、进项发票、销项发票、以及一个供求稳定性的标签。特征构建上我一般分成三组规模特征年销项总额、年进项总额、平均月销项金额稳定性特征月销项标准差、变异系数标准差/均值这个比绝对标准差更合理因为大企业的标准差天然比小企业大趋势特征近三个月销项均值比前九个月均值或者线性回归的斜率df[month] df[开票日期].dt.to_period(M) g df.groupby([企业代号, month]).agg( 销项金额(销项金额, sum), 进项金额(进项金额, sum) ).reset_index() stat g.groupby(企业代号).agg( 月均销项(销项金额, mean), 销项_std(销项金额, std), 销项_变异系数(销项金额, std) / (销项金额, mean), 最近三月均销项(销项金额, lambda x: x.tail(3).mean()), 其余月均销项(销项金额, lambda x: x.iloc[:-3].mean() if len(x) 3 else 0) )这段代码先把日期按月份聚合然后分别计算月均销项、标准差和变异系数。最后一个特征里tail(3).mean()取的是最近三个月的均值iloc[:-3]取的是倒数第四个月往前所有月份两者相除就是趋势比。我通常把这个趋势比大于1.2定义为“增长型”小于0.8定义为“衰退型”中间定义为“平稳型”。注意如果企业只有两三个月的数据iloc[:-3]会返回空所以要加if len(x) 3的守卫否则特征计算直接报NaN。这个细节看起来小但很多队伍就是死在“为什么我的预测结果全是NaN”上。2.3 供求稳定不能靠肉眼把定性标签变成可量化特征2020年C题里有一列“供求稳定性”题面描述的是企业近一年的供求波动情况。很多队伍直接把这个字段当成字符串丢给模型或者用LabelEncoder编码成0、1、2但这两种做法都不理想。原因在于“高”“中”“低”三个等级虽然有序但它们和违约概率的关系不一定是线性的。我一般用两种方式处理第一种是目标编码target encoding用同一等级下的历史违约率替换等级本身。第二种是做成哑变量把“高稳定”“中稳定”“低稳定”拆成三列分别用0/1表示。后者的好处是可解释性强在论文里可以直接说“低稳定企业违约概率比高稳定高X个百分点”前者的好处是能够压缩维度适合后面接逻辑回归时减少参数个数。from sklearn.preprocessing import OneHotEncoder df_encoded pd.get_dummies(df_clean, columns[供求稳定性], prefix稳定)目标编码的做法在信贷业务里更常见因为它保留了等级的信息量而不是把等级当作无序类别。做法是先按等级分组计算违约率均值再把均值映射回对应样本。但要注意这个映射必须在训练集内部完成不能把全量数据的违约率算完再回填否则会引入未来信息也就是泄漏。这一点在C题里不涉及严格的时间序列测试集划分但如果你要写成论文里的“稳健性检验”最好还是按随机划分的逻辑走避免被评委质疑。3. 信贷决策基线模型贷不贷、贷多少、利率怎么定3.1 分层决策框架为什么比单模型更合适信贷决策的目标不是一个回归值而是一个策略组合先判断是否放贷再决定额度最后定价利率。把这个流程放进一个端到端的深度模型里效果未必好而且可解释性极差。评委要看你“为什么给这家企业50万而不是80万”一个黑匣子给不出答案。所以我在这类题里一贯的做法是拆成三层第一层是准入规则第二层是额度测算第三层是利率定价。这样做的好处非常直接——每一层都可以单独写进论文每一层都可以画成流程图每一层调参都有业务含义支撑。3.2 准入规则信誉评级和违约标签的交叉判断先看一个基本功把企业基本信息和违约标签做交叉表观察信誉评级与违约率的关系。2020年C题给的数据里信誉评级分为A、B、C、D四档其中D档基本等于高危客户违约率显著高于其他档。经验上A和B级客户可以全量准入C级需要附加条件D级直接拒绝。但别急着拍脑袋先用代码验证统计规律。cross pd.crosstab(df_clean[信誉评级], df_clean[是否违约], normalizeindex) cross[违约率] cross[1] if 1 in cross.columns else 0 print(cross)这段代码输出的就是每个评级级别下的违约率。如果数据显示A级违约率是2%D级违约率是35%那就说明评级和违约之间有强关联准入规则可以直接引用这个统计结论。我在实际写论文时通常还会加一步卡方检验p值小于0.05才在正文里写“显著相关”否则只作为描述统计。因为建模竞赛评阅时统计显著性是一个得分点不写白不写。3.3 额度测算现金流倍数法还是评分卡映射准入之后额度怎么定最朴素的做法是“年销项金额乘以一个固定系数”比如0.2意思是银行最多给企业一年收入的20%作为贷款。但这么一刀切会让稳定型企业和大规模企业吃亏。更合理的是按评分卡走——先算一个信用得分再把得分映射到额度比例区间。比如得分在90分以上额度比例是0.560分以下只有0.1中间线性插值。这里的评分可以用前面算好的特征加权得到权重的来源可以是逻辑回归系数也可以是专家打分。def calc_limit(row): if row[信誉评级] D: return 0 score row[信用得分] # 由前面的回归模型输出 ratio 0.1 0.4 * (score - 50) / 50 ratio min(max(ratio, 0.1), 0.5) limit row[年销项总额] * ratio return round(min(limit, 500), 1) # 单户授信上限500万 df_result[建议额度] df_result.apply(calc_limit, axis1)这里我把单户上限设在500万是参考银行小微企业信贷普遍的单户限额但这个数不是绝对的你可以根据题面要求调整。ratio线性映射的逻辑很简单——信用得分50分时只给10%的比例100分时给50%中间线性增长超过边界就截断。这个映射的参数0.1下限、0.5上限、500万封顶在论文里就是敏感度分析的对象后面我会专门讲怎么验证“额度系数调大调小对银行总收益的影响”。3.4 利率定价用风险溢价覆盖期望损失利率定价的核心公式是贷款利率 资金成本 运营成本 风险溢价 目标利润。在建模赛里资金成本和运营成本通常给固定值比如4%和2%剩下要计算的就是风险溢价。风险溢价可以从违约概率推导——如果一家企业违约概率是3%那么银行为了覆盖这笔损失至少要在利率上加收相当于3%的溢价粗略算法严格讲还要考虑违约损失率LGD。所以上一节算出来的违约概率直接就是利率定价的输入。def calc_rate(row): if row[建议额度] 0: return None pd_ row[违约概率] lgd 0.5 # 假定损失率50% risk_premium pd_ * lgd rate 0.04 0.02 risk_premium 0.01 return round(min(max(rate, 0.06), 0.15), 4) df_result[建议利率] df_result.apply(calc_rate, axis1)这个函数里lgd 0.5表示一旦违约银行只能收回一半本金这是行业常用的假设。目标利润取1%你也可以改成1.5%或2%利率下限6%和上限15%是参考普惠小微贷款的常见区间。注意这里用的是简化逻辑——真正的风控模型会把EAD违约暴露和LGD分开建模但竞赛场景里你写清楚假设、给出公式和参数评委就能给分。最怕的是你写了一堆模型却连风险溢价从哪来都说不清。4. 把ABC信誉评级变成违约概率评分卡与Logistic回归的落地玩法4.1 为什么用Logistic回归而不是XGBoost2020年C题的训练集规模大概在几百家企业特征维度在十到二十之间这是一个典型的小样本分类问题。小样本场景下XGBoost、随机森林这类强模型容易过拟合而且变量的系数解释不直观。Logistic回归的好处有三点第一输出天然是概率第二每个特征的系数有明确业务含义比如“变异系数每增加0.1违约概率上升X个百分点”第三在几百条样本上训练速度极快可以反复跑交叉验证调参数。很多队伍上来就堆XGBoost结果测试集AUC比训练集低一大截这就是过拟合的典型信号。4.2 特征标准化与标签平衡处理建模前先做两件事连续特征做标准化类别特征做哑变量。标准化用StandardScaler目的是让逻辑回归的梯度下降收敛更快同时让系数之间有可比性——如果不标准化销项金额百万级别和变异系数0.3左右的系数会差几个数量级没法比较哪个特征更重要。标签平衡方面如果违约样本占比低于20%属于类别不平衡我会用class_weightbalanced让模型自动给少数类更高权重。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline features [月均销项, 销项_变异系数, 最近三月均销项, 其余月均销项, 成立年限] X df_model[features] y df_model[是否违约] pipe make_pipeline(StandardScaler(), LogisticRegression(class_weightbalanced, C0.5, max_iter1000)) pipe.fit(X, y) df_model[违约概率] pipe.predict_proba(X)[:, 1]这里有一个容易忽略的点——C0.5是正则化强度的倒数C越小正则化越强越不容易过拟合。在小样本上我一般从0.1开始试用交叉验证选最优C而不是直接用默认值1。max_iter1000是因为特征标准化后虽然收敛更快但某些特征分布极不均匀时默认100次迭代可能不收敛。代码跑完后把系数打印出来看正负方向是否符合业务直觉——比如变异系数应该是正数波动越大违约概率越高成立年限应该是负数经营越久越稳定。如果方向反了先检查特征计算逻辑不要急着调参。4.3 用评分卡映射把概率转成整数分违约概率本身已经可以直接用但在竞赛论文里写“该企业违约概率为0.23”不如写“该企业信用得分为68分”来得直观。评分卡的做法是把违约概率做一个对数比变换再线性映射到一个整数区间。公式是score offset - factor * ln(p / (1-p))其中offset和factor是两个调节参数。我习惯把600分定义为违约率50%的基准分每升高20分违约率减半也就是factor 20 / ln(2) ≈ 28.85offset 600 28.85 * ln(1) 600。这样算出来的分数区间大概在300到800之间和常见征信评分卡量纲一致评委读起来也不费力。import math factor 20 / math.log(2) offset 600 def prob_to_score(p): p min(max(p, 1e-6), 1 - 1e-6) return round(offset - factor * math.log(p / (1 - p))) df_model[信用得分] df_model[违约概率].apply(prob_to_score)这个变换的好处在于它把模型输出的连续概率映射成了业务上一眼能看懂的分数而且分数和违约概率之间有严格单调关系。论文里放一张“分组违约率表”——把得分分成600以下、600-650、650-700、700以上四组分别统计组内实际违约率——就可以证明这个评分卡有区分度。评阅人最喜欢看到这种“模型输出→业务解释”的闭环这比堆AUC数字更打动人。4.4 阈值选择不用默认0.5用利润最优解二分类默认阈值是0.5但信贷场景里即使违约概率只有0.2银行也可能不愿意放贷——因为放贷的收益上限是利息收入而损失的底线是本息全无。最优阈值要让“期望利润最大化”而不是让“准确率最大化”。具体做法是遍历所有可能的概率阈值计算每个阈值下的总放贷利润选利润最高的那个点。thresholds np.arange(0.1, 0.8, 0.05) profits [] for t in thresholds: loan_set df_model[df_model[违约概率] t] if len(loan_set) 0: profits.append(0) continue profit loan_set[建议额度].sum() * loan_set[建议利率].mean() - loan_set[建议额度].sum() * 0.03 profits.append(profit) best_t thresholds[np.argmax(profits)]这里的建议利率是上一章算出来的0.03大概等于无风险利率也就是不放贷时资金的机会成本。通过这个遍历你会发现最优阈值可能落在0.3附近而不是0.5——这意味着按默认阈值会放过一批高风险的坏客户表面上放贷量更大但坏账吃掉利润后总收益反而更低。如果你在论文里能讲清楚这个“阈值不是拍脑袋定的是从利润函数推出来的”这道题已经超过大部分队伍了。5. 避坑指南C题最容易翻车的五个细节与排查思路5.1 销项金额当收入用额度直接虚高一倍现象算出来的年收入高得离谱授信额度随随便便就超过500万上限导致大量客户额度被截断。原因发票上的销项金额是含税价而实际收入口径是不含税销售额增值税税率可能高达13%。用含税价直接乘以额度系数等于多给了10%以上的额度。解决先看题面是否给税率如果给了就做价税分离公式是不含税金额 含税金额 / (1 税率)如果没给在论文里明确假设“所有金额均已做价税分离”避免评委抠口径。df[销项不含税] df[销项含税] / 1.135.2 信誉评级直接编码成1、2、3、4模型学出错误序数关系现象用LabelEncoder把A、B、C、D编码成0、1、2、3后逻辑回归系数显示级别越高违约率越低但真实数据可能B级客户比A级更优质。原因ABCD是业务评级不是线性递增的数值把它强行当作连续变量等于给模型灌输了一个错误先验。解决把信誉评级做OneHot编码或者按“违约率单调性检验”确认是否适合目标编码。如果检验结果是单调的用目标编码如果不是老老实实OneHot。5.3 全年均值抹平了季节性稳定性特征失真现象某企业前11个月几乎无销项12月突然爆发大量发票但算出的变异系数是1.2——看起来波动不大实际经营极不稳定。原因按年聚合时一个月的异常被全年平均稀释了变异系数也相应被拉低。解决改用滚动窗口计算稳定性比如每三个月为一个窗口计算窗口间的变异系数取最大值或者直接用月度变异系数而不是年度变异系数。我在特征工程时用的是月度数据聚合月度变异系数天然能捕捉这类“前冷后热”的模式。5.4 用全量数据训模型评阅一问“你验证了吗”就哑火现象论文里只报训练集AUC没有做任何验证集评估评委一问“你的模型在样本外表现如何”直接哑火。原因常见做法是把所有企业数据一口气丢进模型然后报一个漂亮的准确率。解决先用train_test_split分出一个20%的验证集或者用5折交叉验证报告AUC均值±标准差这样论文里能写“5折交叉验证AUC为0.83±0.04”比单次训练更有说服力。如果样本量实在太小比如低于100不做严格划分也要做Bootstrap重采样来验证稳定性。5.5 只报准确率不分违约类别的“代价敏感”损失现象代码输出confusion matrix准确率92%但仔细一看违约样本只占10%模型把所有客户都预测为不违约。原因类别不平衡下准确率是欺骗性指标——全预测多数类就能拿高分。解决报告AUC、召回率和精确率特别关注违约类别的召回率因为信贷场景里漏掉一个违约客户的损失远大于多拒一个优质客户的遗憾收益。对于C题即使是最简单的决策树也建议用class_weightbalanced和AUC这两个指标双重要求才能论文里站得住脚。6. 验证与进阶用敏感性分析证明你的模型不是玄学如果你不想让评阅人觉得结果都是“调参调出来的”敏感性分析是最实用的辩护利器。具体做法是把关键参数在一定范围内扫描观察核心输出指标的波动。比如额度系数从0.1扫到0.5利率下限从5%扫到10%记录每组参数下的银行总利润和坏账率画一张二维表格——你会发现存在一个最优区间超过这个区间利润曲线快速下滑这就是你在论文里可以理直气壮写下的“参数选择依据”。验证技巧上我习惯同时在边界参数下重跑一遍决策流程如果评级为D的企业被放贷、或者违约率超过40%的企业拿到大额授信说明某个环节的逻辑有漏洞需要回溯到特征计算或信用打分那一步。另一个进阶做法是压力测试——把所有企业的基础利率上浮200个基点或者模拟经济下行时违约率翻倍看银行总利润是否仍然为正。虽然2020年C题没有要求这一步但写进论文里会让方案完整度上一个台阶评委看到的是“这个方案经过了极端条件验证”。我自己的习惯是把所有参数和阈值集中放在论文附录的一张表里正文只保留最优参数组合和敏感性分析的核心结论这样既显得专业又不会让主文被参数淹没。记住建模竞赛比的是“你有多懂这道题的商业逻辑”不是“你调了多少轮参”。希望我的这套流程能帮你在C题上少走弯路也期望你读完能亲手跑一遍这些代码——数据永远不会骗你但代码会。final_columns [企业代号, 建议额度, 建议利率, 信用得分, 违约概率, 准入结论] df_final df_result[final_columns] df_final.to_excel(信贷决策方案.xlsx, indexFalse, sheet_name决策结果)导出Excel时我会顺带把准入结论列加进去——“准入”“拒绝”“有条件准入”三档方便在论文里贴一张结果概览。实际上这一列就是前面准入规则的可视化输出评委最想看到的不是一堆概率值而是“你的决策规则最终圈定了哪些客户”。这一步做完整套方案才算闭环。本文还有配套的精品资源点击获取