news 2026/10/19 14:10:52

泰坦尼克数据集实战:二分类建模与特征工程完整流程解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
泰坦尼克数据集实战:二分类建模与特征工程完整流程解析

泰坦尼克数据集,大概是这个圈子里被跑过最多遍的入门项目了。当年我刚开始接触机器学习时,导师扔过来的第一份资料就是它。现在回头看,这891行数据确实足够经典:既有缺失值要处理,又有类别特征要编码,还能玩出各种特征组合,模型训练和调优的整套流程都能在上面完整走一遍,非常适合作为新手迈入机器学习门槛的练手项目。

这个项目的任务很明确:根据乘客的船舱等级、年龄、性别、票价等属性,预测该乘客是否在沉船事故中幸存。听起来像个历史问题,但本质上和电商预测用户是否复购、银行预测客户是否违约没有区别,都是标准的二分类问题。适合的人群也很广:刚学完Python和pandas基础、准备接触scikit-learn的初学者,以及想巩固特征工程和模型评估思路的从业者,都能从这套流程里收获一些可复用的方法论。

下面我按当时实际做这个项目的流程来拆解,包括每一步的判断依据、踩过的坑和换过几次思路的调整过程。

1. 项目到底在解一道什么题

1.1 任务定义与数据字段盘点

先明确任务类型。目标变量Survived只有0和1两个取值,1代表幸存、0代表未幸存,因此这是一个监督学习下的二分类问题。数据集中共有891条训练样本,包含PassengerId、Pclass、Name、Sex、Age、SibSp、Parch、Ticket、Fare、Cabin、Embarked等12个字段。

这些字段可以分成几类:

  • 数值型:Age、Fare、SibSp(同行的兄弟姐妹/配偶数)、Parch(同行的父母/子女数)。
  • 类别型:Pclass(船舱等级)、Sex、Embarked(登船港口)、Cabin(客舱号)。
  • 文本型:Name、Ticket。
  • 标识类:PassengerId,只作为行索引使用,不能作为特征。

注意:PassengerId和Name这类字段虽然看着像信息,但如果不做处理就丢进模型,很容易造成数据泄漏或引入噪声。实际项目中,ID类字段永远不要直接作为特征。

1.2 评价指标为什么选准确率和AUC

二分类问题的评价指标很多,但这里最适合看的是准确率和AUC,而不是一味追求F1值。

先看类别分布:891人中幸存342人、未幸存549人。幸存比例约38.4%,并不是极度不均衡的数据集,所以准确率能比较客观地反映模型好坏。我当时的基线模型直接用"所有人都死亡"来预测,准确率也有61.5%,所以任何模型如果低于这个数,说明连规律都没学到。

AUC则是另一个视角,它衡量模型把正样本排在负样本前面的能力,不依赖某个具体的判定阈值。这在业务中的含义是:如果我们想调整"倾向于召回幸存者"还是"倾向于精确识别",AUC能给出一个稳定的排序质量参考。推荐在项目里同时输出这两个指标:准确率给你一个直观的"答对比例",AUC给你一个更稳健的模型判别能力度量。

1.3 数据的现实意义与项目价值

别小看这个"玩具数据集"。它包含了真实业务中几乎最常见的三类问题:缺失值、混合类型特征、特征间的交叉影响。Age、Cabin等字段缺失严重,Name听起来毫无用处却能挖出Title特征,SibSp和Parch单独看起来作用一般但组合成FamilySize后效果显著。

这些处理手法放在任何生产环境的数据集上都通用。做完这个项目,你至少能回答三个问题:怎么处理缺失值、怎么从文本中提取特征、怎么用交叉验证防止模型"自我感觉良好"。这也是为什么很多团队在面试新人时,愿意拿这个数据集来考察候选人的基本功。

2. 数据探索与预处理

2.1 先看缺失值全景

拿到数据的第一步,我习惯先跑一遍info()和isnull().sum(),把整体质量摸清楚。泰坦尼克数据集的三个缺口非常典型:

  • Age缺失177条,占比约19.9%。这个比例不算低,不能直接删行,需要填充或模型处理。
  • Cabin缺失687条,占比约77.1%。这个字段缺失太严重,直接作为类别特征会引入大量NaN,更适合"是否缺失"的二值化。
  • Embarked缺失2条,占比忽略不计,用众数填充即可。

