☰
Kaggle泰坦尼克号生存预测:从特征工程到随机森林建模全流程解析
2026/9/28 13:27:07 网站建设 项目流程

简介:一份面向数据科学初学者和Kaggle入门者的泰坦尼克号生存预测实战资源,压缩包共6个文件、大小40KB,包含4个CSV数据文件与2个带注释的Python源码文件。代码覆盖从数据预处理、特征工程到模型训练与评估的完整链路,可学习Pandas处理缺失值、分类变量独热编码、家庭成员与年龄等特征构造,以及逻辑回归、随机森林、XGBoost等常见模型的实现与调参。目前已有1361人学习该资源,适合作为第一次完整跑通Kaggle项目并了解提交与评分机制的参考。通过逐行注释,读者能理解每一步操作的目的,并掌握交叉验证、GridSearchCV超参数搜索和提交预测结果的方法,从而快速建立从数据到结论的整体认知。这套代码不仅是竞赛入门模板,也是后续进行特征选择、模型融合和性能优化时可复用的基础工具。

1. Kaggle 平台泰坦尼克号数据集:从 train.csv 到提交文件的一条完整流程

如果你第一次打开 Kaggle 竞赛官网,大概率会在 Getting Started 赛区看到泰坦尼克号生存预测这个经典项目。数据集不大、规则也简单——根据乘客的年龄、性别、舱位、票价等信息预测生死,但它把数据科学竞赛的完整链路全塞进去了:数据预处理、特征工程、模型构建、评估到最后生成提交文件,一个环节都省不掉。这份带注释源代码和四张 CSV 的压缩包,就是按这条标准流程走到底的参照物。适合刚学完 Pandas、想完整跑一遍比赛流程的新手;也适合已经跑过基线、想对比 One-Hot 编码和特征处理方式的熟手。我按自己拆包的实际顺序来写,把容易翻车的地方也一起标出来。

2. 把包里的五个文件盘清楚:train/test 关系与缺失值预处理

2.1 文件清单里谁是真主角:train.csv、test.csv 与 gender_submission.csv 的关系

先盘文件。这个压缩包里有六个东西:train.csv、test.csv、gender_submission.csv、Taitan_onehot.csv、example1.py、example2.py,外加一个 Titannic 文件。第一次打开的人容易被命名绕晕,其实真正建模时只需要 train.csv 和 test.csv,其他都是阶段产物或辅助文件。

train.csv 是带标签的训练集,最后一列 Survived 就是我们要预测的目标字段,值为 1 代表幸存,0 代表遇难。test.csv 没有这一列,我们的任务就是训练出一个模型,对 test.csv 里的 418 个乘客做预测,生成提交文件。gender_submission.csv 是 Kaggle 官方给的基准提交示例,逻辑很简单:女性全预测为存活,男性全预测为遇难。这个基准线的得分大约是 0.76,第一次做这个项目时先提交一次这个文件,后面所有模型改进就都有了对照基数。

Taitan_onehot.csv 是特征工程做完之后的产物,也就是把原始字段转成 One-Hot 编码之后保存下来的中间数据表。example1.py 和 example2.py 是两个带注释的 Python 脚本:前者是基线模型路线,后者是进阶模型路线。我在后面两章会分别讲这两个脚本的核心逻辑。

接下来看 train.csv 的字段。每个乘客一行记录,列包括 PassengerId(乘客编号)、Survived(是否存活,仅训练集有)、Pclass(舱位等级,1/2/3)、Name(姓名)、Sex(性别)、Age(年龄)、SibSp(同行的兄弟姐妹/配偶数量)、Parch(同行的父母/子女数量)、Ticket(票号)、Fare(票价)、Cabin(船舱号)、Embarked(登船港口,C/Q/S)。其中 Pclass 和 Embarked 是类别字段,Age 有缺失,Cabin 缺失率极高,这三处是后续处理的重点。

2.2 缺失值不是玄学:用 Pandas 先摸清数据再动手

拿到数据集第一步不是急着建模,而是把数据缺成什么样统计出来。缺失值处理直接影响后面所有特征和模型,这一步做错,后面再折腾也是白费。

import pandas as pd train = pd.read_csv("train.csv") test = pd.read_csv("test.csv") print(train.shape) # (891, 12) print(test.shape) # (418, 11) null_count = train.isnull().sum() null_count = null_count[null_count > 0].sort_values(ascending=False) print(null_count)

