泰坦尼克号生存预测:从数据预处理到决策树模型构建

!

​编辑en.wikipedia.org

前言

泰坦尼克号沉船事件是20世纪最著名的海难之一。本次项目旨在利用机器学习方法,根据乘客的各种特征来预测他们在灾难中是否幸存。我们将从数据加载、缺失值处理开始,逐步构建一个决策树分类模型,并对其性能进行评估。

1. 数据加载与初始探索

首先,我们需要加载泰坦尼克号的训练数据集(train.csv)和测试数据集(test.csv)。

# _*_ coding: utf-8 _*_
'''
时间:      2025/6/16 17:20
@author:  andinm
'''
import pandas as pd
from sklearn import linear_model
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.pipeline import make_pipeline
import numpy as np
import joblib
​
def loadData():
    """
    加载训练和测试数据集。
    :return: dfTrain (DataFrame), dfTest (DataFrame)
    """
    dfTrain = pd.read_csv("Data/train.csv")
    dfTest = pd.read_csv("Data/test.csv")
    return dfTrain, dfTest
​
# 在 tatanic_1.py 的 __main__ 块中,我们可以看到数据的初步信息
if __name__ == "__main__":
    dfTrain, dfTest = loadData()
    print("训练集前5行数据:")
    print(dfTrain.head())
    print("\n训练集各列缺失值数量:")
    print(dfTrain.isna().sum())
    # ... (此处省略了tatanic_1.py中剩余的Age模型训练代码,将在后续部分讨论)

数据概览:

加载后,我们通常会检查数据集的结构和缺失值情况。以训练集为例:

列名 描述 缺失值数量 (train.csv)
PassengerId 乘客ID 0
Survived 是否幸存 (0 = 否, 1 = 是) 0
Pclass 船票等级 (1 = 一等舱, 2 = 二等舱, 3 = 三等舱) 0
Name 姓名 0
Sex 性别 0
Age 年龄 177
SibSp 同行的兄弟姐妹/配偶数 0
Parch 同行的父母/子女人数 0
Ticket 船票号 0
Fare 船票价 0
Cabin 船舱号 687
Embarked 登船港口 (C, Q, S) 2

从上述统计可见,Age (年龄) 和 Cabin (船舱号) 列存在大量缺失值。Embarked (登船港口) 也有少量缺失。

2. 缺失年龄值的填充(Ridge 回归模型)

Age 是一个重要的数值特征,为了避免丢失这些样本,我们决定使用其他相关特征(Pclass, Sex, SibSp, Parch, Fare)通过一个 Ridge 回归模型来预测并填充缺失的年龄值。

2.1 数据预处理函数 processData

这个函数负责为年龄预测模型准备数据,包括特征选择、性别编码和初步的缺失值填充(针对非Age的特征)。

# tatanic_1.py 中的 processData 函数
def processData(dataSet):
    '''
    数据预处理函数:用于提取特征,处理缺失值,并分离含有和不含Age的数据。
    :param dataSet: 传入的数据集 (DataFrame)
    :return: 返回XTrain (用于训练年龄模型特征), yTrain (用于训练年龄模型标签),
             XTest (用于预测缺失年龄的特征), trainIdx (非缺失年龄行的原始索引),
             testIdx (缺失年龄行的原始索引)
    '''
    featureName = ["Pclass", "Sex", "SibSp", "Parch", "Fare", "Age"]
    dataSetSub = dataSet[featureName].copy()
    dataSetSub["Sex"] = dataSetSub["Sex"].map({"male":0, "female":1})
​
    for feature in featureName[0:5]: # 填充非Age特征的缺失值
        if dataSetSub[feature].isna().any(): # isna()后要加括号
            dataSetSub[feature] = dataSetSub[feature].fillna(dataSetSub[feature].median())
​
    # 分割数据集:含有Age数据用于训练,不含Age数据用于预测
    dateTrain = dataSetSub[dataSetSub["Age"].notna()].copy()
    dateTest = dataSetSub[dataSetSub["Age"].isna()].copy()
​
    XTrain = dateTrain[["Pclass", "Sex", "SibSp", "Parch", "Fare"]]
    yTrain = dateTrain["Age"]
    XTest = dateTest[["Pclass", "Sex", "SibSp", "Parch", "Fare"]]
​
    trainIdx = dateTrain.index
    testIdx = dateTest.index
​
    return XTrain, yTrain, XTest, trainIdx, testIdx

2.2 Ridge 回归模型的训练与保存

tatanic_1.py 中,我们使用 GridSearchCV 结合 StandardScalerRidge 回归器来寻找最佳的 alpha 参数,并保存训练好的模型。

