☰
决策树模型实战:泰坦尼克号生存预测全流程解析
2026/10/18 8:23:11 网站建设 项目流程

1. 决策树算法案例到底在解决什么问题

第一次接触机器学习的人,十有八九会和“泰坦尼克号乘客生存预测”这个经典案例相遇。表面上,它只是让你根据乘客的船舱等级、性别、年龄、票价、登船港口这些字段,预测一个人最后是否存活。但真正跑一遍你就会发现,这个题一点也不“玩具”:原始数据里有缺失值、有文本字段、有类别字段,还有一眼看不出来怎么用的票号和客舱号。拿它来练决策树算法,等于把一趟真实的数据建模流程完整走了一遍。

这个案例适合三类人:刚学完 Python 和 Pandas、想找一个完整分类任务的入门者;已经能跑通逻辑回归、想弄明白树模型为什么不需要做特征缩放的人;以及正打算用决策树做业务解释、需要把模型规则讲给非技术的人。它解决的核心问题很明确:给定一堆杂乱的历史数据,训练一个可解释的二分类模型,让模型不仅能预测,还能告诉你是“哪几个条件组合”影响了结果。

用一个更通俗的说法:决策树就是一个自动学出来的“如果那么”判断手册。你给它一个乘客,它先问性别,再问仓位等级,再问年龄,一路走到某个叶子节点,给出“存活”或“未存活”的概率。这个过程跟人做判断非常像,所以它天然适合当机器学习的第一站。

1.1 从一个二分类问题说起

这是个典型的监督学习二分问题,目标变量是Survived,1 表示存活,0 表示未存活。原始数据的一共有 891 条乘客记录,其中存活 342 人、未存活 549 人。也就是说不做任何建模,直接猜“所有人都未存活”,准确率也有 61.5%。所以后面无论模型跑到多少分,先要和这个基线对比,别一看到准确率 80% 就激动。

我见过很多新手在这个数据集上追求“更高准确率”,把模型换成随机森林、XGBoost,最后确实能到 85% 左右。但问题在于,这个数据集本身信息量有限,很多字段缺失严重,强行堆复杂模型只会导致过拟合。决策树的价值不在“刷分”,而在“把判断逻辑可视化地摆到桌面上”。你会看到模型为什么认为女性更有优势、年龄小的乘客更有优势、高舱等乘客更有优势,这是黑盒模型很难做到的。

1.2 为什么选决策树做第一个建模算法

决策树有三个优点,特别适合这个案例。

第一,不需要对数值特征做标准化。决策树每次切分只看“某个特征的某个阈值”,比如Age <= 3.5,它并不关心这个特征是不是正态分布、量纲是否统一。相比之下,逻辑回归、SVM 这类模型对特征尺度很敏感,必须先做标准化。

第二,能同时处理数值特征和类别特征。不过类别特征在进入模型前还是需要编码,这点下面会详细说。

第三,结果可以画成树图。一个字段一个字段地看下去,每一步都对应一个可解释的判断。对业务人员来说,告诉他“男性、年龄大于 10、三等舱,这个分支的存活率很低”,比给他一堆特征系数要直观得多。

当然决策树也有明显短板:容易过拟合、对数据噪声敏感、单棵树不够稳定。这个案例里,我会先用一棵基础决策树跑出基线,再通过剪枝和调参控制过拟合,最后解释特征重要性和树的分裂逻辑。

2. 数据预处理与特征工程

很多人学机器学习时,把大量时间花在调参上,却忽略了数据预处理。泰坦尼克号这个数据集最大的贡献,就是逼你先去处理脏数据。它里面缺失值、文本字段、无用字段全齐了,处理完这些,模型反而成了最后最轻松的一步。

2.1 拿到数据先做一次“字段体检”

任何建模任务,拿到数据第一步都不是model.fit,而是先看字段长什么样。这个数据集主要字段如下:

