从数据清洗到模型优化:泰坦尼克号生存预测的完整机器学习流程
1. 从零开始:为什么泰坦尼克号是机器学习的“Hello World”
如果你刚接触机器学习,想找个项目练手,但又怕数据太复杂、理论太深奥,那泰坦尼克号生存预测绝对是你的不二之选。这个项目在Kaggle上被奉为经典,地位堪比编程界的“Hello World”。我第一次接触它的时候,感觉就像拿到了一份精心设计的“闯关地图”,每一步都清晰可见,但又充满了探索的乐趣。
这个项目的魅力在于,它用一个真实的历史事件,把机器学习的核心流程——数据清洗、特征工程、模型训练和优化——完整地串联了起来。数据量不大(训练集891条,测试集418条),特征也相对直观(年龄、性别、船票等级等),但里面藏着不少“坑”。比如,年龄有近20%的缺失,船舱号缺失率高达77%,这些现实问题逼着你去思考:数据不完美怎么办?怎么从有限的信息里“榨”出更多有用的特征?
我见过很多新手一上来就直奔模型,调参调得不亦乐乎,结果准确率死活上不去。其实,在机器学习里,数据和特征的质量往往比模型本身更重要。泰坦尼克号项目就是一个绝佳的课堂,它教会你,模型的表现很大程度上在你动手清洗数据、分析特征的那一刻就已经决定了。接下来,我就带你走一遍我实战过无数次的完整流程,分享一些我踩过的坑和总结出的实用技巧,保证你跟着做一遍,就能对机器学习项目有个扎实的入门理解。
2. 数据清洗:给数据做个“深度体检”
拿到数据后的第一步,千万别急着跑模型。你得像医生一样,先给数据做个全面的“体检”,看看它有没有“生病”。这个“病”就是数据质量问题,最常见的就是缺失值、异常值和格式不一致。
2.1 认识你的数据:读入与初步探索
我们用的工具主要是Python里的Pandas和NumPy。首先把训练集(train.csv)和测试集(test.csv)读进来。
import pandas as pd
import numpy as np
train = pd.read_csv('train.csv')
test = pd.read_csv('test.csv')
# 先看看数据长什么样,有哪些列
print(train.info())
print(train.head())
运行train.info(),你会立刻发现三个“病号”:Age(年龄)、Cabin(船舱)和Embarked(登船港口)存在缺失值。Age缺了大约20%,Cabin缺了惊人的77%,Embarked只缺了2个。不同的缺失率,处理策略也完全不同,这就是我们接下来要做的。
2.2 处理缺失值:因地制宜的“修补术”
对于Age(缺失率~20%):直接删除缺失行会损失不少样本,不太划算。常见的做法是用一个合理的值去填充。我试过好几种方法:用全体乘客年龄的平均值、用同性别同舱位乘客年龄的中位数、甚至用更复杂的模型(如随机森林)来预测缺失年龄。对于新手,我建议先用平均值填充,简单有效。但如果你想追求更高精度,可以试试分组中位数填充,比如按Pclass(舱位等级)和Sex(性别)分组后取中位数,这样更符合现实逻辑(不同群体年龄分布不同)。
# 方法1:简单平均值填充
train['Age'].fillna(train['Age'].mean(), inplace=True)
# 方法2:按Pclass和Sex分组后的中位数填充(更精细)
train['Age'] = train.groupby(['Pclass', 'Sex'])['Age'].apply(lambda x: x.fillna(x.median()))
对于Cabin(缺失率~77%):缺失率太高了,直接作为特征加入模型,很可能引入大量噪声,导致模型学偏。我的做法是,先看看这个特征有没有“剩余价值”。仔细观察Cabin的值(如‘C85’),第一个字母其实代表了甲板区域,这可能与乘客的社会地位和逃生路径有关。所以,我们可以从非缺失的Cabin值中提取甲板编号(Deck)作为一个新特征,然后将缺失值单独归为一类(如‘N’)。如果觉得麻烦,初期也可以直接舍弃这个特征,等基础模型跑通后再考虑优化。
# 提取甲板信息,并处理缺失值
train['Deck'] = train['Cabin'].apply(lambda x: x[0] if pd.notnull(x) else 'N')
# 之后可以对‘Deck’进行独热编码
对于Embarked(缺失率仅0.2%):只缺了2条记录,处理起来最简单。我们可以看看Embarked的分布,哪个港口登船人数最多。用train['Embarked'].value_counts()一看,是‘S’(南安普顿)。直接用众数‘S’填充即可。
train['Embarked'].fillna('S', inplace=True)
处理完缺失值,还要留意异常值。比如Fare(船票价格),可以用箱线图看看有没有极端高价票。通常对于机器学习模型,异常值会影响一些对尺度敏感的模型(如线性回归、KNN),我们可以考虑对其进行对数变换(np.log1p)来平滑数据,减少极端值的影响。
3. 特征工程:从原始数据中“炼金”
数据清洗只是让数据变“干净”,特征工程才是让数据变“聪明”的关键。它的目标是把原始数据转换成模型更能理解、更能挖掘出规律的形式。在泰坦尼克号数据里,藏着不少可以“创造”的特征。
3.1 创造新特征:组合与挖掘
-
家庭规模(FamilySize):数据集里有
SibSp(兄弟姐妹/配偶数)和Parch(父母/子女数)。这两者本质上都代表了家庭成员数量。我习惯把它们加在一起,再加上乘客自己,创造出FamilySize特征。这比单独看两个特征更有意义。train['FamilySize'] = train['SibSp'] + train['Parch'] + 1更进一步,你还可以根据家庭规模大小,创造一个新特征
IsAlone(是否独自一人),因为独自一人的乘客可能在逃生时缺乏互助。train['IsAlone'] = 1 # 默认独自一人 train['IsAlone'].loc[train['FamilySize'] > 1] = 0 # 家庭规模大于1,则不是独自一人 -
称呼(Title):
Name字段看起来杂乱,但里面包含了宝贵的社交等级信息。每个名字里都有称呼,如“Mr.”, “Mrs.”, “Miss”, “Master”(对未成年男孩的尊称),还有罕见的“Dr.”, “Rev.”, “Countess”等。这些称呼隐含了年龄、性别、婚姻状况甚至社会地位。提取它们能显著提升模型效果。train['Title'] = train['Name'].apply(lambda x: x.split(',')[1].split('.')[0].strip()) # 查看称呼分布 print(train['Title'].value_counts()) # 可以将少见的称呼归为‘Rare’一类 -
票价分组(FareBand):
Fare是连续值,有时将其离散化(分桶)能帮助线性模型更好地捕捉非线性关系。我们可以用pd.qcut(按分位数切分)将票价分成几个区间。train['FareBand'] = pd.qcut(train['Fare'], 4) # 分成4个数量相等的区间 # 然后可以基于FareBand进行标签编码或独热编码
3.2 编码分类变量:让模型“读懂”文字
机器学习模型本质是数学运算,无法直接处理“male”、“female”这样的文字。所以我们需要把分类变量(离散的、没有顺序的文字或数字)转换成数值。
- 标签编码(Label Encoding):适用于只有两个类别的特征,比如
Sex。直接映射为0/1。train['Sex'] = train['Sex'].map({'male': 0, 'female': 1}) - 独热编码(One-Hot Encoding):适用于类别数大于2且没有内在顺序的特征,比如
Embarked(S, C, Q)和从Pclass、Title、Deck等衍生出的特征。独热编码会为每个类别创建一个新的二进制列(0或1)。
注意,如果某个特征类别很多,独热编码会导致特征维度急剧膨胀(“维度灾难”),此时可以考虑使用目标编码(Target Encoding),即用该类别的目标变量(这里是Survived)的平均值来编码,但要小心过拟合。embarked_dummies = pd.get_dummies(train['Embarked'], prefix='Embarked') train = pd.concat([train, embarked_dummies], axis=1) train.drop(['Embarked'], axis=1, inplace=True) # 删除原始的Embarked列
做完这些,记得把那些对预测明显没帮助的列删掉,比如PassengerId、Ticket(票号,信息已由Fare和Pclass覆盖)、以及原始的Name、Cabin等。用train.info()检查一下,你的数据现在应该全是数值了,准备迎接模型的训练。
4. 模型训练与初步评估:找到第一个“基准线”
特征准备好了,我们终于可以开始训练模型了。但别一上来就搞复杂的集成模型,先建立一个简单可靠的基准模型(Baseline Model)。它的作用不是追求最高分,而是给你一个性能参照点,告诉你后续的优化到底有没有用。
4.1 划分数据集与基准模型
首先,要把处理好的训练集(带标签Survived)分成两部分:一部分用于训练模型,另一部分用于在提交Kaggle之前验证模型效果,这叫做训练-验证集分割。
from sklearn.model_selection import train_test_split
# 假设X是特征数据,y是标签(Survived)
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)
# 用80%的数据训练,20%的数据做验证,random_state保证每次分割结果一致
然后,选择一个简单、解释性强的模型作为起点。逻辑回归(Logistic Regression) 是完美的选择。它速度快,能直接给出特征重要性(系数),让你理解哪些特征在起作用。
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
baseline_model = LogisticRegression(max_iter=1000) # 增加迭代次数确保收敛
baseline_model.fit(X_train, y_train)
# 在验证集上预测并评估
y_val_pred = baseline_model.predict(X_val)
baseline_accuracy = accuracy_score(y_val, y_val_pred)
print(f"逻辑回归基准模型在验证集上的准确率: {baseline_accuracy:.4f}")
我最初跑的时候,这个基准准确率大概在0.78-0.82之间。这个数字就是你的起跑线。
4.2 尝试不同的基础模型
有了基准,我们可以试试其他几种基础模型,看看数据更适合哪种“口味”。这里可以用Scikit-learn的模型快速实验。
-
决策树(Decision Tree):非常直观,能生成清晰的“如果-那么”规则树。但容易过拟合(在训练集上表现太好,在未知数据上表现差)。
from sklearn.tree import DecisionTreeClassifier dt_model = DecisionTreeClassifier(random_state=42) dt_model.fit(X_train, y_train) # 评估... -
随机森林(Random Forest):决策树的升级版,通过构建大量树并综合它们的投票结果,能有效降低过拟合,通常比单棵决策树更强大、更稳定。
from sklearn.ensemble import RandomForestClassifier rf_model = RandomForestClassifier(n_estimators=100, random_state=42) rf_model.fit(X_train, y_train) # 评估... -
支持向量机(SVM):在特征维度不是特别高的情况下,有时能表现出很好的性能,但对数据缩放(标准化)比较敏感。
from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler # 先标准化数据 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_val_scaled = scaler.transform(X_val) svm_model = SVC(kernel='rbf', random_state=42) svm_model.fit(X_train_scaled, y_train) # 评估...
把这些模型在同一个验证集上跑一遍,记录下它们的准确率。你会发现,可能随机森林或梯度提升树(如XGBoost)初期表现更好。但别急着下结论,因为我们都还没调参呢。
5. 模型优化与集成:从“不错”到“更好”
当几个基础模型都跑出结果后,你可能会发现单个模型的性能似乎遇到了瓶颈。这时候,优化和集成技术就该上场了。
5.1 超参数调优:给模型“微调”
每个模型都有一堆“旋钮”(超参数),比如随机森林里树的棵数(n_estimators)、树的最大深度(max_depth)。手动调参效率太低,我们使用网格搜索(Grid Search) 或随机搜索(Random Search) 来自动寻找最佳组合。GridSearchCV会遍历你给定的所有参数组合,并用交叉验证评估每一组的效果。
from sklearn.model_selection import GridSearchCV
# 定义随机森林的参数网格
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [5, 10, 15, None],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 4]
}
rf = RandomForestClassifier(random_state=42)
grid_search = GridSearchCV(estimator=rf, param_grid=param_grid,
cv=5, scoring='accuracy', n_jobs=-1, verbose=1)
grid_search.fit(X_train, y_train)
print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳交叉验证分数: {grid_search.best_score_:.4f}")
# 用最佳参数重新训练模型
best_rf_model = grid_search.best_estimator_
交叉验证(cv=5)在这里至关重要,它把训练数据分成5份,轮流用4份训练、1份验证,能更稳健地评估模型性能,防止因为某一次数据分割的运气好而得到过于乐观的结果。
5.2 集成学习:团结就是力量
如果调参后的单个模型提升有限,可以试试集成方法。核心思想是“三个臭皮匠,顶个诸葛亮”。
-
投票法(Voting):这是我们最初级、最直观的尝试。比如,让逻辑回归、调优后的随机森林和SVM这三个模型同时对测试样本进行预测,然后采用“少数服从多数”的原则(硬投票)决定最终结果。Scikit-learn提供了
VotingClassifier来方便实现。from sklearn.ensemble import VotingClassifier voting_clf = VotingClassifier( estimators=[('lr', best_lr_model), ('rf', best_rf_model), ('svm', best_svm_model)], voting='hard' # 硬投票 ) voting_clf.fit(X_train, y_train) -
堆叠法(Stacking):这是更高级的集成策略。我们不是简单投票,而是用一组基础模型(第一层)的预测结果作为新特征,再训练一个元模型(第二层) 来做最终决策。这相当于让元模型去学习如何权衡各个基础模型的判断。这通常能获得比投票法更好的性能,但实现稍复杂,也要小心过拟合。
经过集成,你的模型在Kaggle私有排行榜上的分数(注意,要提交到测试集上看)很可能从0.77-0.78提升到0.79甚至更高。别小看这0.01、0.02的提升,在Kaggle比赛中,这常常就是几百个名次的差距。
6. 高级特征工程与思考:突破天花板
当模型性能似乎停滞不前时,你需要回过头再次审视数据和特征。很多时候,瓶颈不在于模型,而在于特征的信息含量不够。
- 交互特征:考虑特征之间的组合。例如,
Age和Pclass的交互可能很重要(头等舱的儿童是否更受保护?)。可以创建Age*Pclass这样的特征。 - 更精细的分箱:对于
Age和Fare,除了简单分桶,可以尝试基于业务理解进行分箱。比如将Age分为“儿童”、“青年”、“中年”、“老年”,将Fare分为“极低票价”、“普通票价”、“高价票”、“奢华票”。 - 利用外部知识:泰坦尼克号的历史资料显示,救生艇的部署有先后顺序,不同甲板(
Deck)的乘客到达甲板的难度不同。如果我们能从Cabin中更精确地推断出乘客所在的甲板区域(甚至侧面),并将其与逃生路线联系起来,可能会创造出一个强特征。 - 模型的特征重要性分析:训练好的随机森林或梯度提升模型可以直接输出特征重要性排名。仔细查看哪些特征最重要,哪些几乎没用。对于不重要的特征,可以考虑剔除;对于重要特征,思考能否进一步细化或从中衍生出新的特征。
我在一次优化中,通过深入挖掘Title特征(将‘Mlle’、‘Ms’归入‘Miss’,将‘Mme’归入‘Mrs’,将稀有头衔归为一类),并结合FamilySize创建了IsMother(是否是带孩子的母亲)特征,使得模型性能得到了可见的提升。这个过程没有定式,需要你不断地假设、验证、分析。
7. 复盘与总结:比分数更重要的收获
走完这一整套流程,无论你的最终分数是多少,收获都远不止一个Kaggle排名。你亲身体验了一个机器学习项目的标准生命周期:从理解问题和数据开始,到数据清洗和探索性数据分析(EDA),再到特征工程的创意发挥,接着是模型选择、训练和调参的反复迭代,最后可能用到模型集成来冲刺。
更重要的是,你学会了如何诊断问题:当准确率不高时,是数据问题(缺失值处理不当、异常值干扰)?还是特征问题(信息量不足、编码方式不对)?或者是模型问题(过拟合、欠拟合、参数没调好)?这种系统性的问题解决能力,才是你从“泰坦尼克号”这个入门项目里带走的、可以应用到任何其他机器学习项目上的宝贵财富。
最后,别忘了把整个过程用代码和文档清晰地记录下来。一个好的Jupyter Notebook不仅记录了你的分析步骤和结果,还记录了你的思考过程。这既是给你自己的复盘资料,也是未来向他人展示你项目能力的绝佳作品。机器学习之路很长,但像这样扎扎实实地完成一个又一个项目,就是最有效的成长方式。
更多推荐
所有评论(0)