# tatanic_1.py 中的 __main__ 块
if __name__ == "__main__":
    dfTrain, dfTest = loadData()
    X, y, XPre, ComIdx, PreIdx = processData(dfTrain)
​
    XTrain_age, XTest_age, yTrain_age, yTest_age = train_test_split(X, y, test_size=0.2, random_state=42)
​
    ridge_pipe = make_pipeline(
        StandardScaler(),
        linear_model.Ridge()
    )
​
    param_test = {'ridge__alpha': np.linspace(13, 13.5, 51)} # 搜索alpha参数
​
    gsearch = GridSearchCV(
        estimator=ridge_pipe,
        param_grid=param_test,
        scoring="neg_mean_squared_error",
        cv=5,
        n_jobs=-1
    )
    gsearch.fit(XTrain_age, yTrain_age)
​
    print(f"最佳参数: {gsearch.best_params_}, 交叉验证MSE = {-gsearch.best_score_:.4f}")
    best_model = gsearch.best_estimator_
    y_test_pred_age = best_model.predict(XTest_age)
    test_mse_age = mean_squared_error(yTest_age, y_test_pred_age)
    print(f"测试集MSE: {test_mse_age:.4f}")
​
    joblib.dump(best_model, "tatanic_ridge_model_pipeline.pkl")

输出示例:

最佳参数: {'ridge__alpha': np.float64(13.48)}, 交叉验证MSE = 170.2656
测试集MSE: 139.3180

该模型会被保存为 tatanic_ridge_model_pipeline.pkl

2.3 填充缺失的年龄值

tatanic_2.py 中,我们加载这个预训练模型,并用其预测值来填充原始 dfTrain 中缺失的 Age 值。

# tatanic_2.py 中的部分代码
# ... (导入及加载数据部分与 tatanic_1.py 类似)
​
# 加载预训练的 Ridge 回归模型管道
model = joblib.load("tatanic_ridge_model_pipeline.pkl")
# 使用模型预测缺失的 Age 值
yPre = model.predict(XPre)
​
# 将预测的年龄根据对应的下标补全到整个 dfTrain 数据集中
# np.maximum(0, yPre) 确保预测年龄非负
dfTrain.loc[PreIdx, "Age"] = np.maximum(0, yPre)
print("补全年龄后 dfTrain 的缺失值情况:")
print(dfTrain.isna().sum()) # 检查 Age 列是否已无缺失

输出示例:

补全年龄后 dfTrain 的缺失值情况:
PassengerId      0
Survived         0
Pclass           0
Name             0
Sex              0
Age              0
SibSp            0
Parch            0
Ticket           0
Fare             0
Cabin          687
Embarked         2
dtype: int64

现在,Age 列已经完全没有缺失值了。

3. 准备分类数据:特征工程与选择

为了构建决策树分类模型来预测 Survived (是否幸存),我们需要进一步处理数据。根据项目要求,我们不考虑 CabinEmbarked 这两个特征,因为 Cabin 缺失值过多,而 Embarked 只有少量缺失。同时,像 Name, Ticket, PassengerId 这些标识性特征也不适用于直接作为模型输入。

3.1 性别特征编码

虽然 processData 函数中对 Sex 进行了编码,但由于我们这里是直接对原始 dfTrain 进行操作,需要再次确保 Sex 被转换为数值型。

# tatanic_2.py 中的部分代码
# ... (Age 填充后)
​
# 将 'Sex' 列的分类文本值("female", "male")转换为数值(1, 0)
dfTrain["Sex"] = dfTrain["Sex"].map({"female":1, "male":0})
​
# 准备用于决策树分类的特征和目标变量
featuresForClassification = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare']
targetForClassification = "Survived" # 目标变量
​
# 提取特征变量 X_clf 和目标变量 y_clf
X_clf = dfTrain[featuresForClassification].copy()
y_clf = dfTrain[targetForClassification].copy()
​
# 确保 'Fare' 列没有缺失值(虽然训练集中没有,但测试集可能有)
if X_clf['Fare'].isna().any():
     X_clf['Fare'] = X_clf['Fare'].fillna(X_clf['Fare'].median())
​
# 此时,所有用于决策树的特征都已是数值类型

至此,我们用于分类模型的特征 (Pclass, Sex, Age, SibSp, Parch, Fare) 都已经是数值类型,且没有缺失值。

4. 决策树模型的构建与超参数调优

接下来,我们将使用处理好的数据构建决策树分类器。为了找到最佳模型,我们将采用 GridSearchCV 进行超参数调优。

4.1 数据分割

在训练模型之前,将数据集划分为训练集和测试集是标准步骤,用于评估模型的泛化能力。