字段类型含义主要问题
PassengerId数值乘客编号对预测无意义
Pclass数值船舱等级 1/2/3有序类别,但树模型可直接用
Name文本乘客姓名高基数,需要提取才能用
Sex文本性别需编码
Age数值年龄缺失较严重
SibSp数值同行的兄弟姐妹/配偶数量可组合为家庭人数
Parch数值同行的父母/子女数量可组合为家庭人数
Ticket文本票号高基数,通常丢弃
Fare数值票价有零星缺失
Cabin文本客舱号缺失率非常高
Embarked文本登船港口仅有 2 个缺失

PassengerId、Name、Ticket、Cabin这四个字段,在大多数入门方案里都会被直接剔除。Name里有称号信息比如 Mr、Mrs、Miss,理论上可以提取出来做特征,但提取过程容易引入目标泄露风险,新手不建议一上来就加。Cabin缺失率超过 77%,简单填缺失值毫无意义,强行编码只会给模型添加大量噪声。

2.2 缺失值怎么填才不浪费信息

这个数据集里最需要处理的缺失值是Age,一共有 177 条记录缺年龄。很多人第一反应是填全局均值,比如直接df['Age'].fillna(df['Age'].mean())。我建议别这么做,原因很简单:全局均值会把不同性别人群的年龄差异抹掉,也让年龄分布严重集中在均值附近。更合理的做法是按Sex和Pclass分组,用组内中位数填充,因为不同性别、不同舱位等级的人,年龄分布有明显差异。用中位数而不是均值,也能减少极端值的影响。

import pandas as pd import numpy as np df = pd.read_csv("titanic_train.csv") df = df.drop(columns=["PassengerId", "Name", "Ticket", "Cabin"]) age_median = df.groupby(["Sex", "Pclass"])["Age"].transform("median") df["Age"] = df["Age"].fillna(age_median)

这里transform("median")会按每个分组计算中位数,并原样返回和原 DataFrame 等长的序列,直接用fillna填充缺失值。Embarked只有两个缺失,直接填众数S即可。Fare偶尔也会缺一两条,先用中位数补。

需要注意,在真正的建模流程里,缺失值处理不能不看业务就乱填。年龄缺失可能跟某些乘客信息登记不完整有关,也可能本身就和存活率有潜在关联。这个案例中字段较少,分组中位数已经够用。

2.3 把文字变成树能用的数字

决策树不能直接处理Sex='male'、Embarked='S'这种文本。最常用的做法是标签编码或独热编码。

性别字段只有 male、female 两个取值,直接映射成 0 和 1:

df["Sex"] = df["Sex"].map({"male": 0, "female": 1})

Embarked有三个取值,用get_dummies做独热编码,并设置drop_first=True避免多重共线性。虽然决策树不依赖于线性模型那种严格的共线性假设,但少一列就意味着树少一些冗余的切分可能。

df = pd.get_dummies(df, columns=["Embarked"], drop_first=True)

除此之外,我通常会顺手做两个衍生特征。第一个是FamilySize,把同行人数和自身加起来,表达这个乘客在船上的家庭规模;第二个是IsAlone,表示是否独自一人。这两个特征在真实案例里很实用,因为团队出行和独自出行的人在船上的生存环境往往差别不小。

df["FamilySize"] = df["SibSp"] + df["Parch"] + 1 df["IsAlone"] = (df["FamilySize"] == 1).astype(int)

特征工程不是越浮夸越好。每加一个特征,决策树就多了一个可能的切分点,也给过拟合多了一次机会。所以加特征之前先问自己:这个特征背后的业务含义是什么?如果只是“觉得可能有用”,那就先不加,等模型诊断完再加。

3. 决策树模型的训练过程

数据整理好了,接下来就是建模。很多人喜欢直接把全量数据丢进模型,再拿同一份数据评估效果,这属于标准错误操作。正确的做法是先把数据切成训练集和验证集,让模型在训练集学习,在验证集检查泛化能力。

3.1 先划好训练集和验证集