这段代码做的事很简单:读取两个 CSV,打印维度,把缺失值按数量从大到小列出来。这里的 train.shape 输出是 891 行 12 列,test.shape 是 418 行 11 列,差的那一列就是 Survived 标签。第一次跑这个项目的人经常会忽略 shape 这一步,直接去看数据内容,但维度检查是最后排查一切索引错位的起点。

在这个标准数据集里,缺失情况是固定的:Age 缺 177 个,Cabin 缺 687 个,Embarked 缺 2 个;test.csv 里还会多一个 Fare 缺失。Cabin 缺失率接近八成,这种字段直接用来训练是没有意义的,最合理的做法是把它转成一个“是否有船舱号”的二值特征,而不是去填充一个根本不存在的舱位号。

# 先复制一份,避免污染原始数据 train["Age"] = train["Age"].fillna(train["Age"].median()) test["Age"] = test["Age"].fillna(test["Age"].median()) train["Embarked"] = train["Embarked"].fillna(train["Embarked"].mode()[0]) # test 里恰有一个 Fare 缺失 test["Fare"] = test["Fare"].fillna(test["Fare"].median())

这段填充逻辑有几个细节值得注意。第一,Age 用中位数而不是均值。Age 分布存在右偏,少数高龄乘客会把均值拉高,中位数对极端值不敏感,这是做这类人口统计字段填充时的通用做法。第二,train 和 test 是分开填充的,不要把两份数据合在一起算中位数再填回去,那等于把测试集的信息泄漏给了训练过程。第三,Embarked 只有两个缺失,直接用众数 S 填充即可,因为 S 港登船人数本来就占了大多数。

我一般会在填充完后立刻做一次校验:train.isnull().sum().sum()输出 0,确保没有漏网的缺失值再进入下一步。这一步不仅是代码习惯,更是一个心理锚点——后面模型分数异常时,你会知道问题不在数据完整性上。

3. 特征工程:从 Name 里挖 Title,从 SibSp/Parch 里生成 FamilySize

3.1 让模型读懂“人”:Title 与家庭规模

模型读不懂原始文本,Name、Ticket 这种字段要么删掉,要么从中提炼新特征。Name 其实是一个信息密度很高的字段——逗号后面的称呼部分,Mr、Mrs、Miss、Master、Rev、Dr,把乘客的性别、年龄层、社会身份全压缩在一起了。比如 Master 通常指未成年男孩,Mrs 和 Miss 区分已婚未婚女性,Rev 是神职人员,这些身份在沉船逃生时是存在优先级差异的。

import re def extract_title(name): # 在逗号之后、句号之前抓取称呼: "Braund, Mr. Owen Harris" -> "Mr" match = re.search(r",\s*([^\.]+)\.", name) return match.group(1).strip() if match else "Other" train["Title"] = train["Name"].map(extract_title) test["Title"] = test["Name"].map(extract_title) title_count = train["Title"].value_counts() rare_titles = title_count[title_count < 10].index train["Title"] = train["Title"].replace(rare_titles, "Rare") test["Title"] = test["Title"].replace(rare_titles, "Rare")

这段正则r",\s*([^\.]+)\."的意思是从第一个逗号后面开始捕获,直到第一个句点前结束的内容。\s*负责跳过逗号后面的空白,[^\.]+匹配所有非句点字符。如果姓名格式不规范,兜底返回 Other。提取完后还要做一步合并:像 Rev、Dr、Major 这种出现次数很少的头衔,如果单独留成一个类别,One-Hot 之后只会制造稀疏列,对模型帮助不大,所以统一替换成 Rare。

家庭规模特征则来自 SibSp 和 Parch。这两列分别代表同行兄弟姐妹/配偶数量、父母/子女数量,直接喂给模型也可以,但更好的做法是把它们合成一个 FamilySize:

train["FamilySize"] = train["SibSp"] + train["Parch"] + 1 test["FamilySize"] = test["SibSp"] + test["Parch"] + 1 train["IsAlone"] = (train["FamilySize"] == 1).astype(int) test["IsAlone"] = (test["FamilySize"] == 1).astype(int)

加 1 是因为要把乘客本人算进去。FamilySize 等于 1 表示独身出行。这个项目里独身乘客生还率明显低于有家人同行的乘客,这里加了一个 IsAlone 二值特征,相当于把这个信息以更直接的方式暴露给模型。注意 FamilySize 要放在数值特征列里参与缩放,IsAlone 是 0/1 列,不需要缩放。

