
泰坦尼克数据集大概是这个圈子里被跑过最多遍的入门项目了。当年我刚开始接触机器学习时导师扔过来的第一份资料就是它。现在回头看这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之间的关联。实测对模型提升很小。用SexPclass分组中位数填充效果更好因为头等舱女性的年龄中位数和统舱男性差异很大凭空填一个全体中位数等于引入噪声。结合Title分组填充这是当时效果最好的方案。先把Name里的Title提取出来再用同Title群体的年龄中位数去填充缺少年龄的样本Mr、Mrs、Miss的年龄分布差异很明显能更精准地还原缺失值。Embarked只有两个缺失值我直接查了一下Fare和Pclass对应关系发现这两个样本都是Pclass1且票价为80的乘客因此填了出现频率最高的S港口后续验证没有带来明显偏置。Cabin字段则直接构造了CabinKnown列有客舱号记1没有记0。这一步在后续模型中贡献了不少提升原因后面特征工程会展开说。3. 特征工程与细节实现3.1 从Name中挖出TitleName在原始数据里长这样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中直接学出规律因为影响是非线性的一个人独行、两个人同行、一家人五口人的生存概率并不按整数递增。所以我构造了FamilySizedata[FamilySize] data[SibSp] data[Parch] 1然后观察FamilySize与Survived的关系发现2到4人的家庭幸存率较高独行FamilySize1和大型家庭大于4幸存率低。这非常符合直觉独行者在混乱中缺少互助而大家庭成员之间互相寻找可能延误逃生时机。于是进一步抽取IsAlonedata[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_firstFalse )独热编码后Sex_male和Sex_female会变成两列实际上它们完全线性相关但树模型不影响逻辑回归则建议drop_firstTrue避免共线性干扰。数值特征方面我对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], axis1) y data[Survived] X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42, stratifyy )stratifyy是为了保证训练集和验证集里的幸存比例一致否则随机划分可能让验证集里正样本占比飙到50%给模型评估带来偏差。4.2 模型基线与交叉验证我先把逻辑回归跑了一版这是二分类问题的常规起点。它可解释性强、速度快也能暴露出特征编码方面的问题。from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score model LogisticRegression(max_iter1000, random_state42) scores cross_val_score(model, X_train, y_train, cv5, scoringaccuracy) print(fCV准确率均值: {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_iter1000, random_state42), RandomForest: RandomForestClassifier(n_estimators200, random_state42), GradientBoosting: GradientBoostingClassifier(random_state42) } for name, model in models.items(): scores cross_val_score(model, X_train, y_train, cv5, scoringaccuracy) 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_state42), param_grid, cv5, scoringaccuracy, 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或5min_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, ascendingFalse) print(importance)第二可以尝试把用到的这些特征扩展到其他类似的数据集上验证比如核潜艇事故或飞机延误的生存预测场景方法论完全一致。第三数据量允许时可以用集成学习把逻辑回归、随机森林、梯度提升的结果做加权投票往往能在验证集上再挤出一个百分点。最后分享一个我在实际项目中的体会不要轻视基线模型和简单特征。这个数据集上逻辑回归配合精心构造的FamilySize、Title、CabinKnown特征几乎能追平默认参数的随机森林。模型从来不是银弹真正拉开差距的是你对业务场景和数据细节的理解深度。把泰坦尼克这个项目吃透后续换到任何真实数据集你都会感谢当初在这里打下的底子。