在划分数据时,我会用train_test_split,并设置stratify=y。因为存活和未存活是约 38% 对 62% 的分布,如果不做分层抽样,随机切出来的验证集很可能幸存者比例偏高或偏低,导致评估结果不稳定。

from sklearn.model_selection import train_test_split y = df["Survived"] X = df.drop(columns=["Survived"]) X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y )

random_state=42也是个不能省的东西。不固定随机种子,每次运行划分都不一样,模型复现和对比都无从谈起。

3.2 用 5 行代码跑一个可解释的基线模型

这里先不用复杂的调参工具,直接用手设一个偏保守的决策树。max_depth=3的意思是树最多往下分裂 3 层,min_samples_leaf=10的意思是每个叶子节点至少要包含 10 个样本。这两个参数组合能明显限制树的复杂度,防止一开始就出现过拟合。

from sklearn.tree import DecisionTreeClassifier from sklearn.metrics import accuracy_score, confusion_matrix, classification_report model = DecisionTreeClassifier( criterion="entropy", max_depth=3, min_samples_leaf=10, random_state=42 ) model.fit(X_train, y_train) for name, y_true, y_pred in [ ("train", y_train, model.predict(X_train)), ("validation", y_val, model.predict(X_val)), ]: print(name, accuracy_score(y_true, y_pred))

我实际跑出来大概是:

train 0.84 validation 0.81

训练集和验证集差距不大,说明这个基线已经比较健康。再看验证集的混淆矩阵:

[[152 13] [ 38 65]]

意思是未存活类别预测对了 152 人、错了 13 人;存活类别预测对了 65 人、错了 38 人。整体准确率约 81%,但存活类别的召回率偏低,只有 63%。如果业务目标是“尽量把有希望活下来的人找出来”,这个结果还需要优化。这里也提醒我们,准确率高不代表每个类别都表现均衡,尤其当正负样本比例不是 1:1 时,光看 accuracy 会骗人。

3.3 用信息熵看懂树在做什么

决策树选择“先按谁切”靠的是信息增益。什么叫信息增益?可以先用“混乱程度”来理解:一个数据集里存活和未存活的人数越接近,它的不确定性就越大;如果全是未存活的,不确定性就是 0。

信息熵的计算公式是:

H(S) = -Σ p_i * log2(p_i)

在根节点上,891 人里有 549 人未存活、342 人存活,信息熵大约是 0.96。决策树会尝试每个特征的每个可能切分点,计算切分后左右子节点的熵的加权平均,再用根节点的熵减去这个加权平均,得到信息增益。增益越大,说明这个特征把数据分得更“纯净”。

以性别为例,粗略估算一下:男性 577 人,其中存活 109 人,这一组的熵约为 0.70;女性 314 人,其中存活 233 人,这一组的熵约为 0.82。按人数加权后大约为 0.74。信息增益就是 0.96 减 0.74,约 0.22。所以树的第一层通常就会选择性别作为根节点的分裂特征,因为它带来的信息增益最大。

这也解释了一个常见困惑:为什么决策树总爱把性别放在最上面?因为在这个数据分布里,性别能一瞬间把数据纯度提高不少。后面再用plot_tree画图,你会发现树的实际判定逻辑非常朴素:女性优先进入存活率更高的分支,男性再往下细分年龄和舱位等级。

4. 剪枝、调参与优化

决策树的“自由生长”能力很强。如果完全不设限制,它会为了让每个叶子节点尽量纯净而一直分裂,最终把训练集背下来。训练集准确率可能高达 98%,验证集却掉到 75%,这就是过拟合。所以决策树的调参核心,就是限制复杂度。

4.1 树越小,泛化越稳

我用同样的min_samples_leaf=10,只改动max_depth,得到的趋势大概是这样:

max_depth训练集准确率验证集准确率
30.840.81
50.880.81
70.940.78
100.970.74