3.2 One-Hot 编码的正确姿势:Taitan_onehot.csv 是怎么来的

Sex、Embarked、Title、Pclass 都是类别字段,不能直接塞给逻辑回归。Pclass 虽然有 1、2、3 的数值,但舱位等级的数值大小本身没有意义,3 不是 1 的三倍。直接把类别映射成 0/1/2 会让模型误以为存在线性关系,标准做法是 One-Hot 编码。

# 先把要用的特征收拢,避免手滑 cat_cols = ["Pclass", "Sex", "Embarked", "Title"] num_cols = ["Age", "SibSp", "Parch", "Fare", "FamilySize"] train_cat = pd.get_dummies(train[cat_cols], dtype=int) test_cat = pd.get_dummies(test[cat_cols], dtype=int) # 关键一步:让 train/test 的列完全对齐 train_cat = train_cat.reindex(columns=test_cat.columns, fill_value=0)

这里有一句很多人忽略但极其关键的代码:reindex(columns=test_cat.columns, fill_value=0)。train 和 test 的类别取值未必完全一致——比如某个稀有 Title 只出现在 train 里,get_dummies 之后 train 会多出一列,而 test 没有。如果不做列对齐,后面模型 fit 和 predict 时的特征维度就不匹配,轻则报错,重则列错位导致结果全乱。

对齐之后,把数值列拼回去,导出 Taitan_onehot.csv:

X = pd.concat([ train_cat.reset_index(drop=True), train[num_cols].reset_index(drop=True) ], axis=1) X.to_csv("Taitan_onehot.csv", index=False) test_final = pd.concat([ test_cat.reset_index(drop=True), test[num_cols].reset_index(drop=True) ], axis=1)

拼表时最隐蔽的坑是索引错位。train 经过前面的填充和过滤后,行顺序可能已经被打乱,如果直接用 train_cat 和 train[num_cols] 横向拼接,pandas 会按索引对齐——一旦两个 DataFrame 的索引不同步,就会错位拼出脏数据。因此在拼接前统一 reset_index(drop=True),强制按位置对齐。这个包里保存 Taitan_onehot.csv 的用意也在这里:特征工程和模型训练解耦,后期调模型参数时不需要每次都重跑一遍特征处理,直接从中间产物读入即可。

4. 建模与评估:逻辑回归起步,随机森林对比

4.1 example1.py 的基线路线:逻辑回归加特征缩放

泰坦尼克项目的第一版模型不宜上来就上 XGBoost,先跑一个最简单的逻辑回归,把流程走通、把提交格式跑对,再谈提升精度。example1.py 走的正是这个思路。逻辑回归对特征尺度敏感,而 Age 和 Fare 的量纲差距极大——Fare 最大值能到几百,Age 只有几十,如果不做缩放,Fare 会在距离计算中占据绝对主导地位。

from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score X = pd.read_csv("Taitan_onehot.csv") y = train["Survived"] X_train, X_val, y_train, y_val = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_val_scaled = scaler.transform(X_val) model = LogisticRegression(max_iter=1000, C=1.0) model.fit(X_train_scaled, y_train) pred = model.predict(X_val_scaled) print(accuracy_score(y_val, pred))

这里有几个参数值得展开说。test_size=0.2表示留出 20% 的数据做线下验证,也就是训练集 891 行中约 712 行训练、178 行验证。stratify=y按标签比例分层采样,确保训练集和验证集里存活与遇难的比例都接近原始分布,不然随机切分可能把某一类全切到验证集里。max_iter=1000是因为 sklearn 逻辑回归默认最大迭代次数是 100,特征多了之后可能不收敛,调大一点避免出现警告。

还有一个细节:fit_transform只用在训练集上,验证集用transform。两者的区别在于 fit_transform 会先计算均值和标准差再缩放,而 transform 直接沿用训练集的均值和标准差做变换。如果验证集也单独 fit,等于让模型间接看到了验证集的分布,这一步属于新手最容易犯的泄漏错误。

逻辑回归在这个数据集上的线下准确率通常在 0.75 到 0.78 之间浮动,和 gender_submission 基准差不多。第一次跑出来这个数字是完全正常的,不要急着调参,先进入下一节做模型对比。

4.2 example2.py 的进阶路线:随机森林与多维评估

随机森林是树模型,不需要特征缩放,对缺失值也有一定韧性,但它有三个需要控制的点:树的数量、树的深度、叶子节点最少样本数。这三个参数控制不好,要么训练慢,要么过拟合到验证集上很好看、提交到 Kaggle 公共榜就崩。

