机器学习这四个字听起来唬人但只要你真拿一个项目完整跑一遍就会发现它更多是“数据耐心”的手艺活。我自己的入门项目就是基于泰坦尼克数据集做二分类预测当初照着教程把代码敲完、跑出0.78的准确率还挺兴奋后来把教程代码全扔了只留数据和问题重新做一遍才真正体会到这个经典任务的价值数据量不大、字段不多、问题定义清晰几乎能把数据分析全流程里最重要的技能全部练到——缺失值处理、特征构造、类别编码、模型对比、过拟合防护。这篇文章不打算贴“照着敲一遍”的完整代码而是把我重做之后的理解写清楚重点讲“为什么这样做”。适合刚学完Python和基础机器学习、想找真实数据集练手的读者也适合那些跑过一遍教程、但总觉得没真正学到东西的人。1. 泰坦尼克数据集为什么能成为机器学习的“必经之路”1.1 我们真正要解决的问题是什么泰坦尼克数据集的预测目标是给定一名乘客的基本资料判断此人是否幸存Survived1或0。这是一个标准的二分类问题属于监督学习里“有标签数据”的教科书级应用。输入是性别、年龄、客舱等级、票价、亲属同行人数等字段输出是一个置信概率通常把概率大于0.5判为幸存。数据规模上经典划分是训练集891条、测试集418条。需要说明的是这里的“测试集”一般指需要提交预测结果的未标签数据自己评估的时候通常还要从训练集里再切出一段验证集或者直接用交叉验证。泰坦尼克数据集本身是个“小数据”项目一整张表也就一千多行十二个字段但这个规模对入门者极其友好每个步骤花费的时间很短你能反复试错能在几分钟之内看到特征改动带来的分数变化。1.2 字段盘点与第一眼的数据质量在动手前先把字段含义理清楚。最常见的版本包含以下12个字段字段含义类型缺失情况PassengerId乘客编号整数无Survived是否幸存标签0/1训练集有测试集无Pclass客舱等级 1/2/3整数无Name姓名文本无Sex性别文本无Age年龄浮点训练集缺177SibSp同行的兄弟姐妹/配偶数量整数无Parch同行的父母/子女数量整数无Ticket船票编号文本无Fare票价浮点无Cabin客舱编号文本缺失比例极高Embarked登船港口取C/Q/S文本训练集缺2一眼就能看出三个问题字段Age缺177个Cabin缺掉约77%Embarked只缺2个。数据清洗的第一原则在这里体现得很明显缺失值不是“填满”就完事而是要判断为什么缺、缺多少、怎么填才合理。比如Cabin缺失到接近四分之三直接丢掉行会损失大量样本简单填一个默认值又会引入大量噪声。这类“高缺失率字段”的正确打开方式往往是变成一个“是否有记录”的0/1特征而不是硬着头皮去填。2. 数据初探先让数据说话再考虑建模2.1 统一的分析顺序先统计后画图再下结论我拿到任何数据集习惯先做三件事查缺失、看分布、看标签和每个字段的关系。泰坦尼克数据量小用pandas做统计就能得到很可靠的直觉画图反而可以稍后一步。直接看数字更有说服力。先看性别与幸存率的交叉统计。训练集中男性约577人幸存约109人幸存率不到19%女性约233人幸存约154人幸存率约66%。这两个数字几乎是整个数据集最强的一击仅用“性别”一个特征很多简单模型就能摸到0.75以上的准确率。这个观察在后续特征工程里极其重要——这个数据集的信号集中度极高性别“一锤定音”其余字段更多是锦上添花。再看客舱等级与幸存率。Pclass1的幸存率约62%Pclass2约43%Pclass3约25%。等级数字越小幸存率越高这和不同客舱在疏散时的优先级、船上所处位置都有关系。这里不讨论历史背景只把数据当成数据来用这个字段是除性别之外最具区分度的输入。2.2 连续字段的“分层直觉”Age是连续型字段全局看均值、中位数意义有限因为幸存和遇难者的年龄分布会互相覆盖。但如果按年龄段切分会发现儿童尤其是低龄段的幸存率明显更高这符合“妇女儿童优先”的常识判断。这个发现意味着年龄字段可以用“是否低龄”这类切分方式参与建模而不是直接喂原始数字。Fare票价与Pclass高度相关头等舱平均票价远高于三等舱所以它适合作为Pclass的补充信息。但要注意共线性问题如果模型是线性模型两个高度相关的特征同时进入会让系数的解释不可靠甚至影响数值稳定性。树模型对此不敏感但在做特征选择时依然要留个心眼。SibSp和Parch这两个字段单独看规律比较细碎。SibSp表示同行的兄弟姐妹/配偶数量Parch表示同行的父母/子女数量两者都属于“家庭关系”维度拆开看反而容易把信号摊薄。后面我会把它们合并成“家庭规模”来用。2.3 缺失值的第一手判断对Age的177个缺失约占20%不能说丢行就丢行——891条样本丢177条太奢侈。常规做法是按某种规则填充但“用全局中位数”只是最懒的办法。更合理的策略是按照Title称谓分组因为不同称谓的乘客年龄分布差异明显比如“Mr”集中在成年男性段、“Master”集中在男童段组内中位数比全局中位数精准得多。这个点在特征工程章节我会展开讲。对Cabin既然缺失率超过四分之三“没有客舱记录”本身就是一个信号。实践中最稳妥的做法是不去猜测缺失的舱室号而是构造一个“是否有Cabin记录”的二值特征。对Embarked缺失只有2条用众数填充即可不必大动干戈。3. 特征工程这里决定的是模型上限而不只是“好模型”3.1 年龄填充用分组中位数别用全局中位数很多人第一次做泰坦尼克Age缺失值直接填训练集Age的中位数约28岁简单省事但丢掉了重要信息。原因是儿童幸存率显著高于成年人如果不分年龄段去填充等于把儿童样本的年龄也强行归到成人段稀释了低龄信号。推荐的做法是从Name中先提取Title再按Title分组填充。一段典型代码如下import pandas as pd train pd.read_csv(train.csv) test pd.read_csv(test.csv) # 提取称谓 for df in [train, test]: df[Title] df[Name].str.extract(r ([A-Za-z])\.) # 按Title分组填充Age for df in [train, test]: df[Age] df.groupby(Title)[Age].transform( lambda x: x.fillna(x.median()) )这样做的道理很直接不同称谓对应的年龄分布有清晰分界。比如“Master”基本是未成年男性“Mr”是成年男性“Mrs/Miss”对应女性群体。用组内中位数填充比全局中位数更贴近真实分布能让模型在年龄维度上保留住“儿童”这个关键信号。3.2 从姓名里提取称谓一个容易被忽略的免费馅饼Name字段本身是文本看似没用但仔细看会发现姓名里藏着称谓。泰坦尼克年代的称谓有Mr、Mrs、Miss、Master、Dr、Rev等不同称谓对应的人群在幸存率上存在实际差异。把称谓提取出来之后可以把它和性别、年龄交叉使用甚至可以把这个衍生特征当作一个轻量级的“社会身份”代理变量。提取之后还需要做归并把频率极低的称谓合并成“Rare”一类避免过度稀疏。比如常见做法是保留Mr、Mrs、Miss、Master其余归为Rare。这一步对随机森林这类模型帮助尤其大模型不再为每个稀有称谓单独构造分支降低了过拟合风险。3.3 家庭规模与“是否独行”SibSp和Parch单独送进模型效果一般因为它们的取值集中在0和1分布很稀疏。一个更稳健的合并方式是构造家庭规模train[FamilySize] train[SibSp] train[Parch] 1 test[FamilySize] test[SibSp] test[Parch] 1加1是因为要把乘客本人算进去。家庭规模的意义在于把“有没有人结伴”这个信息显式化。进一步地还可以生成“是否独行”的二值特征FamilySize1表示独行。为什么这个字段有价值因为在逃生场景下独行乘客和结伴乘客面临的决策环境不同这种行为层面的信息藏在原始字段里只有组合后才能被模型读到。需要注意的是特征合并不是越多越好。FamilySize和“是否独行”之间有强相关同时放进线性模型会带来冗余但在树模型里多一个二值变量影响不大。我的经验是模型用逻辑回归时保留FamilySize即可不再单独加IsAlone用随机森林或梯度提升时两个都留着反而有时更能帮忙。3.4 类别字段编码Sex最简单Embarked和Pclass要分情况Sex只有两个取值直接映射成0/1即可le[Sex] train[Sex].map({male: 0, female: 1})Embarked取值是C/Q/S三种因为缺失极少先用众数填充再做One-Hot编码。One-Hot之后得到三个0/1列但使用时要留意线性模型必须去掉一列以避免多重共线性树模型可以保留全部列也可以不编码直接当类别特征处理。Pclass虽然写成整数1/2/3但它在含义上是等级有序的类别不是连续数值。如果直接把它当作数值特征线性模型会默认“1到3的距离”和“2到3的距离”是等距的这不符合实际。所以Pclass通常有两种处理方式一是One-Hot成三个二值列二是在树模型里直接声明为类别型category特征。对于入门项目我建议One-Hot因为它的解释性更强与逻辑回归配合也最稳妥。3.5 哪些字段可以安心丢掉PassengerId是纯编号没有任何预测意义应该在建模时删除但提交结果时要注意保留对应关系。Ticket是船票编号理论上包含舱位和批次信息但文本杂乱初学者阶段直接丢掉是最现实的取舍。Name在执行完Title提取后也可以丢它的信息已经转移到Title和年龄异常值处理中。Cabin不做填充只保留“是否有记录”的二值列。特征工程做完一个合理的特征集合大约在9到11个左右包括Sex、PclassOne-Hot后、Age填充后、Fare、FamilySize、IsAlone可选、Title归并后、EmbarkedOne-Hot后、HasCabin。数量不多但每个都有来源和解释这正是泰坦尼克项目的精髓特征少而精信息密度高。4. 建模与验证从基线模型到最终选择4.1 基线与基准流程先用逻辑回归打通建任何模型之前先把“一套标准评估流程”建立起来。我推荐直接用StratifiedKFold做5折交叉验证不要先分出一个固定验证集。因为891条样本不够大单次划分运气成分高交叉验证能更稳定地反映模型真实效果。基线模型我习惯用逻辑回归。原因很简单逻辑回归在二分类问题上稳定、可解释、训练快而且对特征工程的改动特别敏感——你加了什么特征、去掉了什么特征几乎能立刻从得分变化里感到它的重要性。一段最小可运行的交叉验证流程如下from sklearn.model_selection import StratifiedKFold from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score import numpy as np skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores [] X feature_matrix y train[Survived] for train_idx, val_idx in skf.split(X, y): model LogisticRegression(max_iter1000) model.fit(X.iloc[train_idx], y.iloc[train_idx]) pred model.predict(X.iloc[val_idx]) scores.append(accuracy_score(y.iloc[val_idx], pred)) print(np.mean(scores), np.std(scores))基线模型配合基础特征Sex、Pclass、Age、Fare等交叉验证准确率通常在0.78到0.80之间。到这里项目已经“跑通”了大部分教程也到此为止。但要不要继续往上走才是真正拉开差距的地方。4.2 模型对比树模型为什么常常赢把随机森林、梯度提升树加进来对比之后一个常见现象是树模型的交叉验证分数普遍高于逻辑回归通常能到0.81到0.83。原因在于树模型能天然处理非线性关系和特征交互比如“女性且头等舱且低龄”这种组合模式逻辑回归需要手动加交互项或者通过特征工程间接表达而树模型自己就能切分出来。下面我列一份典型的对比结果注意这个分数会因特征工程、随机种子不同上下浮动但方向基本稳定模型策略5折交叉验证准确率逻辑回归基础特征0.78 - 0.80逻辑回归加Title、FamilySize等衍生特征0.79 - 0.81随机森林全部特征默认参数0.80 - 0.82梯度提升XGBoost风格全部特征轻调参0.81 - 0.83树模型更强不等于逻辑回归没用。在样本量小、特征少、且你希望给别人讲清楚“为什么预测这个人是幸存者”时逻辑回归的优势反而更明显。泰坦尼克项目既然是练手我建议把两类模型都走一遍重点感受“特征工程对不同模型的收益差”。4.3 调参思路先粗后细别一上来就网格搜索初学者最容易犯的错是一上来就GridSearchCV拉满跑一个巨大参数网格机器卡半天最后分数涨了0.002。调参的正确姿势是先粗调再细调。对随机森林先固定n_estimators为200或300然后看max_depth在3、5、7、10之间的表现确认了max_depth再动min_samples_split和min_samples_leaf。对梯度提升类模型重点是learning_rate和n_estimators的配合learning_rate设小0.01到0.05n_estimators相应加大500到1000再配合早停控制过拟合。一个值得记住的结论在泰坦尼克这种小数据集上调参的收益远小于特征工程的收益。把Title提取、FamilySize合并、缺失值分组填充这些“数据侧”工作做完往往比把某模型的参数调到极致更划算。这也是这个项目最核心的“数据思维”训练。4.4 过拟合防护小数据项目的悬顶之剑891条训练样本对树模型来说非常容易过拟合。判断过拟合的简单办法看训练集准确率和交叉验证准确率的差距。如果训练集已经跑到0.95以上而交叉验证只有0.80说明模型在“背答案”没有学到可泛化的规律。防备手段有三个优先级。第一是交叉验证它本身就是过拟合的探测器绝对不能用单次train/test划分替代。第二是控制树模型的复杂度比如限制max_depth、增加min_samples_leaf让模型无法记住太多噪音样本。第三是简化特征这一点常被忽视特征不是越多越好有些噪声特征会让树模型强行切分最后只是记住了异常组合。我的经验是特征数量控制在10个以内在这个项目里既有解释力又不至于过拟合。5. 实操中最容易踩的坑以及我反复交过的学费5.1 数据泄漏处理缺失和编码时最隐形的坑“数据泄漏”听起来很专业实际上在泰坦尼克项目里极其常见。最典型的一种初学者会把train和test合并成一个DataFrame然后统一做中位数填充、统一做One-Hot编码。表面上看代码简洁但问题在于你在填充Age的缺失值时用的是整个合并表的中位数这个中位数里包含了测试集的行——测试集的“分布信息”就这样溜进了训练流程。结果就是交叉验证分数虚高而提交到隐藏测试集时表现打折扣。正确的姿势是一切fit学习统计量只在训练集上进行然后再transform到测试集。以下代码是踩坑版本和正确版本的对比# 踩坑版本用合并表的全局中位数填充Age all_data pd.concat([train, test], ignore_indexTrue) all_data[Age] all_data[Age].fillna(all_data[Age].median()) train all_data.iloc[:len(train)] test all_data.iloc[len(train):] # 正确版本训练集计算中位数测试集用同一个值填充 age_median train[Age].median() train[Age] train[Age].fillna(age_median) test[Age] test[Age].fillna(age_median)还有一类更隐蔽的泄漏用“性别客舱等级”在训练集上统计存活率把这个存活率做成新特征。如果这项统计只在训练集上计算然后映射到测试集那是合法的但如果你在合并表上算测试集的标签虽然你手里没有测试集标签但统计量本身包含了测试集的行分布就会污染训练过程。最简单的原则是牢记任何从数据中计算出来的统计量必须在训练集内计算再应用到外部数据。5.2 随机种子让结果可复现的第一课做这个项目时我有一阵特别困惑同一套代码今天跑0.81明天跑0.79再跑一次又变回0.80。问题出在没固定随机种子。树模型的随机性、交叉验证打乱顺序的随机性都会导致结果波动。StratifiedKFold里的random_state、模型的random_state、GridSearchCV的cv随机性全部要固定。顺便建议做实验时记录每个关键参数包括随机种子否则很难判断分数变化来自特征工程还是纯运气。5.3 指标问题准确率不是唯一标准泰坦尼克任务在多数场合用准确率评估因为它样本量小且没有明显的代价不对称。但这个项目其实天然带有一点类别不平衡幸存者约占38%遇难者约占62%。如果只看准确率一个“永远预测遇难”的傻瓜模型也能拿到约62%所以准确率在“基线附近”时几乎没有参考价值。更稳健的做法是同时看AUCROC曲线下面积它衡量的是模型对正负样本的排序能力对不平衡更不敏感。很多情况下两个模型准确率相同但AUC却差了好几个百分点AUC高的一般泛化能力更强。我在泰坦尼克项目里的习惯是用准确率做公开对比、用AUC做内部怀疑——准确率涨但AUC跌说明模型只是在迎合样本比例而不是学到了真规律。5.4 最终经验从0.78到0.82瓶颈从来不在模型如果我只能留三条经验给后来者那就是第一把前三章的数据处理和特征工程认真做完模型用什么反而没那么重要第二任何时候都用交叉验证评估并固定随机种子第三警惕数据泄漏所有统计量都只在训练集上计算。做到这三点交叉验证到0.82左右是这个数据集的及格线想再往上推就需要更精细的特征设计和更谨慎的验证策略了而那时候你学到的东西已经完全可以迁移到其他项目里。最后再说一句泰坦尼克数据集虽然已经被写过无数遍但它真正珍贵的地方就是让你在一个足够简单、反馈足够快的环境里亲手体会“数据决定了模型上限模型只是逼近这个上限”。这个认知是你带着它走向任何真实业务问题时最值钱的底子。