随着深度增加,训练集准确率一路走高,验证集却先涨后跌。这说明模型开始记住训练集里那些没有代表性的细节。决策树天生就是高方差模型:同一个数据集换一批样本,树的结构都可能差别很大。所以做决策树,宁可让它“笨”一点,也不能让它“背”下来。

4.2 预剪枝参数和代价复杂度剪枝

限制决策树复杂度主要有两条路。一条是预剪枝,在生长时直接设限制条件,常用参数有:

  • max_depth:限制树的最大深度
  • min_samples_split:内部节点再分裂所需最小样本数
  • min_samples_leaf:叶子节点最小样本数
  • max_features:每次分裂最多考虑的特征数

另一条是后剪枝,让树先长满,再修剪代价太高的分支。决策树实现里最典型的是ccp_alpha,全称是“代价复杂度剪枝”。它会生成一棵子树路径,alpha越大,剪掉的节点越多。实际使用时常配合交叉验证:

path = model.cost_complexity_pruning_path(X_train, y_train) ccp_alphas = path.ccp_alphas

然后对每个alpha训练一棵树,在验证集上打分,选出验证集表现最好的那个alpha。这种方式比手调max_depth更系统,但计算成本也更高。对小数据没感觉,对大数据要谨慎。

4.3 用网格搜索找出更稳的参数组合

手工调参数的效率太低,我习惯用GridSearchCV把几个关键参数组合起来搜索。这里要注意,搜索空间不建议太大,否则训练时间会成倍增长。泰坦尼克号这个数据量很小,可以稍微放开一点:

from sklearn.model_selection import GridSearchCV param_grid = { "criterion": ["gini", "entropy"], "max_depth": [3, 5, 7, 10], "min_samples_leaf": [5, 10, 20], "min_samples_split": [10, 20, 50], } grid = GridSearchCV( DecisionTreeClassifier(random_state=42), param_grid, cv=5, scoring="accuracy" ) grid.fit(X_train, y_train) print(grid.best_params_)

我跑出来的一个较优组合是criterion='entropy', max_depth=5, min_samples_leaf=10, min_samples_split=20。注意,网格搜索不是“找到参数就结束”,还要回到验证集上再做一次确认。GridSearchCV内部用交叉验证选参数,最后由best_estimator_在验证集上的分数才是你该对外汇报的数字。

如果类别不平衡,我建议把scoring改成f1或roc_auc,而不是accuracy。这个案例正负样本比例大约 38:62,虽然不算极端,但只看准确率很容易漏掉少数类的问题。

4.4 特征重要性怎么读才不翻车

训练完成后,model.feature_importances_会输出每个特征的重要程度。这个重要程度是模型基于“这个特征被用来切分时带来了多少纯度提升”统计出来的。我跑出来的大致的排序是:

特征重要程度
Sex0.31
Age0.27
Pclass0.18
Fare0.12
FamilySize0.08
IsAlone0.03
Embarked_Q0.01

这个排序符合常识:性别影响最大,年龄次之,舱位等级和票价也有作用。但要注意,特征重要性不等于因果重要性。如果两个特征高度相关,比如Pclass和Fare,树可能随机选择其中一个承担切分责任,导致两者重要程度呈现不稳定分配。所以解释时不要直接说“票价不重要”,只能说“在这个树结构里,它没有被优先用来做有效切分”。

画树也是验证模型是否合理的重要手段:

import matplotlib.pyplot as plt from sklearn.tree import plot_tree plt.figure(figsize=(20, 10)) plot_tree( model, feature_names=X.columns, class_names=["Died", "Survived"], filled=True ) plt.savefig("tree.png", dpi=120)

画出来之后,一定要用眼睛过一遍每一个分支。如果发现某个分支用了完全没业务逻辑的特征,比如PassengerId < 500,那基本说明特征没清理干净或已经过拟合了。

5. 常见问题与踩坑记录

这个案例我至少带人做过几十遍,踩过的坑可以单独写一份排查清单。很多坑不是模型难,而是流程顺序和数据处理习惯出了问题。

