资讯详情

随机森林预测泰坦尼克号生存率:特征工程与调参实战全解析

📅 2026/10/20 23:59:10 | 华诺云谱 👁 阅读
随机森林预测泰坦尼克号生存率:特征工程与调参实战全解析
随机森林预测泰坦尼克号旅客存活率这个名字在机器学习圈子里几乎是个符号。每一个自学机器学习的人第一次接触分类问题的时候几乎都会撞上它。数据量不大、字段清楚、标签简单却能把你往后要反复用到的特征工程、模型选型、交叉验证、参数调优这些环节全部硬生生走一遍。我在带新人做这个项目时感触很深这个项目真正有价值的不是最后那个准确率数字而是它能让从数据到模型这条链路变得非常透明。这篇文章我就用随机森林来完成这个经典预测任务先拆数据再做特征工程然后讲清楚随机森林为什么适合这种小表格数据再给出一份可以直接跑通的完整代码链路最后把我踩过的坑一次性交代清楚。适合刚学完Python语法、准备动手做第一个机器学习项目的读者也适合拿它当课程设计或比赛热身模板的人。1. 项目拆解与整体设计思路开场先把这个项目拆清楚。泰坦尼克号数据集来自某个经典公开竞赛任务是预测每位旅客是否生还。训练集里有891条记录测试集里有418条记录。特征有十几个字段对于新手来说这个规模刚刚好不会像图片数据那样失控又比皮肤样本种类多足够展示特征工程的完整流程。1.1 泰坦尼克号数据集里到底藏着什么先把字段摸一遍。这是我在项目里做的第一件事用pandas读进来之后逐个字段看而不是急着建模。字段名含义用途提醒PassengerId乘客编号仅用于区分样本不能当特征Pclass舱位等级 1/2/3等级本身有顺序可保留数值Name乘客姓名主要为了提取称呼Sex性别需要编码为数值Age年龄存在缺失需要处理SibSp同行的兄弟姐妹和配偶数量可以组合出新特征Parch同行的父母和子女数量可以组合出新特征Ticket票号杂讯很多一般不直接用Fare票价存在缺失值Cabin客舱编号缺失严重但别急着删Embarked登船港口 C/Q/S分类变量需要编码Survived是否存活1/0这就是我们要预测的标签很多人第一次看到Name会直接忽略心想名字怎么能进模型但其实姓名里暗藏玄机。乘客的名字里通常带着Mr.Mrs.Miss.Master这类称呼这些称呼隐含了性别、年龄阶段、社会身份跟生存率有明显关联。我在后面专门做了称呼提取效果很不错。PassengerId就没什么用了它就是给每位乘客编的号模型不可能从编号里学到任何规律强行放进去只会增加随机噪声。Ticket字段比较脏票号有不同格式处理起来收益也不高所以我在最终特征里没有用它。1.2 为什么是随机森林备选模型逐个过一遍有人可能会问这个分类问题为什么不选逻辑回归或者干脆上XGBoost我把这几个方案的考量说清楚。逻辑回归简单、可解释性强作为基线没问题但它是线性模型很难自动捕捉Pclass、Sex、Age这些字段之间的交互关系。比如女性高舱位这种组合条件逻辑回归需要你手动构造交互特征对新手来说又多了一层门槛。XGBoost在这个数据集上往往能刷到很高的分数但它需要调的重点参数太多学习率、树深、正则系数、子采样比例任何一个没调好都可能翻车。对这个数据量XGBoost的优势并不明显乱调反而浪费时间。随机森林处于一个很舒服的位置它能学习非线性关系不需要数据标准化对缺失值有一定容忍度自带特征重要性评估参数数量适中调起来不容易过拟合。对刚入门的人来说它是拿来做第一个完整项目的最稳妥选择。2. 数据清洗与特征工程先把手里的牌打好很多人一上来就急着跑模型结果分数上不去回头一看问题是数据预处理没做到位。泰坦尼克号竞赛里特征工程基本决定你在公开榜上的上限模型本身反而拉不开多大差距。2.1 缺失值统计这数据比想象中更脏我第一步先统计缺失情况这一步能避免很多后面才爆的雷Age训练集缺失177条左右占比接近20%Cabin训练集缺失约687条占比接近78%Embarked训练集缺失2条Fare训练集无缺失但测试集有1条缺失Cabin缺失这么严重直接删列我建议保留但换一种用法。实际操作中我发现把Cabin是否缺失作为一个二值特征即有没有分配客舱编号这个信息本身就很有预测力。它间接反映了旅客的登记流程和信息完备程度在多个特征重要性列表里都排进过前列。Cabin列本身无法安全填充但是否存在这个标志是有效的。Age缺失量说大不大说小不小不能太粗暴。很多人图省事全局中位数填充我实测过这么做效果明显不如分组填充。原因是不同社会身份的乘客年龄分布相差很大用全局中位数相当于把船长和儿童拉到同一个水平信息损失不小。Embarked只有2条缺失直接用众数填充即可。Fare在测试集里缺失了1条用训练集的中位数填充就行。2.2 年龄和舱位两个最麻烦的字段怎么处理年龄的处理我用了按称呼分组求中位数这个经典方案。先从Name里提取称呼然后按每个称呼组分别算中位数比如Mr组的中位数、Miss组的中位数用对应组的中位数去填充该组缺失的Age。这样填充出来的年龄比全局中位数更贴近真实情况。我拿填充后的数据做过对比验证集F1比全局中位数填充高了好几个百分点。舱位Pclass处理起来比较直接保留数值1、2、3就行因为舱位等级本身是有序的一等舱、二等舱、三等舱之间存在天然的顺序关系。随机森林不依赖特征尺度不需要做标准化。这里插一句很多人问随机森林要不要做数据标准化答案是通常不需要。树模型分裂时只关心特征的相对大小顺序不关心绝对数值范围所以Age是30还是0.3在节点分裂上没有任何区别。这是树模型友好于新手的一个重要原因。2.3 从名字和家庭关系里挖出能用的特征这是整个特征工程里最出彩的部分。先说称呼提取。我用正则从Name字段里提取Mr.、Mrs.、Miss.、Master.这些称呼然后把出现次数很少的稀有称呼映射成Rare。称呼信息之所以有效是因为它同时包含了性别、年龄阶段和社会身份。比如Master通常指未成年的男性Captain、Doctor这些身份在船上获得的救助优先级完全不同。我在实操中发现光加上Title这一个特征验证集准确率就能往上走一个台阶。家庭关系组合也很有讲究。SibSp和Parch是分散的两个字段但把它们合成FamilySize之后含义就清晰多了。FamilySize SibSp Parch 1加的这个1是乘客本人。为什么要构造这个特征因为单独一人出行和全家同行的人在灾难中的生存模式差别很大家庭能形成互助关系但超大又没有组织的家庭可能行动不便。这种分散字段组合成新语义的思路是特征工程里最核心的技法之一。我还加了一个IsAlone特征当FamilySize等于1时取1否则取0。这个特征的作用是把独自一人这个极端状态单独拎出来。很多实战帖子都验证过独自出行标签的人生还率明显更低。2.4 最终特征清单与编码方案特征工程做完之后最终进模型的特征就定下来了Pclass舱位等级保留数值1/2/3Sex性别映射为0/1Age填充后的年龄保留连续值Fare填充后的票价保留连续值Embarked登船港口映射为0/1/2Title称呼映射为0到4的分类值FamilySize家庭总人数IsAlone是否独自一人HasCabin是否有客舱编号Sex和HasCabin这种二分类字段用0/1编码足够了。Embarked三个港口之间没有大小顺序我直接映射成数值随机森林对这种简单分类编码并不敏感不过你也可以尝试OneHot编码对比效果。最终我固定只用了9个特征这符合小数据集用精简特征的直觉特征太多反而容易把噪声学进来。3. 随机森林原理为什么它又稳又省心做完整套特征工程之后我觉得有必要先把随机森林的原理讲透。理解原理不是让你去背概念而是调参的时候你知道自己在干什么模型诊断的时候你知道该看什么。3.1 从一棵决策树说起随机森林的基座是决策树。决策树做的事情很直观在根节点上遍历所有特征的所有可能分裂点选一个让分裂后数据最纯的分裂方式。衡量纯度的指标一般用Gini不纯度分裂之后两类样本分得越开Gini下降越多这个特征和分裂点就越优先。决策树一个人做判断很容易走极端。树一旦长得足够深它可以把训练集里每一条样本都完美背下来但换个新数据就完全认不出来。这就是过拟合。单棵决策树的方差高换个数据分布结论可能发生剧烈变化。我说个生活化的类比决策树像是一个只靠个人经验做决定的店长给他很多数据他就能记住所有熟客的偏好但遇到没见过的客人就懵了。随机森林则是拉了一群店长每个人只随机看了其中一部分客人的资料然后投票做决定。单个人可能猜错但一群人投票出错的概率就低得多。3.2 两个随机行采样与列采样随机森林对每棵树做两件随机关卡这是它区别于单一决策树的核心。第一Bootstrap采样也就是有放回的行采样。每棵树训练时从原始训练集里随机有放回地抽取同样数量的样本这样每棵树的训练集都不完全一样有的样本在树A里出现多次在树B里可能完全没出现。没被抽中的样本就落在袋外后面用来做无偏评估。第二特征随机。每次分裂节点时不是从全部特征里挑最优而是先从特征集里随机挑一个子集再从子集里找最优分裂。参数max_features控制的就是这个子集大小。两个随机关卡的核心目的是一致的让每棵树尽可能差异化。如果所有树长成一样投票就失去意义了。这两个机制叠加起来的效果就是降低方差。单棵树对噪声敏感但几百棵树平均下来噪声会互相抵消模型整体就稳了。3.3 袋外评估与特征重要性免费附赠的两个工具随机森林有两个不需要额外拆分验证集就能使用的副产品。第一个是袋外分数参数里对应oob_score。每棵树都有没用到的样本把这些袋外样本扔进这棵树预测然后汇总所有树的结果就能得到一个类似交叉验证的效果。在训练集比较小的时候这个评估方式非常实用因为它不需要再单独扣出一部分数据做验证集。第二个是特征重要性。随机森林在节点分裂时会记录每个特征带来的Gini不纯度总下降量把所有树的结果累计起来再归一化就能得到每个特征的重要程度。我在这个项目里跑出来的特征重要性排名前几位通常是Sex、Fare、Age、Title这些。这个排名能帮你看清哪些特征真正在起作用也能帮你在特征很多时做取舍。4. 模型训练与调参实战原理过了接下来直接进入训练环节。我的调参策略不是一上来就丢一个网格搜索跑一晚上而是分阶段、有目的地调。4.1 先跑一个默认参数模型观察过拟合信号我先用默认参数建一个模型目的是看看当前特征和模型处在什么状态。之前提到的特征工程做完直接划分训练集和验证集from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score X_train, X_val, y_train, y_val train_test_split( X_feat, y_label, test_size0.2, stratifyy_label, random_state42 ) model RandomForestClassifier(n_estimators100, random_state42) model.fit(X_train, y_train) print(训练集准确率:, accuracy_score(y_train, model.predict(X_train))) print(验证集准确率:, accuracy_score(y_val, model.predict(X_val)))这个结果如果出现训练集准确率在0.92以上验证集只有0.78左右那就说明模型过拟合了。默认参数下树没有被限制深度每片叶子可以只含一个样本它能非常好地记忆训练数据但在新数据上泛化能力就变差。看到这种信号不要慌这正是后面调参要解决的问题。4.2 用分层K折交叉验证做可靠评估单次划分训练集和验证集有运气成分划分方式不同分数波动会很大。更可靠的方式是K折交叉验证。我强烈建议用分层K折也就是StratifiedKFold。为什么要分层因为这个数据集里生还样本比例大约38%如果普通K折某一折里可能恰好生还样本比例特别低或特别高导致评估结果失真。分层K折会在每一折里保持和整体相同的类别比例这才公平。from sklearn.model_selection import StratifiedKFold, cross_val_score kfold StratifiedKFold(n_splits5, shuffleTrue, random_state42) cv_scores cross_val_score( model, X_feat, y_label, cvkfold, scoringaccuracy ) print(每折准确率:, cv_scores) print(平均准确率:, cv_scores.mean())之后所有模型评估我都基于交叉验证不再信任单次切分的结果。4.3 网格搜索调参的正确顺序网格搜索是调参的常用方式但很多人一上来就写一个大网格组合爆炸在普通笔记本上跑半小时起步。我的做法是分阶段调。第一轮先调树的规模和深度也就是n_estimators和max_depth。n_estimators太小模型不稳定太大训练时间涨但精度收益趋缓我在这个数据集上常用200到300。max_depth限制每棵树能长多深是抑制过拟合最直接的参数经验上在8到16之间搜索比较合理。第二轮调min_samples_leaf和min_samples_split。min_samples_leaf控制叶节点至少有多少样本默认是1这个值太小容易过拟合设成2或4会平滑很多。min_samples_split控制内部节点分裂所需的最小样本数默认是2我在2到10之间搜索。第三轮调max_features。分类问题默认用sqrt也就是特征总数的平方根有时设成None让每棵树用全量特征效果反而有差异。最后用GridSearchCV把这些参数组合扫一遍from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [200, 300], max_depth: [8, 12, 16], min_samples_leaf: [1, 2, 4], min_samples_split: [2, 5, 10], max_features: [sqrt, None] } clf RandomForestClassifier(random_state42) grid GridSearchCV( clf, param_grid, cvkfold, scoringroc_auc, n_jobs-1, verbose1 ) grid.fit(X_train, y_train) print(最佳参数:, grid.best_params_) print(最佳得分:, grid.best_score_)这个网格有2乘3乘3乘3乘2共108个组合配合5折交叉验证其实已经不少了。实际操作时我把n_estimators只放了两个值其他参数也尽量精简普通笔记本十几分钟能跑完。4.4 指标选择准确率之外还要看什么很多教程只看准确率但泰坦尼克号这个数据类别本身不平衡生还率大约38%。如果你无脑预测所有人都不生还准确率也能有62%左右看着不低但模型实际上什么都没学到。所以光看准确率是不够的。我在这个项目里通常重点关注三个指标F1分数、ROC AUC和混淆矩阵。F1是精确率和召回率的调和平均能平衡预测生还的人里面有多少是准的和真正生还的人里有多少被找回来了。ROC AUC衡量模型把生还和未生还样本分开的能力不受具体阈值影响更适合作为调参时的评分标准。from sklearn.metrics import f1_score, roc_auc_score, confusion_matrix y_pred grid.best_estimator_.predict(X_val) y_prob grid.best_estimator_.predict_proba(X_val)[:, 1] print(F1:, f1_score(y_val, y_pred)) print(AUC:, roc_auc_score(y_val, y_prob)) print(confusion_matrix(y_val, y_pred))调参时我在GridSearchCV里用scoring参数指定roc_auc调完之后再用F1和混淆矩阵做最终评估这样不容易被单一指标带偏。5. 完整代码链路从数据到预测结果前面这些环节我拆开讲了这一部分我把它们合到一起给你一份可以直接跑起来的完整脚本。你只需要把train.csv和test.csv放到当前目录下按顺序执行就行。5.1 加载数据与缺失值检查import pandas as pd import numpy as np from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import StratifiedKFold, GridSearchCV from sklearn.metrics import accuracy_score, f1_score, roc_auc_score train pd.read_csv(train.csv) test pd.read_csv(test.csv) print(train shape:, train.shape) print(test shape:, test.shape) print(train 缺失情况:) print(train.isnull().sum()) print(test 缺失情况:) print(test.isnull().sum())这一步的目的是确认缺失值位置尤其是测试集的Fare列容易漏掉。5.2 特征工程函数说明我强烈建议把特征工程写成函数这样训练集和测试集复用同一套逻辑不容易出现两边特征不一致的惨案。这个函数在训练集上计算填充统计量在测试集上沿用训练集的统计量这个细节很关键后面避坑部分会细讲。def build_features(df, age_mapNone, emb_modeNone, fare_medNone): data df.copy() # 称呼提取 data[Title] data[Name].str.extract(r ([A-Za-z])\., expandFalse) data[Title] data[Title].replace( [Lady, Countess, Capt, Col, Don, Dr, Major, Rev, Sir, Jonkheer, Dona], Rare ) data[Title] data[Title].replace([Mlle, Ms], Miss) data[Title] data[Title].replace(Mme, Mrs) # 统计量 if age_map is None: age_map data.groupby(Title)[Age].median().to_dict() if emb_mode is None: emb_mode data[Embarked].mode()[0] if fare_med is None: fare_med data[Fare].median() # 缺失值填充 data[Age] data[Age].fillna(data[Title].map(age_map)) data[Embarked] data[Embarked].fillna(emb_mode) data[Fare] data[Fare].fillna(fare_med) # 新特征 data[HasCabin] data[Cabin].notna().astype(int) data[FamilySize] data[SibSp] data[Parch] 1 data[IsAlone] (data[FamilySize] 1).astype(int) # 编码 data[Sex] data[Sex].map({male: 0, female: 1}) data[Embarked] data[Embarked].map({C: 0, Q: 1, S: 2}) title_mapping {Mr: 0, Mrs: 1, Miss: 2, Master: 3, Rare: 4} data[Title] data[Title].map(title_mapping) feature_cols [Pclass, Sex, Age, Fare, Embarked, Title, FamilySize, IsAlone, HasCabin] return data[feature_cols] X_train build_features(train) y_train train[Survived] X_test build_features( test, age_maptrain.groupby(Title)[Age].median().to_dict(), emb_modetrain[Embarked].mode()[0], fare_medtrain[Fare].median() )X_train构造完后第一次调用build_features时会在函数内部计算train的title年龄映射、港口众数和票价中位数。X_test构造时把这些统计量传进去这样测试集的填充完全基于训练集信息不会引入测试集信息污染训练过程。5.3 调参搜索与模型评估kfold StratifiedKFold(n_splits5, shuffleTrue, random_state42) param_grid { n_estimators: [200, 300], max_depth: [8, 12, 16], min_samples_leaf: [1, 2, 4], min_samples_split: [2, 5, 10], max_features: [sqrt, None] } clf RandomForestClassifier(random_state42) grid GridSearchCV( clf, param_grid, cvkfold, scoringroc_auc, n_jobs-1, verbose1 ) grid.fit(X_train, y_train) print(最佳参数:, grid.best_params_) print(交叉验证 AUC:, grid.best_score_) best_model grid.best_estimator_ cv_acc cross_val_score(best_model, X_train, y_train, cvkfold, scoringaccuracy) cv_f1 cross_val_score(best_model, X_train, y_train, cvkfold, scoringf1) print(交叉验证准确率:, cv_acc.mean()) print(交叉验证 F1:, cv_f1.mean())如果你的机器配置一般这个网格可能需要跑十几分钟想快一点就把n_estimators只放[200]或者把max_depth和min_samples_leaf的组合缩小。调参结果不会每次都完全一致但大方向是n_estimators在200到300、max_depth在8到12、min_samples_leaf在2到4这个区间附近。5.4 生成预测提交文件最后一步是把模型应用到测试集生成提交文件。注意测试集没有Survived列模型预测之后按PassengerId对应输出。test_pred best_model.predict(X_test) submission pd.DataFrame({ PassengerId: test[PassengerId], Survived: test_pred }) submission.to_csv(submission.csv, indexFalse) print(submission.shape) print(submission.head())最后这个submission.csv就是标准的提交格式两列数据PassengerId和Survived。这个文件可以直接用于参加公共竞赛排名。如果你只是做练习也可以把测试集预测结果和训练集交叉验证的结果放在一起看判断模型有没有异常。6. 常见问题与避坑实录做完整个项目之后我复盘了几个反复出现的坑。有些坑不自己踩一遍光看教程真看不到。6.1 用测试集统计量填充缺失值造成隐性数据泄露这是最隐蔽的一个问题。很多网上的教程为了省事把train和test拼到一起然后统一算中位数、众数填充完再拆开。这种做法如果只是为了最后交一个结果文件影响不算大但如果你想用交叉验证评估模型的真实水平就会出问题。原因在于填充用的统计量是在全量数据上算出来的其中包含测试集的信息。交叉验证里每一折的训练过程就等于间接看过了测试集的部分统计信息评估结果会偏乐观。我在训练集上用这种方式对比过交叉验证分数能比严格做法高零点几个百分点这就是虚高的信号。我的做法是像第5章的代码那样特征工程函数里传入训练集统计量测试集只用训练集统计量来填充。这才是没有信息泄露的做法。6.2 训练集得分极高、验证集分数不达标怎么办默认参数的随机森林训练集准确率能跑到0.93以上但验证集可能只有0.78。这种差距就是典型的过拟合信号。优先调节点限制参数max_depth设成8到12min_samples_leaf设成2或4。这两个参数的组合对抑制过拟合的效果最明显。我在实际项目里遇到过只调max_depth不调min_samples_leaf的情况过拟合缓解了一些但不彻底。把min_samples_leaf从1提到4之后训练集和验证集的差距缩小了不少而且验证集分数没怎么掉。这说明有时过拟合不是因为树太多而是因为叶片太细碎每个叶子只记一两条样本把噪声当成了规律。6.3 类别特征的编码误区Embarked和Title这类分类字段如果直接用数值映射随机森林通常能处理因为它在节点分裂时并不把特征值当成有大小关系的量。但这里有个细微点需要注意Pclass可以当作有序数值用因为1等舱确实优于2等舱2等舱优于3等舱这是领域知识告诉我们的。Embarked就不一样三个港口没有排序关系映射成0、1、2只是给每个类别贴标签不代表0比1小。随机森林不像线性模型那么介意这种编码方式为稳妥起见你完全可以把Embarked改成OneHot编码再对比一次。代码逻辑上多一列少一列影响不大。6.4 随机种子、并行计算和各类小报错我在调试代码时经常遇到一个问题两次跑同样的代码交叉验证分数不一样。原因往往是没设random_state。交叉验证里shuffle、模型的random_state都定下来结果才能复现我代码里所有关键位置都固定了random_state42。调试阶段这个习惯能帮你确定分数的变化到底是调参带来的还是随机性造成的。还有一个小坑是测试集Fare缺失。train里Fare没有缺失值但test里有一条如果你在特征工程时只检查了train的缺失没检查test在预测时就会报特征数量不一致之类的错误。建模型之前把两个表的缺失情况都扫一遍能省很多事。另一个实用建议是n_jobs这个参数。GridSearchCV里设n_jobs-1可以用满所有CPU核心但如果你同时在跑其他程序机器会明显卡顿。小型数据集不至于跑很久普通双核笔记本设成n_jobs1或2也能忍受。我个人的习惯是每完成一个阶段就记录一下当时的交叉验证得分。阶段一的基准分数、阶段二加上特征工程后的分数、阶段三调参后的分数三个数字一摆就能很清楚地看到每一步到底贡献了多少提升。这个习惯让我在写总结时特别有底气也让我后来面对其他项目时不慌。这个项目做完一次你基本就掌握了处理表格型数据的一整套套路看数据、做特征、选模型、交叉验证、调参、评估、提交。后续再做房价预测、信用评分这类同类型项目思路都是相通的。随机森林在这个数据集上不是绝对的最强模型但它稳定、可控、容易上手作为机器学习路的第一个完整闭环用它来练手再适合不过。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