逻辑回归入门:Kaggle泰坦尼克号生存预测与提交全流程 简介面向Kaggle新手与数据科学入门者的泰坦尼克号生存预测完整实践包以经典竞赛题为核心系统覆盖从数据读取、探索性分析、特征工程到模型构建与评估的完整流程。压缩包共10个文件大小约459KB包含5个CSV数据文件、3个Jupyter Notebook分析文档、1个Python脚本及1份说明文档Notebook中分别演示了EDA分析、逻辑回归建模以及决策树、随机森林、梯度提升树等算法的对比实验Python脚本则提供可直接运行的训练与预测实现方便读者快速复现并在此基础上调整参数。内容针对二分类问题给出了清晰的解决方案包括缺失值填充、类别特征编码、训练集与测试集划分、交叉验证等关键环节并讨论准确率、精确率、召回率与F1分数等评估指标的适用场景有助于理解不同模型在同一数据集上的表现差异。目前已有136人学习适合希望以经典项目入门机器学习、巩固数据科学流程或准备参加Kaggle竞赛的读者参考。1. 从Kaggle泰坦尼克号生存预测开始逻辑回归入门与提交全流程先说一个反直觉的结论泰坦尼克号生存预测这个赛题只用“女性优先”这一条规则就能拿到约0.7655的准确率而很多初学者第一个逻辑回归模型反而只有0.72左右甚至更低。原因不在算法而在缺失值怎么填、验证集怎么切、提交文件怎么对齐。这套流程我会用逻辑回归作为主线从选型理由、数据清洗、特征工程到提交排错完整走一遍。数据量也不大train只有891行、test只有418行十分钟能跑完一轮迭代特别适合把逻辑回归的目标函数、正则化参数和交叉验证吃透。经历过一遍以后你再去玩房价预测、商店销售这类表格赛至少不会在“本地分数和排行榜分数为什么总差一截”这件事上翻车。2. 逻辑回归的选型理由目标函数、数据特性和第一版提交2.1 为什么先选逻辑回归线性分类与概率输出的关键不同泰坦尼克号生存预测是标准的二分类任务输出只有存活和未存活两条路径。逻辑回归对输入特征做线性加权后再通过sigmoid函数把结果压到0到1之间输出可以直接解释为存活概率。它在训练时最小化的是对数损失对单个样本而言预测概率越接近真实标签损失越小越接近0.5梯度越明显模型会更快把样本推开。这个性质让它在中等规模、特征线性可分的数据集上表现稳定。选择它而不是一上来就上随机森林或XGBoost有三个具体理由。第一可解释性每个特征对应的权重能直接说明“这个变量在推高还是压低概率”一旦数据清洗有问题特征权重会给出非常直观的异常信号。第二运行成本891条数据跑一次逻辑回归在普通笔记本上不到一秒迭代调参几乎没有成本。第三作为后续复杂模型的基线先用同一份特征跑逻辑回归拿到基准分数再换成XGBoost分数的增量到底是来自模型还是来自特征一眼就能分清楚。还有个容易忽略的点逻辑回归的predict输出0/1predict_proba输出概率。提交流程只需要0/1但本地评估时我习惯多看一眼概率分布。如果大量样本的预测概率都贴着0.5说明特征区分度不够而不是模型本身有毛病。这个习惯在后续检查特征工程效果时非常有用。2.2 第一个数据摘要目标分布、缺失值与变量尺度开始之前先把train.csv和test.csv读进DataFrame做体检。这一步单独拿出来写是因为后面所有特征工程都建立在缺失值清单上不做体检就训练后面出了问题连排查方向都没有。import pandas as pd train pd.read_csv(train.csv) test pd.read_csv(test.csv) print(train shape:, train.shape) print(test shape:, test.shape) print(Survived 比例:\n, train[Survived].value_counts(normalizeTrue)) for name, df in [(train, train), (test, test)]: miss df.isnull().sum() miss miss[miss 0].sort_values(ascendingFalse) print(f\n{name} 缺失列:\n, miss)逻辑说明train有891行12列test有418行11列。Survived比例用value_counts(normalizeTrue)查看目的是确认类别没有严重失衡——如果幸存比例是9比1那准确率这个指标就要让位于F1或者AUC。这里幸存比例大约38%类别基本均衡可以直接用准确率评估。缺失列统计把Age、Fare、Embarked、Cabin全部列出来其中Cabin缺失率极高初版直接丢弃即可。参数说明pd.read_csv默认utf-8编码如果数据被Excel另存过可能产生BOM头导致第一列表名带上不可见字符打印columns就能发现。test里Fare会有1个缺失这是一个容易漏掉的点很多人只处理train的缺失忘了test也有缺失提交时直接报错。从结果大概会看到Age缺失约177个Cabin缺失约687个Embarked缺失2个。这里不建议第一步就做复杂插补。先把缺失列用最简单的中位数填上跑一遍逻辑回归拿到原始分数后面每做一个特征工程都能和这个原始分数对比才能判断改动到底是正向还是负向。from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score cols [Pclass, Sex, Age, Fare] X train[cols].copy() y train[Survived].copy() for df in [X, test]: df[Sex] df[Sex].map({male: 0, female: 1}) df[Age] df[Age].fillna(df[Age].median()) df[Fare] df[Fare].fillna(df[Fare].median()) X_train, X_val, y_train, y_val train_test_split( X, y, test_size0.2, random_state42 ) model LogisticRegression(max_iter500, random_state42) model.fit(X_train, y_train) val_acc accuracy_score(y_val, model.predict(X_val)) print(val_acc:, round(val_acc, 4))逻辑说明切分采用train_test_splittest_size0.2表示891条里留约178条做验证其余训练。模型用LogisticRegression默认的lbfgs求解器max_iter放到500避免特征没有标准化时收敛过慢导致警告。参数说明random_state固定为42保证每次运行切分一致这是复现的前提。Age用中位数填充是因为Age分布偏态明显个别高龄样本会把均值拉高中位数更稳健。Fare同样受右偏分布影响个别头等舱票价极高所以用中位数而不是均值。Sex映射成0/1后逻辑回归能直接学到女性权重为正、男性权重为负输出的特征含义会非常清晰。这一步跑完val_acc通常在0.75到0.78之间和只用性别规则得到的0.7655相差不大。不要急着换模型先把这个分数当作基准后面所有特征工程的改动目标都是稳定超过0.78。2.3 生成第一版提交PassengerId对齐才是关键有了模型接下来把预测结果写入提交文件。代码很短但这一小步卡住过很多人。pred model.predict(test[cols]) sub pd.DataFrame({ PassengerId: test[PassengerId], Survived: pred }) sub.to_csv(submission_lr_v1.csv, indexFalse) print(sub.shape) print(sub.head())逻辑说明test在2.2的循环里已经完成了Sex映射和缺失值填充这里直接用同一组cols列做预测。预测结果与test的PassengerId对齐后写入DataFrame保存时indexFalse避免把行号写进CSV的第二列。参数说明PassengerId必须保持test原始顺序不能排序不能重置索引。sub.shape应该是(418, 2)如果输出不对先检查前面处理test时是否无意中改变了行数。保存文件名建议带上版本号v1、v2这样方便后面回滚对比。注意准确的提交格式只有两列第一列是PassengerId第二列是Survived值为整数0或1。不要多传其他列Kaggle对提交格式的校验很严格格式错了直接得0分。第一版提交的分数一般就在0.75到0.77之间。拿着这个分数下面才进入真正有价值的部分特征工程和交叉验证。3. 特征工程与交叉验证从0.77到0.79的可执行路径3.1 分组填充Age从全局中位数升级为分群估算Age缺失177个全局中位数填充虽然简单但会带来系统性偏差。泰坦尼克号上不同舱位、不同性别的乘客年龄分布差异很明显头等舱女性年龄中位数偏高三等舱男性整体偏年轻。用全局中位数填等于把所有缺失样本都拉到同一个水平线上后续分舱位统计特征时会丢失信息。常见做法是按Pclass和Sex分组各取组内Age中位数填充。这里有个关键点先按训练集分组统计再把这个统计结果应用到test而不是在train和test上分别算一遍。两个文件的缺失分布不同分开算会引入尺度不一致。import pandas as pd train pd.read_csv(train.csv) test pd.read_csv(test.csv) # 训练集内部分组填充 train[Age] train[Age].fillna( train.groupby([Pclass, Sex])[Age].transform(median) ) # 以训练集分组统计为准填充 test age_map train.groupby([Pclass, Sex])[Age].median() test[Age] test.apply( lambda r: age_map.loc[(r[Pclass], r[Sex])] if pd.isnull(r[Age]) else r[Age], axis1 ) print(train.groupby([Pclass, Sex])[Age].median())逻辑说明train部分用groupbytransformtransform返回与原DataFrame同shape的分组中位数序列fillna直接填入。test部分先用age_map生成一个以(Pclass, Sex)为索引的中位数表再用apply逐行处理遇到缺失就按该行的舱位和性别取值。参数说明age_map里的索引是复合索引loc查找时要用元组顺序不能写反。apply逐行执行效率不高但418行数据总量极小完全够用。对比一下分组填充前后的val_acc一般会有0.5到1个百分点的提升这个提升幅度不大但它是真实信息带来的不是噪声。3.2 FamilySize、One-Hot与Title从离散变量里挤出分数泰坦尼克号里的SibSp和Parch分别代表同行的兄弟姐妹/配偶数、父母/子女数单独看意义不强但加在一起能反映一个关键信息这个人是不是独自一人。生存率分析里独自旅行的乘客生还比例明显偏低。离散特征Pclass和Embarked不适合直接当连续数值喂给逻辑回归。Pclass是1/2/3等级之间不是等差关系Embarked是C/Q/S三个港口更是纯粹的类别。常见做法是用One-Hot展开成多列。还有一个容易拿分的特征Name里的Title。Mr、Mrs、Miss、Master这些称谓隐含了年龄和社会身份极少数乘客的Title是Dr、Rev、Lady这类样本量小但单独分成一类也能减少噪声。def build_features(df): out df.copy() # 家庭规模与是否独行 out[FamilySize] out[SibSp] out[Parch] 1 out[IsAlone] (out[FamilySize] 1).astype(int) # 从姓名中提取称谓 out[Title] out[Name].str.extract(r ([A-Za-z])\\., expandFalse) out[Title] out[Title].replace( [Lady, Countess, Capt, Col, Don, Dr, Major, Rev, Sir, Jonkheer, Dona], Rare ) out[Title] out[Title].replace( [Mlle, Ms], Miss ) out[Title] out[Title].replace( [Mme], Mrs ) # 性别与登船港编码 out[Sex] out[Sex].map({male: 0, female: 1}) out[Embarked] out[Embarked].fillna(S) # One-Hot 展开 out pd.get_dummies( out, columns[Pclass, Embarked, Title], drop_firstFalse ) return out train_feat build_features(train) test_feat build_features(test) print(train_feat.shape)逻辑说明Title提取用的正则([A-Za-z])\\.匹配的是姓名中“空格单词点”的结构例如“Mr.”、“Mrs.”。低频称谓统一归入Rare避免One-Hot后产生大量稀疏列。Mlle和Ms归入MissMme归入Mrs这些是法式称谓的合理映射。FamilySize把SibSp、Parch和本人加在一起IsAlone则直接标记是否独行。参数说明get_dummies里drop_firstFalse会保留所有类别列特征列会变多但泰坦尼克号特征总量小完全能接受。如果觉得共线性问题扎眼也可以drop_firstTrue两种做法分数差异很小。重点是Pclass从单一数值列变成三个0/1列逻辑回归才能学到“二等舱和三等舱不是线性递进”的真实关系。加完这些特征后重新训练val_acc一般能到0.79上下。如果只有0.77甚至更低先检查Title的正则是否提取成功打印几个缺失Title的样本问题基本都出在正则没匹配上。3.3 管道加五折扫描C用标准差不替单次准确率下结论特征数量变多后逻辑回归面临一个新的变量正则化强度C。C越大正则化越弱模型对训练数据拟合越充分C越小正则化越强权重被压得越接近0。泰坦尼克号这种数据量小的赛题C选得太大会过拟合C选得太小会欠拟合不能只看单次验证集分数来定。更重要的是单次train_test_split的切分随机性比较大。同一组参数换一个random_state分数可能波动2个百分点。一个可执行的调参方案是用五折交叉验证扫描C同时观察每折分数的标准差。如果标准差大于0.02说明这个模型对数据切分过于敏感不要用哪怕平均分再高也白搭。import numpy as np from sklearn.model_selection import StratifiedKFold from sklearn.metrics import accuracy_score C_list [0.01, 0.1, 1, 10] skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for C in C_list: scores [] for tr_idx, va_idx in skf.split(train_feat, train[Survived]): lr LogisticRegression(CC, max_iter1000, random_state42) lr.fit(train_feat.iloc[tr_idx], train[Survived].iloc[tr_idx]) va_pred lr.predict(train_feat.iloc[va_idx]) scores.append(accuracy_score(train[Survived].iloc[va_idx], va_pred)) print(fC{C:.2f} mean{np.mean(scores):.4f} std{np.std(scores):.4f})逻辑说明StratifiedKFold按类别比例分层切分保证每一折里Survived的分布都和整体一致避免某折全是男性样本导致分数失真。C_list按数量级扫描覆盖从强正则化到弱正则化这个范围对泰坦尼克号足够了。参数说明max_iter放到1000是因为特征列变多后默认迭代次数可能不够会有收敛警告。如果你看到ConvergenceWarning先把max_iter调大不要急着换求解器。在特征数不超过几十个、样本量不到一千的情况下默认lbfgs完全够用。四种C跑完你会看到C1附近的平均分和标准差通常比较平衡。C0.01可能平均分略低但std很小说明模型被正则化压得太死。C10平均分可能虚高但std偏大这是过拟合信号。调参的目标是找到平均分和std的平衡点。4. 泰坦尼克号避坑清单提交错乱、数据泄漏与验证修复4.1 本地0.74分但提交只有0.72索引错位是头号原因现象本地val_acc稳定在0.74提交到Kaggle后排行榜分数变成0.72左右。反复跑代码没有报错但分数始终对不上。原因生成提交文件时预测结果的行顺序和test的PassengerId没有对齐。常见场景是你对test做特征工程时reset过索引或者用concat把预测结果拼到DataFrame边缘时顺序被打乱。模型预测本身没错错的是提交前数据对齐。解决提交前加一段强制校验不满足条件直接抛异常。assert sub[PassengerId].is_unique, PassengerId 存在重复 assert len(sub) 418, f提交行数异常: {len(sub)} sub sub.set_index(PassengerId).loc[test[PassengerId]].reset_index()逻辑说明第一行检查PassengerId是否唯一第二行检查行数是否为418第三行用test的PassengerId作为基准重新排序sub。这个技巧以test为准对齐行顺序无论前面怎么折腾最后都会回到正确顺序。4.2 验证集分数很高但排行榜很低验证切分里的数据泄漏现象在本地把Age缺失填充做得特别精细验证集准确率稳定在0.80以上提交后只有0.76。反复调特征没有改善。原因你很可能在切分训练集和验证集之前就拿了整个train的全局统计量去填充缺失值。例如先把train的Age中位数计算出来填进全表再train_test_split这时验证集样本的信息已经参与过中位数计算。填缺失值的统计量来自整份数据虽然泄漏程度很轻但会让模型在验证集上表现得比真实水平好一点点这个项目数据量小一分一毫的泄漏都会被放大。解决先切分再用训练部分的统计去填充验证部分。或者在2.2的建模阶段养成习惯把数据变换和缺失填充全部写进sklearn管道。填充步骤放进管道最大的好处是交叉验证时每一折都用对应的训练子折去学统计量从结构上杜绝泄漏。注意这里说的泄漏不是Kaggle违规而是标准流程里犯的统计错误。养成“先切分再填充”的习惯以后你会发现自己很多小项目的分数波动都消失了。4.3 提示“kaggle captcha must be filled out”注册和提交时的浏览器验证问题现象Kaggle注册、登录或者下载数据时直接提示“kaggle captcha must be filled out”没有弹出验证码框页面卡住无法继续。原因绝大多数情况是浏览器插件把验证码组件拦截了广告过滤类插件、隐私模式、过旧的浏览器缓存都可能导致验证码框架加载不出来。数据文件本身没有问题也不需要走第三方渠道下载。解决换用普通浏览器窗口或者临时关闭广告过滤插件清除一个站点的cookie刷新页面重新走一遍验证。验证码出现后按正常流程点击确认即可。如果页面仍无响应再检查是否开了全局的扩展拦截开关。这类问题基本都发生在浏览器环境层面按正常方式重试一次就能解决。4.4 特征堆到0.85提交后大幅回落在验证集上反复调参的循环现象为了把分数刷高不断加新特征最后本地val_acc到了0.85提交后只有0.77甚至低于之前简单版本。然后继续加特征分数继续恶化。原因这是典型的验证集过拟合。你反复用同一个验证集调参验证集已经变成了训练的一部分模型和参数会悄悄记住验证集上的偏好分数虚高。越复杂的特征组合越容易钻这种空子。解决快速砍回最简特征集只保留Sex、Pclass、Fare、Age然后再加FamilySize。每加一个特征必须在五折交叉验证的std维度观察不能只看平均分上升。如果某个特征让std大幅上升即使平均分提高了0.5个百分点也建议放弃。稳定比极端分数重要得多。5. 提交前最后一步k折重放与“自评分数”可信度检查5.1 重放脚本换多个随机种子看分数波动交叉验证只跑一次random_state42还不够因为切分随机性可能被你运气好地选到一个好分布。一个更可靠的检查是固定模型参数换多个随机种子重放五折交叉验证把每组得分列出来。import numpy as np from sklearn.model_selection import StratifiedKFold from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score seeds [42, 2024, 1024] for seed in seeds: skf StratifiedKFold(n_splits5, shuffleTrue, random_stateseed) fold_scores [] for tr_idx, va_idx in skf.split(train_feat, train[Survived]): lr LogisticRegression(C1.0, max_iter1000, random_state42) lr.fit(train_feat.iloc[tr_idx], train[Survived].iloc[tr_idx]) pred lr.predict(train_feat.iloc[va_idx]) fold_scores.append(accuracy_score(train[Survived].iloc[va_idx], pred)) print(fseed{seed}: folds{[round(s, 4) for s in fold_scores]}, fmean{np.mean(fold_scores):.4f}, std{np.std(fold_scores):.4f})逻辑说明三组随机种子对应三种不同的数据切分方式如果模型真的学到了泛化规律分数应该稳定在某个小区间里。如果seed42时均分0.79seed1024时均分只有0.73说明模型对切分过于敏感泛化能力存疑。这个时候回到特征选择找出哪些特征在带动不稳定。参数说明外层seed控制切分内层模型random_state固定为42是为了保持模型初始化一致所有波动都来自数据切分差异。打印每组folds的长列表是必要的只看mean会发现不了某些fold的极端情况。5.2 用波动区间选参数不要选最高分选最稳的这个思路不仅适用于最终验证也适用于参数选择。泰坦尼克号这种小数据赛题public leaderboard的分数和private leaderboard的分数经常差1到2个百分点你本地追求的最高分很可能只是样本噪声的产物。小型止止损的办法是在所有参数候选中先筛掉std大于0.02或低于0.03的配置再从剩余配置里选平均分最高的。如果所有候选的std都很大说明问题出在特征和数据质量层面先把参数调到通常范围然后回头精简特征。从那以后每次提交前我都强制自己走一遍这个流程先固定参数跑三个seed看波动分数区间收敛了才生成提交文件。卡在0.77分上不去时不急着加特征而是先检查是否某个无关特征把分数波动拉大了。这个习惯帮我避免了很多次“本地惊喜、排行榜惊吓”的情况。希望帮到你。本文还有配套的精品资源点击获取