ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Python泰坦尼克号生存预测:从数据清洗到模型部署的完整实战

Python泰坦尼克号生存预测:从数据清洗到模型部署的完整实战 简介这份资源是面向数据科学初学者与Kaggle竞赛入门者的Python实战项目包以经典的泰坦尼克号生存预测为案例帮助读者打通从数据加载、探索性分析到模型训练与评估的完整流程。压缩包共4个文件包含2个csv数据集、1个py实验代码和1份doc实验报告整体约2.35MB其中csv用于训练与测试建模py脚本承载核心分析逻辑doc则记录实验过程与结论。目前已有5207人学习下载热度较高。项目覆盖Pandas数据清洗与缺失值处理、Numpy数值统计、Matplotlib与Seaborn可视化并延伸至特征工程、逻辑回归与随机森林等模型构建、K折交叉验证及网格搜索调优实验报告还总结了模型评估与结果解释。对于希望系统掌握Python数据分析与机器学习工作流的读者这是一份结构完整、可直接复现的学习范例。1. 泰坦尼克号生存预测从一份 zip 到能跑通的基线模型很多人第一次接触 Python 数据分析都是从一份叫python泰坦尼克号.zip的压缩包开始的。解压之后通常是一个 CSV 文件里面是 891 行乘客记录字段包括姓名、性别、年龄、舱位等级、票价、登船港口以及最关键的Survived标签。这个数据集之所以经典是因为它足够小、字段足够杂、缺失值足够多几乎把真实业务里数据清洗的坑都踩了一遍。你拿它练手练的不是模型有多深而是从读文件、看缺失、做特征、跑交叉验证这一整条链路能不能走通。适合刚装完 Python、配好 VSCode 环境、想找一个完整项目把语法和库串起来的人也适合想快速验证某个建模思路的老手。下面我按自己实际做这个项目的顺序把每一步拆开讲。2. 先把数据读进来pandas 读取与第一轮体检2.1 为什么第一件事不是建模而是看数据拿到python泰坦尼克号.zip解压后的train.csv很多人会直接pd.read_csv然后df.head()就冲去建模。我一般会先做三件事看形状、看类型、看缺失。因为泰坦尼克号这个数据集里Age大约缺 20%Cabin缺 77%Embarked只缺 2 条。缺失比例不同处理策略完全不同。Cabin缺得太多直接删列比硬填更合理Age缺两成可以用中位数或分组中位数填Embarked只缺两条按众数填掉就行。这一步不做后面模型报错或者分数虚高你都不知道问题出在哪。import pandas as pd import numpy as np # 读取解压后的训练集注意路径按你实际解压位置改 df pd.read_csv(train.csv) # 第一轮体检形状、字段类型、缺失情况 print(数据形状:, df.shape) print(\n字段类型:\n, df.dtypes) print(\n缺失值统计:\n, df.isnull().sum().sort_values(ascendingFalse))这段代码跑完你会看到PassengerId、Survived、Pclass、Age、SibSp、Parch、Fare是数值型Name、Sex、Ticket、Cabin、Embarked是字符串。缺失值排序后Cabin、Age、Embarked排前面。参数上没什么可调的read_csv默认逗号分隔如果解压出来是别的编码加encodingutf-8或encodinglatin-1试一下。这一步的输出就是你后面所有决策的依据。2.2 用 describe 和 value_counts 摸清分布看完缺失接着看数值分布和类别分布。describe()能告诉你Fare最小值是 0最大值 512均值 32说明票价严重右偏后面可能要做对数变换。value_counts()看Survived和Sex你会发现女性生存率明显高于男性这是这个数据集最强的信号之一。不做这一步你可能会把Sex当成普通类别随便编码丢掉一个关键特征。# 数值字段分布 print(df.describe()) # 关键类别字段的分布与生存率关系 print(\n生存分布:\n, df[Survived].value_counts(normalizeTrue)) print(\n性别与生存:\n, df.groupby(Sex)[Survived].mean()) print(\n舱位与生存:\n, df.groupby(Pclass)[Survived].mean())groupby后接mean()算的是生存率因为Survived是 0/1。这里能看到女性生存率约 74%男性约 19%一等舱生存率约 63%三等舱约 24%。这些数字直接告诉你Sex和Pclass必须保留而且后面做特征工程时可以考虑组合。参数上normalizeTrue让value_counts返回比例而不是计数方便快速判断类别是否平衡。3. 特征工程把姓名、票号和舱位变成模型能吃的数字3.1 从 Name 里抠出称呼从 Ticket 里看出舱位Name字段看起来是纯文本但里面藏着称呼比如Mr.、Mrs.、Miss.、Master.。这些称呼对应不同的性别、年龄和社會角色生存率差异很大。Master.通常是小男孩生存率偏高Mr.成年男性生存率最低。用正则把称呼抽出来比直接用整个姓名有用得多。Ticket字段前缀不统一有的带字母有的纯数字纯数字票号往往对应三等舱带字母的可能是一等或二等。我一般只取前缀没有前缀就标为NUM。import re # 从 Name 中提取称呼 df[Title] df[Name].apply(lambda x: re.search(r ([A-Za-z])\., x).group(1)) # 合并稀有称呼避免类别过多 rare_titles [Lady, Countess, Capt, Col, Don, Dr, Major, Rev, Sir, Jonkheer, Dona] df[Title] df[Title].replace(rare_titles, Rare) df[Title] df[Title].replace({Mlle: Miss, Ms: Miss, Mme: Mrs}) # Ticket 前缀提取 df[TicketPrefix] df[Ticket].apply( lambda x: NUM if x.isdigit() else re.sub(r[^A-Za-z], , x).upper()[:3] ) print(df[Title].value_counts()) print(df[TicketPrefix].value_counts().head(10))正则r ([A-Za-z])\.匹配空格后一串字母再加点group(1)取括号里的内容。rare_titles合并是因为有些称呼只出现一两次模型学不到东西还增加维度。TicketPrefix里re.sub(r[^A-Za-z], , x)把非字母字符去掉再取前三个字母避免前缀太细碎。这一步做完Title和TicketPrefix就可以参与建模了。3.2 年龄和票价的缺失填充与分箱Age缺失 177 条直接填全局中位数会忽略Pclass和Sex的差异。我一般按Pclass和Sex分组用组内中位数填。Fare只有 1 条缺失按Pclass中位数填即可。填完之后Age和Fare都是连续值树模型可以直接吃但线性模型可能受益于分箱。我通常会额外做一列AgeBin把年龄分成儿童、青年、中年、老年方便观察非线性关系。# 按 Pclass 和 Sex 分组填充 Age df[Age] df.groupby([Pclass, Sex])[Age].transform( lambda x: x.fillna(x.median()) ) # Fare 按 Pclass 中位数填充 df[Fare] df.groupby(Pclass)[Fare].transform( lambda x: x.fillna(x.median()) ) # 年龄分箱 df[AgeBin] pd.cut( df[Age], bins[0, 12, 18, 35, 60, 100], labels[Child, Teen, YoungAdult, Adult, Senior] ) print(df[[Age, AgeBin]].head()) print(剩余缺失:\n, df[[Age, Fare]].isnull().sum())transform和apply的区别在于transform返回和原列等长的结果适合填充。pd.cut的bins是左开右闭区间labels对应每个区间。分箱边界不是固定的你可以根据业务理解调整比如把 12 岁改成 14 岁。填完之后一定要再查一次缺失确认没有漏网之鱼。3.3 类别编码独热还是序数取决于模型Sex、Embarked、Title、AgeBin都是类别变量。树模型随机森林、XGBoost对序数编码不敏感可以直接用LabelEncoder转成整数。线性模型逻辑回归更适合独热编码避免人为引入大小关系。我一般会准备两套一套给树模型用序数一套给线性模型用独热。Pclass本身有序保留整数即可。from sklearn.preprocessing import LabelEncoder # 树模型用的序数编码 df_tree df.copy() for col in [Sex, Embarked, Title, AgeBin, TicketPrefix]: df_tree[col] LabelEncoder().fit_transform(df_tree[col].astype(str)) # 线性模型用的独热编码 df_linear pd.get_dummies( df, columns[Sex, Embarked, Title, AgeBin, TicketPrefix], drop_firstTrue ) print(树模型字段:, df_tree.columns.tolist()) print(线性模型字段数:, df_linear.shape[1])LabelEncoder按字母顺序编码astype(str)是为了防止AgeBin的 category 类型报错。pd.get_dummies的drop_firstTrue去掉第一列避免多重共线性。注意独热之后字段数会膨胀如果TicketPrefix类别太多可以先合并低频类别再编码。4. 建模与验证逻辑回归、随机森林和交叉验证怎么选4.1 先跑一个逻辑回归基线别一上来就上集成很多人一上来就 XGBoost结果调参调到怀疑人生。我一般先用逻辑回归跑一个基线因为它的系数可以直接解释能告诉你哪些特征重要。基线分数出来之后再上随机森林看提升空间。逻辑回归之前要做标准化因为Fare和Age量纲差很多。用Pipeline把标准化和模型串起来交叉验证时不会数据泄漏。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.model_selection import cross_val_score # 准备特征和标签 feature_cols [Pclass, Sex, Age, SibSp, Parch, Fare, Embarked, Title, AgeBin, TicketPrefix] X df_tree[feature_cols] y df_tree[Survived] # 逻辑回归管道 lr_pipe Pipeline([ (scaler, StandardScaler()), (lr, LogisticRegression(max_iter1000, random_state42)) ]) # 5 折交叉验证 lr_scores cross_val_score(lr_pipe, X, y, cv5, scoringaccuracy) print(逻辑回归 CV 准确率:, lr_scores.mean().round(4), ±, lr_scores.std().round(4))max_iter1000是因为默认 100 可能不收敛。cross_val_score的cv5表示 5 折scoringaccuracy是分类准确率。基线一般在 0.78 到 0.82 之间。如果低于 0.75检查一下特征里有没有把Survived漏进去或者编码时把标签也编了。4.2 随机森林和特征重要性逻辑回归跑通后换随机森林。随机森林不需要标准化对缺失和异常值也更鲁棒。跑完之后看feature_importances_能验证你前面特征工程有没有白做。如果Title和AgeBin重要性排前面说明抠称呼和分箱是值得的。from sklearn.ensemble import RandomForestClassifier rf RandomForestClassifier( n_estimators300, max_depth6, min_samples_split4, random_state42 ) rf_scores cross_val_score(rf, X, y, cv5, scoringaccuracy) print(随机森林 CV 准确率:, rf_scores.mean().round(4), ±, rf_scores.std().round(4)) # 特征重要性 rf.fit(X, y) importance pd.Series(rf.feature_importances_, indexfeature_cols).sort_values(ascendingFalse) print(\n特征重要性:\n, importance)n_estimators300是树的数量太少不稳定太多训练慢。max_depth6控制树深防止过拟合。min_samples_split4表示节点至少 4 个样本才分裂。这三个参数是我在泰坦尼克号上比较常用的起点。特征重要性里如果TicketPrefix排很低可以考虑删掉减少噪声。4.3 用网格搜索找一组靠谱参数随机森林的默认参数通常不是最优的但也不用搜太大范围。我一般只搜max_depth、min_samples_split和n_estimators三个范围控制在 3 到 8、2 到 10、100 到 500。搜太大容易过拟合交叉验证而且耗时。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 300, 500], max_depth: [4, 6, 8], min_samples_split: [2, 4, 6] } grid GridSearchCV( RandomForestClassifier(random_state42), param_grid, cv5, scoringaccuracy, n_jobs-1 ) grid.fit(X, y) print(最佳参数:, grid.best_params_) print(最佳 CV 分数:, grid.best_score_.round(4))n_jobs-1用满所有 CPU 核心。best_params_出来之后用这组参数重新训练一个模型在测试集上验证。注意网格搜索的分数通常比单次交叉验证略高因为它是从多组里挑最好的存在选择偏差。最终评估要用独立的测试集。5. 避坑与排查泰坦尼克号项目里最容易翻车的 5 个地方5.1 把 PassengerId 当特征分数虚高现象交叉验证准确率 0.95 以上提交到测试集却掉到 0.7。原因PassengerId是顺序编号和生存标签没有真实关系但模型可能记住某些编号段。解决建模前直接drop掉PassengerId、Name、Ticket这些原始字段只保留衍生特征。5.2 先填充再划分训练测试造成数据泄漏现象本地交叉验证分数很高换一份数据就崩。原因用全量数据算中位数填缺失测试集的信息漏进了训练集。解决先train_test_split再在训练集上算填充值用transform应用到测试集。或者用Pipeline把填充步骤包进去交叉验证时自动隔离。5.3 AgeBin 分箱后出现空类别编码报错现象LabelEncoder报ValueError: y contains previously unseen labels。原因训练集里AgeBin有 5 个类别测试集里只有 4 个编码器没见过。解决用pd.cut时固定bins和labels确保两边类别一致或者用OneHotEncoder(handle_unknownignore)。5.4 Fare 的 0 值没处理逻辑回归系数异常现象逻辑回归里Fare系数为负且很大不符合直觉。原因Fare有少量 0 值标准化后变成极端负值影响系数。解决把Fare0当作缺失按Pclass中位数填或者加一个FareIsZero标志列。5.5 交叉验证用了默认 KFold类别不平衡时分数波动大现象5 折分数从 0.72 到 0.88方差很大。原因默认KFold不保证每折里正负样本比例一致。解决分类任务用StratifiedKFold保持每折的类别比例和整体一致。from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) scores cross_val_score(rf, X, y, cvskf, scoringaccuracy) print(分层交叉验证:, scores.mean().round(4), ±, scores.std().round(4))StratifiedKFold的shuffleTrue打乱顺序random_state固定随机种子保证可复现。这一步做完分数波动会明显收窄。6. 把模型存下来再用它生成一份提交文件6.1 用 joblib 保存和加载模型训练好的模型不存下来下次用还得重跑。joblib比pickle更适合存 sklearn 模型因为对 numpy 数组优化过。存的时候把模型和特征列名一起存加载时才能对齐。import joblib # 保存模型和特征列 joblib.dump({model: rf, features: feature_cols}, titanic_rf.joblib) # 加载 saved joblib.load(titanic_rf.joblib) loaded_model saved[model] loaded_features saved[features] print(模型加载完成特征数:, len(loaded_features))joblib.dump第二个参数是文件名建议用.joblib后缀。存字典是为了把特征顺序也保留因为 sklearn 模型对列顺序敏感。加载后直接用loaded_model.predict(X[loaded_features])就能预测。6.2 对测试集做同样的特征工程测试集test.csv没有Survived列但其他字段和训练集一样。你需要把前面所有特征工程步骤在测试集上重跑一遍注意用训练集的填充值和编码器不能重新拟合。这一步最容易出错因为测试集的Title可能多出训练集没有的称呼。test_df pd.read_csv(test.csv) # 同样的 Title 提取 test_df[Title] test_df[Name].apply(lambda x: re.search(r ([A-Za-z])\., x).group(1)) test_df[Title] test_df[Title].replace(rare_titles, Rare) test_df[Title] test_df[Title].replace({Mlle: Miss, Ms: Miss, Mme: Mrs}) # 同样的 TicketPrefix test_df[TicketPrefix] test_df[Ticket].apply( lambda x: NUM if x.isdigit() else re.sub(r[^A-Za-z], , x).upper()[:3] ) # 用训练集的中位数填充 test_df[Age] test_df.groupby([Pclass, Sex])[Age].transform( lambda x: x.fillna(x.median()) ) test_df[Fare] test_df.groupby(Pclass)[Fare].transform( lambda x: x.fillna(x.median()) ) # 分箱 test_df[AgeBin] pd.cut( test_df[Age], bins[0, 12, 18, 35, 60, 100], labels[Child, Teen, YoungAdult, Adult, Senior] ) # 编码注意用训练集的编码器 for col in [Sex, Embarked, Title, AgeBin, TicketPrefix]: le LabelEncoder() le.fit(df[col].astype(str)) # 用训练集拟合 test_df[col] le.transform(test_df[col].astype(str))这里的关键是le.fit(df[col])用训练集拟合然后transform测试集。如果测试集出现训练集没有的类别LabelEncoder会报错这时候要么把稀有类别归到Rare要么用handle_unknown策略。填充也是同理用训练集的中位数不是测试集的。6.3 生成提交文件并检查格式最后把预测结果写成 CSV格式通常是两列PassengerId和Survived。写完之后打开看一眼确认行数对、没有缺失、标签只有 0 和 1。# 预测 X_test test_df[feature_cols] predictions loaded_model.predict(X_test) # 生成提交文件 submission pd.DataFrame({ PassengerId: test_df[PassengerId], Survived: predictions.astype(int) }) submission.to_csv(submission.csv, indexFalse) print(提交文件形状:, submission.shape) print(标签分布:\n, submission[Survived].value_counts()) print(submission.head())indexFalse避免多出一列索引。astype(int)确保标签是整数。检查shape应该是(418, 2)因为泰坦尼克号测试集有 418 行。如果行数不对检查test.csv读取时有没有漏行。6.4 一个我常用的提分小技巧把 Title 和 Pclass 交叉如果基线分数卡在 0.78 上不去可以试试把Title和Pclass组合成一个新特征。比如Mr_3表示三等舱成年男性Mrs_1表示一等舱已婚女性。这个组合特征在树模型里能直接捕捉交互效应比让模型自己学更高效。df[TitlePclass] df[Title].astype(str) _ df[Pclass].astype(str) test_df[TitlePclass] test_df[Title].astype(str) _ test_df[Pclass].astype(str) # 重新编码并加入特征列 le LabelEncoder() le.fit(df[TitlePclass]) df[TitlePclass] le.transform(df[TitlePclass]) test_df[TitlePclass] le.transform(test_df[TitlePclass]) feature_cols_v2 feature_cols [TitlePclass] X_v2 df[feature_cols_v2] scores_v2 cross_val_score(rf, X_v2, y, cvskf, scoringaccuracy) print(加入交叉特征后:, scores_v2.mean().round(4), ±, scores_v2.std().round(4))这个技巧不一定每次都涨分但值得试一次。如果涨了说明交互效应确实存在如果没涨甚至掉了说明树模型已经自己学到了不用硬加。我自己的习惯是每次做完特征工程都存一版模型和提交文件用日期命名这样翻车了还能回滚。泰坦尼克号这个项目我前后跑过十几遍最大的教训就是别急着调参先把缺失值和特征编码做干净分数自然就上来了。希望帮到你。本文还有配套的精品资源点击获取
返回列表