1. 项目概述与整体设计思路
1.1 核心需求解析:这个实验到底在做什么
先说结论:这个实验就是用Python的scikit-learn库,对德国信贷数据集(German Credit Data)跑一棵决策树,训练一个能自动判断“该不该给这个人放贷”的分类模型。数据是UCI机器学习库里的经典公开数据集,专门用于信贷风险评估场景,几乎所有学机器学习的同学都会遇到它。
数据长什么样?1000条样本,每条代表一个贷款申请者,包含20个特征,比如账户余额、贷款金额、还款期限、还款记录、就业年限、婚姻状态、居住信息等。标签是二分类:好客户(1)和坏客户(2)。我用这个数据集的目的是训练决策树模型,让它学会从这些特征中找出“坏客户”的规律,从而对未知样本给出预测。
所以这个实验的本质是解决一个典型的“二分类问题”。网上很多教程只把代码跑一遍就完事,但不告诉你为什么要用决策树、数据为什么这么预处理、树的参数为什么这么调。这篇博文会把这些“为什么”全部拆开,并且附上我实际跑实验时踩过的坑和验证过的参数区间,保证你照着做能出结果,也知道每个步骤在干什么。
1.2 实验环境与工具版本选型
实验环境这块,我建议直接用Anaconda集成环境,省去单独安装各种包的麻烦。我自己用的版本组合如下,实测下来互相兼容,不会出现导入报错:
- Python 3.9
- scikit-learn 1.0.2
- pandas 1.4.3
- numpy 1.21.5
- matplotlib 3.5.1
- pydotplus 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_y=True, as_frame=True, parser='auto') 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+', header=None, names=columns) X = data.drop(columns="credit_risk") y = data["credit_risk"]这里你可能会踩第一个坑:原文件中某些字段的值带下划线(比如“A11”、“A12”这种编码),直接读会变成整数或字符串混合类型。别急,后续统一做编码处理。
2.2 特征组解析:哪些特征最关键
20个特征中,一部分是数值型,一部分是类别型,类别型又分为有序和无序两类,处理逻辑完全不同。
数值型特征有这些:
- 贷款金额(duration):单位是月,范围从4到72
- 贷款额度(credit_amount):几百到一万多马克不等
- 年龄(age):19到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_size=0.3, random_state=42, stratify=y_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~20 |
| min_samples_leaf | 叶子节点最少样本数 | 建议3~10,是抑制过拟合的关键 |
| max_features | 每次寻找最佳切分时考虑的特征数 | 默认None,指考虑全部特征;树多时可考虑sqrt或log2 |
| random_state | 随机种子 | 务必固定一个整数值,否则每次结果漂移 |
| class_weight | 类别权重 | 数据不平衡时可设为"balanced" |
第六项random_state值得多说一句。虽然决策树本身是确定性的算法,但在特征值完全相同的情况下,sklearn会随机打破平局来选择切分特征。不固定种子的话,你同一段代码跑两次,出来的树可能不一样,准确率也会上下波动。我在实验里统一使用了random_state=42,所有结果可复现。
3.4 过拟合的形成与剪枝策略
决策树堪称“天生过拟合选手”。你没有限制深度时,它会把训练集学得面面俱到:不仅学到真正的规律,还把噪声样本、异常样本的个性化模式统统背下来。表现就是训练集准确率接近100%,测试集准确率只有60%,差距巨大。
剪枝策略分两种。事前剪枝是在树生长过程中提前停止分裂,靠的是max_depth、min_samples_leaf等参数;事后剪枝是先让树完全生长,然后从下往上修剪掉不重要的分支。sklearn只实现了前者,所以实验里能做的就是参数搜索。
我的经验是一步一步来。先跑一棵完全生长的树,看它过拟合有多严重;然后先用max_depth限制深度(从3到8逐个试),同时结合min_samples_leaf来稳定最小叶子大小;最后再用GridSearchCV做一次联合搜索。跑下来的参数组合通常落在max_depth=4~6、min_samples_leaf=4~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+', header=None, names=columns) X = data.drop(columns="credit_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_size=0.3, random_state=42, stratify=y_encoded ) # 4. 训练默认决策树,观察过拟合基线 clf_default = DecisionTreeClassifier(random_state=42) 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_state=42), param_grid, cv=5, scoring='accuracy', 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: 5
- min_samples_leaf: 4
- min_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_names=X.columns, class_names=["坏", "好"], filled=True, rounded=True, fontsize=9 ) plt.savefig("decision_tree.png", dpi=150, bbox_inches='tight') plt.show()画出来的图里,每个节点显示切分条件、基尼系数、样本数和类别分布,颜色深浅代表纯度。max_depth=5的树整体不会太庞大,勉强能看清楚细节规则。不过当树的节点超过40个时,图上文字会挤成一团,这时我建议改用文本方式导出规则:
from sklearn.tree import export_text tree_rules = export_text(clf_best, feature_names=list(X.columns), max_depth=5) 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_, index=X.columns).sort_values(ascending=False) print(feature_importance.head(10))实测下来,排名靠前的特征大致有status(账户状态)、credit_history(信用历史)、duration(贷款期限)、age(年龄)、credit_amount(贷款金额)。status的重要性可能超过0.25,credit_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目录配置到系统PATH,pip安装的pydotplus只是Python接口,替代不了本体。如果不想折腾,直接用plot_tree输出png就行,它已经把graphviz的底层调好了,不依赖外部软件。
5.4 类别不平衡导致评估指标失真
德国信贷数据里坏客户只占30%,如果模型全预测“好”,准确率也能到70%。这种“懒模型”会在分类报告里暴露出来:坏客户(标签0)的召回率可能是0。所以不要只看accuracy,classification_report和混淆矩阵才是裁判。我在实验里注意到,调优后的模型对坏客户的召回率在0.4~0.5之间,precision大概也在0.4上下。如果目标是“尽量别放过坏客户”,可以尝试class_weight='balanced'。
但class_weight='balanced'有个副作用:模型会更激进地预测“坏”,误伤好客户的概率也上升。信贷风控里“误杀”和“漏网”两难平衡,到底侧重哪边,取决于放贷机构的经营策略。这个取舍不是纯技术问题,我建议实验中两种方案都跑一下,对比混淆矩阵后再做决定。
5.5 随机森林是决策树的合理延伸
实验做完后,扩展方向很自然地指向随机森林。随机森林就是几十棵决策树的投票集合,通过行采样和列采样制造多样性,显著降低单棵树的方差。用同样的数据跑随机森林,准确率通常能到0.75~0.80,比单棵决策树再提升几个点。如果项目后面有提升效果的要求,可以从RandomForestClassifier开始,其他参数沿用决策树调优时的经验值。
还有一个扩展点是决策树回归。如果标签不是好/坏,而是贷款金额或违约概率,DecisionTreeRegressor就是对应的选择,原理完全一致,只是不纯度度量换成了均方误差。学有余力可以一并尝试。
5.6 排查问题速查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 列名报错或首行当数据 | 原文件无表头 | 手动指定columns,sep='\s+' |
| 字符串特征导致拟合失败 | 未做类别编码 | 对每个object列LabelEncoder/OrdinalEncoder |
| 训练集接近100%、测试集很低 | 过拟合 | 限制max_depth、提高min_samples_leaf |
| 不同次运行结果不一样 | 没有固定随机种子 | random_state设固定整数 |
| accuracy很高但坏客户召回为0 | 类别不平衡 | 看classification_report,必要时class_weight='balanced' |
| graphviz无法导出 | 外部软件未安装 | 改用plot_tree直接出图 |
| GridSearchCV参数过多跑得慢 | 网格膨胀 | 减少候选值、增加cv步数前先手动粗调 |
6. 一点实操心得
实验做到最后,真正有价值的东西反而不是那棵调参调得不错的树,而是理解了这个流程里每个决策背后的权衡逻辑。比如为什么要分层抽样、为什么要看混淆矩阵而不是只看准确率、为什么树的深度不是越深越好。把这些想通了,换任何数据集、任何树模型,你的思路都是通的。
我自己当时做这个实验踩了无数次坑,最深的体会是:不要一上来就调参,先跑一棵默认树,把它的过拟合程度“量化”出来,你就知道该往哪个方向努力了。默认树训练集0.97、测试集0.66,差距摆在那里,接下来调参就不是盲调。
如果你打算在这个基础上做课程报告,建议额外加上:用测试集上挖到的几个典型规则做业务解释(比如“账户余额低于200且贷款期限超过21个月,大概率是坏客户”),这种从模型规则到业务语义的翻译能力,比单纯堆砌代码值钱得多。后续有时间,还可以把数据按时间顺序切分成训练集和验证集,模拟真实放贷场景下的在线评估效果,那又是另一个维度的挑战了。