5.1 年龄缺失值:全局均值是新手最容易掉的坑

如果直接用全局均值填充年龄,会带来两个问题:一是年龄分布被人为向均值压缩,二是不同舱位等级的年龄差异被抹平。你可以自己试试,先按全局均值填,再按Sex+Pclass分组中位数填,分别训练决策树,验证集分数通常差异不大,但画出来的树结构会明显不同。原因在于,全局均值把大量乘客的年龄填成同一个值,决策树就很难利用年龄做有效划分。

正确做法是填充后检查分布,看看是不是有一根明显长高的“假柱子”。如果是,说明填充方式过于粗暴。

5.2 数据划分和预处理顺序不对就是泄露

很多人会先对全量数据做fillna、get_dummies,再做train_test_split。这样做看起来没什么问题,实际上验证集的信息已经偷偷流进了训练过程。最典型的是用全量数据的均值去填验证集缺失值,这个均值里包含了验证集自己的信息。

正确流程是:先划分训练集和验证集,再在训练集上计算填充规则和中位数,然后把同样的规则应用到验证集。不过在泰坦尼克号这类小数据集上,影响可能只是一两个百分点;换成真实业务数据,泄露问题会被放大得非常明显。

5.3 只看准确率会掩盖问题:把混淆矩阵和召回率拿出来

前面说过,全猜“未存活”就能有 61.5% 的准确率。如果模型只有 65% 准确率,它其实只比瞎猜好一点点。所以要同时看混淆矩阵、精确率、召回率和 F1。尤其在存活这个少数类别上,召回率很能说明模型有没有真正学到规律。我的基线模型在验证集上的存活召回率只有 63%,说明漏掉不少存活乘客。这时候可以考虑调整分类阈值,或者给少数类更大的权重。

5.4 常见问题速查表

问题原因解决
训练集准确率很高,验证集掉很多树过拟合增大min_samples_leaf,限制max_depth
树总是在没意义的字段上分裂高基数字段入模先剔除票号、客舱号等字段
分类结果总偏向多数类类别不平衡没处理看召回率,尝试class_weight或调阈值
每次运行分数不一样没固定随机种子设置random_state
文本特征直接塞进模型报错没做编码先映射或独热编码
验证集分数虚高预处理用了全量数据先切分,再 fit 预处理规则

6. 沿着这个案例还能继续深化什么

泰坦尼克号案例最容易被忽略的,是“做完一棵树”之后还能往哪走。我建议不要急着换复杂模型,先沿着两个方向练。

第一个方向,是把决策树换成集成模型,比如随机森林和梯度提升。随机森林的本质是多棵决策树投票,能大幅降低单棵树的方差,但也牺牲了“一眼看懂一棵树”的可解释性。你可以把随机森林当成参照模型,回头再对比单棵决策树的表现,看看复杂模型到底赢在哪里、输在哪里。

第二个方向,是加一层模型诊断。比如画 ROC 曲线、计算 AUC、观察概率校准情况。通过这个案例,你能学会怎么看模型输出的概率是否可靠,而不只是看分类对错。这对真实业务中“预测点击率”“预测流失概率”这类任务非常关键。

如果必须向非技术方解释模型,我强烈建议优先画树。树图可以让人一眼看清“某个分支存活率高、某个分支存活率低”的逻辑。哪怕最终上线用的是随机森林,也可以用一棵浅决策树做一个通俗版的解释模型。真实项目里,信任模型往往比模型精度更重要。

最后分享一个我坚持了很久的习惯:每回跑完模型,我都会把model.get_params()和最终特征列表保存下来。因为一两个星期之后再回头跑这个案例,你大概率会忘记当时用的是哪个参数组合。这个习惯听起来很笨,但能省下大量重复摸索的时间。决策树算法本身的代码就那几行,真正见功夫的地方,永远是对数据的理解和对流程的控制。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询