实操心得:Cabin这类缺失率超过70%的字段,我的第一反应不是"怎么填",而是"是否值得用"。把它变成"是否有客舱号"这个二值特征,往往比强行填充更有效,因为缺失本身可能就是信息(比如未分配客舱的乘客可能对应较低的社会阶层)。

2.2 单变量与目标变量的快速观察

做探索性分析时,不需要画一堆花哨的图,重点看每个特征与Survived的交叉关系。我用pandas的crosstab和groupby就能快速得到结论:

  • 性别差异极大:女性幸存率约74.2%,男性幸存率约18.9%,这是目前区分度最高的单特征。
  • Pclass影响明显:头等舱幸存率约63.0%,三等舱约24.2%,社会阶层与生存概率强相关。
  • 年龄分布上看,儿童幸存率相对较高,年龄特征有用但需要分段处理。

这些结论帮助我决定特征工程的优先级:Sex和Pclass是基础盘,Age和FamilySize是加分的增量,而Cabin和Ticket需要仔细设计才不至于帮倒忙。

2.3 缺失值填充方案的取舍

处理Age缺失时,我尝试过好几种方案,简单列一下对比:

  • 用全体中位数填充:操作简单,但会抹平年龄与Pclass、Sex之间的关联。实测对模型提升很小。
  • 用Sex+Pclass分组中位数填充:效果更好,因为头等舱女性的年龄中位数和统舱男性差异很大,凭空填一个全体中位数等于引入噪声。
  • 结合Title分组填充:这是当时效果最好的方案。先把Name里的Title提取出来,再用同Title群体的年龄中位数去填充缺少年龄的样本,Mr、Mrs、Miss的年龄分布差异很明显,能更精准地还原缺失值。

Embarked只有两个缺失值,我直接查了一下Fare和Pclass对应关系,发现这两个样本都是Pclass=1且票价为80的乘客,因此填了出现频率最高的S港口,后续验证没有带来明显偏置。

Cabin字段则直接构造了CabinKnown列:有客舱号记1,没有记0。这一步在后续模型中贡献了不少提升,原因后面特征工程会展开说。

3. 特征工程与细节实现

3.1 从Name中挖出Title

Name在原始数据里长这样:"Braund, Mr. Owen Harris"。如果不管它,就浪费了一个文本字段。我提取了逗号后面的称谓部分,也就是Mr、Mrs、Miss、Master,此外还有Dr、Rev、Major等少数群体。

具体做法用正则提取:

import re def extract_title(name): pattern = re.compile(r',\s*([A-Za-z]+)\.') match = pattern.search(name) return match.group(1) if match else 'Unknown' data['Title'] = data['Name'].map(extract_title)

提取后把低频Title合并成几个大组:Mr、Mrs、Miss、Master,其余归为Rare。Master在泰坦尼克语境下通常指小男孩,这个特征和年龄、性别都有交叉意义。我观察到Master组的幸存率明显更高,因为儿童优先逃生是当时的规则。

注意:正则提取名称时,一定要处理匹配不到的情况。虽然这个数据集里每行Name都带称谓,但保底逻辑不能省,否则线上数据一旦格式变化,程序就崩了。

3.2 FamilySize与IsAlone的构造

SibSp和Parch单独做特征时,模型很难从"0"或"1"中直接学出规律,因为影响是非线性的:一个人独行、两个人同行、一家人五口人的生存概率并不按整数递增。所以我构造了FamilySize:

data['FamilySize'] = data['SibSp'] + data['Parch'] + 1

然后观察FamilySize与Survived的关系,发现2到4人的家庭幸存率较高,独行(FamilySize=1)和大型家庭(大于4)幸存率低。这非常符合直觉:独行者在混乱中缺少互助,而大家庭成员之间互相寻找可能延误逃生时机。

于是进一步抽取IsAlone:

data['IsAlone'] = (data['FamilySize'] == 1).astype(int)

这个二值特征在逻辑回归这类线性模型中表现很好,因为它直接把非线性关系喂给了模型。

3.3 Ticket与Cabin的隐藏信息

Ticket字段看起来是一串无意义的字母加数字,但里面藏着团体信息。相同Ticket号码通常代表一起买票、同行或同舱室的一群人。我按Ticket分组,统计每组的人数:

ticket_group_size = data.groupby('Ticket')['PassengerId'].transform('count') data['TicketGroupSize'] = ticket_group_size