from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, roc_auc_score rf = RandomForestClassifier( n_estimators=300, max_depth=6, min_samples_leaf=2, random_state=42, n_jobs=-1 ) rf.fit(X_train, y_train) # 树模型不需要 scaler pred_rf = rf.predict(X_val) print(accuracy_score(y_val, pred_rf)) print(classification_report(y_val, pred_rf)) print("AUC:", roc_auc_score(y_val, rf.predict_proba(X_val)[:, 1]))

参数含义逐个说。n_estimators=300是随机森林中决策树的数量,太少则模型方差大、结果不稳定,太多则训练时间线性增长而收益递减;max_depth=6限制每棵树的最大深度,防止单棵树把训练集背下来;min_samples_leaf=2要求每个叶子节点至少 2 个样本,这也是正则化手段,让树的决策边界更平滑。n_jobs=-1表示用满全部 CPU 核心并行训练。

预测部分有个容易被忽略的点:随机森林的predict直接输出类别 0 或 1,但计算 AUC 需要的是预测概率,所以要调用predict_proba(X_val)[:, 1],取第二列代表“存活”类别的概率。泰坦尼克项目的 Kaggle 评分标准是准确率,但只看准确率有个盲区:如果模型倾向于把所有乘客都预测为遇难,准确率可能是 0.62 左右,对应的存活召回率却是 0,这在业务上是不可接受的。所以线下评估至少要同时看准确率、分类报告和 AUC,三个维度结合才能判断模型是真的学到了规律,还是只在走极端。

随机森林的准确率一般能到 0.78 到 0.81,AUC 在 0.82 到 0.86 之间。如果你跑出来的数字显著低于这里,先回去检查第 3 章的列对齐是否做了,再检查第 2 章缺失值是否 Fillna 完。这两个环节出错,模型再换算法也救不回来。

5. 常见问题排查与避坑:五个典型翻车纪录

5.1 现象→原因→解决:提交得 0 分这类必死坑

做这个项目的人踩过的坑,翻来覆去就那么几个。我把最常见的高频问题按现象、原因、解决的顺序整理出来,每一条都是真实发生过、且排查成本最低的。

第一个必死坑:本地准确率 0.79,提交后 Kaggle 公共榜显示 0.00000。原因九成是索引错位——清洗数据时对 train 做过 fillna 或重置索引,预测时直接把结果和原始 test.csv 拼接,行对不上。解决方法是输出提交文件前检查三件事:len(predictions) == len(test)、第一行的 PassengerId 是否等于 test 第一行的值、是否误将 test 的行索引当作 PassengerId 用。只要把passenger_id显式从 test 里取出来,而不是用 test 的行号,这个坑就堵死了。

第二个必死坑:验证集分数 0.83,一上公共榜掉到 0.76。原因是数据泄漏——对 Age 做 fillna 时把 train 和 test 合并在一起计算中位数,或者对数值特征做缩放时把整个 X 一起 fit,验证集和测试集的分布信息混进了训练过程。解决方法是严格坚持第 4 章的写法:所有 fillna 统计量、缩放器的均值和标准差,只从训练集计算,测试集只应用不参与计算。

第三个高频坑:模型报ValueError: feature names mismatch或者“训练时 12 列,预测时 11 列”。原因是 One-Hot 之后 train 和 test 的类别列不一致。比如某个 Title 只在 train 里出现,pd.get_dummies会给他单独生成一列,而 test 里没有。解决就是第 3 章那句reindex(columns=test_cat.columns, fill_value=0),强制对齐列。如果后续加了新特征,每次都要重新检查两边的列名集合是否完全一致。

第四个坑:同一份代码跑两次,分数忽高忽低。原因很直白——没固定 random_state。随机森林训练时如果树的样本抽样和特征抽样完全随机,每次的模型都会有差异;train_test_split 不固定随机种子,切出来的训练验证集会不同。解决方法是把random_state=42统一写到 train_test_split、模型初始化、交叉验证函数里。在泰坦尼克这种小数据集上,不固定随机种子的分数波动可能超过 0.02,这在竞争榜上是非常大的差距。

第五个坑是 Kaggle 新号常遇到的:注册或登录时验证码不显示,提示captcha must be filled out。原因是浏览器插件拦截了验证脚本,或者 kaggle.com 的 Cookie 异常,导致验证码组件根本没加载出来。解决方法是先用无痕窗口打开 kaggle.com,关掉翻译插件和去广告扩展,清理一遍该站点的 Cookie,再重新登录。这个操作和网络环境没有任何关系,纯粹是浏览器层面的问题。

