☰
泰坦尼克号生存预测:从数据清洗到特征工程的Kaggle实战指南
2026/10/10 1:05:51 网站建设 项目流程

简介:面向刚接触Kaggle的机器学习和数据科学初学者,泰坦尼克号生存预测资源覆盖探索性数据分析、特征工程到逻辑回归建模的完整流程,将经典二分类赛题拆解为可上手的实战项目。压缩包共十个文件,包含五份CSV数据表格、三个交互式分析笔记、一个Python脚本及一份说明文档,整体约459KB,便于快速下载与对照学习。目前已有136人浏览学习,适合希望理解准确率、精确率、召回率及交叉验证等评估用法的读者。资源不仅提供逻辑回归基线实现,还对比了决策树、随机森林与梯度提升机等算法,并附有缺失值处理与特征构造思路,可直接复用于后续同类竞赛。

1. Kaggle 泰坦尼克号生存预测,为什么十年过去仍是新手绕不开的第一题

Kaggle 泰坦尼克号生存预测 这个比赛,至今仍是大部分人提交到 Kaggle 的第一个 kernel。它只有 891 行训练数据和 11 个字段,却把数据清洗、特征工程、模型评估、提交验证这四段最容易翻车的路完整覆盖了一遍。我见过太多人拿默认参数跑完逻辑回归,本地交叉验证 0.80,提交却只有 0.75,问题全出在预处理和索引对齐上。这篇文章不复述教程,只按我自己复现的路径,把字段理解、特征构造、模型选型和隐藏的坑一次讲清楚,适合第一次做完整数据竞赛,也想搞懂 Kaggle 提交机制的人。

2. 先摸清数据再动手:train.csv 与 test.csv 的字段解读和三个缺失值重灾区

2.1 字段语义:11 个字段里哪些直接可用,哪些必须加工

原始数据集拆成 train.csv(891 行)和 test.csv(418 行),字段一共 11 个。先说角色划分:PassengerId 只做唯一标识,不参与预测;Survived 是标签,只在训练集里存在;Pclass、Sex、Age、SibSp、Parch、Fare、Embarked 是直接的模型输入;Name、Ticket、Cabin 这三个看起来像文本,实际上藏着很强的结构化信息,需要拆。

我的处理习惯是先给字段分三类:直接数值型(Pclass、Age、SibSp、Parch、Fare)、类别型(Sex、Embarked)、待加工型(Name、Ticket、Cabin)。Pclass 虽然也是数字,但它本质是等级类别,1/2/3 之间没有线性关系,所以我更倾向把它当类别特征用;Fare 有明显长尾,后面要考虑做尺度变换。SibSp 和 Parch 单独看表达能力有限,但两者相加再 +1 变成 FamilySize 之后,就是另一个故事了——这个放到第 4 章细说。

用一段代码把数据读进来,确认列名和类型,这一步花不了两分钟,但能避免后面索引错位的血泪事故:

import pandas as pd train = pd.read_csv('train.csv') test = pd.read_csv('test.csv') print(train.shape, test.shape) print(train.info()) print(train.isnull().sum())

这段逻辑很简单:shape 确认行数,info 看每列类型和缺失标记,isnull().sum() 告诉你要处理的缺失值集中在哪。train 的 Age 缺 177 条、Cabin 缺 687 条、Embarked 缺 2 条;test 集也有类似的缺失分布。Cabin 缺失率超过七成,意味着直接填值意义不大,得换思路。

2.2 缺失值分布与数值统计:Age、Cabin、Embarked 各是各的处理套路

三个缺失值重灾区,处理方式完全不同。Embarked 只有 2 个缺失,最常见做法是补众数,因为港口对生存率的影响并不显著,补 S 或 C 对分数影响很小,但 drop 掉这两行会让测试集索引对不上,属于给自己挖坑。Age 是 177 个缺失,用全体中位数填充属于能跑但粗糙的做法——头等舱平均年龄比三等舱大好几岁,按 Sex+Pclass 分组填充会更稳。Cabin 缺失 687 个,直接填字符串会让模型以为所有缺失都是同一类,常见做法是提取首字母作为甲板号,缺失的单独归为 U。