实际跑下来,TicketGroupSize和FamilySize有部分重叠,但也包含了许多非亲属同行者的情况,加入后对AUC有微小正向提升。

Cabin的处理除了CabinKnown外,我还尝试过提取客舱编号首字母(如C、D、E等),因为不同甲板区域可能对应不同等级的船舱。但考虑到缺失率太高,首字母特征在验证集上的稳定性不足,最后只保留了CabinKnown。

这里说一下保留CabinKnown的逻辑:有客舱号的人通常预订了具体舱位,这本身就与Pclass、Fare高度相关,能帮模型捕捉"是否提前安排住宿"的隐性信号。虽然它和Pclass有共线性,但在树模型里,共线性影响不大,在逻辑回归里也可以用正则化去压制。

3.4 编码与归一化的正确姿势

建模前需要把类别特征转成数值。Pclass本身是1、2、3,可以直接当数值用,但Sex、Embarked、Title必须处理。

我用的是pandas的get_dummies做独热编码,方便快速展开:

data = pd.get_dummies( data, columns=['Sex', 'Embarked', 'Title'], drop_first=False )

独热编码后,Sex_male和Sex_female会变成两列,实际上它们完全线性相关,但树模型不影响,逻辑回归则建议drop_first=True避免共线性干扰。

数值特征方面,我对Age和Fare做了标准化:

from sklearn.preprocessing import StandardScaler scaler = StandardScaler() data['Age_scaled'] = scaler.fit_transform(data[['Age']]) data['Fare_scaled'] = scaler.fit_transform(data[['Fare']])

标准化不是必须的,但对逻辑回归和SVM这类基于距离的模型很关键。如果不做,Fare的量级远大于Age,会导致模型优化时的梯度被大数值特征主导。对随机森林和梯度提升这类树模型,标准化不影响结果,所以我在统一流程里直接做了,省得切换模型时再补。

4. 建模、验证与调参

4.1 划分训练集与验证集

这个项目最常见的一个错误就是:用全量数据训练,然后在同一份数据上报准确率。891个样本量本来就小,这样做百分百过拟合。

我按8:2划分训练集和验证集,同时固定random_state保证结果可复现:

from sklearn.model_selection import train_test_split X = data.drop(['Survived', 'PassengerId', 'Name', 'Ticket'], axis=1) y = data['Survived'] X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y )

stratify=y是为了保证训练集和验证集里的幸存比例一致,否则随机划分可能让验证集里正样本占比飙到50%,给模型评估带来偏差。

4.2 模型基线与交叉验证

我先把逻辑回归跑了一版,这是二分类问题的常规起点。它可解释性强、速度快,也能暴露出特征编码方面的问题。

from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score model = LogisticRegression(max_iter=1000, random_state=42) scores = cross_val_score(model, X_train, y_train, cv=5, scoring='accuracy') print(f'CV准确率均值: {scores.mean():.4f}, 标准差: {scores.std():.4f}')

之所以用5折交叉验证而不是单次验证,是因为小数据集上单次划分的偶然性太大。某些划分方式下验证集可能恰好包含更多难分类的三等舱男性,导致分数波动。交叉验证则能给出一个更稳妥的估计。

第一版逻辑回归的5折CV准确率大约是0.81,验证集准确率在0.81到0.83之间浮动。这个数字已经超过了"全部预测死亡"的61.5%基线,说明特征确实携带了有效信息。

4.3 多模型横向对比

有了基线之后,我同时跑了随机森林和梯度提升两个模型,用同一份训练集做交叉验证,这样对比公平。

from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier models = { 'LogisticRegression': LogisticRegression(max_iter=1000, random_state=42), 'RandomForest': RandomForestClassifier(n_estimators=200, random_state=42), 'GradientBoosting': GradientBoostingClassifier(random_state=42) } for name, model in models.items(): scores = cross_val_score(model, X_train, y_train, cv=5, scoring='accuracy') print(f'{name}: {scores.mean():.4f} ± {scores.std():.4f}') ]

在默认参数下,随机森林和梯度提升的CV准确率通常会略高于逻辑回归,但差距并不大,大约在0.82到0.84之间。逻辑回归的优势在于稳定、方差小;随机森林能捕捉非线性关系,但更容易在小数据上过拟合;梯度提升表现通常最好,但调参不当时也有风险。

我自己的选择是:不动逻辑回归和随机森林,重点调随机森林,因为它在小数据集上配合合理剪枝能取得不错效果,而且训练速度快,方便反复实验。

