泰坦尼克数据集,大概是这个圈子里被跑过最多遍的入门项目了。当年我刚开始接触机器学习时,导师扔过来的第一份资料就是它。现在回头看,这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特征,几乎能追平默认参数的随机森林。模型从来不是银弹,真正拉开差距的,是你对业务场景和数据细节的理解深度。把泰坦尼克这个项目吃透,后续换到任何真实数据集,你都会感谢当初在这里打下的底子。