数值分布也值得留个心眼:Fare 的最小值是 0,说明有人没买票或票价缺失,这部分人和某些舱位、某些生存结果可能有相关性;Age 的分布整体呈右偏,孩子和老人占比都不高,这两类恰恰是高生存率群体。用 describe() 扫一遍最大值、最小值和中位数,看看有没有明显异常值,这比直接进模型要省很多事。

2.3 初始基准:一个什么都不加的逻辑回归先拿到 0.75 附近

动手做任何特征工程之前,先跑一个最朴素的基线。这样后续每一步改动都能和基线比,而不是凭感觉说“加了特征肯定变好”。我的基线只做三件事:Age 用中位数填充、Embarked 用众数填充、Cabin 先丢弃,然后跑逻辑回归 5 折交叉验证。

from sklearn.model_selection import cross_val_score from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import LabelEncoder train['Age'] = train['Age'].fillna(train['Age'].median()) train['Embarked'] = train['Embarked'].fillna(train['Embarked'].mode()[0]) features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare'] X = train[features].copy() X['Sex'] = LabelEncoder().fit_transform(X['Sex']) y = train['Survived'] model = LogisticRegression(max_iter=500, random_state=42) scores = cross_val_score(model, X, y, cv=5, scoring='accuracy') print(f'Baseline CV accuracy: {scores.mean():.4f} (+/- {scores.std():.4f})')

逻辑说明:这里只用了六个字段,Sex 做标签编码,其余保留原值。Pclass 和 SibSp 在逻辑回归里会被当作连续数值处理,这是一个妥协,因为基线阶段图快;到了第 4 章特征工程部分我会把它们改成更合理的离散类别编码。max_iter=500 是因为 sklearn 新版默认 100 次迭代在部分特征组合下不收敛,会报警告,调大以后警告消失。random_state=42 用于复现。

参数说明:cv=5 表示把训练集切 5 份,轮流拿 4 份训练 1 份验证,最终分数是 5 次的均值。scoring='accuracy' 是这个比赛的标准评估口径——直接算预测正确的比例,和 Kaggle 的 Public Score 口径一致。这个基线跑下来一般落在 0.75 附近,如果你跑出来偏差超过 0.02,优先检查是不是填充方式或标签编码出了顺序问题。

提示:基线不是用来刷高分的,是用来当标尺的。后续所有特征工程和模型调整,都要回答“比基线涨了多少”这个问题。

3. EDA 要看什么:性别、舱位、年龄的交叉观察,以及数据泄漏的边界

3.1 性别与 Pclass:两个最该先看的单变量交叉表

泰坦尼克号的数据里,性别和舱位是预测力最强的两个特征,强到单独用其中一个就能到 0.78 附近。这不是玄学,而是灾难场景下的真实规律:妇女和儿童优先上救生艇,头等舱乘客的活动区域离甲板更近、获救机会更大。用代码把这两个变量的生存率打出来,比看任何图表都直观:

print(train.groupby('Sex')['Survived'].mean()) print(train.groupby('Pclass')['Survived'].mean()) print(pd.crosstab([train['Sex'], train['Pclass']], train['Survived'], margins=True))

groupby算出全局视角下的性别差异和舱位差异,crosstab则展示两者的交叉效果——女性头等舱生存率极高,男性三等舱生存率惨不忍睹。这三个输出能帮你快速验证一个猜测:女性加头等舱的同时出现时,生存率是不是单调上升。实际数字我见过很多次:女性整体在 0.74 上下,男性只有 0.19 左右;Pclass=1 接近 0.63,Pclass=3 只有 0.24。交叉表会让结论更细:头等舱女性的生存率明显高于三等舱女性,说明舱位不是简单叠加在性别上的次要因素。

3.2 Age 分段与 FamilySize:组合观测的两种常用切法

