简介:这份资源面向参加全国大学生数学建模竞赛的选手、指导教师,以及对金融风控与数据分析感兴趣的学习者,聚焦2020年C题「中小微企业信贷决策」的完整解题方案。压缩包共160个文件,约248.35MB,以xlsx数据表、txt说明、jpg与png图表、m脚本、csv数据集及docx论文为主,另含pdf与嵌套rar,覆盖原始数据、代码与成稿论文三类材料。论文部分系统阐述信贷风险评估建模思路,涉及信息不对称、信用评估困难等背景,并给出缺失值处理、异常值检测、特征工程、交叉验证等完整流程;MATLAB源码则实现数据导入、模型构建、训练测试与结果可视化,便于读者对照复现线性回归、决策树、随机森林等方法的细节。图表数据可直观呈现预测值与实际值对比、特征重要性等分析结果。目前已有663人学习,适合需要完整赛题方案、代码实现与论文写作参考的读者。
1. 从2020年国赛C题看中小微企业信贷决策:一份论文加源代码能复现出什么
2020年全国大学生数学建模竞赛C题,题目背景是银行对中小微企业放贷,要求结合企业信贷数据做风险评估与信贷策略设计。这道题在数学建模圈子里被反复讨论,原因很直接:它不像纯优化题那样有唯一解,也不像纯统计题那样只跑一个模型就完事,而是要把数据清洗、特征工程、风险评分、决策优化串成一条完整链路。很多队伍当年卡在第二问的信贷策略上,不是模型不会写,而是没想清楚“给谁贷、贷多少、利率怎么定”这三个变量之间的耦合关系。如果你手上有这道题的论文和源代码,想真正吃透它,或者想拿它当模板迁移到2025数学建模国赛C题、华为杯数学建模这类赛题上,那这篇笔记就是按一线复现的节奏来拆的。我会把论文里常见的建模路径、源代码里容易翻车的地方、以及怎么把这套东西改成自己能用的版本,一层层讲清楚。适合已经跑过Python基础建模、想从“能出图”进阶到“能落地”的参赛者和数据分析从业者。
2. 信贷决策题的建模骨架:从数据到策略的四层结构
2.1 数据层:附件1到附件3到底该怎么读
2020年C题给了三个附件,常见做法是:附件1是123家有信贷记录企业的进项发票和销项发票信息,附件2是302家无信贷记录企业的发票信息,附件3是各企业的信誉评级和是否违约标签。很多人一上来就把附件1和附件2合并,然后直接丢进模型,结果发现特征维度爆炸且大量缺失。我一般会先做三件事:
第一,按企业代码聚合发票数据,把每张发票的金额、税额、价税合计分别求和,得到每个企业的总进项、总销项、总税额。第二,计算衍生指标,比如进销比、税额占比、月度波动率。第三,把附件3的标签对齐到企业代码上,作为监督学习的y值。
import pandas as pd import numpy as np # 读取附件1的进项发票和销项发票 input_invoice = pd.read_excel('附件1.xlsx', sheet_name='进项发票信息') output_invoice = pd.read_excel('附件1.xlsx', sheet_name='销项发票信息') credit_info = pd.read_excel('附件3.xlsx') # 按企业代码聚合进项 input_agg = input_invoice.groupby('企业代号').agg( 进项总金额=('金额', 'sum'), 进项总税额=('税额', 'sum'), 进项发票数=('发票号码', 'count') ).reset_index() # 按企业代码聚合销项 output_agg = output_invoice.groupby('企业代号').agg( 销项总金额=('金额', 'sum'), 销项总税额=('税额', 'sum'), 销项发票数=('发票号码', 'count') ).reset_index() # 合并进销项 firm_features = pd.merge(input_agg, output_agg, on='企业代号', how='outer').fillna(0) # 衍生指标 firm_features['进销比'] = firm_features['进项总金额'] / (firm_features['销项总金额'] + 1) firm_features['税额占比'] = (firm_features['进项总税额'] + firm_features['销项总税额']) / (firm_features['进项总金额'] + firm_features['销项总金额'] + 1) # 对齐标签 firm_features = pd.merge(firm_features, credit_info[['企业代号', '信誉评级', '是否违约']], on='企业代号', how='left')这段代码的关键参数是聚合方式。进项和销项必须分开聚合再合并,不能先concat再groupby,否则发票类型会混淆。fillna(0)是因为有些企业只有进项没有销项,或者反过来,缺失值填0比填均值更合理,因为“没有发生”本身就是信息。进销比分母加1是防止除零,这是血泪经验,当年有队伍没加,跑出来一堆inf直接导致模型崩掉。
2.2 特征层:哪些指标真正影响违约标签
论文里常见的特征工程会堆几十个指标,但真正对违约预测有区分度的往往就那几个。我一般会先用IV值(信息价值)筛一遍,再用随机森林看特征重要性。2020年C题的数据量不大,123家有标签的企业,特征太多必然过拟合。
| 特征名 | 计算方式 | 对违约的区分度 |
|---|---|---|
| 进销比 | 进项总金额/销项总金额 | 高,比值异常往往对应经营异常 |
| 月均销项波动率 | 销项金额按月分组后的标准差/均值 | 中高,波动大说明收入不稳定 |
| 税额占比 | 总税额/总金额 | 中,反映企业实际税负 |
| 发票作废率 | 作废发票数/总发票数 | 高,作废多说明交易异常 |
| 信誉评级 | 附件3直接给出 | 高,但要注意评级和违约不是线性关系 |
这里有个容易翻车的地方:信誉评级是离散的A/B/C/D,直接做one-hot会引入多重共线性,我一般会按违约率排序后做有序编码。另外,附件2的302家企业没有标签,不能直接拿来训练,但可以用训练好的模型做预测,这就是第二问信贷策略的输入。
2.3 模型层:为什么我最终选了XGBoost而不是逻辑回归
逻辑回归可解释性强,论文里写起来好看,但2020年C题的数据非线性明显,逻辑回归的AUC很难超过0.75。XGBoost在这类表格数据上通常能到0.85以上,而且能直接输出特征重要性,论文里也能解释。代价是调参麻烦,但用网格搜索加交叉验证可以解决。
from xgboost import XGBClassifier from sklearn.model_selection import GridSearchCV, StratifiedKFold from sklearn.metrics import roc_auc_score from sklearn.preprocessing import LabelEncoder # 准备特征和标签 feature_cols = ['进项总金额', '销项总金额', '进销比', '税额占比', '进项发票数', '销项发票数'] X = firm_features[feature_cols].values y = LabelEncoder().fit_transform(firm_features['是否违约'].fillna('否')) # 网格搜索 param_grid = { 'n_estimators': [100, 200, 300], 'max_depth': [3, 4, 5], 'learning_rate': [0.01, 0.05, 0.1], 'subsample': [0.7, 0.8, 1.0] } cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) grid = GridSearchCV(XGBClassifier(use_label_encoder=False, eval_metric='logloss'), param_grid, cv=cv, scoring='roc_auc') grid.fit(X, y) print('最佳参数:', grid.best_params_) print('最佳AUC:', grid.best_score_)参数说明:max_depth控制在3到5之间是因为样本量小,树太深必过拟合;learning_rate用0.05是精度和速度的折中;subsample小于1能增加模型鲁棒性。StratifiedKFold保证每折里违约和非违约比例一致,否则小样本下AUC波动会很大。跑完这一步,你会得到一个AUC在0.85左右的模型,接下来才能进入信贷策略优化。
2.4 策略层:信贷额度与利率的联合优化怎么建
第二问的核心是:对每个企业,决定贷不贷、贷多少、利率多少,使得银行收益最大同时风险可控。常见做法是建一个带约束的优化模型,目标函数是期望收益,约束包括总信贷额度、单户额度上限、风险敞口等。
from scipy.optimize import minimize # 假设有n家企业,预测违约概率为p_i,贷款利率为r_i,贷款额为L_i # 银行期望收益 = sum((1-p_i) * r_i * L_i - p_i * L_i * 损失率) # 约束:sum(L_i) <= 总信贷额度,L_i <= 单户上限 n = len(firm_features) p = grid.best_estimator_.predict_proba(X)[:, 1] # 违约概率 loss_rate = 0.6 # 违约时的本金损失率 def objective(L): return -np.sum((1 - p) * 0.05 * L - p * L * loss_rate) # 假设利率5% constraints = [ {'type': 'ineq', 'fun': lambda L: 10000000 - np.sum(L)}, # 总信贷额度1000万 ] bounds = [(0, 1000000) for _ in range(n)] # 单户上限100万 result = minimize(objective, x0=np.ones(n) * 100000, bounds=bounds, constraints=constraints) print('最优信贷分配:', result.x)这段代码的逻辑是:目标函数里利率固定为5%,实际论文里利率也是决策变量,需要和L一起优化,但那样会变成非凸问题,求解难度大。我一般会先固定利率档位(比如4%、5%、6%),对每个档位求最优L,再比较总收益。loss_rate取0.6是经验值,表示违约后能收回40%的本金。约束里的总信贷额度1000万是题目给的,单户上限100万是常见风控要求。跑出来的结果就是每个企业该贷多少,再结合违约概率决定利率档位。
3. 源代码复现时最容易翻车的五个地方
3.1 发票数据的时间字段解析错误
现象:按月份聚合销项金额时,发现有些月份数据为空,或者月份数量对不上。原因:附件里的开票日期格式不统一,有的是“2020/1/1”,有的是“2020-01-01”,还有的是Excel日期序列号。直接pd.to_datetime会有一部分解析失败变成NaT。解决:先用pd.to_datetime(..., errors='coerce'),然后检查NaT数量,如果超过5%,说明格式问题严重,需要手动写解析函数。
def parse_date(x): if pd.isna(x): return pd.NaT if isinstance(x, (int, float)): return pd.Timestamp('1899-12-30') + pd.Timedelta(days=x) for fmt in ['%Y/%m/%d', '%Y-%m-%d', '%Y.%m.%d']: try: return pd.to_datetime(x, format=fmt) except: continue return pd.NaT input_invoice['开票日期'] = input_invoice['开票日期'].apply(parse_date)3.2 信誉评级和违约标签的对应关系搞反
现象:模型训练出来AUC只有0.5,等于随机猜。原因:附件3里“是否违约”列,有的版本是“是/否”,有的版本是“1/0”,还有的版本是“违约/未违约”。如果LabelEncoder把“否”编码成1,“是”编码成0,那模型就完全学反了。解决:先打印credit_info['是否违约'].value_counts(),确认标签分布,再手动映射。
print(credit_info['是否违约'].value_counts()) # 确认后手动映射 label_map = {'否': 0, '是': 1, '未违约': 0, '违约': 1} credit_info['违约标签'] = credit_info['是否违约'].map(label_map)3.3 进项和销项合并时企业代码对不齐
现象:合并后的特征表里,有些企业只有进项没有销项,或者反过来,导致进销比计算出来是inf或0。原因:附件1的进项和销项是两张表,企业代码的格式可能不一致,比如一个有空格一个没有。解决:合并前先做str.strip(),再用how='outer'合并,缺失值填0。
3.4 优化模型求解时初始值选得太随意
现象:minimize跑出来的结果全是0,或者全是上限。原因:初始值x0如果离最优解太远,SLSQP算法可能陷入局部最优或者直接不收敛。解决:用违约概率的倒数作为初始值的权重,违约概率低的企业多贷,违约概率高的少贷。
x0 = (1 - p) / np.sum(1 - p) * 10000000 # 按信用好坏分配初始额度 x0 = np.clip(x0, 0, 1000000) # 限制在bounds内3.5 论文里的结果和代码跑出来的对不上
现象:论文里写AUC=0.87,你跑出来只有0.82。原因:随机种子没固定,或者特征工程步骤有细微差异。解决:固定所有random_state,包括train_test_split、XGBoost、交叉验证。另外,检查论文里是否用了附件2的数据做半监督学习,如果用了,你只用附件1训练,结果肯定有差距。
4. 把2020年C题的方法迁移到新赛题:三个可复用的模块
4.1 风险评分卡模块:从XGBoost到标准评分卡
XGBoost输出的是概率,但银行实际用的是评分卡,分数越高信用越好。转换公式是:score = 600 + 50 * log(odds),其中odds = (1-p)/p。这个模块可以直接复用到任何二分类信用风险场景。
def prob_to_score(p, base=600, pdo=50): odds = (1 - p) / (p + 1e-6) score = base + pdo * np.log(odds) / np.log(2) return np.clip(score, 300, 900) firm_features['信用评分'] = prob_to_score(p)参数说明:base=600是基准分,pdo=50表示odds每翻一倍分数增加50分。这是行业惯例,迁移到新赛题时可以根据数据分布调整base。
4.2 信贷策略优化模块:从单目标到多目标
2020年C题只要求收益最大,但实际业务里还要考虑风险分散、行业集中度等。可以把目标函数改成加权形式:max(收益 - λ * 风险),λ是风险厌恶系数。这个模块可以复用到2025数学建模国赛C题这类涉及决策优化的题目。
4.3 论文写作模块:图表和公式的标准化
源代码跑出来的图,直接贴到论文里往往不够清晰。我一般会用matplotlib统一风格:字体用SimHei,字号12,分辨率300dpi,图例放右上角。公式用LaTeX写,变量用斜体,下标用正体。这些细节在评阅时很加分。
import matplotlib.pyplot as plt plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False plt.rcParams['figure.dpi'] = 300 plt.rcParams['savefig.dpi'] = 3005. 从跑通到跑好:一个验证模型是否过拟合的笨办法
最后一章说一个我自己的习惯:每次跑完模型,不要只看AUC,要把训练集和验证集的AUC都打印出来,如果差距超过0.1,基本就是过拟合了。2020年C题数据量小,过拟合是常态。我的做法是,先用全部数据跑一遍看特征重要性,然后只保留前5个特征重新训练,如果AUC没怎么降,说明之前的特征堆多了。另外,我会把违约概率分成10档,看每档的实际违约率是否单调,如果不单调,说明模型排序能力有问题,需要检查特征工程。
from sklearn.metrics import roc_auc_score from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y) model = XGBClassifier(n_estimators=200, max_depth=4, learning_rate=0.05, subsample=0.8, random_state=42) model.fit(X_train, y_train) train_auc = roc_auc_score(y_train, model.predict_proba(X_train)[:, 1]) test_auc = roc_auc_score(y_test, model.predict_proba(X_test)[:, 1]) print(f'训练集AUC: {train_auc:.4f}, 测试集AUC: {test_auc:.4f}, 差距: {train_auc - test_auc:.4f}')如果差距大于0.1,我会做三件事:减少max_depth、增加subsample比例、或者用早停。早停的用法是model.fit(X_train, y_train, eval_set=[(X_test, y_test)], early_stopping_rounds=20),这样模型会在验证集AUC不再提升时自动停止。这个笨办法帮我省了很多调参时间,也避免了很多论文里“训练集AUC 0.99,测试集0.7”的尴尬。
最后说一个习惯:我每次复现完一道赛题,都会把代码里的硬编码参数抽出来做成配置文件,比如config.yaml,这样下次遇到类似赛题,改几个参数就能跑。2020年C题的代码我后来迁移到华为杯数学建模的信用风险题上,只改了数据读取路径和特征列名,模型部分几乎没动。希望这个思路帮到你。
本文还有配套的精品资源,点击获取