资讯详情

决策树实战:Adult收入预测的数据清洗、剪枝与调参全流程

📅 2026/10/17 9:57:30 | 华诺云谱 👁 阅读
决策树实战:Adult收入预测的数据清洗、剪枝与调参全流程
很多人第一次接触机器学习分类问题都喜欢拿Adult数据集练手也就是常说的美国收入预测。这个数据集本身不算复杂特征也规整但真要把它做好尤其是用决策树这一套模型玩出点花样来里面有太多不经踩不知道的坑。我最初做这个项目的时候以为把数据读进来、跑个DecisionTreeClassifier就完事了结果一堆细节根本没过关缺失值处理方式不对、类别特征编码策略粗糙、剪枝参数全凭感觉最后模型在训练集上漂亮一到测试集就露馅。这篇文章就把我整个实操过程、踩过的坑和最终的调优思路完整拆给你适合正在学机器学习的基础型选手也适合复习决策树细节的人当作练兵场。1. 内容整体设计与思路拆解1.1 项目核心是什么Adult数据集来自美国人口普查收入数据库每一条记录代表一个人包含年龄、教育年限、婚姻状况、职业、每周工时等属性标签是年收入是否超过5万美元。表面上看这是一个二分类问题但它真正考验的是特征工程与模型泛化的平衡能力14个原始特征里有连续数值也有无序类别甚至还有被“?”替代的缺失值如果不做充分的预处理再好的算法也会白搭。我的核心目标是做一个端到端的决策树分类方案重点关注三件事正确解析与清洗Adult原始数据利用决策树模型进行可解释性分类预测通过网格搜索做剪枝与超参数调优让模型在测试集上有稳定表现。很多人喜欢上来就堆模型、比精度但这个项目最大的价值不是刷多高的准确率而是搞懂决策树是怎么根据特征分裂的。收入预测场景里树模型天然能回答“什么特征最能区分高收入人群”这类问题这是黑盒模型做不到的。所以我在整个设计里刻意让决策树留到两棵——一棵保持浅层结构方便直接观察规则另一棵做完整调优用来评估最终效果。1.2 为什么选决策树而不选别的模型有的同学会问都2025年了直接上XGBoost、LightGBM或者随机森林不是更省事当然可以但决策树作为弱学习器是所有树模型家族的地基。Adult数据集规模适中大约48842条样本放到单棵决策树下面训练复杂度完全可控训练速度又极快还有额外的可解释性优势。更重要的是收入预测潜在的业务场景往往需要跟业务方解释判定规则这时候“年收入是否超5万”的决策路径可以直接翻译成一句人话比如“如果每周工作超过50小时且教育年限超过14年则判定为高收入”这种透明度调参时帮了我大忙。我用到的技术栈也很常规但不代表没有讲究pandas负责数据清洗与特征工程scikit-learn提供决策树、评估指标和网格搜索工具matplotlib画树结构图辅助观察分裂规则。搞明白选择逻辑后面每一步才有依据。决策树唯一的软肋是过拟合所以在做整个项目时剪枝几乎成了每一步都要惦记的事情。后面我会专门说明怎么控制树深、叶节点数量和最小样本数这里先按下不表。2. 数据预处理与特征工程细节2.1 原始数据长什么样Adult数据集的官方列包括age年龄workclass工作类型如Private、Self-emp-not-incfnlwgt抽样权重本质上是人口权重education学历文字描述如Bachelors、HS-gradeducation-num学历数值等级范围1到16marital-status婚姻状态occupation职业relationship家庭角色race种族sex性别capital-gain资本收益capital-loss资本损失hours-per-week周工时native-country原籍国income标签50K或50K。其中fnlwgt我最终选择不使用原因是它代表抽样权重不是收入预测的有效因果特征。直接用会引入噪声有时还会让某些异常样本占据过高的分裂权重得不偿失。2.2 缺失值的坑Adult数据集里缺失值处理是第一步。原始文件用?表示未知值而不是空值如果用pandas直接读?会被当成普通字符串后续数值计算就会崩或者产生一堆无效类别。我做了这样几步处理加载所有列为字符串类型统一将?替换为NaN对缺失列分类统计需要处理的列主要是workclass、occupation、native-country在策略上把缺失项单独作为一类不直接删除样本。关于第三点我踩过坑。一开始我图省事把所有含缺失值的行直接dropna()删掉。听起来没什么但Adult数据集的缺失值不完全是随机缺失某些职业和工种类的信息缺失可能有隐藏模式。删掉之后训练集直接少了三千多条样本评估结果受到干扰。正确做法是保留这些样本让缺失本身成为一个可区分的属性。比如把workclass缺失的样本单独填充为Unknown字符串决策树分裂时就能判断“缺失工作类型的人”是否具备统计显著性。2.3 编码策略有序与无序要分开Adult数据里有一类特征很容易被忽略——教育程度。education和education-num描述的是同一个信息前者是文本后者是数值等级。有人两个都留下这就造成了多重共线性决策树会在冗余特征间随机选择分裂树的可解释性反而变差。我只保留education-num因为等级本身有天然顺序比如HS-grad对应9Bachelors对应13Master对应14这让树的分裂阈值更自然。其余类别特征比如workclass、marital-status、occupation、relationship、race、sex、native-country都是无序类别标准做法是One-Hot编码。但我没有盲目把所有类别都展开像native-country这种高基数列转成One-Hot后会增加大量稀疏维度决策树的效率反而降低。我参考了常见实践把native-country里出现频次过低的取值统一合并成Other再进入One-Hot编码流程。数值特征方面capital-gain和capital-loss非常稀疏大量样本的值都是0。很多人直接归一化但我保留原值原因是决策树不依赖特征尺度它找的是切分点归一化不会带来额外好处反而丢失了可解释性。如果用的不是树模型这条建议就另当别论了。2.4 数据集切分与标签处理训练集和测试集我保持官方的划分方式直接用adult.data和adult.test两个文件。但有个经典大坑adult.test里面有个多余的奇怪空格点导致列数跟训练集不同而且第一行的标签后面带个后缀测试集读进来经常错位。我的处理是自己清洗测试文件头部同时把标签统一成二值income_map {50K: 0, 50K: 1, 50K.: 0, 50K.: 1}为了后续统计方便特征矩阵统一用X_train、X_test表示目标向量用y_train、y_test表示。每次模型训练前我都检查一下X_train.shape和y_train.shape是否对齐之前因为多删行少删列导致维度不匹配的报错排查起来真够折腾。3. 决策树核心机制与参数选型3.1 分裂原理一句话讲透决策树的工作方式就是递归地把数据按特征阈值切分使得每个子节点内的样本尽量属于同一类别。切分质量的衡量指标常用两个信息熵和基尼系数。以基尼系数为例如果某节点中高收入样本占比为$p$那么基尼系数为$$Gini 1 - (p^2 (1-p)^2)$$当所有样本都属于同一类别时基尼系数为0表示纯度最高。决策树选特征时就是计算每个候选特征切分前后的基尼增益挑增益最大的那个做分裂。Adult数据集里收入分布天然不平衡大约75%的人收入不超过5万25%高收入。这种不平衡会诱导树一味把样本分成低收人类为了压制这个问题我重点依赖两个手段调class_weight和限制树深度。class_weightbalanced会让少数类的误分类代价更高树分裂时会更倾向把高收入群体分出来。在Adult场景里如果业务目标是尽量找出高收入潜力人群这比单纯刷准确率有意义得多。3.2 关键超参数与调优逻辑scikit-learn的决策树可调参数很多但真正值得在Adult数据集上优先抠的是下面这几个max_depth控制最大深度防止树无限分裂min_samples_split分裂一个内部节点至少需要的样本数min_samples_leaf叶节点至少需要的样本数criterion不纯度指标gini或entropy都可以试效果差异不大class_weight类别权重重点在缓解不平衡max_features每次分裂时考虑的特征数量设成sqrt能增加随机性但单棵树上我一般用默认。一开始我用默认参数训练树直接长到三十多层训练集准确率98%以上测试集却只有83%不到标准过拟合。后来我把max_depth限制在5到15之间、min_samples_leaf设为20以上训练和测试的差距才逐渐收窄。这个经验在类似表格数据集上普遍适用。3.3 基于网格搜索的调参套路我采用了两阶段网格搜索第一阶段先粗调固定criteriongini把max_depth、min_samples_split、min_samples_leaf组合成小网格用5折交叉验证搜索最稳妥的组合范围。这一步不需要把参数网格设得太细先把量级定下来。第二阶段细调在粗调结果附近缩小步长加入class_weight和max_features选项。之所以分两步是因为如果一开始就把所有组合铺开网格规模会非常大训练耗时成倍增加完全没有必要。下面是最终使用的调参代码框架我做了注释你可以直接复制改from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import GridSearchCV param_grid { criterion: [gini, entropy], max_depth: [6, 8, 10, 12, 14], min_samples_split: [20, 50, 100], min_samples_leaf: [10, 20, 40], class_weight: [None, balanced] } dt_clf DecisionTreeClassifier(random_state42) grid_search GridSearchCV( estimatordt_clf, param_gridparam_grid, scoringroc_auc, cv5, n_jobs-1, verbose1 ) grid_search.fit(X_train, y_train) print(best_params_:, grid_search.best_params_) print(best_score_:, grid_search.best_score_)为什么评分函数选roc_auc而不是accuracyAdult的类别分布并不均衡准确率会被大多数低收人类样本带跑偏用AUC评估排序能力更贴合实际业务中“找到高收入人群”的目标。实际结果出来后最优参数里class_weight几乎必定是balanced这也在侧面印证了数据不平衡的影响。4. 实操过程与最终效果4.1 完整数据处理与训练流程先看数据清洗这一块怎么落地。加载数据时如果直接用read_csv你会遇到两个经典问题一是列名需要自己指定二是?缺失值被当成普通对象。以下是我清洗操作的核心部分import pandas as pd import numpy as np columns [ age, workclass, fnlwgt, education, education_num, marital_status, occupation, relationship, race, sex, capital_gain, capital_loss, hours_per_week, native_country, income ] train_df pd.read_csv(adult.data, headerNone, namescolumns, skipinitialspaceTrue) test_df pd.read_csv(adult.test, headerNone, namescolumns, skipinitialspaceTrue, skiprows1)注意我特意加了skipinitialspaceTrue专门对付文件里的前导空格。测试集用skiprows1跳过那个多余行这样训练和测试结构就对齐了。接下来统一替换缺失值train_df train_df.replace(?, pd.NA) test_df test_df.replace(?, pd.NA)然后对三个类别列做缺失填充单独成类for col in [workclass, occupation, native_country]: train_df[col] train_df[col].fillna(Unknown) test_df[col] test_df[col].fillna(Unknown)这里的逻辑是保留缺失样本让“是否缺失”也成为可用信息。像native_country的缺失可能意味着人口流动记录不全统计意义上可能有微弱信号。4.2 编码与建模主体代码类别特征处理我统一封装成函数方便训练集和测试集保持同样的编码规则避免测试集出现训练集没见过的类别而报错。主要用OneHotEncoder配合ColumnTransformerfrom sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder, StandardScaler categorical_cols [workclass, marital_status, occupation, relationship, race, sex] # native_country 做了低频合并后再进入这里 numeric_cols [age, education_num, capital_gain, capital_loss, hours_per_week] preprocessor ColumnTransformer( transformers[ (num, passthrough, numeric_cols), (cat, OneHotEncoder(handle_unknownignore), categorical_cols) ] ) X_train preprocessor.fit_transform(train_df.drop(columns[income, fnlwgt])) y_train train_df[income].map({50K: 0, 50K: 1}).astype(int) X_test preprocessor.transform(test_df.drop(columns[income, fnlwgt])) y_test test_df[income].map({50K.: 0, 50K.: 1}).astype(int)为什么数值列用passthrough而不用标准化决策树做的是阈值切分特征的绝对尺度不影响分裂点选择。如果换成逻辑回归或SVM数值列就必须标准化这里不能照搬。实际调参训练时我把最佳参数放进最终模型再训练一次随后在测试集上评估。这里强调一下最终模型一定要在全部训练数据上重新拟合而不是直接把网格搜索里的best_estimator_拿过来用因为网格搜索内部使用的是交叉验证的训练折不是全量训练集。4.3 关键评估指标与特征重要性解读我在测试集上计算了准确率、精确率、召回率、F1和AUC并根据grid_search.best_params_得到的结果类似下面这样模型方案测试集准确率测试集AUC训练集准确率默认决策树0.83410.87630.9850深度限制100.85320.89510.8814调参平衡权重0.86050.90980.8712看到训练集与测试集的差距从15个百分点缩到1个百分点左右你就知道剪枝的力量有多直接。不要觉得测试集准确率86%不够惊艳Adult数据集本身特征维度有限很多噪声和隐藏因素没法靠这十几个字段完全刻画能做到这个水准并且保持规则的可解释性已经是决策树在这份数据上的合理表现了。特征重要性输出时排在前面的通常是capital_gain、education_num、hours_per_week、age、marital_status这跟收入预测的生活常识完全吻合。资本收益和教育水平是最强的区分因素反而是fnlwgt这类权重变量几乎不起作用也验证了我前面丢弃它的选择。4.4 树结构可视化与规则提取调优后我建议你把树画出来。对于可解释性强的浅层树一棵深度5的树可以直观展示“月收入50K”背后的决策路径。这里给一个极简可视化方案import matplotlib.pyplot as plt from sklearn.tree import plot_tree plt.figure(figsize(18, 10)) plot_tree( best_model, feature_namesfeature_names, class_names[50K, 50K], filledTrue, roundedTrue, max_depth4 ) plt.savefig(tree_visual.png, dpi200, bbox_inchestight)输出图形后你可以沿着根节点到叶节点把最重要的几条路径变成自然语言规则。比如若capital_gain 5095.5且education_num 12.5且hours_per_week 42.5则预测为低收人类若capital_gain 5095.5则预测为高收人类的概率明显升高。光看数值4848这种阈值你可能不理解但配合业务就能读出含义资本收益一旦超过一定水平收入就很容易跨过5万门槛。这就是决策树在这个项目中最大的价值——模型不是黑箱规则可以反哺业务理解。5. 常见问题与排查技巧实录5.1 测试集读入错位adult.test文件因为头部有个奇怪的点号最开始我用pd.read_csv直接读发现数据列对不上全被并到了最后一列。排查时打印test_df.head()就发现了端倪。解决办法就是skiprows1同时给测试集的标签单独做映射。这个问题在网上讨论度很高但自己做一遍才真正长记性。5.2 维度不匹配与编码规则不一致这在做ColumnTransformer时比较常见训练集有的类别测试集没有直接transform就会报错。两点经验全部类别特征用OneHotEncoder(handle_unknownignore)低频类别合并时要保证训练集和测试集使用的是同一个合并规则不能单独统计。我在处理native_country时是先从训练集统计频次把低于指定阈值的所有取值统一替换成Other再用同一份映射作用到测试集。如果不这样做两边One-Hot后的列数不一致后续建模会直接崩。5.3 准确率虚高与过拟合的识别默认参数下训练集准确率接近98%那时候我还暗自高兴结果测试集只有83%明显过拟合。判断依据不仅仅是两个准确率的差值还能看树的max_depth默认情况下树会长到40多层这样深的树在表格数据上基本是拿运气硬记样本。处理方式很简单限制max_depth通常Adult上6到14之间比较合理提高min_samples_leaf让叶节点不要太细碎结合交叉验证结果再微调。5.4 类别不平衡带来的假象Adult里低收人样本占75%如果什么都不管模型永远预测低收入准确率也有75%。这种准确率没有业务意义。要识别这个问题不要只看准确率而是看AUC、召回率和F1。我用class_weightbalanced之后高收入类别的召回率有了明显提升而低收人类别的召回率稍微下降整体更均衡。类权重参数是决策树处理不平衡数据时最轻量的武器比采样更省事。5.5 状态随机性导致复现失败决策树分裂时特征顺序、随机种子都会影响结果。我做了两个固定动作在模型里传random_state42在划分或交叉验证时固定shuffle的随机数值避免结果漂移。如果这些没固定你连续跑两次网格搜索最优参数都可能不同后面所有结论都会跟着变。6. 实际扩展方向Adult数据集配合决策树虽然经典但如果你想让这个项目更有分量我个人比较推荐几个扩展方向第一个是把决策树换成梯度提升树或者随机森林做模型对比分析。对比时统一用同一份清洗和编码后的数据记录准确率、AUC、训练时间和特征重要性差异能更立体地看到不同树模型的优劣。第二个是不用顺序拆分改为分层抽样交叉验证。对类别不平衡数据分层交叉验证能保证每一折的类别比例跟总体一致评估结果更稳定。第三个是做一份收入预测规则的业务化包装。比如把树路径转成SQL规则模拟“哪些人群更可能年收入超5万”的筛选场景。这个方向会跳出纯算法真正把模型价值落到实处。我在实际操作中也试过用逻辑回归和朴素贝叶斯做对比前者在AUC上和调参后的决策树接近但规律的可读性明显差后者受特征独立性假设影响效果稳定偏弱。如果手头时间有余把这些模型横向比较一番得出的结论会比你单独展示一棵决策树更有说服力。最后再分享一个小心得做这类经典数据集项目没必要一开始就追求复杂模型先用手头最基础的算法走通全流程把数据清洗、编码、评估、调参这些环节练成肌肉记忆再逐步加复杂度。你踩过的每一个维度坑、每一个handle_unknown报错都会在以后接触更大规模数据时变成经验储备。决策树在Adult上做到AUC稳定在0.91左右已经足以说明你把这套流程掌握扎实了。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