# tatanic_2.py 中的部分代码
# ... (特征准备后)
​
# 数据分割:将用于决策树的特征和目标变量划分为训练集和测试集
# test_size=0.2 表示20%的数据用于测试
# random_state=42 保证每次运行结果可复现
# stratify=y_clf 确保训练集和测试集中 'Survived' 类别的比例与原始数据集相同,这对于分类问题很重要
X_clf_train, X_clf_test ,y_clf_train, y_clf_test = train_test_split(
    X_clf, y_clf, test_size=0.2, random_state=42, stratify=y_clf
)
​
# 打印训练集和测试集中目标变量的类别分布(用于验证分层抽样效果)
# print(y_clf_train.value_counts()/len(y_clf_train))
# print(y_clf_test.value_counts()/len(y_clf_test))
# 输出类似:
# Survived
# 0    0.616573
# 1    0.383427
# ...

4.2 决策树初始化与参数定义

  • DecisionTreeClassifier(random_state=42): 初始化决策树模型。random_state 的作用是确保每次运行代码时,决策树在处理内部随机性(如特征选择顺序)时能得到一致的结果,从而保证模型的可复现性。

  • max_depth: 树的最大深度。

    • None:表示不限制深度,树可以尽可能地分裂,直到每个叶子节点只包含一个类别,或者直到 min_samples_leaf 被满足。这可能导致过拟合。

    • 较小的整数值(如 5, 7, 10):限制树的深度,有助于防止过拟合,使模型更简单、泛化能力更强。

  • min_samples_leaf: 叶节点(决策树的最终节点)必须包含的最小样本数量。

    • 1 (默认值):允许叶节点只包含一个样本,容易导致过拟合。

    • 较大的整数值(如 5, 10, 20):强制叶节点包含更多样本,使决策更加稳健,有助于防止过拟合,但可能导致欠拟合。

# tatanic_2.py 中的部分代码
# ... (数据分割后)

# 初始化决策树分类器,设置 random_state 保证结果可复现
treeModel = DecisionTreeClassifier(random_state=42)

# 定义决策树的网格参数,用于 GridSearchCV 搜索最佳超参数
parame_grid_tree = {
    "max_depth": [None, 5, 7, 10, 12], # 树的最大深度,None表示不限制
    "min_samples_leaf": [1, 2, 3, 5, 7, 10],   # 叶子节点(决策树末端节点)的最小样本数
}

# 使用 GridSearchCV 进行决策树超参数调优
# estimator: 要优化的模型 (treeModel)
# param_grid: 参数网格 (parame_grid_tree)
# cv=5: 5折交叉验证
# scoring='accuracy': 使用准确率作为评估指标
# n_jobs=-1: 使用所有可用CPU核心进行并行计算
grid_search_tree = GridSearchCV(
    estimator=treeModel,
    param_grid=parame_grid_tree,
    cv=5,
    scoring='accuracy',
    n_jobs=-1
)

# 在训练数据上拟合 GridSearchCV,执行网格搜索和交叉验证
grid_search_tree.fit(X_clf_train, y_clf_train)

# 打印决策树的最佳参数组合
print(f"\n决策树的最佳参数: {grid_search_tree.best_params_}")
# 打印决策树在交叉验证中的最佳准确率
print(f"\n决策树最佳交叉验证的准确率: {grid_search_tree.best_score_}")

5. 模型评估与分类报告解读

模型训练完成后,我们需要在独立的测试集上评估其性能。Scikit-learn 的 classification_report 提供了一个详细的评估报告。

# tatanic_2.py 中的部分代码
# ... (GridSearchCV 拟合后)

# 获取最佳的决策树模型
bestTreeModel = grid_search_tree.best_estimator_
# 使用最佳模型在测试集上进行预测
y_clf_test_pre = bestTreeModel.predict(X_clf_test)
# 打印决策树在测试集上的准确率
print(f"\n决策树在测试集上面的准确率{accuracy_score(y_clf_test, y_clf_test_pre): 0.4f}")
# 打印决策树的分类报告
# 注意:classification_report 的第一个参数是真实标签 (y_true),第二个参数是预测标签 (y_pred)
print(f"\n决策树分类报告: ")
print(classification_report(y_clf_test, y_clf_test_pre))

分类报告解读

以下是两次运行的输出结果示例及其详细解释:

第一次运行示例输出:

决策树的最佳参数: {'max_depth': 5, 'min_samples_leaf': 5}
决策树最佳交叉验证的准确率: 0.8090416625627892
决策树在测试集上面的准确率 0.7821
决策树分类报告: 
              precision    recall  f1-score   support

           0       0.85      0.80      0.83       117
           1       0.67      0.74      0.70        62

    accuracy                           0.78       179
   macro avg       0.76      0.77      0.77       179
weighted avg       0.79      0.78      0.78       179

