资讯动态

决策树信贷风险评估实战:scikit-learn与德国信贷数据集

发布时间:2026/10/15 7:00:25 来源:尧图企业网站定制
1. 项目概述与整体设计思路1.1 核心需求解析这个实验到底在做什么先说结论这个实验就是用Python的scikit-learn库对德国信贷数据集German Credit Data跑一棵决策树训练一个能自动判断“该不该给这个人放贷”的分类模型。数据是UCI机器学习库里的经典公开数据集专门用于信贷风险评估场景几乎所有学机器学习的同学都会遇到它。数据长什么样1000条样本每条代表一个贷款申请者包含20个特征比如账户余额、贷款金额、还款期限、还款记录、就业年限、婚姻状态、居住信息等。标签是二分类好客户1和坏客户2。我用这个数据集的目的是训练决策树模型让它学会从这些特征中找出“坏客户”的规律从而对未知样本给出预测。所以这个实验的本质是解决一个典型的“二分类问题”。网上很多教程只把代码跑一遍就完事但不告诉你为什么要用决策树、数据为什么这么预处理、树的参数为什么这么调。这篇博文会把这些“为什么”全部拆开并且附上我实际跑实验时踩过的坑和验证过的参数区间保证你照着做能出结果也知道每个步骤在干什么。1.2 实验环境与工具版本选型实验环境这块我建议直接用Anaconda集成环境省去单独安装各种包的麻烦。我自己用的版本组合如下实测下来互相兼容不会出现导入报错Python 3.9scikit-learn 1.0.2pandas 1.4.3numpy 1.21.5matplotlib 3.5.1pydotplus 2.0.2画树结构图用后面细说如果你本机已经装了旧版sklearn差别也不大决策树的API这些年几乎没有变过DecisionTreeClassifier这个类的核心参数一直是那几个。唯一需要注意的是graphviz的可视化方案在不同版本上略有差异这个在后面的实操章节我会专门记录一个完整的替代方案因为我知道很多人在画树这一步会卡住。提示不要用最新的Python 3.12去跑这个实验sklearn的依赖库尤其scipy在新版本上偶尔会有二进制兼容问题。3.9到3.11之间的版本最保险。1.3 决策树方法选型逻辑为什么标题里指定的是“决策树”而不是逻辑回归、KNN或者朴素贝叶斯关键原因有三个第一信贷风控场景极度看重可解释性。银行审核一笔贷款必须能够明确说出“为什么不放贷”——比如“该客户近3个月账户余额低于阈值且过去存在逾期记录”。决策树天然就是一组嵌套的if-then规则树的每一条从根节点到叶子节点的路径都是一条可读性极高的决策规则。这在金融风控里是巨大的优势。第二决策树对特征尺度不敏感。数据里既有“年龄”这种量纲特征也有“贷款金额”这种大数值特征还有“信用历史等级”这种离散有序特征。对KNN和逻辑回归来说必须先做标准化或归一化否则数值大的特征会主导距离计算。但决策树做的是信息增益类的划分用的是阈值比较完全不受量纲影响可以少做一步预处理。第三决策树能自动处理特征之间的非线性关系。比如“年龄段收入水平”的组合作用逻辑回归需要手动构造交互项决策树的结构本身就可以表达交互不需要额外做特征工程。当然单棵决策树容易过拟合这是它著名的短板所以实验里必须配合剪枝参数。我的策略是先让树长出来看它在训练集和测试集上的差距再用参数把它压回去这个操作流程在第五章会详细演示。2. 数据加载与预处理细节2.1 数据接入方式的两种选择德国信贷数据的获取方式有两条路。第一条是从UCI官网直接下载german.data文件这是纯文本格式没有表头。第二条是使用sklearn自带的数据加载器直接一行代码搞定from sklearn.datasets import fetch_openml X, y fetch_openml(credit-g, return_X_yTrue, as_frameTrue, parserauto) print(X.shape, y.shape) # 输出: (1000, 20) (1000,)这个方式最省心数据是DataFrame格式特征带上名字标签是二值字符串good 和 bad。但注意sklearn版本较老时fetch_openml可能不稳定如果你用的是旧版建议直接从本地CSV加载。import pandas as pd # 本地读取列名按照german.doc文档定义 columns [ status, duration, credit_history, purpose, credit_amount, savings, employment_duration, installment_rate, personal_status, other_debtors, residence_since, property, age, other_installment, housing, number_credits, job, people_liable, telephone, foreign_worker, credit_risk ] # sep\s 是因为原始文件用空格分隔 data pd.read_csv(german.data, sep\s, headerNone, namescolumns) X data.drop(columnscredit_risk) y data[credit_risk]这里你可能会踩第一个坑原文件中某些字段的值带下划线比如“A11”、“A12”这种编码直接读会变成整数或字符串混合类型。别急后续统一做编码处理。2.2 特征组解析哪些特征最关键20个特征中一部分是数值型一部分是类别型类别型又分为有序和无序两类处理逻辑完全不同。数值型特征有这些贷款金额duration单位是月范围从4到72贷款额度credit_amount几百到一万多马克不等年龄age19到75岁分期付款率installment_rate占可支配收入的比例分4档居住年限residence_since在本地址居住的年数类别型特征像账户状态status、信用历史credit_history、贷款用途purpose、储蓄账户savings、就业时间employment_duration等。每个类别型特征的取值都编码成A11、A12这种字符串含义是明确的等级比如A11代表“余额小于0”A12代表“余额大于等于0小于200”等。这类特征细节很多做特征说明时最好做一个映射字典方便后续查阅。pandas的describe函数只能看数值特征类别特征要用value_counts。我建议在跑模型之前先用一行代码把所有类别特征的分布拉出来看看categorical_cols X.select_dtypes(include[object]).columns for col in categorical_cols: print(f\n {col} ) print(X[col].value_counts())这一步不是可有可无的。它帮你提前发现数据质量问题和类别分布失衡问题比如“foreign_worker”特征里几乎95%以上的样本都是同一个取值yes那它提供的信息量就很低树模型会自动不太用它。注意决策树对类别特征不需要做one-hot编码直接用label encoding把类别转成整数就行。因为CART算法本质上是二分切分它自己就能决定怎么切分一个多分类变量。如果你做了one-hot反而会让树变深、分叉变多、解释性变差。2.3 数值型特征要不要归一化这个问题的标准答案很明确不需要。决策树每次切割时比较的是“特征值是否大于某阈值”这个阈值是在该特征自己的取值范围内搜索的。打个比方年龄的候选切分点是40、50贷款金额的候选切分点是5000、6000它们各自在自己的数值空间内部比较互不干扰完全不存在KNN里“大数吞小数”的问题。所以实验里我直接跳过了StandardScaler这一步。省掉它不会让准确率变差反而保留了树的可解释性——到时候你打印出来的树节点会是“年龄 25.5”这种人类可读的规则而不是“标准化后的age_zscore -0.32”这种让人摸不着头脑的东西。2.4 标签编码与数据划分标签是“good”和“bad”两个字符串sklearn要求是数值型所以做一次映射from sklearn.preprocessing import LabelEncoder encoder LabelEncoder() y_encoded encoder.fit_transform(y) # good - 1, bad - 0 print(encoder.classes_)这里有个细节值得记住LabelEncoder会把字母序的“bad”编码成0、“good”编码成1。所以分类结果里1代表好客户0代表坏客户。后面做混淆矩阵分析时坐标轴的语义不要搞反了。数据划分我沿用常见的7:3比例随机种子固定为42便于复现from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X_encoded, y_encoded, test_size0.3, random_state42, stratifyy_encoded )stratify参数是我特别要强调的。德国信贷数据本身不平衡好客户700条坏客户300条比例7:3。如果不用分层抽样随机划分时测试集里的坏客户占比可能忽高忽低影响评估的稳定性。用了stratify两边分布保持一致实验结果更可信。3. 决策树核心原理与算法推导3.1 决策树的生长逻辑信息增益与不纯度很多人跑完决策树只盯着准确率不知道树内部是怎么做出切割决策的。这里必须把原理讲透否则你连max_depth参数从小到大该怎么选都没概念。决策树的训练过程其实是一个“递归划分”过程从根节点出发遍历所有特征的所有候选切分点计算哪种切分方式能最大程度降低“不纯度”。不纯度有三种度量方式对应三种算法信息熵Information Entropy对应ID3算法信息增益比对应C4.5算法基尼系数Gini Impurity对应CART算法sklearn里的DecisionTreeClassifier默认用的是基尼系数。基尼系数的公式是Gini(D) 1 - Σ(p_i)^2其中p_i是第i个类别在节点中的占比。假设一个节点有100个样本好客户70个、坏客户30个基尼系数就是1 - (0.7^2 0.3^2) 1 - 0.58 0.42。如果这个节点里两类各占一半基尼系数为0.5说明乱成一锅粥如果节点里全是一个类别基尼系数为0说明纯得不能再纯。所以基尼系数越小纯度越高决策树的目标就是每次都找到让基尼系数下降最多的那个切分点。CART的思想和ID3类似但实现方式有区别CART是严格二叉的。即使某个特征有5个类别取值CART也会把这5个类别划分为两堆比如{取值A, B} vs {取值C, D, E}再用这两堆作为左右子节点。这个特性我后面会再次提到因为它让树的结构完全可控。3.2 信息增益的直观理解与计算示例“信息增益”听起来学术味很重拆开看其实就是“划分前的不纯度减去划分后的加权不纯度”。划得越好这个差值越大树就越优先采用这个切分。给你算一个具体例子。假设根节点有100个样本70好30坏基尼Gini_root 0.42。现在考虑用“是否拥有房产”去切分结果为有房产50个样本40好10坏基尼 1 - (0.8^2 0.2^2) 0.32无房产50个样本30好20坏基尼 1 - (0.6^2 0.4^2) 0.48加权后的子节点基尼Gini_weighted (50/100)*0.32 (50/100)*0.48 0.40信息增益 0.42 - 0.40 0.02。假设另一个候选切分是“是否有逾期记录”切分结果为逾期20个样本5好15坏基尼 0.375无逾期80个样本65好15坏基尼 0.3281加权后0.20.375 0.80.3281 ≈ 0.3375信息增益 0.42 - 0.3375 0.0825。后者增益更大树会优先选择“是否有逾期记录”作为根节点的切分特征。正是每次从所有特征、所有切分点中选出增益最大的那一个树的生长就是在一组嵌套的if-then规则上不断精化的过程。max_depth调大就允许if-then规则嵌套更多层模型就能捕捉到更复杂的信息组合但也容易掉进过拟合的坑里。3.3 sklearn的DecisionTreeClassifier核心参数解读理解了原理再回头看sklearn的参数就不会两眼一摸黑。DecisionTreeClassifier有十几个参数真正需要你亲手调的其实没那么多。我把最关键的罗列一下附上它们的实际作用参数名作用备注与典型值criterion不纯度度量方式默认gini可选entropy。实测差异很小一般不用改max_depth树木的最大深度最重要的剪枝参数范围4~8之间通常效果较好min_samples_split内部节点分裂所需最小样本数防止节点样本太少继续分建议5~20min_samples_leaf叶子节点最少样本数建议3~10是抑制过拟合的关键max_features每次寻找最佳切分时考虑的特征数默认None指考虑全部特征树多时可考虑sqrt或log2random_state随机种子务必固定一个整数值否则每次结果漂移class_weight类别权重数据不平衡时可设为balanced第六项random_state值得多说一句。虽然决策树本身是确定性的算法但在特征值完全相同的情况下sklearn会随机打破平局来选择切分特征。不固定种子的话你同一段代码跑两次出来的树可能不一样准确率也会上下波动。我在实验里统一使用了random_state42所有结果可复现。3.4 过拟合的形成与剪枝策略决策树堪称“天生过拟合选手”。你没有限制深度时它会把训练集学得面面俱到不仅学到真正的规律还把噪声样本、异常样本的个性化模式统统背下来。表现就是训练集准确率接近100%测试集准确率只有60%差距巨大。剪枝策略分两种。事前剪枝是在树生长过程中提前停止分裂靠的是max_depth、min_samples_leaf等参数事后剪枝是先让树完全生长然后从下往上修剪掉不重要的分支。sklearn只实现了前者所以实验里能做的就是参数搜索。我的经验是一步一步来。先跑一棵完全生长的树看它过拟合有多严重然后先用max_depth限制深度从3到8逐个试同时结合min_samples_leaf来稳定最小叶子大小最后再用GridSearchCV做一次联合搜索。跑下来的参数组合通常落在max_depth4~6、min_samples_leaf4~8的范围内验证集上的准确率在0.70到0.75之间这个成绩对于单棵决策树在这个数据集上已经算是不错的水平了。注意德国信贷数据本身很杂网上很多博客晒出80%甚至更高的准确率那多半是数据泄漏或者只跑了训练集。评估模型竞争力时一定要以独立测试集的结果为准。4. 实操过程完整代码与关键节点记录4.1 完整模型训练流程下面这段代码是完整可运行的从数据加载到评估一步到位。建议在Jupyter Notebook里逐格运行方便观察中间结果import pandas as pd from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score, classification_report, confusion_matrix from sklearn.preprocessing import LabelEncoder # 1. 数据加载与标签处理 columns [ status, duration, credit_history, purpose, credit_amount, savings, employment_duration, installment_rate, personal_status, other_debtors, residence_since, property, age, other_installment, housing, number_credits, job, people_liable, telephone, foreign_worker, credit_risk ] data pd.read_csv(german.data, sep\s, headerNone, namescolumns) X data.drop(columnscredit_risk) y data[credit_risk] # 2. 类别特征编码 categorical_cols X.select_dtypes(include[object]).columns X_encoded X.copy() for col in categorical_cols: X_encoded[col] LabelEncoder().fit_transform(X_encoded[col]) encoder_y LabelEncoder() y_encoded encoder_y.fit_transform(y) # 3. 分层划分 X_train, X_test, y_train, y_test train_test_split( X_encoded, y_encoded, test_size0.3, random_state42, stratifyy_encoded ) # 4. 训练默认决策树观察过拟合基线 clf_default DecisionTreeClassifier(random_state42) clf_default.fit(X_train, y_train) train_acc_default accuracy_score(y_train, clf_default.predict(X_train)) test_acc_default accuracy_score(y_test, clf_default.predict(X_test)) print(f默认树 - 训练集准确率: {train_acc_default:.4f}) print(f默认树 - 测试集准确率: {test_acc_default:.4f}) # 5. 参数网格搜索 param_grid { max_depth: [3, 4, 5, 6, 8, 10], min_samples_leaf: [2, 4, 6, 8], min_samples_split: [5, 10, 15] } grid_search GridSearchCV( DecisionTreeClassifier(random_state42), param_grid, cv5, scoringaccuracy, n_jobs-1 ) grid_search.fit(X_train, y_train) print(f最佳参数: {grid_search.best_params_}) print(f交叉验证最佳准确率: {grid_search.best_score_:.4f}) # 6. 最优模型评估 clf_best grid_search.best_estimator_ y_pred clf_best.predict(X_test) print(f测试集准确率: {accuracy_score(y_test, y_pred):.4f}) print(\n分类报告:) print(classification_report(y_test, y_pred, target_names[坏客户, 好客户])) print(\n混淆矩阵:) print(confusion_matrix(y_test, y_pred))这段代码跑完你就能看到一棵从默认状态到调优状态的完整过程。如果你的sklearn版本在1.0以下GridSearchCV的n_jobs-1可能偶尔报错把它改成1或2即可。4.2 第一次运行的结果解读我本地实测的结果先贴出来给你做对照参考默认树训练集准确率0.9786默认树测试集准确率0.6644这个差距就是典型的过拟合信号将近31个百分点的差距。默认树会一直生长到所有叶子节点都纯净为止树的深度甚至可以达到20多层这种复杂度的模型对训练集“倒背如流”是必然的但遇到新数据就露馅了。网格搜索给出的最佳参数是max_depth: 5min_samples_leaf: 4min_samples_split: 10交叉验证平均准确率0.7429测试集最终准确率0.7367。虽然绝对数字不算惊艳但关键是训练集准确率降到了0.7820左右和测试集的差距被压缩到了5个百分点以内说明模型泛化能力大幅提升。有个细节值得关注默认树的测试集准确率还没有调优后高。很多人以为“模型复杂度越高表现越好”在这个实验里恰恰相反。限制树的复杂度不仅有利于解释而且实实在在地提升了测试集准确率。这就是偏差和方差的博弈默认树方差太大剪枝后偏差略微增加但整体泛化误差显著下降。4.3 树结构可视化与规则提取训练完模型后把树画出来是理解模型决策逻辑最直观的方式。在sklearn 1.0版本里自带的plot_tree方法最简单import matplotlib.pyplot as plt from sklearn.tree import plot_tree plt.figure(figsize(24, 12)) plot_tree( clf_best, feature_namesX.columns, class_names[坏, 好], filledTrue, roundedTrue, fontsize9 ) plt.savefig(decision_tree.png, dpi150, bbox_inchestight) plt.show()画出来的图里每个节点显示切分条件、基尼系数、样本数和类别分布颜色深浅代表纯度。max_depth5的树整体不会太庞大勉强能看清楚细节规则。不过当树的节点超过40个时图上文字会挤成一团这时我建议改用文本方式导出规则from sklearn.tree import export_text tree_rules export_text(clf_best, feature_nameslist(X.columns), max_depth5) print(tree_rules)输出是一长串if-then嵌套格式如下|--- status 1.50 | |--- duration 21.50 | | |--- credit_history 2.50 | | | |--- class: 1从规则中我们能直观看到账户状态status是模型最看重的特征它被放在了根节点的位置。这个结论和业务经验完全吻合账户余额状况是判断信贷风险的第一信号。另外credit_history信用历史、duration贷款期限等特征也在树的较上层出现说明重要性排名和常识判断是呼应的。4.4 特征重要性与业务解读sklearn提供了一个现成的特征重要性评估方法feature_importances_。它统计的是所有节点在特征上做切分时带来的不纯度下降总量再按特征汇总并归一化。直接看数值import numpy as np feature_importance pd.Series(clf_best.feature_importances_, indexX.columns).sort_values(ascendingFalse) print(feature_importance.head(10))实测下来排名靠前的特征大致有status账户状态、credit_history信用历史、duration贷款期限、age年龄、credit_amount贷款金额。status的重要性可能超过0.25credit_amount往往只有0.05左右。这揭示了一个反直觉的结论信贷风险的核心信号是申请人的既有财务状态和历史信用记录而不是贷款金额本身。这个结论对于理解风控业务非常有价值。特征重要性还能用来做简单的特征筛选。比如你可以把重要性低于0.01的特征从训练集里剔除重新训练模型通常准确率不会下降甚至因为噪音减少略有提升。这也是一种变相的特征工程。5. 常见问题与排查实录5.1 数据读取时列名对不上这是最常见的问题。原始german.data文件没有表头如果你直接用pd.read_csv默认参数第一行会被当成数据而不是列名模型训练时所有列都被当成特征标签数据也会混进去。解决办法就是我之前给的columns列表必须严格按照german.doc里的特征顺序定义。任何一列错位后面全乱。另外有些版本的数据文件每行末尾多了一个空格sep\s能自动消化不需要手动清洗。5.2 字符串特征报错ValueError拿到数据后直接丢给决策树拟合会报“could not convert string to float”的错误。决策树底层用的是数值比较不接受字符串类型。解决方式有两种一是对全表调用pd.get_dummies做one-hot不推荐会让树变复杂二是对每个类别特征做LabelEncoder推荐。我这里再强调一次CART即使面对编码后的整数类别仍然把它当离散值处理不会引入不必要的顺序关系所以放心用LabelEncoder。实际实验中有个容易被忽略的排查点经过LabelEncoder处理后的数据训练集和测试集的编码映射必须一致。在sklearn 1.0以后LabelEncoder不再支持fit_transform在合并数据上的用法所以如果分开编码一定要先对整列fit再transform或者用OrdinalEncoder统一处理from sklearn.preprocessing import OrdinalEncoder enc OrdinalEncoder() X_encoded enc.fit_transform(X[categorical_cols])5.3 graphviz可视化失败很多人跑plot_tree没问题但想导出SVG或PDF时单独安装graphviz软件结果卡住。graphviz是独立于Python的软件包必须在系统层面安装还要把它的bin目录配置到系统PATHpip安装的pydotplus只是Python接口替代不了本体。如果不想折腾直接用plot_tree输出png就行它已经把graphviz的底层调好了不依赖外部软件。5.4 类别不平衡导致评估指标失真德国信贷数据里坏客户只占30%如果模型全预测“好”准确率也能到70%。这种“懒模型”会在分类报告里暴露出来坏客户标签0的召回率可能是0。所以不要只看accuracyclassification_report和混淆矩阵才是裁判。我在实验里注意到调优后的模型对坏客户的召回率在0.4~0.5之间precision大概也在0.4上下。如果目标是“尽量别放过坏客户”可以尝试class_weightbalanced。但class_weightbalanced有个副作用模型会更激进地预测“坏”误伤好客户的概率也上升。信贷风控里“误杀”和“漏网”两难平衡到底侧重哪边取决于放贷机构的经营策略。这个取舍不是纯技术问题我建议实验中两种方案都跑一下对比混淆矩阵后再做决定。5.5 随机森林是决策树的合理延伸实验做完后扩展方向很自然地指向随机森林。随机森林就是几十棵决策树的投票集合通过行采样和列采样制造多样性显著降低单棵树的方差。用同样的数据跑随机森林准确率通常能到0.75~0.80比单棵决策树再提升几个点。如果项目后面有提升效果的要求可以从RandomForestClassifier开始其他参数沿用决策树调优时的经验值。还有一个扩展点是决策树回归。如果标签不是好/坏而是贷款金额或违约概率DecisionTreeRegressor就是对应的选择原理完全一致只是不纯度度量换成了均方误差。学有余力可以一并尝试。5.6 排查问题速查表问题现象可能原因解决方案列名报错或首行当数据原文件无表头手动指定columnssep\s字符串特征导致拟合失败未做类别编码对每个object列LabelEncoder/OrdinalEncoder训练集接近100%、测试集很低过拟合限制max_depth、提高min_samples_leaf不同次运行结果不一样没有固定随机种子random_state设固定整数accuracy很高但坏客户召回为0类别不平衡看classification_report必要时class_weightbalancedgraphviz无法导出外部软件未安装改用plot_tree直接出图GridSearchCV参数过多跑得慢网格膨胀减少候选值、增加cv步数前先手动粗调6. 一点实操心得实验做到最后真正有价值的东西反而不是那棵调参调得不错的树而是理解了这个流程里每个决策背后的权衡逻辑。比如为什么要分层抽样、为什么要看混淆矩阵而不是只看准确率、为什么树的深度不是越深越好。把这些想通了换任何数据集、任何树模型你的思路都是通的。我自己当时做这个实验踩了无数次坑最深的体会是不要一上来就调参先跑一棵默认树把它的过拟合程度“量化”出来你就知道该往哪个方向努力了。默认树训练集0.97、测试集0.66差距摆在那里接下来调参就不是盲调。如果你打算在这个基础上做课程报告建议额外加上用测试集上挖到的几个典型规则做业务解释比如“账户余额低于200且贷款期限超过21个月大概率是坏客户”这种从模型规则到业务语义的翻译能力比单纯堆砌代码值钱得多。后续有时间还可以把数据按时间顺序切分成训练集和验证集模拟真实放贷场景下的在线评估效果那又是另一个维度的挑战了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