泰坦尼克号数据集大概是机器学习入门阶段被用得最多的一个了,分类决策树又是其中最直观的模型之一。但网上多数教程要么只讲调库跑个准确率,要么把决策树原理长篇大论讲一遍,真正从拿到数据到决策树落地、再到结果怎么解读的完整流程反而少见。这篇我想换个方式,直接用sklearn把Titanic数据集从头到尾做一遍分类决策树实践,把数据清洗、特征工程、模型训练、调参优化、可视化解读这些环节全部串起来,顺带把我在实际项目中踩过的坑和总结出来的判断经验也一并写出来。适合刚学完sklearn基础、准备动手做第一个完整分类项目的朋友参考,也适合已经跑通过流程但想进一步理解特征工程和决策树调参逻辑的读者。
1. 项目整体设计思路拆解
1.1 为什么选Titanic数据集和决策树
Titanic数据集能成为经典不是没有原因的。它规模适中、特征类型丰富,既有年龄、票价这样的连续数值型特征,也有性别、舱位、登船港口这类类别型特征,还有舱房号、票号这种看起来用不上、实际却可能暗藏信息的文本特征。你几乎可以把实际业务建模中会遇到的典型问题都碰到一遍:缺失值、无用特征、类别编码、数值标准化带来的问题。而目标变量"是否生还"又足够清晰,适合做二分类建模。
选择决策树作为模型,则是看中了它很强的解释性。第一次做完整项目的人往往只看重准确率,但懂行的人都知道,业务方问得最多的不是"你的模型准不准",而是"为什么这个人被预测为会生还"。决策树天然具备回答这类问题的能力——它把判断逻辑直接画成树结构,每个节点就是一条规则。在Titanic这种样本量不算大(训练集只有几百人)的数据集上,树模型也不容易像深度学习那样陷入数据饥饿的困境。实践阶段用决策树作为切入点,性价比是最高的。
1.2 整体流程框架
整个项目我按业界通用的标准建模流程来推进,这一套流程放之四海而皆准,从Titanic换到其他结构化数据项目也同样适用。先加载数据和探索性分析,理解每个特征的含义与分布;接着做数据清洗和特征工程,这是整个流程里最耗时、也最影响最终效果的环节;然后划分训练集和验证集,搭建基线模型;再对模型做参数调优,用交叉验证验证稳定性;最后把决策树可视化,回归业务本身解读模型的判断逻辑。
这里有一个关键认知需要提前说清楚。很多初学者容易把数据清洗和特征工程混为一谈,觉得处理缺失值、做编码就是特征工程的全部。实际上两者有本质区别——清洗是让数据达到"可用"状态,比如去掉错误值、填上缺失值;特征工程是让数据变得"好用",比如把类别转为数值、把文本信息提炼成统计特征、构造交叉特征。将这两者分开思考,后续步骤才不容易乱。
2. 数据探索与预处理:建模前必须摸清底细
2.1 数据概况与缺失值分布
加载数据之后,第一步一定不要急着建模,先看清楚数据的全貌。泰坦尼克号数据集按惯例分为训练集和测试集,训练集提供标签,测试集用来模拟新乘客到来时的预测场景。
import pandas as pd import numpy as np train_df = pd.read_csv('titanic_train.csv') test_df = pd.read_csv('titanic_test.csv') # 先看基本信息 train_df.head()通过info()可以快速掌握各字段的非空统计量:
train_df.info()Real项目里这一步建议打印出每列的空值数量、数据类型、唯一值个数,形成一张数据体检表。Titanic数据集的典型情况是Age字段缺失约两成、Cabin字段缺失约七成、Embarked只有少量缺失。缺失比例的不同决定了后续处理方式的选择:Age缺失比例尚在可接受范围,可以用模型或统计量填充;Cabin缺失太严重,当作普通特征填充意义不大,更适合从中提取"有无记录"这类信息;Embarked缺失极少,直接删除样本或用众数填充都是合理选择。
2.2 特征语义分析与初步筛选
拿到特征后,逐列过一遍它的业务含义比直接写pd.get_dummies更重要。Pclass虽然是数字1/2/3,本质却是有序类别特征,代表了舱位等级,直接作为数值放进模型会让模型误认为3是1的3倍大,事实上它只是等级递进关系。Age是连续年龄特征,SibSp是兄弟姐妹加配偶数量,Parch是父母加子女数量,这两个组合起来可以提炼出"家庭规模"。Fare是票价,在不同舱位等级下分布差异很明显。Sex是最强的单一预测特征。Embarked是登船港口,Cabin是舱房号字符串,Ticket是票号字符串,Name是乘客姓名。
初步筛选在这一步就能确定:Cabin和Ticket不适合直接作为模型输入,但可以从中做特征衍生。Name看起来与生还无关,实则里面藏着身份称号信息,比如Mr、Mrs、Miss、Master以及一些具有特定社会地位的头衔,这些头衔与年龄、性别高度相关,可以作为衍生特征使用。Pclass、Sex、Age、SibSp、Parch、Fare、Embarked都是核心候选特征。
2.3 缺失值的填充策略
Age字段用中位数填充还是按Pclass和Sex分组后取中位数填充,效果差别很大。泰坦尼克号不同舱位的乘客年龄分布差异明显,头等舱整体年龄偏大,三等舱儿童比例更高。按分组中位数填充比全局中位数填充保留了更多群体差异信息。实际操作中我用的策略是:
# 按Pclass和Sex分组计算年龄中位数 age_median = train_df.groupby(['Pclass', 'Sex'])['Age'].transform('median') train_df['Age'] = train_df['Age'].fillna(age_median) test_df['Age'] = test_df['Age'].fillna(test_df.groupby(['Pclass', 'Sex'])['Age'].transform('median'))Embarked缺失只有两三个样本,冗余度大,直接用众数填充即可。这里一个值得注意的细节是训练集和测试集必须用同样的填充逻辑,而且填充值应只从训练集计算,避免测试集信息泄漏到训练过程中。Cabin字段我在这一步只做一个标志位特征:
# 用是否有Cabin记录作为特征 train_df['Has_Cabin'] = train_df['Cabin'].notnull().astype(int)这个处理方式的原理是:Cabin缺失严重并不意味着信息全无——缺失本身就是一种信息。部分旅客没有舱房记录,可能与身份等级或获救概率存在相关性。
3. 特征工程:从原始字段到模型可用特征
3.1 类别特征的编码处理
sklearn的决策树本质上不支持字符串输入,所有输入必须是数值型。这就需要对类别特征做编码。实现方式不少,常见的有标签编码、独热编码、映射编码。对于只有两个取值的Sex,映射成0/1是最直接高效的。对于Embarked这种多类别无序特征,独热编码是稳妥做法。对于Pclass这种有序类别特征,直接保留数值1/2/3反而符合它的语义。
train_df['Sex'] = train_df['Sex'].map({'male': 0, 'female': 1}) test_df['Sex'] = test_df['Sex'].map({'male': 0, 'female': 1}) # LabelEncoder方式,性别同理 from sklearn.preprocessing import LabelEncoder le_sex = LabelEncoder() train_df['Sex'] = le_sex.fit_transform(train_df['Sex']) test_df['Sex'] = le_sex.transform(test_df['Sex'])这里要注意一个常见的坑:fit_transform和transform的使用。一定是先对训练集fit,再同时用于训练集和测试集的transform。如果对训练集和测试集分别fit,类别到数字的映射关系可能不一致,结果会产生严重偏差。
Embarked取C和Q和S三个港口,独热编码处理:
embarked_dummies = pd.get_dummies(train_df['Embarked'], prefix='Embarked')3.2 家庭规模与单人标识特征
把SibSp和Parch合并成家庭规模这个特征,是我觉得这个数据集中最值得做的一步特征工程。原理在于,在紧急疏散场景中,家庭成员的互助会增加本人获救的概率,但家庭规模过大又可能因为寻找亲友而降低逃生几率。
train_df['FamilySize'] = train_df['SibSp'] + train_df['Parch'] + 1再加一个是否独乘的标识:
train_df['IsAlone'] = (train_df['FamilySize'] == 1).astype(int)如果你再仔细观察FamilySize与生还率的分布,会发现它并非简单的线性关系,而是呈现先升后降的形态。等后续使用决策树时,模型可以在FamilySize的不同取值处自动寻找切分点,从而捕捉这种非线性关系。这也是树模型的天然优势。
3.3 姓名称号特征与分段年龄
从Name中提取称号是很多玩家容易遗漏的一步。Master通常是男孩称号,Miss多为年轻女性,Mrs和Mr对应已婚成年男女。这些称号高度浓缩了性别加年龄的信息,对预测生还非常有用。
train_df['Title'] = train_df['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)提取后做一个低频称号合并,将少数特殊头衔归入一个Rare类别,避免类别过散导致过拟合。这样做也能让决策树在选择切分点时更集中于主要类别。
年龄除了直接用原始数值,还可以分箱后转为有序特征。但在决策树场景下,分箱并不总是一件必要步骤——树模型自己具备寻找切分点的能力,连续特征直接输入即可。分箱的价值更多体现在与线性模型配合时,对决策树则显得多余,反而可能丢失精细的分界信息。所以这一步我选择保留原始数值。
3.4 用pipeline管理转换流程
上面一步一步的手工变换在实践项目中容易出现一个问题:训练集和测试集的转换代码分散在各处,一旦漏改一处,效果就会拉胯。更规范的做法是把特征转换封装成一个Transformer,配合sklearn的ColumnTransformer使用。
from sklearn.compose import ColumnTransformer from sklearn.preprocessing import OneHotEncoder from sklearn.pipeline import Pipeline # 定义需要不同处理的列 numeric_features = ['Age', 'SibSp', 'Parch', 'Fare', 'FamilySize'] categorical_features = ['Pclass', 'Sex', 'Embarked'] preprocessor = ColumnTransformer( transformers=[ ('num', 'passthrough', numeric_features), ('cat', OneHotEncoder(handle_unknown='ignore'), categorical_features) ])这套封装的好处是后续做交叉验证和网格搜索时,整个预处理流程可以在每一折内部重新fit,避免数据泄漏。虽然入门阶段看着增加了复杂度,但这是一个从"能跑"走向"跑得规范"的关键一步。
4. 决策树建模与参数调优实战
4.1 建立第一个基线模型
先不做任何过度加工,用默认参数跑一个决策树作为基线,快速建立对整个流程的感知。
from sklearn.tree import DecisionTreeClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score X = train_df_feat # 经过特征工程后的特征矩阵 y = train_df['Survived'] X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y) clf = DecisionTreeClassifier(random_state=42) clf.fit(X_train, y_train) y_pred = clf.predict(X_val) print(accuracy_score(y_val, y_pred))stratify=y这个参数很多新手容易忽略,但它的作用很关键。泰坦尼克号数据集中生还和非生还的比例大约为38%对62%。如果不做分层抽样,随机切分可能在某个子集中出现生还样本比例偏离整体过多的情况,导致验证结果失真。加上分层抽样后,训练集和验证集中的正负样本比例保持一致,模型的准确率评估更可信。
默认参数的决策树往往在训练集上准确率极高,在验证集上却明显下降。这是过拟合的典型信号。决策树默认不限制深度,它会不断分裂直到叶子节点足够纯,几乎把训练集模式背了下来。理解这一点是接下来所有调参工作的出发点。
4.2 核心参数与剪枝策略解读
决策树有三个参数对最终效果影响最大,理解它们就理解了决策树的调参逻辑。
max_depth控制树的最大深度。深度越深,模型越复杂,越容易过拟合。限制深度就是最直接的预剪枝手段,相当于给树的生长设置天花板。3到5的深度在Titanic这个体量的数据上往往是合理区间。
min_samples_split控制内部节点再分裂所需的最少样本数。当某个节点包含的样本数小于这个阈值时,即使还能分裂也不再继续。这个参数提升后能有效阻止树在数据稀疏区域继续细分,避免学到噪音信息。
min_samples_leaf控制叶子节点最少样本数。这个参数对最终效果的影响往往比max_depth更平滑,因为即使深层节点被剪掉,模型也能保留合理的分支结构。实际操作中我习惯先固定min_samples_leaf,再调整max_depth。
clf = DecisionTreeClassifier( max_depth=4, min_samples_split=10, min_samples_leaf=6, random_state=42 )这三个参数本质上都是对模型复杂度的约束。决策树角度上,他们做的事情是相通的——防止模型对训练集中的局部异常反应过度。
4.3 通过GridSearchCV网格搜索找最优参数
手动试参数虽然直观,但效率低且容易陷入局部最优。更科学的做法是定义一组候选参数空间,让GridSearchCV自动搜索,配合交叉验证评估每组参数的真实表现。
from sklearn.model_selection import GridSearchCV param_grid = { 'max_depth': [3, 4, 5, 6, 7], 'min_samples_split': [5, 10, 15, 20], 'min_samples_leaf': [2, 4, 6, 8] } grid_search = GridSearchCV( DecisionTreeClassifier(random_state=42), param_grid, cv=5, scoring='accuracy', n_jobs=-1 ) grid_search.fit(X_train, y_train) print(grid_search.best_params_) print(grid_search.best_score_)交叉验证的原理形象点说就是把训练数据分成5份,每次拿其中4份训练、1份验证,轮流5次。这样的好处是每一组参数都有5次验证结果,评估更稳定。Titanic数据集本身不大,5折交叉验证的计算成本完全可以接受。
使用交叉验证结果时要留意,best_score_是交叉验证的平均准确率,这个数值一般会略低于在单独验证集上跑出来的准确率,原因在于交叉验证用的训练数据略少,且平均了不同折上的起伏表现。实际评估模型最终效果还是要用之前留出的验证集。
4.4 特征重要性与过拟合现象分析
训练完成后,查看决策树的feature_importances_属性是理解模型洞察的重要环节:
importances = clf.feature_importances_ for name, imp in zip(feature_names, importances): print(f"{name}: {imp:.4f}")多个项目跑下来,Titanic数据集里Sex和Age几乎始终是重要性最高的特征,Fare次之,Pclass也占有一定分量。这其实很符合历史背景——泰坦尼克号疏散时妇女儿童优先,票价和舱位等级则刻画了社会阶层差异带来的生存率差异。
过拟合的典型判断方法是对比训练集准确率和验证集准确率。限制max_depth之前,训练集可能逼近0.95以上而验证集只在0.75左右徘徊;加上合理剪枝参数后,训练集准确率降到了0.83左右,但验证集反而提升到了0.82左右,训练集和验证集的差距显著减小。
这个现象可能让很多人困惑:训练集准确率明明下降了,模型不是变差了吗?实际上更合理的泛化性能才是建模的目标。过拟合模型的问题在于它对未知数据的预测能力被削弱了,原因是模型记忆了训练集中的噪声。
5. 决策树可视化与规则解读
5.1 使用plot_tree绘制决策树
决策树可以可视化,是它区别于其他模型的最大卖点。sklearn提供了plot_tree函数,可以直接把训练好的树结构画出来。
import matplotlib.pyplot as plt from sklearn.tree import plot_tree plt.figure(figsize=(30, 20)) plot_tree( clf, feature_names=feature_names, class_names=['Not Survived', 'Survived'], filled=True, rounded=True, fontsize=10 ) plt.show()生成的图像中,每个节点都会显示分裂条件、分类后的样本数、类别分布情况和基尼不纯度。通过颜色深浅可以直观看出样本流动方向,这比盯着数字去想象要方便太多了。
5.2 从树结构中提炼业务规则
将可视化结果转回业务语言之后,决策树的判断逻辑就会非常清晰。以max_depth=3的一棵树为例,根节点通常以性别为第一层分裂条件。女性乘客往右分支,男性乘客往左分支。女性的生还概率本身接近70%以上,此时再结合Pclass和Fare做细分:头等舱和二等舱的女性大多预测为生还,三等舱的女性则需要结合年龄继续判断。男性乘客的情况相反,绝大多数被预测为不能生还,只有在年龄很小的情况下才会有较高生还概率。
这正是决策树的可解释性价值所在——它会告诉你在海难逃生这样的场景下,性别、年龄、舱位等级是如何分层起作用的。模型给出的规则可以反推回业务背景,用于向别人解释模型,也用于验证模型学习的模式是否与常识相符。如果模型学到全无逻辑的规律,就需要回头检查数据与特征工程的问题了。
5.3 决策边界与样本路径追踪
对单个样本做路径追踪,是让决策树"说人话"的另一个实用技巧。用apply方法看看某个样本落到了哪个叶子节点,再通过decisions_path查看它经过的每一条分支。对于需要解释个别预测结果的场景,这样的追踪是评估模型最直观的路径。
决策树本质是分段常数函数,它在特征空间中切出一块块矩形区域,每一块对应一个预测结果。这个特性决定了它对特征交互作用的捕捉能力有限,所以单个决策树的精度通常赶不上随机森林或梯度提升树。但它的强解释性正是从这种简单结构中得来的。
6. 常见问题排查与经验总结
6.1 高频报错与解决思路速查表
实践过程中遇到报错是家常便饭,把最常见几类问题整理成速查表,遇到类似报错可以直接对照排查。
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| ValueError: could not convert string to float | 类别特征没做编码 | 对字符串类型列做LabelEncoder或OneHotEncoder |
| train_test_split后准确率波动很大 | 没有设置random_state且没有分层抽样 | 固定random_state,并设置stratify=y |
| 训练集准确率接近1.0,验证集却很低 | 决策树过度生长导致过拟合 | 限制max_depth,提高min_samples_leaf |
| GridSearchCV执行时间很长 | 参数组合过多、数据集偏大 | 先粗调锁定区间,再细调,或者加大n_jobs |
| 预测结果全是同一类别 | 数据分割或编码出现问题 | 检查训练集与测试集是否做了相同预处理 |
6.2 训练集与测试集一致性这个大坑
这个坑在结构化数据项目里很隐蔽,出现频率却相当高。训练集和测试集的特征工程必须完全一致,但很容易出现的情况是:训练集填充缺失用的mean是全局算的,测试集忘记用同样的值填充;或者LabelEncoder对训练集fit了,测试集却只调用了transform却忘了保存映射对象。这些细节一旦不一致,模型在测试集上的表现会忽高忽低,且难以复现。
代码上最稳妥的规避是使用Pipeline把所有预处理和模型封装在一起。Pipeline保证了每次运行都是同样的处理顺序、同样的参数、同样的填充值。GridSearchCV与Pipeline配合更是好习惯,预处理流程会在每折内部单独fit,从流程上杜绝数据泄漏问题。
6.3 决策树实践后的几点体验
做多了这类结构化数据实践项目之后,会发现数据质量与特征工程的价值远高于模型本身。Titanic数据集只有几百条样本,这种情况下再高的模型复杂度也发挥不出来,真正拉开效果差距的往往是特征工程的细腻程度。
对于刚开始接触sklearn和决策树的朋友,我建议一定要亲手把可视化做出来看一遍,理解树的生长过程,有机会可以逐步尝试最大深度参数从1到10的变化,观察训练集和验证集准确率曲线的变迁。这个过程能帮助建立对模型复杂度与泛化能力关系的直观认识。
最后多说一句,决策树虽然基础,但它是理解随机森林、梯度提升树这类进阶模型的基础。先把树的原理和应用练扎实,后续学集成模型会轻松得多。做这样一个完整的实践项目,比单纯刷算法题和堆教程有用的多——动手写完一遍,这些知识才真正属于你了。