各项指标含义:

  • precision (精确度):模型预测为某个类别中,真正属于该类别的比例。

    • 类别 0 (未获救):0.85。在模型预测为未获救的所有乘客中,有 85% 确实未获救。

    • 类别 1 (获救):0.67。在模型预测为获救的所有乘客中,只有 67% 确实获救。这意味着模型在预测“获救”时有较多误报。

  • recall (召回率/查全率):在实际属于某个类别的所有样本中,模型正确识别出的比例。

    • 类别 0 (未获救):0.80。在所有实际未获救的乘客中,模型成功识别了 80%。

    • 类别 1 (获救):0.74。在所有实际获救的乘客中,模型成功识别了 74%。这意味着模型在找出实际获救者方面表现尚可。

  • f1-score (F1 分数):精确度和召回率的调和平均值,综合衡量两者的平衡。

    • 类别 0: 0.83。表示模型对未获救者的识别性能良好且均衡。

    • 类别 1: 0.70。表示模型对获救者的识别性能一般,尤其受较低精确度的影响。

  • support (支持度):测试集中,该类别实际包含的样本数量。

    • 类别 0: 117。测试集中有 117 名未获救的乘客。

    • 类别 1: 62。测试集中有 62 名获救的乘客。

    • 总计 179 样本。

  • accuracy (准确率):总体正确预测的比例。0.78,表示模型总体预测准确率为 78%。

  • macro avg (宏平均):不考虑样本数量,直接计算所有类别指标的平均值。

  • weighted avg (加权平均):根据每个类别的样本数量 (support) 进行加权平均。由于类别 0 的样本更多,加权平均值会更接近类别 0 的指标,这在类别不平衡时更能反映整体性能。

分析: 该模型在预测“未获救”类别(0)时表现较好,精确度和召回率都较高。但在预测“获救”类别(1)时,精确度相对较低,说明模型容易将一些未获救的乘客错误地判断为获救。

第二次运行示例输出:

决策树的最佳参数: {'max_depth': 7, 'min_samples_leaf': 1}
决策树最佳交叉验证的准确率: 0.8119078104993598
决策树在测试集上面的准确率 0.7989
决策树分类报告: 
              precision    recall  f1-score   support

           0       0.91      0.79      0.85       126
           1       0.62      0.81      0.70        53

    accuracy                           0.80       179
   macro avg       0.77      0.80      0.78       179
weighted avg       0.82      0.80      0.81       179

分析: 这次调参得到了不同的最佳参数,模型在测试集上的准确率提升到了 0.7989。

  • 类别 0 (未获救):精确度高达 0.91,召回率 0.79。模型预测“未获救”的准确性进一步提高,误报减少。

  • 类别 1 (获救):精确度 0.62,召回率 0.81。召回率有明显提升,但精确度有所下降。这意味着模型能找出更多实际获救的人,但同时也将更多未获救的人误判为获救。

比较两次结果: 第二次的 max_depth=7, min_samples_leaf=1 组合使得模型更复杂,它在召回率(特别是类别 1)上表现更好,但精确度可能有所牺牲。选择哪个模型取决于具体业务需求:如果更关注尽可能找出所有获救者(高召回),那么第二次结果可能更好;如果更关注预测结果的可靠性(高精确),则需要权衡。

结论

本次项目通过以下步骤构建了泰坦尼克号生存预测模型:

  1. 数据预处理:利用 Ridge 回归模型成功填充了缺失的 Age 特征。

  2. 特征工程:将 Sex 特征数值化,并根据要求选择了用于分类的特征子集。

  3. 模型构建与调优:使用决策树分类器,并通过 GridSearchCV 进行了超参数调优。

  4. 模型评估:通过准确率和分类报告(精确度、召回率、F1-score、支持度)对模型性能进行了全面评估。

尽管模型达到了一定的准确率,但在识别少数类别(幸存者)的精确度上仍有提升空间。未来的工作可以尝试:

  • 更复杂的特征工程:例如,从 TicketCabin 中提取更多有用的信息。

  • 处理 Embarked 缺失值:少量缺失值可以通过众数填充或更高级方法处理。

  • 尝试其他模型:如随机森林、梯度提升树(XGBoost, LightGBM)或神经网络,这些模型通常在分类任务上表现更优。

  • 处理类别不平衡:如果幸存者和未幸存者比例非常不平衡,可以考虑过采样、欠采样或使用处理不平衡数据集的算法(如 SMOTE)。

希望这篇博客文章能帮助您理解泰坦尼克号生存预测项目的整个流程!

Logo

脑启社区是一个专注类脑智能领域的开发者社区。欢迎加入社区,共建类脑智能生态。社区为开发者提供了丰富的开源类脑工具软件、类脑算法模型及数据集、类脑知识库、类脑技术培训课程以及类脑应用案例等资源。

更多推荐