泰坦尼克号生存预测实战:用Python从数据清洗到模型调优全流程解析

在数据科学领域,泰坦尼克号数据集就像机器学习界的"Hello World",它提供了一个完美的沙盒环境,让我们能够练习从原始数据到预测模型的完整流程。这个数据集包含了891名乘客的信息,从姓名、年龄到舱位等级和票价,每个特征都可能与生存率有着微妙而有趣的关联。本文将带你深入这个经典项目,不仅学习如何处理真实世界中的"脏数据",还会探索如何通过特征工程挖掘数据中的隐藏价值,最终构建出预测能力出色的机器学习模型。

对于初学者来说,这个项目最迷人的地方在于它完美模拟了真实业务场景中的数据挑战:缺失值、异常值、类别不平衡,以及如何将原始数据转化为模型能够理解的数值特征。我们将使用Python生态中的经典工具链——Pandas进行数据处理,Seaborn进行可视化分析,Scikit-learn构建和评估模型。通过这个项目,你将掌握数据科学项目从0到1的全套方法论,这些技能可以无缝迁移到任何结构化数据的预测任务中。

1. 数据探索与可视化分析

在动手清洗数据之前,我们需要先了解数据的"脾气秉性"。泰坦尼克号数据集虽然规模不大,但包含了丰富的人口统计学信息和乘船细节。让我们先加载数据并观察其结构:

import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

# 加载数据
train_df = pd.read_csv('train.csv')
print(f"数据集形状: {train_df.shape}")
print(train_df.info())

运行这段代码,我们会发现数据集有891条记录和12个特征列。初步观察显示Age(年龄)、Cabin(舱位)和Embarked(登船港口)列存在缺失值。这种数据不完整的情况在实际项目中非常常见,也是我们需要解决的第一个挑战。

1.1 生存率的关键影响因素

让我们通过可视化来探索哪些因素与生存率关系最密切。先从最直观的性别因素开始:

# 性别与生存率关系
plt.figure(figsize=(8,5))
sns.barplot(x='Sex', y='Survived', data=train_df, 
            palette='pastel', errorbar=None)
plt.title('不同性别乘客的生存率对比')
plt.show()

这个简单的条形图揭示了一个残酷但符合历史事实的现象:女性的生存率(约74%)远高于男性(约19%)。这种显著的差异意味着性别将是模型预测的一个强特征。

接下来,我们看看舱位等级(Pclass)的影响:

舱位等级乘客比例生存率
1等舱24.2%62.9%
2等舱20.7%47.3%
3等舱55.1%24.2%

表格清晰地展示了社会阶层对生存机会的决定性影响——头等舱乘客的生存几率几乎是三等舱的三倍。这种层级关系提示我们,Pclass应该作为有序类别特征处理,而不是简单的无序分类。

1.2 年龄与生存率的非线性关系

年龄对生存率的影响更为复杂,不是简单的线性关系。我们可以将年龄分段后分析:

# 创建年龄分段
train_df['AgeGroup'] = pd.cut(train_df['Age'], 
                             bins=[0, 12, 18, 30, 50, 100],
                             labels=['儿童', '青少年', '青年', '中年', '老年'])

# 绘制生存率分布
plt.figure(figsize=(10,6))
sns.barplot(x='AgeGroup', y='Survived', data=train_df,
            palette='coolwarm', errorbar=None)
plt.title('不同年龄段乘客的生存率')
plt.show()

分析结果显示:

  • "妇女儿童优先"的原则在数据中得到印证,12岁以下儿童生存率最高
  • 18-30岁青年组生存率低于青少年组,可能与男性比例较高有关
  • 老年乘客生存机会显著降低

这种非线性关系提示我们,在特征工程阶段,可能需要将年龄转换为分段特征而非直接使用原始数值。

2. 数据清洗与缺失值处理

真实世界的数据从来不会完美无缺。泰坦尼克号数据集中,Age、Cabin和Embarked列都存在缺失值。如何处理这些缺失值将直接影响模型的性能。我们需要针对不同特征采取不同的处理策略。

2.1 年龄缺失值的智能填充

Age列约有19.9%的缺失值。简单的均值或中位数填充会忽略乘客其他特征提供的信息。更聪明的方法是使用基于相似乘客的模式进行填充:

from sklearn.impute import KNNImputer