4.4 网格搜索调参与关键参数

随机森林需要重点调的参数包括n_estimators、max_depth、min_samples_split、min_samples_leaf。我用网格搜索加5折交叉验证来找参数。

from sklearn.model_selection import GridSearchCV param_grid = { 'n_estimators': [100, 200, 300], 'max_depth': [3, 5, 7, None], 'min_samples_split': [2, 5, 10], 'min_samples_leaf': [1, 2, 4] } grid_search = GridSearchCV( RandomForestClassifier(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('最佳CV分数:', grid_search.best_score_)

这里关于max_depth多说一句:很多新手直接把max_depth设成None,让树无限生长,在小数据集上很容易把训练集准确率推到接近100%,验证集却一塌糊涂。限制max_depth和min_samples_leaf的意义在于给每棵树"反过拟合"的约束,让树不至于记下每一个样本的噪声。

调完参之后,我在独立验证集上做最终评估。实测下来验证集准确率能稳定在0.83附近,AUC大约在0.86到0.88之间。对这个数据量来说,已经是一个相当合理的成绩了。

4.5 混淆矩阵与错误分析

验证集上的准确率只能看到一个片面的数字。我习惯再输出混淆矩阵,看看模型到底在哪些样本上犯错:

from sklearn.metrics import confusion_matrix, classification_report y_pred = grid_search.best_estimator_.predict(X_val) print(confusion_matrix(y_val, y_pred)) print(classification_report(y_val, y_pred))

典型的结果是这样:预测死亡的正确率较高,预测幸存时会有一些假阳性。仔细看错误样本,我发现模型最容易搞错的是"三等舱的年轻女性"。这类样本在数据集中数量偏少,而且她们的生存结果受到同行人、登船口等多个因素影响,信息量不足导致预测困难。

了解这一点后,我意识到硬调参数已经很难再涨分,这时真正的增量在于特征,而不是模型复杂度。这也是一个很宝贵的认知:当特征信息已达上限时,换更强的模型只会过拟合,不如回头挖掘数据。

5. 常见问题与排查实录

5.1 训练集接近满分,验证集垮掉怎么办

有段时间我在随机森林上把深度调得过高,结果训练集准确率到了0.98,验证集只剩0.78。这就是典型的过拟合。

排查思路是先把模型简化:max_depth降到3或5,min_samples_leaf调大到4,同时观察训练集和验证集之间的差距。差距从0.2缩小到0.05左右,才算回到正常范围。另一个技巧是增加交叉验证的折数,比如从5改成10,更充分地利用训练数据。

5.2 差点忘了移除数据泄漏字段

一开始我把PassengerId留在了特征矩阵里,模型居然也跑到了0.84的CV准确率。表面看上去很美,但仔细一想,PassengerId和Survived没有任何逻辑关系,它能涨分纯粹是因为拟合了随机噪声。

更隐蔽的泄漏来自Fare。Fare存在几个极端高值,这些往往是多人共享票价被重复记录造成的。如果不处理,模型可能会抓住"票价极高=幸存"的假规律。我的处理方式是:训练时保留原Fare做标准化,但验证集上如果出现超出训练分布的新范围,就必须警觉,因为预测阶段的数据分布可能已经变了。

5.3 缺失值填充顺序影响结果

填充Age时,一定要先构造Title特征再填充,而不是先填充再提取Title。逻辑在于:如果用全体中位数先填出值,再提取Title做分组,Title和年龄之间的真实分布关系会被污染;相反先提取Title并用分组中位数填充,才能保留"年轻女孩对应的Miss群体年龄中位数偏高"这种真实结构。

5.4 随机种子带来的分数波动

小数据集上,随机种子的影响被放得很大。同一个模型,random_state从42换到0,验证集准确率可能从0.83变成0.80。建议固定一套随机种子,把所有实验放在同一个划分基准上比较,而不是每跑一次就换一个运气。

注意:不要为了"刷高分"反复换随机种子挑最好看的结果。这在竞赛里或许能骗过排行榜,但真实场景下模型会原形毕露。固定种子、诚实地看交叉验证均值,才是做项目该有的态度。

5.5 特征编码后维度爆炸怎么办

虽然这个数据集不大,但做了Title、Embarked的独热编码后,维度也不至于爆炸。真遇到高基数类别特征时,我建议先做频次编码或者目标编码,而不要无脑get_dummies。泰坦尼克数据里唯一需要留意的就是Title的Rare组,低频类别全部分到一个组里,避免不必要的稀疏。

6. 实操心得与后续扩展方向

做完整套流程,我最大的感受是:这个项目能教给你的不是"怎么调出一个高分模型",而是"怎么系统地理解和处理一份不干净的数据"。很多人在这个数据集上跑到0.85就停下来了,但如果你愿意多花一小时去深挖,还有几个方向能继续玩。

第一,试试特征重要度分析。随机森林训练完,把feature_importances_按从大到小排列,你能直观看到Sex、Pclass、Age、FamilySize这些特征的贡献排序。这比盲目加特征有用得多。

import pandas as pd importance = pd.DataFrame({ 'feature': X_train.columns, 'importance': grid_search.best_estimator_.feature_importances_ }).sort_values('importance', ascending=False) print(importance)

第二,可以尝试把用到的这些特征扩展到其他类似的数据集上验证,比如核潜艇事故或飞机延误的生存预测场景,方法论完全一致。第三,数据量允许时可以用集成学习把逻辑回归、随机森林、梯度提升的结果做加权投票,往往能在验证集上再挤出一个百分点。

最后分享一个我在实际项目中的体会:不要轻视基线模型和简单特征。这个数据集上逻辑回归配合精心构造的FamilySize、Title、CabinKnown特征,几乎能追平默认参数的随机森林。模型从来不是银弹,真正拉开差距的,是你对业务场景和数据细节的理解深度。把泰坦尼克这个项目吃透,后续换到任何真实数据集,你都会感谢当初在这里打下的底子。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 14:09:19

AI辅助教材创作如何降查重?五层方法论与全流程实操

1. 项目概述与核心需求拆解1.1 这个项目解决什么问题做教材、写讲义、编课程资料,这事在教育培训行业里是刚需。以前靠人工一个字一个字地磨,一份像样的教材动辄几万字,从大纲设计到素材整理,再到逐章逐节地写、改、校&#xff0c…

作者头像 李华
网站建设 2026/10/19 14:09:12

开源CLI代码审查工具:本地LLM+Git驱动的可审计Code Review方案

1. 项目概述:一个真正能落地的开源代码审查 CLI 工具“open-code-review”这个名字乍一听像某个 GitHub 上刚建的空仓库,但如果你最近在团队里被拉进过几次“人工 Code Review 拉锯战”,或者自己写完 PR 后盯着 diff 界面发呆超过 15 分钟——…

作者头像 李华
网站建设 2026/10/19 14:07:43

Windows下用Bat脚本实现Laya项目一键压缩解压

这两年做Laya项目,最让我上瘾的其实不是引擎本身,而是把那些重复到吐的流程用脚本串起来。今天想聊一个看着不起眼、但用好了能帮你省下大量摸鱼时间的技能——在Windows环境下用Cmd批处理脚本(Bat)处理Laya项目里的日常杂活&…

作者头像 李华
网站建设 2026/10/19 14:07:41

C# TCP通信实战:TcpClient/TcpListener用法与粘包心跳处理

做C#的后端同学,基本都会有一阵子绕不开TCP通信。我最早做设备对接项目时,傻乎乎地直接用Socket类,Bind、Listen、Accept、BeginReceive一路写下来,光处理半包粘包就折腾了一周。后来换成TcpClient和TcpListener这套高层封装&…

作者头像 李华
网站建设 2026/10/19 14:07:40

HFSS Help.pdf高效检索指南:从文档结构到仿真排错实战

简介:《HFSS Help.pdf》是ANSYS HFSS 2020 R1官方帮助文档的完整PDF版,面向从事高频电磁仿真、天线设计、信号完整性及微波电路研发的工程师和研究人员,可当作随查随用的工具手册。压缩包内共1个PDF文件,约105.29MB,内…

作者头像 李华
网站建设 2026/10/19 14:06:58

Kafka源码阅读实战:从rebalance卡死定位四大核心模块

1. 为什么“看源码”不是摆拍,而是 Kafka 使用者绕不开的生存技能“Kafka 源码学习”这六个字,在很多人的技术履历里像一枚镀金勋章——写在简历上显得厚重,面试时被问到能支吾两句,日常工作中却常年锁在收藏夹最底层。我见过太多…

作者头像 李华