资讯动态

Titanic数据集实战:用sklearn构建可解释的决策树分类模型

发布时间:2026/10/22 3:18:31 来源:尧图企业网站定制
最近不少读者在留言里提到学完了sklearn的基础操作接下来要找一个能完整走一遍分类决策树实战流程的数据集问我有什么推荐。我的答案一直是同一个——Titanic数据集。这个数据集不复杂但是五脏俱全有数值字段、类别字段、缺失值、弱相关的干扰字段正好能把决策树建模的全流程练一遍。而且决策树最大的优点就是“可解释”训练完之后你能亲手画出这棵树看清楚模型每一步是根据什么下判断的。所以这篇就用Titanic数据集完整走一遍sklearn分类决策树的实践流程。1. 为什么拿Titanic练决策树一个能看清模型“逻辑”的入门数据集1.1 数据集长什么样Titanic数据集来自一个全球知名的数据竞赛社区是机器学习入门最经典的表格型数据之一。它的任务是预测船上乘客是否生还属于典型的二分类问题。训练集有891条样本每条样本包含了一批乘客的基础信息。我这里把核心字段列一下字段名含义类型补充说明Survived是否生还标签0代表否1代表是Pclass船舱等级离散1/2/3三个等级Sex性别类别male / femaleAge年龄数值有缺失SibSp同船的兄弟姐妹/配偶数数值整数Parch同船的父母/子女数数值整数Fare票价数值连续值Embarked登船港口类别C / Q / S有缺失Cabin客舱号类别缺失非常严重Ticket票号类别基本是噪音实战中一般丢弃这类数据的典型特征是字段不多但类型足够杂覆盖了“数值字段如何处理”“类别字段如何编码”“缺失值怎么补”这几个数据预处理阶段的高频问题。891条的体量也刚刚好——模型训练秒级完成你能在几分钟内反复迭代实验而不是像深度学习那样跑一次等半天。另外要注意原始的乘客信息里还有Name和Ticket这种自由文本字段。网络上不少教程会把Name里的称呼比如Mr、Mrs拆出来当特征这个属于锦上添花。第一次做决策树实践建议先不做这种花活把上面表格里的结构化字段处理好就已经足够说明问题了。1.2 为什么是决策树而不是别的模型面对同一个分类问题可以选的模型很多逻辑回归、SVM、随机森林、神经网络都能做。但作为入门实践决策树有几个不可替代的优势第一决策树不需要对特征做标准化。神经网络和SVM对特征的尺度会很敏感数值大的特征容易主导训练而决策树每次切分只比较单个特征的阈值特征本身的缩放不影响分类结果。这意味着拿到数据后少操心一道工序。第二决策树天然兼容数值与类别特征。虽然类别特征还是要编码成数字但决策树对编码方式的宽容度远高于线性模型哪怕你把有序的Pclass直接当成整数1、2、3来用树也能通过合理的阈值切分把它玩好。第三决策树可视化之后几乎等于一份可读的规则说明书。你可以把整棵树的判断逻辑画出来一格一格地读。这对理解机器学习“特征怎么影响预测结果”这件事帮助远比一个冷冰冰的准确率数字大得多。2. 数据处理缺失值、离散字段与特征构造的实操细节2.1 读数据与信息核查实战第一步不是急着建模而是先确认数据质量。拿到csv文件之后先用pandas读进来然后看基本结构import pandas as pd df pd.read_csv(titanic_train.csv) print(df.shape) print(df.info()) print(df.isnull().sum())执行完以后应该能看到Age有177个缺失Cabin有687个缺失Embarked有2个缺失。这三个缺失值情况完全不同处理方式也应该区别对待这是初学者最容易忽略的地方——以为“缺了就用均值补”结果所有字段一视同仁其实不对。我的习惯是先想清楚每个字段缺失背后的业务含义再决定处理方法。Cabin缺失这么严重硬补是补不出来的Age缺失属于局部缺失可以用统计量填充Embarked只缺2条几乎不影响大局。2.2 缺失值处理中位数、众数与“有/无”特征Age字段的处理我推荐用中位数填充而不是平均数。原因是年龄分布偏态比较明显个别年龄很大的乘客会把平均值拉高而中位数更能代表“普通乘客”的水平。在sklearn里可以用SimpleImputer但为了第一次看代码不头晕直接用pandas的fillna也行df[Age] df[Age].fillna(df[Age].median()) df[Embarked] df[Embarked].fillna(df[Embarked].mode()[0])Embarked只有2个缺失用众数S补齐即可。这里必须插一句关于数据泄漏的提醒。理论上的严谨流程是先把数据分成训练集和测试集然后在训练集上计算出中位数再用这个中位数去填充训练集和测试集。我上面的写法直接用了全量数据的中位数这在891条数据上对年龄填充几乎不会有可感知的影响很多基础教程也这么干。但如果你养成了“先看全量统计再填充”的习惯以后处理规范化、标准化这类操作时很容易把测试集的信息无意中代入训练过程这是一个非常隐蔽的工程隐患。养成好习惯涉及可学习的参数均值、标准差、分位数定点的预处理一律先拆分再fit_transform。Cabin字段我不建议直接删掉而是构造了一个衍生特征“是否有客舱记录”df[HasCabin] df[Cabin].notna().astype(int) df df.drop(columns[Cabin, Ticket, Name, PassengerId])逻辑很简单缺失比例超过70%的字段直接填充只会制造大量虚假信息但不等于这个字段没有预测力——有没有登记到客舱号本身就代表着一种信息状态。这种“缺失也作为特征”的思路在真实业务里经常能用上。2.3 离散特征编码与手工特征构造Sex和Embarked是类别特征需要转成数值。Sex用最简单的映射Embarked用one-hotdf[Sex] df[Sex].map({male: 0, female: 1}) df pd.get_dummies(df, columns[Embarked], drop_firstTrue)关于Pclass我特意没做one-hot而是保留整数。有些教程一看到“类别”就习惯性one-hot但决策树与线性模型不同Pclass的1、2、3是有序的保留整数后树可以切出“Pclass 1.5”这类对业务有明确含义的规则而one-hot之后每一列单独参与分裂反而丢掉了等级之间的顺序关系。逻辑回归对数值刻度敏感这类有序变量需要谨慎处理但在树模型里直接用整数一般没有坏处。再补两个手工特征家庭规模、是否独身。SibSp和Parch单独看比较碎合并之后信息更集中df[FamilySize] df[SibSp] df[Parch] 1 df[IsAlone] (df[FamilySize] 1).astype(int)这个特征是典型的领域常识驱动船上救援时有家人在身边和独自一人生存概率会有系统性差异。这种“从业务常识里长出特征”的做法比无脑堆更多字段重要得多。2.4 特征列表定型最后定下进入模型的特征features [Pclass, Sex, Age, SibSp, Parch, Fare, HasCabin, FamilySize, IsAlone, Embarked_Q, Embarked_S] X df[features] y df[Survived]到这里数据预处理就算完成了。这一套流程下来原始14列被处理成了10个干净特征每一步都能说出为什么这比“对着网上的代码抄一遍”有价值得多。3. 训练一棵决策树之前先看懂剪枝参数的博弈逻辑3.1 不纯度指标gini与entropysklearn的DecisionTreeClassifier有两个常用的特征选择准则criteriongini和criterionentropy。这俩到底怎么选很多初学者会纠结。简单理解两者都在量化“这个节点有多不纯”。如果某个节点里两类样本各占一半那它非常不纯如果某个节点几乎全是同一类那它已经很纯了。树的分裂过程就是在不停寻找一个特征和阈值把当前节点切成两个更纯的子节点。基尼系数的直观解释是从节点里随机抽两个样本它们属于不同类别的概率。这个概率越小节点越纯。信息熵则是信息论里的概念衡量的是节点内部的混乱程度。实操上二者的差异通常很小sklearn默认用gini主要是因为它不涉及对数运算计算更快。所以第一次实验直接用默认的gini就好不要在这个选项上耗太多时间。3.2 剪枝参数是怎么作用的决策树最核心的问题只有一个如何平衡拟合程度与泛化能力。一棵不限制生长的树可以一直分裂到每个叶子节点只有一个样本训练集准确率直接逼近100%但测试集表现会大幅下滑。这就是过拟合。控制过拟合靠的就是一组剪枝相关参数。我把最常用的几个整理成一张对照表参数默认值作用我的经验取值max_depthNone限制树的最大深度3~8min_samples_split2内部节点至少需要多少样本才继续分裂5~20min_samples_leaf1叶子节点至少要有多少样本2~10max_featuresNone每次分裂考察的特征数量默认即可random_stateNone随机种子影响结果可复现性必须固定这几个参数里max_depth是最直观的“刹车”。设成3树就最多做3层判断规则简单清晰画出来也不会糊。min_samples_leaf则相当于给每个最终结论设置了一个“最小调查样本数”防止模型因为个别样本的巧合就单独开一条规则。我的习惯是先靠这两个参数稳住结构再根据效果微调其他项。3.3 先跑一个默认参数的基线开头不要直接上网格搜索先把一个默认参数的模型跑出来当作基线后面所有调优才有对照from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) base_tree DecisionTreeClassifier(random_state42) base_tree.fit(X_train, y_train) print(train acc:, base_tree.score(X_train, y_train)) print(test acc:, base_tree.score(X_test, y_test))我这里跑出来的结果训练集准确率接近0.98测试集大概0.78。这个差距就是在告诉你树已经严重过拟合了。它把训练数据里的噪声都背了下来但在没见过的数据上表现明显下滑。默认参数接近无限生长的状态几乎没有做任何剪枝。记住这个数字下面所有操作都冲着“缩小训练集与测试集差距”这一个目标去。4. 把训练好的树“画”出来读树、找分裂点、发现过拟合4.1 可视化一棵深度为3的树限制深度以后树就具备可读性了。这里用深度3做一个示例from sklearn.tree import DecisionTreeClassifier, plot_tree import matplotlib.pyplot as plt tree_vis DecisionTreeClassifier(max_depth3, min_samples_leaf5, random_state42) tree_vis.fit(X_train, y_train) plt.figure(figsize(18, 10)) plot_tree( tree_vis, feature_namesfeatures, class_names[Not Survived, Survived], filledTrue, roundedTrue, fontsize10 ) plt.show()运行这段代码前确认你的sklearn版本在0.21以上否则没有plot_tree这个函数。如果版本太老要么升级要么退回用export_graphviz导出后用graphviz软件查看后者麻烦不少。4.2 怎么读一棵树可视化出来之后这棵树会告诉你很多信息。最顶端的根节点就是整个模型的第一个判断依据也是全局最重要的特征。在Titanic这个数据集上根节点的分裂特征几乎必定是Sex。为什么会是它因为性别对生存率的区分度最强女性生还比例远高于男性。决策树在每一步都会自动计算所有特征带来的不纯度下降选出下降最多的那个这本质上就是一个自动的特征重要性排序。往下走一层你会看到Sex分裂之后男性分支下一个重要特征大概率是Age或Pclass女性分支则可能是Fare或Pclass。这相当于树自己找到了数据里的规律男性中更年轻或更高船舱等级的人群生还率更高女性中舱位等级和票价也进一步区分了结果。每一片叶子节点上的value数组和samples数量值得重点看。比如叶子节点显示[200, 30]意味着这个分组里200人未能生还、30人生还于是这个叶子最终预测为“Not Survived”。你可以顺着一条路径读出一条人类语言规则“男性并且年龄大于13.5岁并且舱位等级不是一等舱预测为未生还。”这就是决策树可解释性的价值所在。4.3 从画图里一眼看出过拟合画图不仅能帮你理解模型还能帮你直观感知过拟合。你把max_depth分别设成3和8各画一棵树对比会看到两个明显差异深度8的树节点数量爆炸很多叶子节点的samples只有1到2深度3的树叶子节点通常有几十个样本。叶子样本数量少意味着模型在做极细粒度的区分。这不是在学规律而是在背样本。当你感觉一棵树的图像已经密密麻麻看不清的时候基本可以断定它过拟合了即使不跑测试集也能判断个大半。所以我做决策树实验的一个习惯是调参之前先画图视觉上先淘汰一批明显不合理的配置。5. 评估模型可信度准确率之外还要看什么5.1 划分数据时别忘了stratify前面代码里我特意用了stratifyy很多初学者不知道这个参数的意义。Titanic训练集中生还者的比例大约是38%如果随机划分后碰巧测试集里生还者比例变成了50%那你在测试集上评估的结果就有偏。stratify能让划分前后的类别比例保持一致相当于给测试集做了一次“配额抽选”。这在类别不平衡的场景下尤其重要。如果你的数据里正样本只有5%随机划分后测试集甚至可能一个正样本都没有评估结果完全失真。5.2 混淆矩阵与精确率、召回率准确率只是评估的起点。生还者比例本来就偏低如果一个模型什么都预测为“未生还”准确率也能到60%多。要想看得更细需要混淆矩阵和分类报告from sklearn.metrics import confusion_matrix, classification_report y_pred tree_vis.predict(X_test) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred))以深度3加min_samples_leaf5的模型为例混淆矩阵可能是这样的预测未生还预测生还实际未生还15216实际生还5149这个矩阵告诉我们模型在“未生还”类别上表现很好但在“生还”类别上51个实际生还的人被漏判了。查全率recall低意味着模型对生还者的识别不够敏感。如果你关心的是“尽可能找到所有生还者”那只看准确率是不够的必须盯住生还类别的recall指标。分类报告里的precision和f1-score同样要看。f1-score是精确率和召回率的调和平均适合在两者之间取平衡。每次实验后在纸上记录这些指标比只记一个准确率高一个层级。5.3 用交叉验证替代单次划分train_test_split的结果有一个问题随机性很大。同样的模型换一次随机种子测试集准确率可能差出三五个百分点。你很难判断这个波动到底是模型好坏还是划分运气。交叉验证能缓解这个问题。把数据分成k份轮流拿其中1份当验证集、其余k-1份训练最终得到k个分数取平均。sklearn里直接一行from sklearn.model_selection import cross_val_score scores cross_val_score( DecisionTreeClassifier(max_depth3, min_samples_leaf5, random_state42), X, y, cv5, scoringaccuracy ) print(scores) # 每一折的分数 print(scores.mean()) # 平均分交叉验证的另一个好处是能观察方差。如果5折的分数分别是0.80、0.78、0.76、0.85、0.70波动明显说明模型在不同数据子集上稳定性不足此时就算平均值好看上线也要慎重。6. 调参过程的完整记录与几个隐蔽的坑6.1 网格搜索参数候选与结果手工一个个试参数太慢直接上GridSearchCV做参数搜索from sklearn.model_selection import GridSearchCV param_grid { max_depth: [3, 4, 5, 6, 8], min_samples_split: [2, 5, 10, 20], min_samples_leaf: [1, 2, 5, 10] } grid_search GridSearchCV( DecisionTreeClassifier(random_state42), param_grid, cv5, scoringaccuracy, n_jobs-1 ) grid_search.fit(X_train, y_train) print(grid_search.best_params_) print(grid_search.best_score_)在我这边跑下来最优参数基本会落在max_depth4或5、min_samples_leaf5左右的组合上。这时测试集准确率大概在0.82上下比默认参数提升了4个百分点左右而训练集准确率回落到0.87附近。训练集和测试集的差距从0.20缩小到0.05说明过拟合得到了明显抑制。网格搜索的本质是“拿计算资源换调参效率”。参数组合数不多的时候非常划算但如果你要搜索的参数量再翻几倍建议改用RandomizedSearchCV它会在参数空间中随机采样能在更少的实验次数里找到差不多的好参数。6.2 实战中容易踩的几个坑第一个坑是sklearn版本差异。plot_tree是0.21版本才加入的如果环境里的sklearn是0.20甚至更老的版本跑可视化会直接报错。GridSearchCV和cross_val_score这类基础API倒是老版本也有但行为细节可能略有差异。建议建模之前先print(sklearn.__version__)确认版本。第二个坑是类别特征编码不一致。如果你对训练集和测试集分别执行pd.get_dummies两个数据集的列会不一致——比如某类别只出现在训练集中没出现在测试集中列数就对不上了。实战中通常把原始数据一次性处理完再拆分或者用Pipeline把预处理与建模绑定在一起避免这种不一致。第三个坑是不固定随机种子。同一个参数、同一份数据你跑三次得到三个不同的准确率然后你就会开始怀疑自己是不是操作错了。给自己定个习惯所有涉及随机性的地方都写random_state42。这样实验结果可复现别人拿到你的代码也能跑出相同数字。第四个坑是只看训练集分数就下结论。默认参数的决策树训练集几乎满分这个分数没有任何参考价值。一定要在留出的测试集或交叉验证上做判断否则你只是在自我感动。6.3 调参之外从单棵树到集成模型网格搜完单棵决策树在这个数据集上的表现基本到头了准确率上限大概在0.83左右再往上榨空间不大。原因很简单单棵树学到的规律有限而且对数据中的噪声很敏感。这时候可以顺手试一下随机森林——把几百棵树的结果做投票。在同样的训练测试划分下随机森林通常能把准确率抬到0.84到0.85。代码上只差一行from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier(n_estimators100, max_depth5, random_state42) rf.fit(X_train, y_train) print(rf.score(X_test, y_test))但要注意集成模型的代价是牺牲了单棵树的直观可解释性。你没法像前面那样画一棵树然后把判断规则一条条念出来。所以我个人建议入门学习阶段先把单棵决策树吃透把树的生成逻辑、剪枝逻辑、可视化都玩明白再去碰集成模型。否则你一上来就随机森林看着特征重要性排序却根本不知道这些重要性分数是怎么来的反而不利于建立直觉。我在实际跑完这个数据集之后最大的体会是决策树的调参难度不在于参数多而在于你得时刻盯着过拟合这个隐形敌人。每次调完都要问自己三个问题——训练集和测试集差距缩小了吗测试集分数真的涨了吗树的规则还能不能看懂三个问题都答“是”这次调参才算有效。只要能把这套思路迁移到其他数据集上这一篇实践就没有白读。

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

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

免费获取报价 →
↑