# 选择用于预测年龄的相关特征
age_features = ['Pclass', 'Sex', 'SibSp', 'Parch', 'Fare']

# 将性别转换为数值
train_df['Sex_encoded'] = train_df['Sex'].map({'male':0, 'female':1})

# 使用KNN算法填充缺失年龄
imputer = KNNImputer(n_neighbors=5)
train_df[age_features + ['Age']] = imputer.fit_transform(
    train_df[age_features + ['Age']])

KNNImputer会找到与缺失年龄乘客最相似(基于Pclass、Sex等特征)的5位乘客,用他们的年龄均值来填充缺失值。这种方法比简单填充更能保持数据的统计特性。

2.2 舱位数据的创造性处理

Cabin列的缺失率高达77%,看似无法使用。但仔细观察会发现,舱位编号的首字母实际上代表了甲板区域:

Cabin缺失值处理步骤:
1. 提取舱位号首字母(如C123 → C)
2. 缺失值标记为'N'(None)
3. 统计各甲板区域的生存率

处理后我们发现,不同甲板区域的生存率确实存在差异:

甲板区域生存率乘客比例
N(缺失)30%77.1%
C59%4.9%
B76%4.3%
D76%3.1%
E75%3.8%

这表明即使有大量缺失,Cabin信息仍然值得保留作为分类特征。

2.3 登船港口的处理

Embarked只有2条记录缺失,我们可以用众数(最常见的值)填充:

# 查找Embarked的众数
mode_embarked = train_df['Embarked'].mode()[0]

# 填充缺失值
train_df['Embarked'] = train_df['Embarked'].fillna(mode_embarked)

提示:对于类别型特征的少量缺失值,众数填充通常是安全的选择。对于数值特征,则更适合使用中位数,因为它对异常值不敏感。

3. 特征工程的艺术

原始数据就像未经雕琢的玉石,特征工程就是将其转化为价值连城的艺术品的过程。在泰坦尼克号数据集中,我们可以通过创造性思维从现有特征中提取更多信息。

3.1 从姓名中提取社会头衔

乘客的Name列包含诸如"Braund, Mr. Owen Harris"这样的信息。其中的"Mr."、"Mrs."等头衔可能反映社会地位:

# 提取头衔函数
def extract_title(name):
    title = name.split(',')[1].split('.')[0].strip()
    return title

train_df['Title'] = train_df['Name'].apply(extract_title)

# 查看头衔分布
print(train_df['Title'].value_counts())

我们发现头衔包括:

  • 常见头衔:Mr(517), Miss(182), Mrs(125)
  • 特殊头衔:Dr(7), Rev(6), Major(2)等

这些稀有头衔可以归为一类"Rare",因为它们可能代表特殊社会地位:

# 归并稀有头衔
rare_titles = ['Dr', 'Rev', 'Col', 'Major', 'Sir', 'Lady']
train_df['Title'] = train_df['Title'].apply(
    lambda x: 'Rare' if x in rare_titles else x)

3.2 家庭规模与生存率

单独看SibSp(兄弟姐妹/配偶数量)和Parch(父母/子女数量)可能不够直观。我们可以组合这两个特征创建新的"家庭规模"特征:

train_df['FamilySize'] = train_df['SibSp'] + train_df['Parch'] + 1

# 分析家庭规模与生存率的关系
family_survival = train_df.groupby('FamilySize')['Survived'].mean()

有趣的是,生存率与家庭规模呈现非线性关系:

  • 独自旅行的乘客生存率较低(30.4%)
  • 2-4人家庭的生存率最高(55-72%)
  • 大家庭(5人以上)生存率骤降(13-20%)

这表明我们可能需要将FamilySize转换为分类特征,而非直接使用连续值。

3.3 票价的分段处理

Fare(票价)与Pclass高度相关,但同一舱位内票价也有差异。我们可以创建票价分段:

# 创建票价分段
fare_bins = [0, 10, 20, 30, 50, 100, 550]
fare_labels = ['0-10', '10-20', '20-30', '30-50', '50-100', '100+']
train_df['FareGroup'] = pd.cut(train_df['Fare'], bins=fare_bins, 
                              labels=fare_labels)

# 查看各票价段的生存率
print(train_df.groupby('FareGroup')['Survived'].mean())

结果显示票价越高生存率越高的趋势,即使在同舱位内也是如此。这提示票价信息提供了超出舱位等级之外的额外信息价值。