Age 直接作为连续值丢进模型,逻辑回归只能学到线性关系,但年龄和生存率之间不是线性的:10 岁以下的孩子生存率高于成年人,80 岁以上的老人几乎没人救。常见做法是把 Age 切段,比如 [0, 10)、[10, 20)、[20, 40)、[40, 60)、[60, 100],然后看每段的生存率。切段的边界不用太讲究,但要保证每段都有足够的样本,否则统计意义不稳。

同样值得看的是家庭规模。SibSp 和 Parch 单独画出来,分布很零散,很多人是 0,少数人是 1 到 4,合并成 FamilySize = SibSp + Parch + 1 之后规律清晰得多:独身乘客和超大(4 人以上)家庭生存率低,2 到 3 人的小家庭生存率高。背后逻辑也说得通,独身的人没人照应,家庭太大又容易被拆散。把这两个变量组合起来再画一张生存率热力图,会比单独看更好判断特征怎么构造。

3.3 数据泄漏的边界:为什么测试集的信息不能进训练步骤

数据泄漏是新手翻车最隐蔽的原因之一,泰坦尼克号这个题目里尤其常见。最典型的误用是把 train 和 test 拼在一起,然后对整个合并集做fillna或SimpleImputer,再用处理完的数据训练模型。这在 Kaggle 社区里其实有争议,因为填充缺失值用的是无监督信息,严格说不算标签泄漏,但它确实让测试集分布参与了训练步骤,会让交叉验证分数虚高。

我一般分两层来看:像 Age 中位数这种填充统计量,如果只在 train 上 fit 再 transform test,是最稳妥的做法,任何评测环境下都不会被质疑;像 Cabin 首字母这种纯映射规则,不涉及统计量,可以直接在合并集上操作。真正的红线是不能让 Survived 的信息通过任何路径流进特征——比如有人用全体乘客的最终生存情况去反推 Age 填充值,这就是直接泄漏。判断标准只有一条:你用来拟合特征处理器的数据里,包不包含标签信息。

4. 特征工程:从缺失值填充到 Title 提取,一套够用到 0.78 的特征组合

4.1 缺失值填充:Age 用分组中位数而不是全体中位数,Cabin 填 U 之后再拆分

进入特征工程阶段,先把第 2 章的粗糙填充换掉。Age 的填充方式我固定用 Sex + Pclass 分组中位数,因为头等舱乘客平均年龄明显大于三等舱,男性女性也有差异,分组填充能让分布更贴近真实情况。Cabin 的 687 个缺失值不填具体字符串,而是构造两个新特征:CabinKnown(0/1 表示是否有 Cabin 记录)和 CabinLetter(提取首字母,缺失的填 U)。有记录本身可能就是信息——有 Cabin 的人通常住在上层甲板。

train['Age'] = train.groupby(['Sex', 'Pclass'])['Age'].transform(lambda x: x.fillna(x.median())) test['Age'] = test.groupby(['Sex', 'Pclass'])['Age'].transform(lambda x: x.fillna(x.median())) for df in [train, test]: df['CabinKnown'] = df['Cabin'].notna().astype(int) df['CabinLetter'] = df['Cabin'].str[0].fillna('U') df['Embarked'] = df['Embarked'].fillna(df['Embarked'].mode()[0])

逻辑说明:transform会把分组计算得到的中位数广播回每一行,不会改变行索引顺序。先对 train 和 test 分别做,保证统计量只来自各自数据。Embarked 用众数填充是安全牌,没有任何调参空间。

参数说明:分组键用['Sex', 'Pclass']而不是单独的['Sex']或['Pclass'],是因为这两个维度对年龄分布都有显著影响,只按一个分组会留下偏差。str[0]取 Cabin 首字母,相当于把 A、B、C、D、E 这些甲板编号提取出来,比整套 Cabin 字符串更稳定。

4.2 构造新特征:FamilySize、IsAlone、Title、Ticket 频次按顺序做

将新特征按依赖顺序构造。FamilySize 依赖 SibSp 和 Parch;IsAlone 又依赖 FamilySize;Title 从 Name 里提取;Ticket 频次则统计每张票对应的人数。每个特征之间没有交叉依赖,但代码里我会把有依赖关系的放前面,方便阅读和排错:

for df in [train, test]: df['FamilySize'] = df['SibSp'] + df['Parch'] + 1 df['IsAlone'] = (df['FamilySize'] == 1).astype(int) df['Title'] = df['Name'].str.extract(r' ([A-Za-z]+)\.', expand=False) df['Title'] = df['Title'].replace( ['Lady', 'Countess', 'Capt', 'Col', 'Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare') df['Title'] = df['Title'].replace(['Mlle', 'Ms'], 'Miss') df['Title'] = df['Title'].replace(['Mme'], 'Mrs') df['TicketFreq'] = df.groupby('Ticket')['Ticket'].transform('count')

逻辑说明:IsAlone 是把 FamilySize 为 1 的情况单独二值化,给决策树一个明确的分裂点。Title 用正则抓出 Name 里的称谓,把 Miss、Mrs、Mr、Master 保留,稀有称谓合并成 Rare——这一步非常关键,因为 Master 通常指未成年男孩,其生存规律和成年男性完全不同。TicketFreq 反映同票人数,多人共享一张票说明同行,和 FamilySize 有重叠但不完全一样,能帮模型捕捉“团体票”这个信号。

参数说明:正则r' ([A-Za-z]+)\.'的意思是匹配一个空格、一串字母、一个点号,Name 字段的标准格式让这个提取非常稳定。替换列表里的词是按泰坦尼克号数据里实际出现过的称谓整理的,不同比赛的术语集合不一样,换数据集时这段要重新核对,不能照搬。

4.3 编码与尺度:One-Hot 优于 LabelEncoder 的几个场景,Fare 要做对数变换

构造完特征之后,进入编码环节。Sex、Embarked、CabinLetter、Title 都是类别型,类别数量分别是 2、3、8、5,全部用 One-Hot。Pclass 我同样按类别处理,因为 1/2/3 的间距不等,给逻辑回归当成连续值会扭曲舱位差异。LabelEncoder 适合树模型,因为它不要求特征有线性顺序;但逻辑回归是线性模型,类别编码的数值大小会被当作权重,One-Hot 更安全。

from sklearn.preprocessing import OneHotEncoder, FunctionTransformer import numpy as np def build_features(df): df['Fare'] = df['Fare'].fillna(df['Fare'].median()) df['FareLog'] = np.log1p(df['Fare']) return df train = build_features(train) test = build_features(test) cat_cols = ['Sex', 'Embarked', 'CabinLetter', 'Title', 'Pclass'] enc = OneHotEncoder(handle_unknown='ignore', sparse_output=False) train_enc = enc.fit_transform(train[cat_cols]) test_enc = enc.transform(test[cat_cols])

逻辑说明:Fare 有少数缺失,用中位数填充后取np.log1p,把长尾压缩。票价从 0 到 512 的跨度非常大,直接进逻辑回归会主导梯度,对数变换后变成相对平稳的分布。OneHotEncoder 的handle_unknown='ignore'保证换数据集时没见过的类别不会报错,只会生成全零向量。

参数说明:sparse_output=False让编码结果直接输出为稠密数组,方便和数值特征拼接;这在 sklearn 新版本里很重要,旧版sparse=True会输出稀疏矩阵,后面对接np.hstack时要额外处理。编码器只在 train 上 fit,test 是 transform——这是上一章反复强调的防泄漏底线。

5. 模型调参与避坑:三组模型对比、交叉验证换算,和五个必须排掉的雷

5.1 逻辑回归、随机森林、XGBoost 的基线对比与参数网格

特征工程做完后,我在同一个特征集上跑三个模型:逻辑回归作为线性基准,随机森林和 XGBoost 作为非线性模型代表。泰坦尼克号数据量小,非线性模型的优势有限,但树模型能自动捕捉特征之间的交互。对比三个模型的 5 折交叉验证均值,能快速判断这个特征工程是在往哪个方向起作用:

from sklearn.ensemble import RandomForestClassifier from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from xgboost import XGBClassifier import numpy as np num_cols = ['Age', 'FareLog', 'FamilySize', 'SibSp', 'Parch', 'TicketFreq', 'IsAlone', 'CabinKnown'] X_final = np.hstack([train_enc, train[num_cols].values]) y = train['Survived'].values models = { 'lr': make_pipeline(StandardScaler(), LogisticRegression(max_iter=500, random_state=42)), 'rf': RandomForestClassifier(n_estimators=200, max_depth=5, random_state=42), 'xgb': XGBClassifier(n_estimators=200, max_depth=3, learning_rate=0.05, random_state=42) } for name, model in models.items(): scores = cross_val_score(model, X_final, y, cv=5, scoring='accuracy') print(f'{name}: {scores.mean():.4f} (+/- {scores.std():.4f})')

逻辑说明:三种模型共用一个特征矩阵。逻辑回归对数值尺度敏感,所以套了 StandardScaler;随机森林和 XGBoost 不需要标准化,单独列出来。随机森林限制max_depth=5,XGBoost 用learning_rate=0.05,都是为了在小数据集上控制过拟合——泰坦尼克号只有 891 行,不限制深度的话训练集得分能到 0.98,验证集只有 0.80,典型的高方差。

参数说明:n_estimators=200在这个数据规模下足够,再往上加树对分数的提升微乎其微,只会拖慢运行时间。max_depth=5是我在这个数据集上试过 3、4、5、6 之后相对稳定的选择。XGBoost 的学习率 0.05 配合 200 棵树,是在速度与精度之间的折中,学习率过低需要更多树才能收敛。

5.2 交叉验证与本地得分的换算:为什么本地 0.82 提交只有 0.78

这是泰坦尼克号项目最让新手困惑的现象:本地交叉验证 0.82,提交到 Kaggle 只有 0.78。原因有三个。第一,交叉验证把训练集切成 5 份,每轮只有 80% 数据训练模型;Kaggle 的评分流程是让你提交测试集预测结果,然后用在线的私有测试集算分,模型面对的是完全没见过的 418 行数据,分布多少有些偏移。第二,数据量太小,891 行样本里随机切一折出来,验证集只有 178 条,单折分数的标准差经常超过 0.02,0.82 的均值可能只是某一折偏高拉起来的。第三,也是最容易被忽视的,提交文件本身的索引或行数错误会直接把分数拉低。

解决本地分和线上分不一致的办法只有一个方向:让本地验证尽可能接近线上逻辑。固定随机种子,让每次切分结果可复现;用 StratifiedKFold 而不是简单 KFold,保证每一折的标签比例和全集一致;模型调参阶段不追求单次最高分,而是看均值与标准差的组合。如果本地均值和线上分数长期稳定差 0.03 左右,说明偏移是系统性的,不是代码问题。

from sklearn.model_selection import StratifiedKFold, cross_val_score skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(model, X_final, y, cv=skf, scoring='accuracy') print(f'Stratified CV: {scores.mean():.4f} (+/- {scores.std():.4f})')

逻辑说明:shuffle=True让切分前先打乱样本顺序,避免原始数据集里按舱位或姓氏排序造成的顺序偏差。random_state=42固定 shuffle,保证你每次跑出来的分数一致。分层抽样保证每折的 Survived=1 比例和全集一致,这是小数据集交叉验证稳定性的底线。

参数说明:StratifiedKFold 的n_splits=5是精度和方差的折中——折数越多,训练数据越多,但验证集越小、单折分数波动越大。这个数据量下 5 折是普遍选择,10 折时验证集只有 89 条,波动会大到没法判断特征好不好。

5.3 五个常见翻车点:现象、原因、解决

翻车点一:Cabin 直接丢弃,丢失甲板位置信号

现象:特征工程做完以后分数没有明显变化,甚至比基线还低。 原因:Cabin 缺失率超过 70%,很多教程直接 drop,但剩余 30% 的甲板位置信息对头等舱乘客的生存率有明显区分度。 解决:用第 4 章的做法,构造 CabinKnown 和 CabinLetter 两个特征。即使缺失太多,CabinKnown 也能保住“有记录”这个信号。

翻车点二:Age 用全体中位数填充,掩盖分组差异

现象:加入 Age 相关特征后,模型分数停滞在 0.77 上不去。 原因:全体中位数把不同性别、不同舱位的年龄差异抹平了,模型学不到“头等舱中年女性”这类组合特征。 解决:改成groupby(['Sex', 'Pclass'])['Age'].transform('median')分组填充,这个改动单枪匹马能把分数提 0.005 到 0.01。

翻车点三:预测测试集时用了训练集的均值填充器

现象:提交后分数和本地交叉验证差出 0.05 以上。 原因:如果先合并 train 和 test 再统一填充,填充统计量里包含了测试集信息,交叉验证分数虚高。严格评测环境下模型没见过测试集,表现自然回落。 解决:所有填充器、编码器、缩放器都在 train 上 fit,test 只做 transform,代码上拆成两步。

翻车点四:Embarked 的 2 个缺失值直接 drop 行

现象:提交时报错,或 Survived 行数不等于 418。 原因:drop 掉 train 里的两行数据没问题,但如果有人顺手把 test 里含缺失值的行也 drop 了,就破坏了提交格式。 解决:Embarked 用众数填充,一行代码解决问题。不要为两个缺失值承担行数不一致的风险。

翻车点五:submission 索引错位,本地 0.82 提交 0.55

现象:本地交叉验证分数正常,提交后分数惨到怀疑人生。 原因:特征工程过程中 DataFrame 索引被重排或重置过,to_csv时 pandas 默认按当前索引顺序输出,导致预测结果和 PassengerId 错位。 解决:提交前强制检查submission.shape等于 418 行,submission['PassengerId'].is_monotonic_increasing确认顺序,必要时reset_index(drop=True)。

6. 进阶:把流程固化成一条流水线,用三查法确认提交文件

6.1 从 EDA 到提交的固定流水线,参数只留两个口子

做到这个阶段,最忌讳的是每次跑实验都从头执行一遍 Notebook。我把前面所有步骤按依赖顺序封装成函数——特征构建、编码、模型训练、预测输出各占一个函数,以后换数据集或者调参数,只改两个口子:特征列清单和模型参数。这样做的理由很朴素:泰坦尼克号这种小数据项目,一次完整跑通只需要几十秒,但每次手动复制粘贴改代码,就容易在填充器或编码器上出现上一章说的那种肉眼看不见的泄漏。

def make_submission(model, train_df, test_df, filename): # 统一走同一套特征工程,保证 train/test 处理方式完全一致 X_train = build_features(train_df) X_test = build_features(test_df) model.fit(X_train, train_df['Survived']) preds = model.predict(X_test) sub = pd.DataFrame({'PassengerId': test_df['PassengerId'], 'Survived': preds}) sub.to_csv(filename, index=False) return sub make_submission(models['xgb'], train, test, 'submission.csv')

逻辑说明:这个函数把两个最重要的约束写死——测试集必须走和训练集完全相同的特征处理路径,输出文件必须按 PassengerId 对齐且不写索引。把所有预处理逻辑封装进build_features后,模型替换只是一行参数的事。

6.2 最后验证:提交前必须过的三查法

文件生成之后不要急着上传,先过三查。一查行数,submission.shape[0]必须等于 418,多一行少一行都是索引事故。二查列名,第一列必须是 PassengerId,第二列必须是 Survived,Kaggle 对列名有严格校验,拼写错了直接零分。三查顺序,PassengerId必须是 892 到 1309 递增,和 test.csv 的原始顺序完全一致。这一步我可以负责任地说,至少三分之一的新人翻车都翻在这里,而不是模型本身。

最后讲一个我自己的教训:早期做这个题的时候,我在特征工程里把 DataFrame 按 Fare 排序了一遍,忘掉 reset_index,提交后分数只有 0.55,排查了一整天才发现是索引错位。从那以后我把三查法写进每个项目的收尾流程,再没犯过同样的错。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询