1. 为什么叫“边境的悍匪”
1.1 一个看着简单、实际很不讲理的题目
“泰坦尼克号生还预测”大概是所有机器学习初学者第一个听到的比赛名。训练集不到900行,字段也就十来列,连图片、文本、时序这些复杂数据都没有,怎么看都像是个拿来练手的Hello World。但真动手做你就会发现,这个题就像蹲在应用机器学习边界线上的一股悍匪:外表粗犷、路径险恶,凡是带着“跑个模型就能交差”的心态来的人,基本都要在这里挨一顿毒打。
我说这话不是吓唬新手。当年我第一次做这个题,上来就取性别、年龄、票价三个字段丢进逻辑回归,自我感觉良好,结果本地准确率连0.78都不到,提交到榜上排名被一堆0.80+的压着。后来我才慢慢想明白,这道题真正考的不是你会不会用某个模型,而是你有没有把数据当成一个需要破译的系统,而不是一行行光秃秃的数字。
悍匪的凶残之处在于四个字:藏得深。表面上每个字段都一目了然,但年龄有缺失、船舱有大量缺失、票号看起来毫无规律,名字里却藏着身份信息。你要是只用现成字段硬跑,模型虽然也能出分,但永远卡在一个平庸的位置。等到你把缺失值、衍生特征、交叉验证这些手段都用上,才发现原来这个题的题库里早就埋好了下一关的钥匙。
1.2 从这个题里你能真正带走什么
这个题适合谁来做?我认为最适合两类人:一类是刚学会Pandas和Sklearn基础、想体验完整竞赛流程的人;另一类是已经跑过几个Demo、但从未认真评估过自己模型的人。对前者,它能让你走一遍“读数据、画图、清洗、建模、提交”的完整闭环;对后者,它能把你的自满情绪按在地上摩擦。
你以为你在学“预测生还”,其实你在学三件事:
- 如何处理真实数据里的缺失与噪声,而不是用现成的干净数据集糊弄自己。
- 如何构造特征,让人肉眼能看出的模式变成模型能计算的数值。
- 如何用交叉验证说服自己,而不是被一波测试集分数冲昏头。
这三件事,放到任何一个工业界的建模项目里都是基本功。泰坦尼克号只是恰好用一个有趣的历史背景把它们串了起来。别把它当比赛,把它当成一次“从数据到决策”的完整演练,你才不会空手而归。
2. 动手之前先梳理比赛规则和指标
2.1 这次到底在预测什么
泰坦尼克号这个比赛,目标非常直接:给出一份测试集里每个人是否生还的预测,生还标为1,死亡标为0。训练集有891条乘客数据,测试集有418条。你的任务就是训练集里学出规律,再对测试集乘客输出一个两列提交文件:PassengerId和Survived。
听起来简单,但有个隐藏前提:历史和规则。泰坦尼克号沉没时,救生艇数量不足,船员遵循了“妇女儿童优先”的疏散规则。这导致幸存模式不是均匀随机的,而是高度偏向特定人群。所以数据里那些看似无关的性别、年龄、船舱等级,其实都是当年救生艇分配规则留下的记录。
理解这一点对建模至关重要。纯粹的数据竞赛可以靠暴力调参刷分,但泰坦尼克最能出分的模型,几乎都是因为特征里还原了“谁先上救生艇”这个决策逻辑。性别字段就是最典型的例子:船上女性生还比例远高于男性,任何合理的模型都应该把Sex作为最重要的变量之一。
2.2 评估指标里有个容易让人翻车的细节
这个比赛用的评估指标是Accuracy,也就是预测正确的比例。先别急着高兴,Accuracy在类别不平衡时是很阴险的。泰坦尼克数据里,生还人数比死亡人数少一些,整体大概四六开。如果你无脑预测“全员死亡”,也能拿到约60%的准确率。也就是说,准确率低于0.62的模型,还不如一个猜谜机器人。
所以评估模型时不能只看总准确率,我会同时看三样东西:混淆矩阵、精确率、召回率。尤其是混淆矩阵,它能告诉你模型是把“死亡者猜对了”还是“生还者猜对了”。在泰坦尼克这种题目里,生还者识别得准不准确,往往反映了你有没有真的抓到特征规律,而不只是把多数类蒙对了。
另外,Kaggle排行榜上的公榜分数来自你提交到测试集的预测。但训练集和测试集不是同一个样本,线上分数和本地交叉验证分数之间会有波动。绝对不要为了刷公榜分数去反复调整个别样本的预测,那是自欺欺人。正确的做法是建立可靠的本地交叉验证流程,让它成为你判断模型好坏的唯一标准。
3. 把训练数据翻个底朝天:探索性数据分析
3.1 先看字段,再谈建模
拿到的原始数据常见字段包括:PassengerId、Survived、Pclass、Name、Sex、Age、SibSp、Parch、Ticket、Fare、Cabin、Embarked。Survived是只有训练集才有的标签字段,测试集没有。剩下的每一个字段都在描述一个人的身份或行程信息。
第一件事是把这些字段分类:
- 数值连续型:Age、Fare。
- 数值离散型:Pclass、SibSp、Parch、Survived。
- 类别型:Sex、Embarked、Cabin、Ticket、Name。
一眼就能看出问题:Name这种字符串不可能直接进模型,Cabin和Ticket的格式也很乱,Age还有不少缺失。先把这些字段摊在桌面上,你才能规划哪些需要清洗、哪些能挖出衍生信息。
这里我习惯用.info()快速体检:
import pandas as pd train = pd.read_csv('train.csv') test = pd.read_csv('test.csv') print(train.info()) print(train.isnull().sum())打印结果里最扎眼的就是Age约缺177个、Cabin约缺687个、Embarked缺2个。Cabin缺失这么多,很多初学者直接选择删除,这其实丢掉了一大块信息。有没有救生艇编号的人,本来就可能意味着不同的身份和生存条件。
3.2 画图看关系,别用肉眼硬猜
探索数据最忌讳只盯数值表格。我一般会先画几个箱线图和柱状图,看每个变量与Survived之间的关系。最经典的结论是:女性的生还率明显高于男性;头等舱的生还率高于三等舱;儿童生还率相对较高,而老年人生还率偏低。
别小看这几条结论。它们直接决定你后面特征工程的方向。你会发现,能让你从0.75提升到0.80的,不是换了个更强的模型,而是把“性别与船舱等级的组合”这个交互信号喂进了模型。比如女性且头等舱的乘客,生还概率会进一步拉高;男性且三等舱的乘客,生还概率近乎惨淡。
画图工具我一般直接用Matplotlib和Seaborn:
import seaborn as sns import matplotlib.pyplot as plt sns.barplot(data=train, x='Sex', y='Survived') plt.show()看到这张图你就会明白,为什么Sex会是建模第一功臣。但EDA不是只看单变量,还要看多变量交叉。用Pclass和Sex组合的透视表看看生还率,也能发现很多单看一列时发现不了的规律。这一步做得越细,特征工程就越有谱。
4. 特征工程:把肉眼看到的规律变成模型能懂的东西
4.1 缺失值处理:最不能偷懒的一步
泰坦尼克题目里,Age缺失值处理是第一个分水岭。常见做法是直接填中位数或均值,简单但粗鲁。更好的做法是按Pclass、Sex、Title分组后再填充,因为不同身份的人年龄分布差异很大。我用的方法通常是:
- 先从Name里提取Title字段,比如Mr、Mrs、Miss、Master。
- 再把Title和Pclass结合,对每个分组计算Age中位数。
- 用分组中位数去填充该组内的缺失年龄。
这么做比全局填充更接近真实分布,对后续模型收益明显。有人可能会问,是不是直接用模型预测缺失年龄更好?可以是可以,但要小心不要引入额外噪声。在泰坦尼克这种小数据量场景里,我倾向于分组填充加一个“Age是否缺失”的标识特征,让模型自己去判断。
Cabin的处理思路不太一样。它缺失太严重,直接填值没什么意义。我更常做的是构造一个“是否有Cabin记录”的二值特征,因为从表现来看,有Cabin记录的人与没有Cabin记录的人,生存模式有明显差异。这就是从残缺数据里挖出的有效信号。
Ticket字段也是一种看似无用的文本。但如果把Ticket前缀拆出来,比如“PC”开头、“CA”开头,你会发现某些前缀对应了不同的舱区和购票渠道,对未来存活率也有轻微影响。我做的时候会把前几位字母提取出来,统一成几大类,再当类别特征编码。
4.2 衍生特征:让模型看到隐藏信息
单一字段能提供的信息有限,但组合之后往往威力惊人。我最常用的三个衍生特征:
FamilySize = SibSp + Parch + 1。一个人出行和带着全家人出行,行动逻辑完全不同。想想就知道,独自一人时逃生更灵活,拖家带口时决策会更复杂。这个特征提出来后,可以再划分成“Solo / Small / Large”三档,转成类别特征效果更直观。
Title从姓名里提取身份称谓。这个特征浓缩了年龄、性别、社会地位甚至婚姻状态,别看只是字符串里截几个字母,信息量极大,对缺失年龄的辅助填充也很有价值。
Fare分箱。Fare是连续数值,但票价和船舱等级、位置强相关。我会用分位数的形式把票价切成几个区段,或直接取对数压缩幅度,避免高端票价把模型带偏。
还有一个容易被忽略的组合特征:Pclass和Sex的交叉。比如“Female + Pclass 1”可以编码成一个新特征,因为这类乘客的生还率极高。本质上,这是在帮模型建立交互项,而不是只把字段平铺给它。做这步时不要贪多,每加一个特征之前都问问自己,它背后有没有业务逻辑支撑,没有逻辑的特征只会增加过拟合风险。
4.3 哑变量编码的细节
类别特征不能直接喂给线性模型,常规做法是转为哑变量。Pandas的get_dummies很方便,但要注意几点:
- 训练集和测试集必须用相同的列结构,防止一边多出一列、另一边没有。
- 对高基数类别,考虑合并稀有类别,避免哑变量矩阵过于稀疏。
- 数值编码要区别对待:Pclass虽然写成1、2、3,但它是有序类别,可以用作数值特征,也可以做哑变量,两种方式都试一下,以验证结果为准。
我一般先用pd.get_dummies(train, columns=['Sex', 'Embarked', 'Pclass'])生成基础版本,再手动合并衍生特征,最后统一对齐训练集和测试集的列集合。
5. 建模与验证:不要一上来就调参
5.1 先跑一个正经的基线模型
很多人一上来就调到随机森林或XGBoost,调了半天参数发现分数波动很大。我的习惯是先跑一个逻辑回归,把所有基础特征喂进去,得到一个可以解释的基线分数。逻辑回归的优势是透明,你能看到每个特征的系数,判断哪些信息被模型真正用上了。
这个阶段的目标不是拿高分,而是建立一条完整的流水线:从原始数据到特征矩阵,再到模型预测,最后输出提交文件。只要这条流水线通了,后面换模型就是改三行代码的事。
from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, random_state=42 ) model = LogisticRegression(max_iter=1000) model.fit(X_train, y_train) print(model.score(X_val, y_val))如果基线逻辑回归能到0.78左右,说明特征工程没有白做。如果连0.75都不到,先回头检查缺失值填充和编码方式,不要急着换模型。
5.2 交叉验证才是市侩大佬的底牌
固定划分训练集和验证集有一个隐患:你可能会在某个随机划分上运气好或运气差。泰坦尼克数据量小,随机性影响很大。我改用5折交叉验证之后,发现同一模型在5组不同划分下,分数能波到0.75到0.81之间。
交叉验证的目的不是让你挑出最高分的那一折,而是把5折分数的均值和标准差都看明白。均值代表真实水平,标准差代表稳定性。一个均值0.78、标准差0.01的模型,比一个均值0.80、标准差0.03的模型更值得信任。
from sklearn.model_selection import cross_val_score cv_scores = cross_val_score(model, X, y, cv=5, scoring='accuracy') print(cv_scores.mean(), cv_scores.std())在这个基础上再去尝试随机森林、梯度提升树,你会发现它们的优势更明显。但这些树模型也需要小心的预处理和参数约束。别让单棵树的深度太大,别让叶子节点太少,防止模型把训练集里某些生还率特异的个案背下来,一到测试集就露馅。
5.3 从单个模型到组合模型
树模型在新样本上容易不稳定,所以我会考虑两个方向的改进:一是调参约束模型的复杂度,二是做模型融合。泰坦尼克最常见的融合方式是逻辑回归、随机森林、梯度提升树三者投票或取平均概率。
不过做融合前,必须保证每个模型都已经调得不错。如果底模本身有缺陷,融合只是把多个坏预测综合到一起。另一个常见做法是使用简单的Stacking:把基模型的预测概率作为新特征,再交给逻辑回归做第二层学习。在小数据集上要小心过拟合,所以我会给第二层模型很轻的复杂度,比如只用少量严格正则项的逻辑回归。
6. 全流程提交:从预测到出分
6.1 对齐训练集和测试集是硬道理
整个流程最后一步,最容易翻车的就是测试集预处理的列对齐。训练集经过特征工程后生成了一组列,测试集可能因为某个类别值没出现,生成的是另一组列。直接拿模型预测测试集会报错,或者更可怕的是不报错但列顺序错位。
我写了一个通用的小函数来处理对齐:
def align_columns(train_df, test_df): missing_in_test = set(train_df.columns) - set(test_df.columns) for col in missing_in_test: test_df[col] = 0 test_df = test_df[train_df.columns] return test_df先把缺失列补成0,再按训练集的列顺序重排测试集。这一步做了之后,预测出来的结果才是可信的。另一个容易翻车的点是把PassengerId带丢了,提交文件里没有PassengerId会直接导致无法评分。
6.2 提交文件格式与线上分数回验
提交文件必须是一个CSV,两列,第一行是表头“PassengerId,Survived”,第二行起是每一行乘客ID和0/1预测值。我通常会让模型预测概率,然后按0.5阈值转成0或1。如果想让成绩再稳一点,也可以对阈值做小范围搜索,比如0.45到0.55之间,看交叉验证哪个阈值最合适。
test_pred = model.predict_proba(test_X)[:, 1] submission = pd.DataFrame({ 'PassengerId': test['PassengerId'], 'Survived': (test_pred >= 0.5).astype(int) }) submission.to_csv('submission.csv', index=False)第一次提交后,经常会发现线上分数比本地低0.01到0.02。这不一定是代码有问题,而是公共测试集和训练集分布有差异。只要差值在合理范围内,我一般不会强烈针对公榜调参。但如果你写了一个本地0.85、线上0.78的模型,那基本可以断定过拟合了,回去查特征里是不是塞入了太多特殊个案的信息。
7. 避坑清单与进一步扩展
7.1 我踩过的那些坑
第一坑是手写验证集且不复用。早期我为了省时间,只划分一次验证集,分数稍微提升就以为自己进步了。后来发现,很多时候只是这次随机划分对某个模型更友好。现在无论做什么实验,我都会固定随机种子,并且用交叉验证而不是单次划分。
第二坑是特征工程和调参顺序弄反。先调参后做特征,会导致整个实验空间混乱,参数是为了某个特定特征组合调的,特征一变,参数全废。正确顺序是先定特征组合,再做参数搜索,最后才碰融合。
第三坑是忽略类别编码的一致性。曾有一次我在训练集对Cabin类别做了编码,但在测试集因为某些类别不存在,编码结果多了一列少了一列,模型预测时直接报错。从那以后,我在跨数据集操作后都会加一个shape检查,确保训练矩阵和测试矩阵列数一致。
还有一个情绪坑:公榜排名焦虑。泰坦尼克的测试集只有418条,预测结果的随机波动比想象中更大。有时候你辛辛苦苦加了特征,公榜分数反而下降0.005,这不一定是特征没用,可能只是运气偏差。以交叉验证为准,别被单次数值牵着鼻子走。
7.2 这个题之后还可以往哪里走
做完泰坦尼克,下一个方向不要急着追求复杂比赛。可以试试两类任务:一类是类似的结构化表格题,比如房价预测,它同样考验特征工程和数据清洗;另一类是带文本或图像的数据集,开始接触非结构化数据。
如果想把泰坦尼克本身玩得更深,可以试着改进特征工程:用更细致的名字称谓分组,用乘客姓名里的家族关键词做家庭分组,甚至用Cabin的楼层和位置信息做空间特征的还原。每动一步,都用交叉验证对比一下是否真的变好,这个过程本身就是机器学习里最重要的一项能力:学会判断什么改变是有效的,什么只是偶然波动。
我个人在实际操作中的体会是:泰坦尼克不是一个比谁跑得快的问题,而是一个比谁对数据更耐心的问题。那些看起来花哨的0.83、0.84分数,背后的功夫几乎都下在了建模之前的数据整理里。先把数据摸透,把特征做扎实,再用简单的模型拿稳分数,比一上来就堆复杂模型要靠谱得多。