4. 模型构建与调优

经过充分的数据准备,我们现在可以进入模型构建阶段。泰坦尼克号数据集适合比较多种分类算法的表现,并探索如何通过调优提升模型性能。

4.1 特征选择与数据准备

首先,我们需要将处理好的数据转换为模型可用的格式:

from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.compose import ColumnTransformer

# 选择最终特征
features = ['Pclass', 'Sex', 'Age', 'FamilySize', 'Fare', 
           'Embarked', 'Title', 'Cabin']

# 定义预处理管道
preprocessor = ColumnTransformer(
    transformers=[
        ('num', StandardScaler(), ['Age', 'Fare', 'FamilySize']),
        ('cat', OneHotEncoder(handle_unknown='ignore'), 
         ['Pclass', 'Sex', 'Embarked', 'Title', 'Cabin'])
    ])

# 准备特征和目标变量
X = preprocessor.fit_transform(train_df[features])
y = train_df['Survived']

4.2 多种模型对比

让我们比较几种常见分类算法的基线表现:

from sklearn.model_selection import cross_val_score
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC

# 初始化模型
models = {
    'Logistic Regression': LogisticRegression(max_iter=1000),
    'Random Forest': RandomForestClassifier(random_state=42),
    'Gradient Boosting': GradientBoostingClassifier(random_state=42),
    'SVM': SVC(random_state=42)
}

# 交叉验证评估
results = {}
for name, model in models.items():
    scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
    results[name] = (scores.mean(), scores.std())

# 打印结果
for name, (mean, std) in results.items():
    print(f"{name}: 准确率={mean:.4f} (±{std:.4f})")

典型结果可能显示:

  • 随机森林和梯度提升树表现最佳(约82-83%准确率)
  • 逻辑回归和SVM稍逊(约79-81%)

4.3 随机森林调优

随机森林表现良好且不易过拟合,让我们通过网格搜索优化其参数:

from sklearn.model_selection import GridSearchCV

# 定义参数网格
param_grid = {
    'n_estimators': [100, 200, 300],
    'max_depth': [None, 5, 10],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4]
}

# 初始化网格搜索
grid_search = GridSearchCV(
    estimator=RandomForestClassifier(random_state=42),
    param_grid=param_grid,
    cv=5,
    scoring='accuracy',
    n_jobs=-1
)

# 执行搜索
grid_search.fit(X, y)

# 输出最佳参数
print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳分数: {grid_search.best_score_:.4f}")

调优后模型准确率通常能提升1-2个百分点。更重要的是,我们可以分析特征重要性:

# 获取特征重要性
best_rf = grid_search.best_estimator_
feature_names = (preprocessor.named_transformers_['cat']
                .get_feature_names_out().tolist())
feature_names += ['Age', 'Fare', 'FamilySize']

importance = pd.DataFrame({
    'Feature': feature_names,
    'Importance': best_rf.feature_importances_
}).sort_values('Importance', ascending=False)

# 可视化
plt.figure(figsize=(10,6))
sns.barplot(x='Importance', y='Feature', data=importance.head(15))
plt.title('随机森林特征重要性')
plt.show()

不出所料,性别、舱位等级和票价相关特征位居重要性前列。但有趣的是,从姓名提取的头衔也表现出较高预测价值,验证了我们特征工程的有效性。

4.4 模型集成策略

为了进一步提升性能,我们可以尝试模型集成。例如,将调优后的随机森林与梯度提升树结合:

from sklearn.ensemble import VotingClassifier

# 定义调优后的模型
rf = RandomForestClassifier(
    n_estimators=200,
    max_depth=10,
    min_samples_split=5,
    min_samples_leaf=2,
    random_state=42
)

gb = GradientBoostingClassifier(
    n_estimators=100,
    learning_rate=0.1,
    max_depth=3,
    random_state=42
)

# 创建投票分类器
ensemble = VotingClassifier(
    estimators=[('rf', rf), ('gb', gb)],
    voting='soft'
)

# 评估集成模型
scores = cross_val_score(ensemble, X, y, cv=5, scoring='accuracy')
print(f"集成模型准确率: {scores.mean():.4f} (±{scores.std():.4f})")

集成模型通常能比单一模型获得更稳定和略高的准确率。在实际项目中,这种提升可能意味着挽救更多生命或创造更多商业价值。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