ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Kaggle Spaceship Titanic实战:用pandas与XGBoost走通二分类全流程

Kaggle Spaceship Titanic实战:用pandas与XGBoost走通二分类全流程 最近把 Kaggle 上的 Spaceship Titanic 从头到尾完整刷了一遍。作为入门赛里少有的“特征带缺口、文本有含义、目标不平衡不严重”的二分类题目它非常适合用来走通一条标准的数据科学流水线pandas 做数据清洗与特征工程、XGBoost 建二分类模型、交叉验证评估泛化能力、最后生成符合平台要求的提交文件。这篇博文会把我的完整过程、代码思路、参数选择的理由以及实际踩过的坑全部写出来希望给正在入门 Kaggle 或者准备打表格型比赛的朋友一份可以直接抄作业的参考。1. 比赛全貌与任务拆解1.1 这场比赛到底在预测什么Spaceship Titanic 是 Kaggle 官方推出的入门级比赛之一故事背景是停在半人马座方向的“泰坦尼克号飞船”遇到了时空异常一部分乘客被传送到了另一个维度我们需要根据乘客的登记信息预测哪些人“被传送”了。数据本身并不复杂训练集大约 8693 行测试集约 4277 行这个体量在小型比赛里属于“单机轻松处理”的范围直接全部读进内存用 pandas 操作即可完全不需要上分布式框架。任务类型是标准的二分类目标列Transported取值只有True和False评估指标用的是 accuracy也就是预测正确的比例。字段方面一共 12 个输入特征PassengerId乘客编号、HomePlanet出发行星、CryoSleep是否进入低温休眠、Cabin船舱号、Destination目的行星、Age、VIP、RoomService、FoodCourt、ShoppingMall、Spa、VRDeck这几列是船上消费金额以及Name。这个题有意思的地方在于它不像经典的 Titanic 那样“缺值少、字段简单”它把很多现实问题故意压缩在了一起字符串特征包含复合信息、多列缺失呈现结构性规律、部分缺失值可以通过业务逻辑推导出来。换句话说数据清洗做得是否到位直接决定后续模型的分数上限。1.2 为什么这个赛题适合作为入门首选我推荐新手优先做这个比赛而不是直接上一些规模大、特征多的真实场景竞赛理由有这么几条。第一是计算资源要求低。四五千行的测试集、一万行不到的训练集跑一个 XGBoost 雏形模型只需要几秒钟完全可以在一台普通笔记本上反复实验不用担心 GPU 预算和训练时长。对于还在熟悉 sklearn 和 XGBoost 接口的人来说这种“即时反馈”非常重要。第二是特征工程的空间足够大且方向明确。它有一个典型的复合字段Cabin可以拆分出甲板、编号、左右舷三个子特征有PassengerId里的前缀可以作为同组乘客的标识。你不需要自己凭空想特征赛题设计里就埋了很多线索做出来的特征也确实能提升验证分数。第三是评估指标足够简单accuracy 不需要任何曲折理解就是“猜对了多少个人”。比起 AUC、F1、logloss 这些指标它对初学者友好得多也能让大家把注意力集中在数据理解和模型流程本身。1.3 工具链与整体流程规划我把整场比赛的流程拆成四个环节每个环节用一个核心库解决环节核心工具主要任务数据读取与合并pandas读入训练集、测试集合并便于统一处理清洗与特征工程pandas / numpy缺失值填充、Cabin 拆分、类别编码、组特征构造模型训练XGBoost二分类模型配合早停和参数搜索验证与提交sklearn / pandasStratifiedKFold 交叉验证、生成提交文件所有环节都可以在 Jupyter Notebook 里顺序执行。实际工作中我习惯先把训练测试两份数据横向合并成一个 DataFrame 统一清洗最后再切回两份这样能保证测试集和训练集执行完全相同的特征处理逻辑避免“训练集处理了、测试集忘了处理”的低级事故。2. 数据清洗与特征工程pandas 的实操细节2.1 先读数据别急着写清洗代码拿到数据的第一件事不是立刻 fillna而是先认真看每一列的缺失率、类型分布和取值样例。用df.info()和df.head()扫一眼你会发现几个关键的清洗点。CryoSleep是布尔值但有少量缺失Cabin是字符串形如B/0/S可以拆成甲板B、编号0、左右舷S三个部分消费类特征RoomService、FoodCourt、ShoppingMall、Spa、VRDeck缺失数量不少且它们的缺失和CryoSleep有强关联——简单来说低温休眠的乘客不会产生消费记录所以如果一个人是CryoSleepTrue消费列缺失可以被合理推断为 0而不是平均值。这一步很多新手会直接跳过直接对所有列调用fillna结果模型分数能到 0.79 左右就上不去了。问题不在于模型不够强而在于把“业务含义上本就该缺失”和“数据丢失造成的缺失”混为一谈处理。2.2 用 pandas 完成四类缺失值的差异化处理我自己的处理顺序是先对每个缺失列做个“缺失猜测”再写统一的清洗函数。这里把四类典型情况列出来。数值型消费列RoomService、FoodCourt、ShoppingMall、Spa、VRDeck。先判断该乘客是否CryoSleepTrue如果是则缺失值填 0如果不是再按乘客所属HomePlanet或Destination分组后取中位数填充。这个方案利用了组内分布更平滑的特点比全局中位数更稳。布尔型CryoSleep如果五个消费字段全部为 0 或全部缺失可以推断为True否则False。这是一个基于赛题故事背景的业务推断实测能让验证分数提升一点点更重要的是让数据自洽。类别型HomePlanet、Destination、VIP使用分组众数填充或者简单填Unknown。XGBoost 本身能容忍Unknown这种新类别。字符串类型Cabin拆出甲板和左右舷后缺失部分单独作为一个类别。代码大致长这样import pandas as pd import numpy as np train pd.read_csv(train.csv) test pd.read_csv(test.csv) train[is_train] 1 test[is_train] 0 df pd.concat([train, test], axis0, ignore_indexTrue) spend_cols [RoomService, FoodCourt, ShoppingMall, Spa, VRDeck] df[spend_cols] df[spend_cols].fillna(0) # CryoSleep 缺失时若消费全为 0认为是低温休眠 mask df[CryoSleep].isna() (df[spend_cols].sum(axis1) 0) df.loc[mask, CryoSleep] True df[CryoSleep] df[CryoSleep].fillna(False) # Age 按 HomePlanet 分组中位数填充 df[Age] df.groupby(HomePlanet)[Age].transform(lambda x: x.fillna(x.median()))这里补充一点为什么我合并训练集和测试集之后再做填充因为对测试集来说它的分布同样受到分组统计的影响。如果分开处理测试集里某个HomePlanet的年龄中位数可能“恰好偏离”造成不必要的偏差。合并处理后填充逻辑的一致性有保障这在真实比赛里是常见的做法。2.3 Cabin 字段拆分与乘客小组特征Cabin这种一个字段装多重信息的格式在真实业务中非常常见比如“楼栋-楼层-房间号”。我把它拆成三个新列Deck字母部分取值 A/B/C/D/E/F/G/T代表甲板层。CabinNum数字部分连续的整数。SideP 或 S代表飞船左右舷。其中Side可以当作二分类特征Deck可以当成类别特征直接编码。CabinNum本身偏序意义不大但在部分人群组合里用起来效果有限我最终保留它只是为了不破坏原始信息。比拆分本身更值得关注的是PassengerId的前缀。它的格式是0001_01下划线前面的部分是小组编号后面是成员编号。同一小组的人大概率拥有相同的HomePlanet、Destination、CryoSleep状态所以我把小组规模算了出来作为新特征GroupSize。更进一步可以用组内其他乘客的信息去补缺失值。比如某人的CryoSleep缺失但同行乘客都是CryoSleepFalse那这个人大概率也不是低温休眠。这个逻辑用 pandas 的groupby(GroupId).transform(first)就能实现实测能小幅提升交叉验证分数而且逻辑可信度高。2.4 类别编码何时用 One-Hot何时用 Ordinal处理完缺失和拆分后剩下的类别特征需要转成模型能理解的数值。XGBoost 自身不直接接受字符串常见的做法是HomePlanet、Destination、Deck、Side这类取值数量少且没有天然顺序的特征用OrdinalEncoder编成 0、1、2、3 即可。XGBoost 对类别顺序并不敏感因为树分裂会自己找切分点不需要像线性模型那样严格 One-Hot。如果特征取值特别多且分布稀疏可以尝试 One-Hot但在这种小数据比赛里收益不大还会增加内存。这里有一个容易被忽略的坑千万不要用LabelEncoder对目标变量以外的高基数列做编码后把它当作连续数值特征传给模型。虽然 XGBoost 不会报错但某些编码顺序意外地和目标产生了相关就会在训练集中制造“虚假信号”导致交叉验证分数虚高线上却崩盘。另外如果你用了sklearn的编码器切记先fit在合并后的全量数据上还是先fit在训练集上我的习惯是先合并训练测试再做编码这样测试集中出现训练集没有的类别时不会报 unknown category 错误。这个方案在生产环境不一定对但比赛场景下非常省事。2.5 数据清洗的通用逻辑写到这里我想多说一句。数据清洗表面上是“填缺失、删异常”本质上是在重新组织一份乘客登记表——把错位的、缺失的、编码不当的信息逐步校正成模型可以高效利用的特征矩阵。热词里有“mapreduce 招聘数据清洗”之类的场景原理也是一样的无论是 pandas 单机处理还是 mapreduce 分布式清洗核心都是先确定每个字段的“标准形态”再写转换逻辑。只是比赛规模下pandas 的向量化操作已经足够快没必要引入分布式框架。我在清洗阶段踩过的一个小坑是对整列的fillna用得太随意导致某些“缺失合理含义”的字段被错误填充。比如VIP列缺失其实意味着“未登记 VIP 信息”而不是“不是 VIP”。这种情况填False虽然不会带来灾难性影响但要意识到每个缺失值背后都有一个业务解释应该尽量让填充方式匹配业务逻辑。3. XGBoost 二分类模型搭建与调参3.1 为什么选 XGBoost而不是其他算法对于 Spaceship Titanic 这种以表格型稀疏、多类别、含噪声特征为主的数据XGBoost 几乎是最稳健的默认选择。原因有三个。第一是它对特征缩放不敏感。树模型在分裂时只关心特征值之间的相对大小因此Age的 0 到 80 和RoomService的 0 到 15000 并存也不会带来问题不需要做标准化。相比逻辑回归和 SVM省了很关键的一步预处理。第二是它能自动处理特征交互。比如“同组人数大于 3 且甲板为 A”这种组合条件树分裂可以自动捕捉省去手工做交叉特征的精力。第三是它的工程化程度高。XGBoost 自带缺失值处理、早停、样本权重、内置交叉验证等功能很多周边代码不用自己写。配合RandomizedSearchCV做参数搜索能在大范围参数空间里快速找到不错的一组配置。3.2 XGBoost 参数速览与二分类配置XGBoost 用于二分类时最核心的参数是objectivebinary:logistic它输出的是概率值需要在最后用阈值 0.5 转成True/False。我第一版跑的基线参数如下import xgboost as xgb params { objective: binary:logistic, eval_metric: logloss, learning_rate: 0.05, max_depth: 6, subsample: 0.8, colsample_bytree: 0.8, min_child_weight: 1, gamma: 0, tree_method: hist, device: cuda, # 如果没 GPU直接删掉这一行 random_state: 42, }这里有几个参数的作用值得展开说一下。learning_rate是每一轮迭代的步长。调低学习率能让模型学得更慢、更稳但需要更多的树。我习惯先用 0.05 做探索最终如果时间允许可以降到 0.01 并调高n_estimators。max_depth控制单棵树的深度太小会欠拟合太大会过拟合。对这种一万行规模的数据6 左右是安全的起点。subsample和colsample_bytree分别控制行采样和列采样。它们的作用类似随机森林里的随机性能有效降低过拟合。对小数据量比赛0.8 是比较中间的选择。tree_methodhist是 XGBoost 的直方图算法比默认的exact算法在速度和内存上都有明显优势。在 2026 年的版本里已经是默认推荐选项用它能顺带解决一部分“XGBoost 提升运行效率”的诉求。另外需要明确一点这个赛题是分类任务不是回归所以objective不要设成reg:squarederror。不过如果你是在做房价预测之类的回归比赛也有对应的objectivereg:squarederror配置配合RandomizedSearchCV的调参思路是完全相同的只是评估指标和输出处理不同。3.3 用早停控制迭代轮数避免手动猜 n_estimators我最开始的经验是所有参数里最头疼的就是n_estimators设多少。设少了欠拟合设多了白白增加训练时间还容易过拟合。XGBoost 的early_stopping_rounds就是专门解决这个问题的。早停的思路很简单每一轮训练之后用验证集计算一次评估指标比如 logloss如果连续 N 轮验证集指标都没有变好就停止训练并且回滚到历史上最优的那一轮模型。model xgb.XGBClassifier( **params, n_estimators2000, early_stopping_rounds50, ) model.fit( X_train, y_train, eval_set[(X_valid, y_valid)], verbose100, )注意这里有个容易出的错early_stopping_rounds必须结合eval_set使用而且eval_set不能是训练集本身否则模型会一直认为自己在变好直到把n_estimators全部跑完早停等于失效。正确做法是留出一部分数据作为验证集或者配合交叉验证在每一折里都做一次早停。n_estimators可以设得很大比如 2000模型在早停触发后会自动停在合适的轮数不会真的跑满 2000 棵。这个方案比手动搜索n_estimators高效得多。3.4 用 RandomizedSearchCV 搜索关键参数基线模型跑通之后我会用随机搜索做一轮参数调优。随机搜索的优势在于不需要穷举所有参数组合而是从分布中随机抽取组合在参数空间比较大的时候能更快找到可行解相比网格搜索在运行效率上提高很多。from sklearn.model_selection import RandomizedSearchCV from scipy.stats import randint, uniform param_dist { max_depth: randint(3, 9), subsample: uniform(0.6, 0.3), colsample_bytree: uniform(0.6, 0.3), min_child_weight: randint(1, 6), gamma: uniform(0, 0.5), learning_rate: uniform(0.01, 0.09), } base_model xgb.XGBClassifier( objectivebinary:logistic, eval_metriclogloss, tree_methodhist, n_estimators500, random_state42, ) search RandomizedSearchCV( base_model, param_distributionsparam_dist, n_iter30, cv5, scoringaccuracy, n_jobs-1, random_state42, )这里有一个经验点随机搜索的cv5内部已经做了交叉验证所以不需要额外再传eval_set这个搜索的核心目的不是拿最优一轮早停而是比较不同参数组合的泛化表现。如果你在搜索时还想用早停正确的做法是给fit传入eval_set但由于内部 CV 会划分数据容易造成早停验证集与训练折叠重叠的问题我建议初期不要混用先把参数搜索跑完再用搜出来的参数单独跑一次带早停的最终模型。搜索完成后把最优参数取出来重新在完整训练集上训练最后预测测试集。虽然理论上更好的做法是把参数搜索放进每一折交叉验证里做模型选择但那会让计算时间成倍增加。比赛阶段可以用一个折衷策略先做一轮随机搜索找到较优参数再固定参数跑 5 折交叉验证得到更稳定的预测。3.5 特征工程与模型调参的顺序问题很多新手把时间和精力全花在调参上忽略了特征工程。根据我的经验这个赛题里特征工程带来的提升远大于调参。举个例子只做简单缺失填充时XGBoost 交叉验证分数大概在 0.78 左右当我拆出Deck、Side、GroupSize并完善CryoSleep推断后分数直接跳到 0.80 以上。而参数搜索调到极致通常也就再涨 0.005 到 0.01 左右。所以建议的顺序是先用默认参数跑通完整流程记录分数然后做特征工程每加一个特征重新验证一次看分数是否真的提升最后在特征基本定型后再做参数搜索。这样每一步的变量控制是清晰的不会出现“改了三个东西不知道哪个起作用”的情况。4. 交叉验证设计与提交4.1 为什么只切一次训练验证集不够入门阶段最常见的验证方式是直接把训练集按比例切成 train 和 valid比如 8:2然后看验证集分数。这种方式的问题是如果这 20% 的数据恰好比较特殊分数就会偏高或偏低导致你对模型真实水平的判断失真。交叉验证的做法是把训练集分成 K 份轮流拿其中 1 份做验证其余 K-1 份做训练最终得到 K 个模型的平均表现。这样每一行数据都有机会当验证集得到的评估结果对随机划分不敏感更稳定。Spaceship Titanic 数据量只有八千多行完全可以跑 5 折甚至 10 折交叉验证而不担心耗时。5 折是最常见的选择稳定性和计算成本均衡。4.2 StratifiedKFold 与 KFold 的区别对于二分类问题我强烈建议用StratifiedKFold而不是KFold。KFold只是把数据按顺序均分成几份可能出现某一折里TransportedTrue的比例远高于其他折的情况。而StratifiedKFold会保证每一折里正负样本的比例和原始数据集大致相同这对准确率评估更公平。如果目标列分布特别不平衡比如 95% 是负样本那直接用 accuracy 评估就没有意义了但本题的目标列比例接近五五开StratifiedKFold足够。4.3 从 OOF 到测试集预测的完整模板这是我整个流程里最核心的一段代码单独拿出来说清楚。from sklearn.model_selection import StratifiedKFold from sklearn.metrics import accuracy_score import numpy as np N_FOLDS 5 skf StratifiedKFold(n_splitsN_FOLDS, shuffleTrue, random_state42) oof_pred np.zeros(len(X_train)) test_pred np.zeros(len(X_test)) models [] for fold, (tr_idx, val_idx) in enumerate(skf.split(X_train, y_train)): X_tr X_train.iloc[tr_idx] X_val X_train.iloc[val_idx] y_tr y_train.iloc[tr_idx] y_val y_train.iloc[val_idx] model xgb.XGBClassifier( objectivebinary:logistic, eval_metriclogloss, learning_rate0.02, max_depth5, subsample0.8, colsample_bytree0.8, tree_methodhist, n_estimators3000, early_stopping_rounds100, random_state42, ) model.fit( X_tr, y_tr, eval_set[(X_val, y_val)], verboseFalse, ) oof_pred[val_idx] model.predict_proba(X_val)[:, 1] test_pred model.predict_proba(X_test)[:, 1] / N_FOLDS models.append(model) oof_acc accuracy_score(y_train, oof_pred 0.5) print(fOOF Accuracy: {oof_acc:.5f})这段代码里值得注意的几个细节。第一oof_pred的尺寸等于训练集行数。每一折的验证集预测概率被填到对应位置最终得到的是“任何一个样本都没有参与过自身预测”的 OOF 预测。用accuracy_score(y_train, oof_pred 0.5)算出来的分数基本能代表模型在未知数据上的真实表现。第二测试集预测是所有折模型预测概率的平均值。为什么不直接取某一折的模型因为集成多个模型的预测通常更稳定方差更小。第三learning_rate降到了 0.02n_estimators提高到了 3000 并配合早停。低学习率加高迭代次数的组合往往能得到更好的最终精度代价是训练时间变长但对这个数据量完全可接受。4.4 生成提交文件时最容易犯的几个错误模型跑完最后一步是生成提交文件这一步出错的概率远比你想象的高。提交文件必须包含两列PassengerId和Transported。PassengerId要和原始测试集的PassengerId完全一致顺序都不用打乱。Transported必须是True或False的布尔值不是 1/0也不是字符串True/False。submission pd.DataFrame({ PassengerId: test[PassengerId], Transported: test_pred 0.5, }) submission.to_csv(submission.csv, indexFalse)这里test_pred 0.5得到的是布尔数组pandas 写入 CSV 时会自动变成True/False符合 Kaggle 要求。另一个常见错误是把训练集和测试集合并清洗后在恢复测试集时不小心把PassengerId的顺序打乱了。我的处理方式是在最开始合并前就保存一份test[PassengerId]的顺序最终提交时按这个顺序对齐避免任何潜在错位。4.5 本地分数和公开榜分数为什么不一样刚接触 Kaggle 的人经常困惑本地 OOF accuracy 是 0.813为什么公开榜上显示 0.805这太正常了。本地交叉验证用的是自己的特征工程和划分方式而公开榜是平台拿你的提交在它自己的隐藏测试集上算的。隐藏测试集和你本地手动切的验证集分布不可能完全一致再加上随机种子和早停轮数的影响微小的波动完全合理。只要本地分数和线上分数差距不超过 0.02基本可以判断模型没有显著的过拟合或数据泄漏问题。正确的使用方式是把本地分数当模型选择的依据而不是追求“本地分和线上分完全一致”。如果本地很高线上很低优先怀疑你处理测试集时出了问题或者发生了特征泄漏。5. 常见问题与排查实录5.1 提交时遇到“Kaggle captcha must be filled out”怎么办第一次提交时我卡在了验证码这一步。Kaggle 提交脚本要求你先登录并且完成人机验证如果没有完成验证就尝试提交平台会返回类似 “captcha must be filled out” 的错误信息。解决办法很简单在浏览器里访问 Kaggle登录账号完成一次页面上的验证码验证让浏览器会话保持有效然后重新提交。这个问题只影响提交动作不影响模型文件本身所以不用慌也不是数据或代码的问题。5.2 交叉验证分数一直卡在 0.79 左右下一步做什么如果你发现自己的交叉验证分数在 0.79 附近上不去大概率是这两个原因之一。一个是缺失值处理太粗暴所有列都用全局均值或众数填充。回到 2.2 节把每个缺失字段单独拿出来看分布和业务含义尤其是CryoSleep和消费字段的关系。另一个是特征工程没做够。Cabin拆分、GroupSize、同组其他乘客信息推断这些特征在多数上分方案里都能带来肉眼可见的提升。不要急着调参先把特征查漏补缺。做完这些0.80 到 0.81 是完全可以达到的水平。再往上就需要更精细的类别嵌入、多模型集成或者拼接其他方案的预测超过入门赛的合理投入了。5.3 eval_set 数据泄漏的隐蔽问题早停机制在交叉验证里有一个隐蔽问题如果你在RandomizedSearchCV里使用eval_set内部 CV 划分出来的验证集可能和训练子集存在重叠导致早停信息泄漏。解决办法分两种情况。如果只是单独训练一个模型做验证用 4.3 节的手动 5 折循环自己控制每一折的验证集完全没问题。如果要做参数搜索我建议让RandomizedSearchCV只负责搜索不加eval_set搜完后再用最优参数手动跑交叉验证。这样虽然多花一点时间但逻辑上没有硬伤。5.4 随机种子与结果可复现性XGBoost 在启用了随机采样后不同运行之间会有微小差异。为了让实验对比可信需要固定多个部分的随机种子。我统一使用random_state42并且固定n_jobs和tree_method保证每一次跑出来的 OOF 分数可以互相比较。如果你换了随机种子之后分数波动很大说明模型方差偏高这时不要单纯调参先考虑增强特征稳定性或使用更多折的交叉验证。5.5 特征编码顺序造成的数据泄漏还有一个非常隐蔽的坑在合并的训练测试集上做OrdinalEncoder等编码时把编码器在整个数据集上 fit这样的类别映射本身利用了测试集信息。这不会直接导致数据泄漏因为在监督学习里测试集无标签信息被“偷看”程度很低。但如果你的编码顺序是根据类别和目标变量的相关性排的比如用目标编码那就必须只在训练集每一折内 fit否则线上分数会崩。对于这个比赛我建议只使用普通编码不做 target encoding一来避免泄漏风险二来入门阶段不需要这个复杂度。5.6 常见问题速查表问题现象可能原因解决方案本地分数高线上分数明显低特征编码泄漏或测试集处理不一致检查编码是否在全量数据上 fit提交后报错与行数不符提交文件行数不等于测试集行数检查是否误删行或合并时索引错位布尔提交列变成 0/1用 int 类型保存了预测值用test_pred 0.5转为布尔值早停从未触发eval_set 用了训练集改用单独验证集或交叉验证XGBoost 报 object dtype 错误类别列未编码使用 OrdinalEncoder 转换训练速度慢tree_method 默认 exact使用 hist 并开启 GPU最后再分享一个我自己的习惯每跑完一版特征工程或参数调整就把OOF Accuracy和特征列表记录在一个表格里方便对比。这个比赛真正有价值的不是最后那个公开榜排名而是你完整跑通了“数据清洗—特征工程—模型训练—交叉验证—提交”这一整条链路。后续哪怕换到更复杂的比赛这套流程骨架依然通用你只需要在每一个环节里换更高级的技巧而已。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进