简介:这是基于泰坦尼克号生存预测经典赛题的 Python 完整大作业资源,面向需要完成期末作业或课程设计的入门新手,提供从数据加载、可视化探索到模型预测的整套代码与文档。压缩包共5个文件,涵盖 Python 脚本、交互式分析笔记本、CSV 数据集、Markdown 说明文档等类型,体积仅33KB,整体轻量且便于直接部署运行。代码内部附有详细注释,分析文档按步骤展示数据清洗、特征分析与建模思路,可视化脚本帮助直观理解乘客属性与幸存率的关系,说明文档则写明运行环境与使用方式,便于快速复现与二次开发。目前已有746人学习/下载,尤其适合零基础新手对照学习,也便于教师作为教学示例或评分参考,按自己数据或环境稍作调整即可完成一份高分的大作业交付。
1. 泰坦尼克号幸存者预测:为什么一个“老掉牙”大作业值得认真做
泰坦尼克号幸存者预测大概是机器学习圈子里被做烂的入门项目,可每年的大作业提交里,依然能看到很多“拿到数据就训练”的翻车现场。有人把乘客ID直接喂给模型,有人把包含答案的训练集和测试集混在一起清洗,有人调参调了三天最后提交分数反而比基线还低。这个项目真正的价值不在于把准确率从 0.80 提到 0.82,而在于它用最短的路径,把数据清洗、特征工程、模型选型、交叉验证、结果解释五个环节完整串起来,而且是少有的“数据量小、变量不多、业务背景清晰”的完整案例。这篇笔记写给三类人:第一次独立跑通整个数据流程的学生,交大作业前想检查自己哪一步会扣分的开发方向同学,以及想快速验证特征工程到底值多少钱的从业者。照着走一遍,提交和答辩时你会少踩不少坑。
2. 先建立判断框架:幸存者预测到底在预测什么
很多人拿到数据后的第一个动作是打开 Jupyter 开始写模型,我建议先停一下。这个数据集的每个字段背后都对应着船上的一条真实规则,先花二十分钟把字段读透,后续特征工程的方向自然就出来了。
2.1 数据里能拿到什么:字段本身就是一张生存画像
泰坦尼克号数据集通常包含 12 个字段,Survived 是标签,其余都是候选特征。下面的表是我每次做这个项目都会先列出来的字段说明,先看清每个字段的性质,再决定它在模型中的地位。
| 字段 | 含义 | 类型 | 对幸存预测的作用 |
|---|---|---|---|
| PassengerId | 乘客编号 | 整数 | 无,纯标识,必须排除 |
| Survived | 是否幸存 | 0/1 | 标签值 |
| Pclass | 舱位等级 | 整数 1/2/3 | 重要,反映社会阶层与逃生优先级 |
| Name | 姓名 | 文本 | 可提取称谓(Mr/Miss/Master),隐含年龄与性别信号 |
| Sex | 性别 | 文本 | 非常重要,女性优先逃生规则 |
| Age | 年龄 | 浮点 | 重要,儿童可能被优先,但缺失较多 |
| SibSp | 同船兄弟姐妹/配偶数 | 整数 | 弱特征,可与 Parch 合成家庭规模 |
| Parch | 同船父母/子女数 | 整数 | 同上 |
| Ticket | 票号 | 文本 | 可能含同行人/家庭线索,处理成本高 |
| Fare | 票价 | 浮点 | 与舱位强相关,可作连续补充特征 |
| Cabin | 客舱号 | 文本 | 缺失严重,通常转为“是否有客舱记录” |
| Embarked | 登船港 | C/Q/S | 弱特征,偶尔有少量缺失 |
Pclass、Sex、Age 这三个字段基本决定了 60% 的预测能力,而 PassengerId 是最容易让人翻车的字段:它是连续的升序编号,和生存结果没有任何机械关系,但树模型会在缺乏信息时尝试拿高基数编号做分裂点,产生完全无意义的规则,后面避坑章节会专门讲。
2.2 用交叉统计验证直觉:先别建模,先看数据怎么分布
建模前的第一步不是训练,而是做交叉统计,用最朴素的分组平均值验证你对这个数据的直觉。泰坦尼克号业务规则里的“妇女儿童优先”“头等舱离救生艇更近”应该在统计结果里被直接看到。下面的代码不需要机器学习库,只用 pandas 就能完成。
import pandas as pd df = pd.read_csv("train.csv") # 按性别统计幸存率,预期女性远高于男性 sex_group = df.groupby("Sex")["Survived"].mean() print("Survived ratio by Sex:\n", sex_group) # 按性别和舱位交叉观察,看女性+高舱位的叠加效应 pclass_sex = df.groupby(["Pclass", "Sex"])["Survived"].mean().unstack() print("Pclass x Sex ratio:\n", pclass_sex) # 年龄分箱统计,重点看儿童年龄段是否有一个高幸存段 df["AgeBand"] = pd.cut(df["Age"], bins=[0, 12, 18, 35, 60, 100]) age_group = df.groupby("AgeBand")["Survived"].agg(["mean", "count"]) print("AgeBand ratio and count:\n", age_group)这段代码的逻辑是:groupby 按字段分组后,mean() 得到组内 Survived 的平均值,也就是该组的生存比例;unstack() 把分组的行索引转成列索引,输出一个行是舱位、列是性别的矩阵,方便观察叠加效应;pd.cut 把连续年龄切成五个区间,count 列用来检查每组样本量。
参数说明里最需要留意的是 bins 的切分。0 到 12 岁是明确的儿童段,12 到 18 是少年段,35 到 60 是中老年段,这种分法参考了当年救生艇规则里的优先级逻辑。不要切得太细,比如每 3 岁一个箱,样本量太小会出现个别箱里占比异常,干扰判断。如果你手头数据 Age 缺失多,可以先做一次简单填充再统计,但这一步只用于观察,不影响后续正式处理。
从交叉统计里能得到一个几乎不会出错的结论:女性幸存率明显高于男性,一等舱幸存率高于三等舱,儿童年龄段有一截高幸存区间。这三个结论不是用来写报告的,而是后续特征工程的动作来源——你会知道在哪里补缺失值、在哪里造新特征。
2.3 缺失值为什么必须先处理:模型比你想象的更脆弱
泰坦尼克号数据里 Age 缺失约 20%,Cabin 缺失约 77%,Embarked 缺失两个值。第一次做这个项目的人最容易犯的错是把含缺失的行直接删掉。训练集只有 800 多行,删掉 Age 缺失的样本等于丢掉四分之一的数据,而且这些缺失样本并不是随机分布的——三等舱乘客的年龄缺失率更高,直接删行会把样本分布整体带偏。
为什么不能带缺失进模型?逻辑回归的数学形式要求每个特征都有数值,缺失值无法参与线性运算;scikit-learn 里的随机森林虽然理论上能处理缺失,但它的实现会在 fit 时直接报错。所以处理缺失是硬前提。但“填空”和“处理好缺失”是两回事:全局中位数填充最简单,但会丢失分组信息;按组填充更合理;而对 Cabin 这种缺失率高达七成以上的字段,填充本身没有意义,更合适的做法是把它转成一个二值标记,表示“是否有客舱记录”,让模型自己学习这个标记和生还的关系。
理解了字段和缺失分布之后,才算真正拿到这张船的乘客名单。接下来就可以进入特征工程环节,也就是这个项目里对分数影响最大、也最容易被过度操作的一步。
3. 把原始数据变成模型能吃的特征:特征工程实操
特征工程在泰坦尼克号这个项目里占总工作量的一半以上。数据量小意味着模型提升空间有限,分数差距几乎完全取决于谁能从 Name、Ticket、Cabin 这些“看似没用”的字段里挖出更多结构。
3.1 年龄缺失值的三档补法及其代价
Age 是模型里的重要特征,但缺失量不小。补法有三种常见选择,代价完全不同。
第一种是用 SimpleImputer 填全局中位数,优点是代码最短、不会出幺蛾子,缺点是会抹平年龄在不同人群里的差异。第二种是按性别和舱位分组取中位数,因为当年头等舱和二等舱乘客的年龄结构差异很大,未婚年轻女性多集中在下等舱,分组中位数更贴合实际。第三种是用回归或随机森林预测年龄,把 Age 当成标签用其他特征去拟合,理论上最精确,但在这个小数据上容易引入额外噪声,而且答辩时很难三句话说清楚。
import pandas as pd def fill_age_by_group(df): # 按性别+舱位组合填充年龄缺失,避免全局中位数掩盖人群差异 df = df.copy() df["Age"] = df["Age"].fillna( df.groupby(["Sex", "Pclass"])["Age"].transform("median") ) return df df = fill_age_by_group(df)逻辑说明:groupby 先按 Sex 和 Pclass 分组,transform("median") 返回一个长度与原 DataFrame 相同的 Series,缺失值会拿到自己所在分组的年龄中位数。这样填充后,三等舱男性和头等舱中年女性不会共享同一个填空值,后续模型在年龄维度上的区分度更好。
参数说明里最值得说的是“为什么不用均值”。Age 分布右偏,存在少数高龄乘客,均值会被拉高,而中位数更稳健,对异常值不敏感。如果直接用全局中位数,代码省事但会在答辩时被问到“为什么不分组”,回答成本反而更高。分组中位数是综合代码复杂度、信息量和可解释性的最佳折中。
3.2 从姓名提取称谓:把文本变成结构化信号
Name 字段粗看是纯字符串,但里面藏着称谓。Titanic 时代英国的称谓体系在姓名字符串里保留得非常规整,Mr、Mrs、Miss、Master 这几个称谓直接对应性别、年龄段和婚姻状态,而 Master 在当年专门指未成年男性,这个称谓的幸存率统计上和成年男性差别很大,是一个高区分度特征。
df["Title"] = df["Name"].str.extract(r" ([A-Za-z]+)\.")[0] # 合并罕见称谓,避免产生过稀疏的类别 rare_title_map = { "Mlle": "Miss", "Ms": "Miss", "Mme": "Mrs", "Lady": "Rare", "Sir": "Rare", "Countess": "Rare", "Don": "Rare", "Jonkheer": "Rare", "Rev": "Rare", "Capt": "Rare", "Major": "Rare", "Col": "Rare" } df["Title"] = df["Title"].replace(rare_title_map)逻辑说明:str.extract 用正则从姓名里提取最后一点前的单词,比如“Braund, Mr. Owen Harris”会提取出 Mr。正则([A-Za-z]+)\.确保了只取空格后、点前的那一段,点前面的转义很关键,不加反斜杠会误匹配整句中的任意点号。replace 将罕见称谓归为 Rare,避免它们编码成独立列后大部分取值为零。
参数说明一个细节:Mlle 和 Mme 是法语的 Miss 和 Mrs,Titanic 上确实有法国乘客,映射回常规类别能减少稀疏性。Rare 这一组包含上校、医生、牧师、贵族等,这些人群在数据集中各自只有几行,独立编码对树模型没有任何增益。如果你拿到一组新的测试数据,发现出现了训练集没有见过的称谓,记得在编码阶段用列表白名单而不是 set 自动扩展,否则测试集特征列会和训练集不匹配。
3.3 类别字段编码:直接映射还是 One-Hot
Sex 和 Embarked 是典型的类别字段。Sex 只有男和女,直接映射成 0/1 即可,因为二分类变量映射后不存在虚假的大小关系。Embarked 有 C/Q/S 三个值,新人常见错误是手写字典 {C: 1, Q: 2, S: 3},把类别变成有序整数,线性模型会把 S 的 3 当作比 C 的 1 更“大”的量,这是完全错误的信号。
df["Sex"] = df["Sex"].map({"male": 0, "female": 1}) # 对登船港做 one-hot,drop_first 避免和截距线性相关 embarked_dummies = pd.get_dummies(df["Embarked"], prefix="Emb", drop_first=True) df = pd.concat([df, embarked_dummies], axis=1)逻辑说明:get_dummies 会把 C/Q/S 三列展开,加上 drop_first=True 后删除第一列以避开共线性,实际生成 Emb_Q 和 Emb_S 两列。Sex 不需要 get_dummies,因为它本身就是两分类,映射成 0 和 1 后没有顺序偏差,再拆一列反而是冗余。
编码阶段最常见的隐藏坑是测试集类别不一致。假设测试集里只有 C 和 S,没有 Q,训练集有 Q,那么测试集 one-hot 后的列数会比训练集少一列。解决办法是编码后统一对齐列结构,训练集和测试集都按同一个列顺序重排,缺失列补 0。这部分代码建议用小工具函数封装,避免每次拼接后手动检查。
3.4 连续特征归一化与 FamilySize 新特征
Fare 的分布非常右偏,极少数头等舱旅客支付了高额船票,直接喂给逻辑回归会让这一特征主导梯度,所以要对它做压缩变换。另一个值得构造的新特征是家庭规模,SibSp 加 Parch 再加 1 就是船上同行人数,而单独一人出行在逃生优先级上明显靠后,这个特征在小样本上经常能带来一两个百分点的提升。
import numpy as np # 票价补缺失后做 log1p,压缩大值,兼顾 0 票价 df["Fare"] = np.log1p(df["Fare"].fillna(df["Fare"].median())) # 家庭规模与是否独行的二值标记 df["FamilySize"] = df["SibSp"] + df["Parch"] + 1 df["IsAlone"] = (df["FamilySize"] == 1).astype(int) # Cabin 缺失 77%,不做填充,转为是否有客舱记录 df["HasCabin"] = df["Cabin"].notna().astype(int)逻辑说明:np.log1p(x) 计算 ln(1+x),输入 0 时会输出 0,避免 log0 报错;同时它能把 100 的票价压到 4.6 左右,极大缓解偏态。FamilySize 从 1 到 11 不等,是 Int 型特征,树模型可以直接用,线性模型则建议先归一化。IsAlone 是对 FamilySize 的二值化简化,让模型不需要自己学阈值。HasCabin 处理的思路是:与其填充一个不可靠的客舱号,不如让模型知道这条记录有没有 Cabing 信息。
特征工程这里有一个我很看重的主观纪律:每生成一个特征,先问它来源有没有业务逻辑。像 FamilySize 有明确来源,可以保留;如果把 Fare 除以 FamilySize 再减去 Age,这种“数学游戏”只会让训练集分数好看,验证集基本纹丝不动。特征不是越多越好,而是每一个都能被一句话解释。
4. 从逻辑回归到随机森林:模型选型与参数设置的取舍
模型选择在这个项目里不是拉开分数的关键,但错误的选型流程会浪费大量时间。我的固定套路是从逻辑回归起步,再用随机森林做主力,最后用统一脚本对比两者的交叉验证分数,而不是一开始就上复杂模型。
4.1 逻辑回归作为基线的理由
逻辑回归在泰坦尼克号数据集上是一个被低估的基线。它的可解释性最好,每个特征的系数方向都能直接说明问题,而且对特征工程的反馈非常敏感。如果某个新特征没有让逻辑回归分数变化,那它大概率真的是噪声。逻辑回归还有个额外优势:训练速度快,在 800 行数据上几乎是瞬时的,方便反复做特征试错。
from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler feature_cols = ["Pclass", "Sex", "Age", "SibSp", "Parch", "Fare", "FamilySize", "IsAlone", "HasCabin"] X = df[feature_cols] y = df["Survived"] # 先标准化再训练,避免 Fare 和 Age 量纲影响正则 model_lr = make_pipeline( StandardScaler(), LogisticRegression(max_iter=500, C=0.1, random_state=42) ) lr_scores = cross_val_score(model_lr, X, y, cv=5, scoring="accuracy") print("LogisticRegression CV:", lr_scores.mean(), "+/-", lr_scores.std())逻辑说明:make_pipeline 把标准化和逻辑回归串成一条处理链,每一个交叉验证 fold 都在训练折内部 fit 标准化参数,再应用到验证折,这样可以避免标准化统计量从验证集泄漏进训练过程。StandardScaler 会对数值型特征做零均值化,原因在于逻辑回归使用了 L2 正则,特征尺度不一致时,正则会偏向惩罚大数值特征。
参数说明里 C=0.1 是正则强度的倒数,数值越小正则越强。特征之间存在明显相关性,比如 Pclass 与 Fare、Pclass 与 Age,较强的正则可以抑制系数扰动。max_iter=500 是收敛上限,默认 100 在这个量级的数据上偶尔会报未收敛警告,直接调大到 500 并固定 random_state,保证每次跑的结果可复现。基线分数一般在 0.78 到 0.80 之间,这是之后所有模型对比的参照物。
4.2 随机森林的关键参数:n_estimators、max_depth、min_samples_leaf
随机森林是泰坦尼克号项目的主流主力模型,原因是它能自动捕捉特征交互,比如“女性 + 二等舱”“男性 + 未成年”这类组合规则,不用手工造交叉特征。但它最大的风险是过拟合,因为数据量只有 800 出头,完整生长的树可以轻松把训练集拟合到 0.99,而交叉验证分数可能只有 0.80。限制树深和叶子大小是必要操作。
from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier( n_estimators=300, max_depth=6, min_samples_leaf=4, min_samples_split=8, max_features="sqrt", random_state=42, n_jobs=-1 ) rf_scores = cross_val_score(rf, X, y, cv=5, scoring="accuracy") print("RandomForest CV:", rf_scores.mean(), "+/-", rf_scores.std())逻辑说明:max_depth=6 限制每棵树最多 6 层,在这个样本量下足够表达主要的特征交互,又不会深到把个别噪声样本记住。min_samples_leaf=4 强制每个叶节点至少有 4 个样本,让预测值来自至少 4 个样本的平均,避免末端过细。max_features="sqrt" 让每次分裂只随机挑选总特征的平方根个候选,增加树与树之间的多样性。
参数调优的推荐顺序是先定 max_depth,再调 min_samples_leaf,最后确认 n_estimators。可以从 max_depth=3 开始观察交叉验证分数,逐步加到 7,如果分数不再上升甚至下降,说明已经过深。min_samples_leaf 从 2 到 8 逐个试,取分数最高的那个。n_estimators 在 100 到 300 之间就够稳定了,往上的收益微乎其微,不要在这个参数上浪费网格搜索时间。如果你用 GridSearchCV,网格里优先放 max_depth 和 min_samples_leaf,组合别超过 12 组,否则在小数据上会严重过拟合验证集。
4.3 交叉验证分数的“可信度”问题
泰坦尼克号数据只有 800 多行,交叉验证分数本身波动很大。同一个模型换个随机种子,5 折平均分可能在 0.79 到 0.83 之间跳。很多人在比较两个模型时直接看平均分,差 0.01 就断定 A 比 B 好,这在小数据集上是不成立的。
from sklearn.model_selection import StratifiedKFold skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) rf_scores = cross_val_score(rf, X, y, cv=skf, scoring="accuracy") print("StratifiedKFold scores:", rf_scores)逻辑说明:StratifiedKFold 在切分时会维护每一折的标签比例,使每一折的正负样本比例与全量数据一致。这一点在正类只占 38% 的数据集上尤其关键,否则某折里幸存者比例过高,该 fold 的准确率就会虚高,整体分数失去参考价值。shuffle=True 打散顺序后再切分,避免原始数据按舱位排列导致的序列偏差,配合 random_state=42 保证可复现。
模型对比的正确姿势是在同一套交叉验证对象 skf、同一组特征下分别跑逻辑回归和随机森林,并列输出每折分数。只看平均分远远不够,还要看分数方差。如果随机森林的交叉验证均分高于逻辑回归 0.02,且最高最低折差距在可接受范围内,才值得把它作为最终提交模型。下面这个表格是我做模型选择时的常用对照逻辑。
| 对比项 | 逻辑回归 | 随机森林 |
|---|---|---|
| 特征反馈 | 系数方向直接可读 | 特征重要性可读 |
| 缺失值容忍度 | 低,必须先填充 | 中,但 sklearn 实现仍不能直接吃 NaN |
| 过拟合风险 | 低 | 高,必须限制深度 |
| 典型 CV 分数 | 0.78~0.80 | 0.80~0.83 |
| 调参成本 | 低 | 中 |
我一般把逻辑回归当“标准答案生成器”:它能快速回答特征是否有效;随机森林再在这个基础上做最后的精度提取。两个模型都固定随机种子,保证同一份代码重跑结果一致,这是作业提交的基本信誉。
5. 泰坦尼克号预测的避坑手记:五个真实踩坑点
这一章集合我在类似项目里看到过、自己也踩过的真实坑点。每一个都按“现象 → 原因 → 解决”展开,代码是可直接拿走的修复方案。
5.1 训练集分数虚高,测试集成绩拉胯
现象:自己跑交叉验证准确率 0.83,但最终成绩只有 0.75 左右;查看代码时发现预处理阶段是先把训练集和测试集拼在一起做中位数填充、标准化、编码,然后再拆分训练和验证。
原因:把测试集带进 fit 过程属于信息泄漏。填充用的中位数、标准化用的均值和方差、编码用的取值集合,这些统计量都在训练阶段不该被测试集影响。更隐蔽的是,有人在训练结束后用整个训练集重新 fit 了一遍最终模型,然后用这个模型的训练集准确率当结果汇报,等于是让模型在包含答案的数据上“开卷考试”。
解决:所有填充、标准化、编码统计量只从训练集学习,测试集只能 transform。下面的代码演示了用 SimpleImputer 的正确姿势:
from sklearn.impute import SimpleImputer # 训练集 fit,测试集只能 transform imputer = SimpleImputer(strategy="median") X_train_imp = imputer.fit_transform(X_train) X_test_imp = imputer.transform(X_test)逻辑说明:fit_transform 只在训练集上计算中位数并替换缺失,随后 transform 直接把训练集算好的中位数套到测试集上。测试集的缺失值不会参与统计量计算。这一点在交叉验证里同样成立,所以用 make_pipeline 让预处理和模型一起放进 KFold 才是最省心的方案。
5.2 PassengerId 混进特征矩阵
现象:特征重要性榜单前五里出现 PassengerId,模型分数没跌但解释完全不通。
原因:PassengerId 是升序编号,和幸存与否没有关系,但树模型在选择分裂特征时只看信息增益,ID 这种高基数编号偶尔能拟合出看似有效的分支,实际是记忆编号的偶然规律。这种特征在训练集内部自洽,换一组数据就完全失效。
解决:从读入数据的第一行就把 PassengerId 从特征列表中删除。你可以保留它用于最后的提交行对齐,但任何情况下都不要把它写进特征矩阵。对应地,Name 和 Ticket 也不该直接以原始字符串进模型,应该先提取结构化特征再使用。
5.3 特征工程过度导致分数虚高
现象:连续加了十几个特征后,训练集准确率逼近 0.94,交叉验证却停在 0.81 左右无法突破。
原因:小样本训练下,模型会把新特征里偶然出现的规律当作真实信号来记。特征越多,过拟合空间越大。特别是那些“为造而造”的交叉特征,比如 Age * Fare / Pclass 这种没有任何业务逻辑的组合,它们只在当前数据里碰巧有解释力。
解决:给特征工程设硬性门槛,每加一个新的候选特征,重跑一次交叉验证,分数上升才保留,持平或下降就丢弃。下面是检查方式:
def evaluate_features(feature_list): X = df[feature_list] skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores = cross_val_score(rf, X, y, cv=skf, scoring="accuracy") return scores.mean() # 计算加特征前后的分数差异,只保留有正向贡献的特征 baseline_score = evaluate_features(base_features) new_score = evaluate_features(base_features + ["NewFeature"]) print(f"delta: {new_score - baseline_score:+.4f}")逻辑说明:用一个固定函数把特征列表变成交叉验证分数,对比加新特征前后的差值。正 delta 保留特征,负 delta 直接删除。看 delta 而不是只看新分数,能避免被特征数量本身带来的虚高带偏。
5.4 决策边界永远取 0.5
现象:分类报告里精确率 0.82、召回率 0.68,模型在女性样本上都预测对了,但漏掉了一批预测概率在 0.4 到 0.5 之间的幸存者。
原因:样本正类占比约 38%,不是严格均衡分类。0.5 只是默认阈值,并不一定对应你的目标。泰坦尼克号项目如果用准确率评分,0.5 没问题;但如果评分指标是 F1 或召回率,默认阈值往往不是最优选择。
解决:训练完后先看 predict_proba 输出的分布,再决定阈值。下面是观察概率分位数和调阈值的代码:
# 取正类概率列,查看分位数 proba_train = rf.predict_proba(X_train)[:, 1] print(pd.Series(proba_train).describe()) # 按目标指标搜索阈值,比如让 F1 最大化 from sklearn.metrics import f1_score best_thr, best_f1 = 0.5, 0 for thr in np.arange(0.3, 0.71, 0.05): pred = (proba_train >= thr).astype(int) f1 = f1_score(y_train, pred) if f1 > best_f1: best_f1, best_thr = f1, thr print("best threshold:", best_thr, "F1:", best_f1)逻辑说明:predict_proba 返回两列,第一列是反类概率,第二列是正类概率,取第二列就是模型对每位乘客幸存的主观置信度。滑动阈值后重新计算 F1,能在不重训模型的前提下适应不同评分口径。
参数说明里要注意:在训练集上调阈值也一样有轻微过拟合风险,因为你是用同一批样本选的最佳切点。更稳妥的做法是把阈值也放进交叉验证流程,在每一折内单独挑阈值,再对验证折打分。泰坦尼克号这种量级的数据不太值得这么做,但答辩时能说出这一句,会显得你很懂验证严谨性。
5.5 提交结果行序错位
现象:模型交叉验证 0.82,最终提交却被判定为 0.55,几乎等于随机猜测。
原因:测试集在预处理阶段用了 dropna 删除缺失行,行号没有重置,或者用 reset_index(drop=True) 后没有按原始 PassengerId 排序,导致预测结果和原始行对应不上。
解决:测试集永远不要删除任何行。所有缺失都用填充来处理,保证行数不变,并在提交前对齐 PassengerId 顺序,最后做一次防御性检查:
# 提交前校验行数一致 assert len(test_pred) == len(test_df), "prediction length mismatch" # 将预测值拼接回测试集乘客ID上 submission = pd.DataFrame({ "PassengerId": test_df["PassengerId"], "Survived": test_pred }) # 确保恰好两列,且不包含训练集里带出的任何分类列 submission.to_csv("submission.csv", index=False)逻辑说明:assert 在预测数量与测试集行数不一致时直接抛出异常,而不是带着错误继续跑。拼接时从原测试集取 PassengerId,而不是用排序后的行号重建,这样才能保证预测值和对应的乘客编号在一条线上。这五条坑里,行序错位是最冤枉的丢分方式,跟模型能力无关,纯代码习惯问题。
6. 把作业做成“能讲的东西”:验证、可解释性与最后的提交习惯
6.1 用预测概率而非只看准确率
答辩时被问“模型哪里最没把握”,如果你手里只有一条准确率,基本回答不上来。我习惯直接把训练集的预测概率按分位数摊开看。概率落在 0.4~0.6 区间的样本,通常是中等舱位的年轻男性或个别罕见称谓的乘客,模型对这些人缺少足够的区分信号。把这一段人群圈出来,写进文档说明里,比贴一张漂亮的混淆矩阵更有说服力。
6.2 让文档说明和代码互相引用
带有“文档说明”后缀的大作业,评审看重的不是模型有多好,而是你能不能把自己的决策过程讲清楚。文档不需要写成代码流水账,而是回答四个问题:数据缺了哪些信息、特征为什么这样构造、模型为什么这样选、结果怎么验证。代码里每个关键函数加 docstring,文档里引用函数名,比如统计缺口时引用 fill_age_by_group,说明选型理由时引用第 4 章的交叉验证脚本。代码归代码、文档归文档,但两者要在函数级别对得上号。
6.3 一点个人习惯
我自己在类似的作业上踩过最大的坑是贪多。第一次把 GridSearchCV、Stacking、模型融合全部塞进去,代码好看,但每一个环节都讲不透彻,评审一问就露馅。现在是反过来,先有一个能稳定跑通的基线,把主流程控制在 150 行以内,每个函数只做一件事,确认交叉验证稳定后再加复杂度。最后一次提交前固定随机种子、重跑一遍全部脚本、检查测试集行数、核对输出 CSV 字段名。这套流程听起来琐碎,但每一道都是实际出现过的扣分点。希望这篇笔记能帮你少走一段弯路,也希望能带着你把“老掉牙”的泰坦尼克号项目做出一份像样的交付。
本文还有配套的精品资源,点击获取