
先说一个很多人不愿意点破的事实泰坦尼克号生还预测之所以能在 Kaggle 上火了这么多年不是因为它简单而是因为它看起来简单。作为一个在数据科学这行摸爬滚打了十几年的老手我见过太多新手在这道入门题上交了学费——不是不会跑模型而是根本不知道怎么把一个半原始的数据集收拾成模型能吃下去的样子。你打开比赛页会看到一个温柔得不像话的标题但真做起来就会发现它就是边境线上拦路的那道悍匪专门治各种以为自己会了。这篇文章我不会只给你一串代码让你复制粘贴而是从比赛机制、数据体检、特征工程、模型选型到最后调参复盘把整个链路从头到尾过一遍。每个关键操作我都会解释为什么这么做并附上我实测过的坑。如果你是一个刚接触机器学习的人或者想在 Kaggle 上拿到第一块牌子这篇文章就是为你准备的。1. 比赛机制与评分逻辑先摸清这场悍匪战的潜规则1.1 赛题到底在问什么泰坦尼克号赛题的原始目标非常简单给你一份船上的乘客名单大概 891 条训练数据里面有姓名、性别、年龄、舱位等级、票价、船舱号、登船港口这些字段外加一个用于训练的生还标签Survived0 代表遇难1 代表生还。你需要根据这份名单训练出模型然后对另外 418 名乘客测试集预测他们是否生还。表面上是二分类问题但真正考察的其实是数据清洗、特征工程和模型泛化这三项基本功。这里有个细节很容易被忽略测试集是没有标签的你只能自己评估模型。训练集和测试集的规模也不一样891 vs 418这意味着你在本地做的任何验证都必须讲究策略否则就会出现本地分数高、提交分数低的尴尬局面。1.2 从准确性得分到提交格式你究竟在跟谁对决很多人拿到比赛介绍就开始跑模型却忽略了评分指标的细节。泰坦尼克号用的是分类准确率Accuracy公式是预测正确的样本数除以总样本数。这看起来简单但你得意识到一个小陷阱测试集里生还者的比例是固定的大约 38%如果无脑预测全部遇难你也能拿到约 0.62 的分数。这个分数就是你的基准线任何模型如果不能超越它就不是在预测而是在碰运气。另一个容易踩坑的是提交文件的格式。比赛提供了一个叫gender_submission.csv的示例文件里面只有两列PassengerId和Survived而且它默认所有女性生还、所有男性遇难。很多新手直接把模型输出塞进这个模板却忘记保证PassengerId的行顺序与提交要求完全一致。我见过有人因为索引错位本来能拿 0.79 的成绩硬生生掉到 0.5 以下。切记提交文件的索引顺序一定要和测试集的原始索引一致最好是submit_df pd.DataFrame({PassengerId: test_df[PassengerId], Survived: y_pred})然后保存为 CSV。1.3 没有标签的测试集既是公平也是最大的挑战比赛将测试集的标签彻底隐藏目的是防止你用某种作弊方式直接得到答案。但这也带来一个实际问题你无法准确知道模型在测试集上的真实表现只能靠交叉验证或者公共排行榜Public Leaderboard的分数来推测。而公共排行榜的分数是基于测试集的一部分子集算出来的这个子集大约占 50%。剩下的另一半会根据最终排行榜Private Leaderboard重新打分。这就是为什么反复强调要在本地建立可靠的验证方案。公共分数波动很大可能差 0.01 的范围浮动但排名就会波动几百位。你在本地交叉验证得出的分数越接近公共分数说明你的验证方案越可靠。而如果两者差距巨大通常是发生了过拟合或数据泄漏。2. 数据体检与缺失值处理别急着扔字段先看缺失背后的信息2.1 一份典型的训练数据里都有什么我把训练集打印出来你会看到这些字段字段含义缺失情况训练集类型PassengerId乘客编号无缺失整数Pclass舱位等级1/2/3无缺失分类Name姓名无缺失文本Sex性别无缺失分类Age年龄177 个缺失浮点SibSp在船上的兄弟姐妹/配偶人数无缺失整数Parch在船上的父母/子女人数无缺失整数Ticket票号无缺失文本Fare票价1 个缺失浮点Cabin客舱号687 个缺失文本Embarked登船港口2 个缺失分类一眼看过去最明显的两个坑是Age和Cabin。Age缺了约 20%Cabin缺了将近 77%。很多教程面对这种情况直接选择删除有缺失的行或用整列中位数填充这在实践里并不正确。你需要先想清楚这个缺失本身是不是一个特征2.2 缺失值分布用代码快速摸清底细拿到数据的第一步永远是df.info()和df.isnull().sum()这两个命令足够告诉你哪里需要处理。以Age为例我的建议是先做一个交叉分组再决定插补方式。代码可以这样写import pandas as pd train pd.read_csv(train.csv) test pd.read_csv(test.csv) full pd.concat([train, test], axis0, sortFalse) # 观察不同舱位和性别下的年龄中位数 age_group_median full.groupby([Pclass, Sex])[Age].median() print(age_group_median)如果你运行这段代码会发现头等舱女性的年龄中位数明显高于三等舱男性。这意味着如果直接用整列中位数约 28 岁去填充会模糊掉舱位等级与年龄相关这一隐含模式。更合理的做法是按PclassSex分组填充中位数甚至再用上Title和Fare这些相关字段做参考。2.3 三个关键缺失值的处理策略Age年龄缺失年龄对生存率的影响很大儿童特别是 10 岁以下的生存率显著高于成年人。处理缺失的核心原则是尽量不去破坏年龄与其它特征的相关性。我用的是分组中位数填充稍微进阶一点的人会考虑用随机森林或者 KNN 预测缺失年龄。但基于经验在这种小数据集上分组中位数与复杂模型预测的差异并不大所以我建议新手工期从分组中位数开始省时且稳定。full[Age] full[Age].fillna( full.groupby([Pclass, Sex])[Age].transform(median) )Cabin客舱号缺失这是一个非常典型的信息缺失案例。77% 缺失不能用均值填充也不建议直接删除这个字段。因为客舱号缺失本身就可能是一个信号——它暗示这位乘客的客舱信息没有被登记通常集中在三等舱或者服务人员群体。实操中我一般保留一个Cabin_Unknown的 0/1 标志再尝试提取客舱区域的首字母比如C、D、E作为分类特征因为不同甲板区域的生存率差异很大。代码类似full[Cabin_Letter] full[Cabin].str[0] # 或者只做有无标记 full[Cabin_Unknown] full[Cabin].isnull().astype(int)Embarked登船港口缺失只有 2 个缺失处理起来很简单。先数一数 S、C、Q 三个港口的乘客分布通常 S 最多直接用 S 填充即可。但也有更讲究的做法结合 Fare 和 Pclass 去看缺失样本是谁。在我的记忆里这两个缺失样本的票价是 80舱位是一等舱而C港口一等舱的中位票价更像 80所以用C填充更合理。对于这种极少数样本手动判断比机械填充更好。3. 特征工程实战把乘客身份翻译成模型能听懂的信号3.1 从姓名里挖出 Title尊称如果你把Name这列当作纯文本扔进模型那就太浪费了。泰坦尼克号数据里最经典的隐藏信号是姓名中的尊称部分比如 Mr.、Mrs.、Miss.、Master.Master 一般指少年男性还有像 Dr.、Rev.、Lady. 这些稀有头衔。头衔不仅反映乘客的社会身份也间接与年龄、家庭地位、生存率相关。full[Title] full[Name].str.extract(r ([A-Za-z])\., expandFalse) print(full[Title].value_counts())统计一下你会发现大部分头衔集中在 Mr、Mrs、Miss、Master 这四种。其他稀有头衔可以归并成一个Rare类别这样特征不会太稀疏。我当时的做法是full.loc[~full[Title].isin([Mr, Mrs, Miss, Master]), Title] Rare full[Title] full[Title].map({Mr: 1, Miss: 2, Mrs: 3, Master: 4, Rare: 5})这一步看起来简单但价值非常大一个优秀的Title特征在随机森林里的重要性往往排在前三名因为它同时隐含了年龄、性别和家庭地位的复合信息。3.2 FamilySize 与 IsAlone单独使用SibSp和Parch时模型的感知能力比较弱因为它们是平行字段。更常见的做法是合成一个新特征full[FamilySize] full[SibSp] full[Parch] 1加 1 是把你本人算进去。这个特征的含义很直观一个人是否是单独出行或者跟一个大家庭出行。模型更容易从这个整体数字中学到模式而不是分别去拟合两个离散字段。进一步你可以构建IsAlonefull[IsAlone] 0 full.loc[full[FamilySize] 1, IsAlone] 1这个特征看似简单但它捕捉了一个经典的社会学规律独自旅行者生还率较低可能与没有同伴互救、决策犹豫等因素相关。在实际测试中加入IsAlone后逻辑回归的分数会小幅提升而树模型也能更快地收敛。3.3 票价分箱与舱位组合Fare是连续的浮点数分布严重右偏。少数一等舱乘客票价极高直接喂给线性模型可能会导致梯度计算不稳定。处理连续值特征有两个选择一是取对数二是分箱。我建议在泰坦尼克号上用分箱因为票价区间本身对应舱位阶级划分几个档位更直观full[Fare] full[Fare].fillna(full[Fare].median()) full[FareBin] pd.qcut(full[Fare], 5, labelsFalse)如果你观察分箱后的生存率柱状图会发现票价越高生存率越高。这个现象的根源不在于票价本身而在于舱位和乘客的社会经济地位以及遇难时优先逃生顺序。换句话说FareBin是Pclass的补充信号而不是替代。3.4 不需要强行使用的特征Ticket票号是一个常见的争议项。票号里可能带有某些前缀信息但它的重复率极低单个票号几乎无法泛化。除非你和朋友共同研究时发现特定前缀与生存率有稳定关联否则我建议放弃它省得引入不必要的噪声。PassengerId是纯粹的索引无意义绝对不能作为特征进入模型。新手如果忘了剔除它模型可能会把它当作一个高方差的连续变量来使用造成莫名其妙的过拟合。4. 模型选择与过拟合博弈本地分数和榜单分数的差距从哪来4.1 从逻辑回归到树模型各模型的真实表现经历过特征工程之后训练模型的代码反而很简单。我在 891 条样本上做了分层交叉验证对比过几种常见模型的平均准确率大致是这个水平模型交叉验证准确率约特点逻辑回归0.782 - 0.795稳定适合作为基准决策树0.755 - 0.775容易过拟合随机森林0.785 - 0.805如果调参得当上限最高XGBoost / LightGBM0.790 - 0.815需要小心小样本下的过拟合SVMRBF核0.780 - 0.790对特征缩放敏感可以看到在这个数据集上任何模型都很难把交叉验证分数推到 0.83 以上。原因很简单训练样本只有 891 条而真正影响生还的因素如遇难时的实际位置、救援艇分配顺序并没有完整记录在字段里。所以如果你刷到某些帖子说随便跑跑就 0.85要么是数据泄漏要么是过拟合。4.2 为什么随机森林在小数据集上容易翻车随机森林本身是一种稳定的集成模型它通过多棵决策树投票来降低方差。但在泰坦尼克号这种小样本、较多离散特征的数据上一个常见的错误是把max_depth或者min_samples_leaf设置得过小导致每棵树记忆了训练数据的噪声。我来举个具体的例子。训练集 891 条里某位乘客是 Pclass1、Female、Fare 特别高、TitleMrs、Cabin 区域是 D她恰好遇难了。模型如果把这套特征组合当成必死的规律那泛化到测试集时碰到相似但不完全一样的组合就会给出错误预测。我做过实验把max_depth从 10 改到 5min_samples_leaf从 1 改到 4本地交叉验证分数反而从 0.79 升到了 0.805。原因就是模型不再死记硬背而是学更普遍的模式。4.3 交叉验证的正确姿势StratifiedKFold这里必须强调一下验证策略。直接使用train_test_split(test_size0.2, random_state42)并不理想因为数据里的生还者比例约 38%如果你运气不好拆分出来的子集可能只有 30% 生还导致一次验证的结果波动很大。正确做法是使用分层 K 折交叉验证Stratified K-Fold保证每一折中的生还者比例和整体一致。from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.ensemble import RandomForestClassifier cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) model RandomForestClassifier(n_estimators200, max_depth5, min_samples_leaf4, random_state42) scores cross_val_score(model, X_train_scaled, y_train, cvcv, scoringaccuracy) print(scores.mean(), scores.std())如果scores.mean()和scores.std()的差距大比如多折中最高和最低相差 0.05 以上说明模型不稳定。这时候我会考虑增加树的数量n_estimators或者简化特征集而不是急着换模型。4.4 模型集成的收益与代价当你把单个模型的分数冲到 0.80 附近时会看到更多人开始提及融合模型。在我的实践里融合并不能无中生有地创造精度它更像是把几个不同类型模型逻辑回归、随机森林、XGBoost的预测结果做加权平均从而抵消各自的偏差。比如逻辑回归擅长捕捉线性关系随机森林擅长处理特征交互而 XGBoost 擅长处理非线性边界。一种简单的集成方式是投票from sklearn.linear_model import LogisticRegression from sklearn.ensemble import VotingClassifier lr LogisticRegression(max_iter500) rf RandomForestClassifier(n_estimators200, max_depth5, min_samples_leaf4, random_state42) ensemble VotingClassifier(estimators[(lr, lr), (rf, rf)], votingsoft) ensemble.fit(X_train_scaled, y_train)实验下来融合后的交叉验证分数大约比最好的单模型高出 0.0050.01。不要小看这 0.01在公共 Leaderboard 上可能就让你的排名提升数百位。但集成的代价是调试难度变高如果你还没有理解每个基模型的误差来源盲目集成反而会让结果更难解释。4.5 本地分数与榜单分数差异的根源这是很多人最容易焦虑的地方为什么本地交叉验证 0.81提交后公共榜只有 0.76最常见的解释有三个第一公共榜的评测样本占比不一样。公共榜可能只用了测试集的前半部分而交差验证用的是训练集的随机抽取两者存在分布差异。第二输入特征分布偏移。某些特征如Age、Fare的缺失值在测试集里的比例高于训练集即使你用了分组中位数填充训练和测试的差值仍然存在。第三过拟合。你在训练集上调参过多模型死记了训练集的特殊模式。这种模式下交叉验证分数当然高但一遇到新的测试样本就露馅。应对方案是本地交叉验证时尽量模拟提交过程比如训练集内部再次切分一个 sub-validation用来调整阈值或者验证模型稳定性。如果你在我这个阶段操作最终提交前会跑至少三轮交叉验证只提交那几轮结果都稳定的模型。5. 进阶路线与复盘心法从 0.79 冲 0.81 的关键习惯5.1 特征重要性分析看看模型到底在听谁的当你的模型跑通后不要急着提交先看一眼特征重要性。对随机森林来说直接读取feature_importances_对逻辑回归来说可以看系数的绝对值。以下面这段示例代码为例import matplotlib.pyplot as plt import seaborn as sns importance pd.Series(model.feature_importances_, indexX_train_scaled.columns) importance.sort_values(ascendingFalse).plot.bar(figsize(12, 6)) plt.show()在我的多次实验结果里排名靠前的通常是Title、Sex、Pclass、Fare而IsAlone和Cabin_Letter重要性相对靠后。这说明真正决定生还的仍然是社会经济地位 性别 舱位。如果某次实验中IsAlone的重要性反而极高那就要警惕过拟合了。5.2 三个看似合理但会坑你的操作第一个坑用整个数据集做归一化再切分。特征缩放标准化应当只用训练集的均值和标准差来做测试集应用同一组参数。如果先把train和test合并再统一缩放测试集信息就间接泄漏到了训练过程这会让交叉验证分数虚高。第二个坑对年龄缺失值用全量数据的统计量来填充。如果你先填充分再切分训练集和测试集同样属于泄漏。正确做法是只针对训练集的缺失部分计算中位数或众数然后把这个中位数应用到测试集。第三个坑反复在公共 Leaderboard 上碰运气。Kaggle 每日提交次数有限公共榜的结果也不是最终成绩。与其反复提交看分数不如记录下每次的配置、本地分数和公共分数做成表格。我自己的习惯是建模前先将猜测试集全部预测为 0提交一个 baseline确保流程跑通且提交格式正确然后再逐步改进。5.3 调参顺序先数据后特征再模型超参很多初学者把时间花在疯狂调n_estimators和max_depth上但经过前面几步你会明白特征工程的质量决定分数上限模型只是无限接近这个上限。调参的合理顺序是先保证数据没有缺失处理不当和泄漏问题再确认特征是否有实际含义最后才轮到网上抄一段GridSearchCV。使用GridSearchCV的时候注意给的参数范围不要太大否则会过拟合验证集本身。我在那个阶段常用范围是param_grid { n_estimators: [100, 200, 300], max_depth: [4, 5, 6, None], min_samples_leaf: [2, 4, 6] }搜索完后不是选分数最高的那组而是选在多个随机种子下都稳定的那组。例如在同一个参数下跑random_state分别为 0、42、2024 的三次交叉验证取平均分最高且方差最小的那组。这个习惯帮我避开了无数次局部最优。5.4 复盘模板每次提交后你应该记录什么最后分享一个多次实战中沉淀下来的记录习惯。每次提交后我会建一条笔记包含六项内容特征列表、模型类型、关键参数、本地交叉验证分数、公共排行榜分数、备注这次改了哪些特征/怎么处理了某个缺失值。这张表格看起来平淡无奇但坚持记录后你能清晰看到自己的每一步提升是来自于加了一个特征还是换了模型而不是凭感觉在多个方案之间来回试探。如果你问我整个 Kaggle 新手旅程中最值得学习的是什么我会说不是盯着排名而是学会有依据地做决定。泰坦尼克号如此别的赛题也是如此。先让数据可信再让特征合理最后才轮到模型炫技。按这个顺序走你的分数也许不会是榜首但一定稳定、可解释并且能在下一个赛题里复制这套方法论。