5.2 更隐蔽的边界情况:Cabin 缺失、票价为 0、标签比例失衡

上面五个坑属于“报错或分数归零”级别的,下面这几个更隐蔽,不一定报错,但会持续拖低分数。

Cabin 字段的缺失率接近 77%,如果你直接把它 fillna 成 "Unknown" 再当类别特征用,等于生成了一个占比极高的无意义类别,模型会在这个类别上学到噪声。常见做法是不要保留 Cabin 原始内容,只构造一个Cabin_NotAvailable的 0/1 特征:有舱位号记 1,缺失记 0。因为在泰坦尼克事故里,没有舱位记录本身往往和舱位等级、逃生路线安排存在关联,但这个关联必须由模型自己学,而不是靠填一个占位符。

Fare 字段里有少量 0 值。票价为 0 不代表真的免费,很可能是数据记录的缺失或船员票的特殊处理方式。如果直接保留 0,逻辑回归会把 Fare=0 当成一个很强的信号。我一般会先看test["Fare"].value_counts()确认 0 值的数量,再考虑把它单独抽成一个Fare_Zero特征,或者对 Fare 做np.log1p(Fare)压缩右偏。注意 log 变换之后,0 值会变成 0,和正数票价的分布差距依然明显。

标签比例问题也值得说一句。泰坦尼克训练集里遇难人数约 549、存活 342,不是一个完全均衡的二分类。如果你只看准确率,一个把所有样本都预测为死亡(0)的模型也能拿到约 0.61 的分数,而性别基准能到 0.76——说明准确率本身对这类偏态分布不够敏感。用classification_report里的召回率和 F1 值加上 AUC 一起看,才能确定模型是不是真的分出了规律,而不是在走捷径。

6. 提交与验证:从本地交叉验证到公共榜分数的最后一步

6.1 提交 CSV 的三件套:顺序、列名、无索引

模型调完,最后一步是把预测结果输出成 Kaggle 要求的格式。提交文件必须是 CSV,包含两列:PassengerId 和 Survived,且 Survived 必须是整数 0 或 1。顺序要和 test.csv 原始顺序一致,不能排序、不能去重、不能改索引。

submission = pd.DataFrame({ "PassengerId": test["PassengerId"], "Survived": rf.predict(test_final).astype(int) }) submission.to_csv("submission_rf.csv", index=False)

这里的index=False是很多人漏掉的——不写的话 pandas 会把行号 0、1、2 作为额外的一列写进去,提交后 Kaggle 会报列名不匹配,直接得 0 分。PassengerId 必须从 test 原始表里取,而不是用 test_final 的行号,因为行号是 0 到 417,而真正的 PassengeId 是 892 到 1309,混用了提交也一样废。

6.2 先交一份基准,再交模型结果

我习惯在正式提交自己的模型前,先把包里的 gender_submission.csv 原样提交一次,拿到约 0.76 的基准分。这样做不是为了刷存在感,而是给后面的模型分数建立参照系。当你把随机森林的提交结果放上去,看到 0.78 或者 0.80,你才能确认特征工程和模型参数确实带来了增量,而不是靠运气。

公共榜分数和本地验证分之间通常存在约 0.01 到 0.03 的波动,这不是模型有问题,而是 test.csv 只有 418 行,线下验证集也只有大约 178 行,样本量太小,随机切分方式不同就会导致分数浮动。想要更稳的本地评估,可以用 5 折交叉验证取平均准确率,而不是单次 train_test_split 的结果。交叉验证的均值更接近公共榜真实分数,也能暴露单次切分带来的偶然性。

最后分享一个我自己的习惯。每次跑这种小竞赛,我都强制自己按固定顺序走一遍流程:先读数据、再统计缺失值清单、固定所有随机种子、提交一份基准、再开始迭代特征和模型。这个顺序看起来机械,但它能在模型翻车时帮你快速定位是哪个环节动了数据。泰坦尼克这个项目最典型的翻车方式,就是在你改了十几个特征之后,忘了某一步把索引弄乱了,然后得到一个完全没意义的分数。相信我,这种亏吃过一次就够了。希望这份拆解能帮你在开始阶段就避开这些坑,把精力留在真正有价值的特征迭代上。

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

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

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

立即咨询