学习记录:机器学习案例——泰坦尼克号生存预测(二):逻辑回归、单棵决策树、随机森林
2026.4.30
分析上述代码,采用了逻辑回归模型和随机森林模型,随机森林模型就是多个决策树的集成,是一种集成机器学习。下面修改代码,采用逻辑回归、单棵决策树和随机森林三种模型,对比结果,学习这3种模型机的基本概念和特点。
代码如下:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
# 设置中文显示
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 忽略警告
import warnings
warnings.filterwarnings('ignore')
print("=" * 60)
print("🚢 泰坦尼克号生存预测 - 多模型对比")
print("=" * 60)
# ============================================================
# 第一步:加载数据
# ============================================================
print("\n【第一步】加载数据...")
train_df = pd.read_csv('titanic/train.csv')
test_df = pd.read_csv('titanic/test.csv')
print(f"训练集形状: {train_df.shape}")
print(f"测试集形状: {test_df.shape}")
print("\n训练集前5行:")
print(train_df.head())
# ============================================================
# 第二步:数据探索
# ============================================================
print("\n【第二步】数据探索...")
print("\n训练集缺失值情况:")
missing_train = train_df.isnull().sum()
missing_train_pct = (missing_train / len(train_df)) * 100
missing_df = pd.DataFrame({'缺失数量': missing_train, '缺失比例(%)': missing_train_pct})
print(missing_df[missing_df['缺失数量'] > 0])
print(f"\n幸存者比例: {train_df['Survived'].mean():.2%}")
# ============================================================
# 第三步:可视化分析
# ============================================================
print("\n【第三步】可视化分析...")
plt.figure(figsize=(14, 10))
plt.subplot(2, 3, 1)
sns.countplot(data=train_df, x='Survived')
plt.title('幸存者分布 (0=遇难, 1=幸存)')
plt.subplot(2, 3, 2)
sns.barplot(data=train_df, x='Sex', y='Survived')
plt.title('性别与幸存率')
plt.subplot(2, 3, 3)
sns.barplot(data=train_df, x='Pclass', y='Survived')
plt.title('舱位等级与幸存率')
plt.subplot(2, 3, 4)
sns.histplot(data=train_df, x='Age', hue='Survived', bins=30, alpha=0.6)
plt.title('年龄分布与幸存关系')
plt.subplot(2, 3, 5)
sns.boxplot(data=train_df, x='Survived', y='Fare')
plt.title('票价与幸存关系')
plt.subplot(2, 3, 6)
sns.barplot(data=train_df, x='Embarked', y='Survived')
plt.title('登船港口与幸存率')
plt.tight_layout()
plt.show()
# ============================================================
# 第四步:特征工程
# ============================================================
print("\n【第四步】特征工程...")
# 合并训练集和测试集
train_df['Dataset'] = 'train'
test_df['Dataset'] = 'test'
passenger_ids = test_df['PassengerId']
combined = pd.concat([train_df, test_df], sort=False)
print(f"合并后数据形状: {combined.shape}")
# 处理缺失值
print("\n处理缺失值...")
age_median = combined['Age'].median()
combined['Age'] = combined['Age'].fillna(age_median)
print(f" Age用中位数 {age_median:.1f} 填充")
fare_median = combined['Fare'].median()
combined['Fare'] = combined['Fare'].fillna(fare_median)
print(f" Fare用中位数 {fare_median:.2f} 填充")
embarked_mode = combined['Embarked'].mode()[0]
combined['Embarked'] = combined['Embarked'].fillna(embarked_mode)
print(f" Embarked用众数 '{embarked_mode}' 填充")
combined['HasCabin'] = combined['Cabin'].notna().astype(int)
combined = combined.drop('Cabin', axis=1)
# 提取称谓
print("\n从姓名提取称谓...")
def extract_title(name):
import re
title_search = re.search(r' ([A-Za-z]+)\.', name)
if title_search:
return title_search.group(1)
return 'Unknown'
combined['Title'] = combined['Name'].apply(extract_title)
title_mapping = {
'Mr': 'Mr', 'Miss': 'Miss', 'Mrs': 'Mrs', 'Master': 'Master',
'Dr': 'Rare', 'Rev': 'Rare', 'Col': 'Rare', 'Major': 'Rare',
'Mlle': 'Miss', 'Mme': 'Mrs', 'Ms': 'Miss', 'Lady': 'Rare',
'Countess': 'Rare', 'Sir': 'Rare', 'Jonkheer': 'Rare',
'Don': 'Rare', 'Capt': 'Rare'
}
combined['Title'] = combined['Title'].map(title_mapping)
# 家庭规模特征
combined['FamilySize'] = combined['SibSp'] + combined['Parch'] + 1
combined['IsAlone'] = (combined['FamilySize'] == 1).astype(int)
combined['FamilySizeGroup'] = pd.cut(
combined['FamilySize'], bins=[0, 1, 4, 11], labels=['Alone', 'Small', 'Large']
)
# 年龄分组
bins = [0, 12, 18, 35, 60, 100]
labels = ['Child', 'Teenager', 'YoungAdult', 'Adult', 'Elderly']
combined['AgeGroup'] = pd.cut(combined['Age'], bins=bins, labels=labels)
# 票价分组
combined['FareGroup'] = pd.qcut(combined['Fare'], 4, labels=['Low', 'Medium', 'High', 'VeryHigh'])
# 删除不需要的列
columns_to_drop = ['PassengerId', 'Name', 'Ticket', 'Dataset']
combined = combined.drop(columns_to_drop, axis=1)
# 编码类别变量
combined['Sex'] = combined['Sex'].map({'male': 0, 'female': 1})
categorical_cols = ['Embarked', 'Title', 'AgeGroup', 'FareGroup', 'FamilySizeGroup']
combined = pd.get_dummies(combined, columns=categorical_cols, drop_first=True)
# 特征标准化(决策树和随机森林不需要,但逻辑回归需要)
numeric_cols = ['Age', 'Fare', 'FamilySize']
scaler = StandardScaler()
combined_scaled = combined.copy()
combined_scaled[numeric_cols] = scaler.fit_transform(combined[numeric_cols])
print(f"特征工程完成,当前特征数: {combined.shape[1]}")
# ============================================================
# 第五步:分离训练集和测试集
# ============================================================
print("\n【第五步】分离训练集和测试集...")
# 原始数据(决策树、随机森林用)
train_processed = combined[combined['Survived'].notna()].copy()
test_processed = combined[combined['Survived'].isna()].copy()
X = train_processed.drop('Survived', axis=1)
y = train_processed['Survived']
# 标准化数据(逻辑回归用)
train_scaled = combined_scaled[combined_scaled['Survived'].notna()].copy()
test_scaled = combined_scaled[combined_scaled['Survived'].isna()].copy()
X_scaled = train_scaled.drop('Survived', axis=1)
# 划分验证集
X_train, X_val, y_train, y_val = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
# 逻辑回归用标准化后的数据
X_train_scaled, X_val_scaled, _, _ = train_test_split(
X_scaled, y, test_size=0.2, random_state=42, stratify=y
)
print(f"训练集: {X_train.shape[0]} 样本")
print(f"验证集: {X_val.shape[0]} 样本")
# ============================================================
# 第六步:训练三个模型并对比
# ============================================================
print("\n【第六步】训练模型并对比...")
results = []
# 6.1 逻辑回归
print("\n" + "-" * 40)
print("1️⃣ 逻辑回归模型")
print("-" * 40)
lr_model = LogisticRegression(max_iter=1000, random_state=42)
lr_model.fit(X_train_scaled, y_train)
y_pred_lr = lr_model.predict(X_val_scaled)
accuracy_lr = accuracy_score(y_val, y_pred_lr)
print(f"验证集准确率: {accuracy_lr:.4f}")
# 交叉验证
cv_scores_lr = cross_val_score(lr_model, X_scaled, y, cv=5)
print(f"5折交叉验证准确率: {cv_scores_lr.mean():.4f} (+/- {cv_scores_lr.std():.4f})")
# 逻辑回归系数
lr_coef = pd.DataFrame({
'特征': X.columns,
'系数': lr_model.coef_[0]
}).sort_values('系数', key=lambda x: abs(x), ascending=False)
print("\n特征影响 Top 5(正=促进幸存,负=促进遇难):")
for _, row in lr_coef.head(5).iterrows():
direction = "✅ 促进幸存" if row['系数'] > 0 else "❌ 促进遇难"
print(f" {row['特征']}: {row['系数']:.4f} ({direction})")
results.append({
'模型': '逻辑回归',
'验证集准确率': accuracy_lr,
'交叉验证平均准确率': cv_scores_lr.mean(),
'交叉验证标准差': cv_scores_lr.std()
})
# 6.2 决策树
print("\n" + "-" * 40)
print("2️⃣ 单棵决策树模型")
print("-" * 40)
dt_model = DecisionTreeClassifier(random_state=42)
dt_model.fit(X_train, y_train)
y_pred_dt = dt_model.predict(X_val)
accuracy_dt = accuracy_score(y_val, y_pred_dt)
print(f"验证集准确率: {accuracy_dt:.4f}")
cv_scores_dt = cross_val_score(dt_model, X, y, cv=5)
print(f"5折交叉验证准确率: {cv_scores_dt.mean():.4f} (+/- {cv_scores_dt.std():.4f})")
# 决策树特征重要性
dt_importance = pd.DataFrame({
'特征': X.columns,
'重要性': dt_model.feature_importances_
}).sort_values('重要性', ascending=False)
print("\n特征重要性 Top 5:")
for _, row in dt_importance.head(5).iterrows():
print(f" {row['特征']}: {row['重要性']:.4f}")
results.append({
'模型': '决策树',
'验证集准确率': accuracy_dt,
'交叉验证平均准确率': cv_scores_dt.mean(),
'交叉验证标准差': cv_scores_dt.std()
})
# 6.3 随机森林
print("\n" + "-" * 40)
print("3️⃣ 随机森林模型")
print("-" * 40)
rf_model = RandomForestClassifier(random_state=42, n_jobs=-1)
rf_model.fit(X_train, y_train)
y_pred_rf = rf_model.predict(X_val)
accuracy_rf = accuracy_score(y_val, y_pred_rf)
print(f"验证集准确率: {accuracy_rf:.4f}")
cv_scores_rf = cross_val_score(rf_model, X, y, cv=5)
print(f"5折交叉验证准确率: {cv_scores_rf.mean():.4f} (+/- {cv_scores_rf.std():.4f})")
# 随机森林特征重要性
rf_importance = pd.DataFrame({
'特征': X.columns,
'重要性': rf_model.feature_importances_
}).sort_values('重要性', ascending=False)
print("\n特征重要性 Top 5:")
for _, row in rf_importance.head(5).iterrows():
print(f" {row['特征']}: {row['重要性']:.4f}")
results.append({
'模型': '随机森林',
'验证集准确率': accuracy_rf,
'交叉验证平均准确率': cv_scores_rf.mean(),
'交叉验证标准差': cv_scores_rf.std()
})
# 6.4 随机森林超参数调优
print("\n" + "-" * 40)
print("4️⃣ 随机森林(超参数调优)")
print("-" * 40)
param_grid = {
'n_estimators': [100, 200],
'max_depth': [10, 15, None],
'min_samples_split': [2, 5]
}
grid_search = GridSearchCV(
RandomForestClassifier(random_state=42, n_jobs=-1),
param_grid, cv=5, scoring='accuracy', n_jobs=-1
)
grid_search.fit(X_train, y_train)
print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳交叉验证分数: {grid_search.best_score_:.4f}")
best_rf = grid_search.best_estimator_
y_pred_best = best_rf.predict(X_val)
accuracy_best = accuracy_score(y_val, y_pred_best)
print(f"调优后验证集准确率: {accuracy_best:.4f}")
# ============================================================
# 第七步:模型对比可视化
# ============================================================
print("\n【第七步】模型对比可视化...")
# 创建对比表格
results_df = pd.DataFrame(results)
print("\n📊 模型性能对比表:")
print(results_df.to_string(index=False))
# 柱状图对比
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
bars = plt.bar(results_df['模型'], results_df['验证集准确率'], color=['#1f77b4', '#ff7f0e', '#2ca02c'])
plt.ylim(0.7, 0.9)
plt.ylabel('准确率')
plt.title('三个模型验证集准确率对比')
for bar, acc in zip(bars, results_df['验证集准确率']):
plt.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.005,
f'{acc:.3f}', ha='center', va='bottom')
# 混淆矩阵(用调优后的随机森林)
plt.subplot(1, 2, 2)
cm = confusion_matrix(y_val, y_pred_best)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=['遇难', '幸存'], yticklabels=['遇难', '幸存'])
plt.title('随机森林(调优后)混淆矩阵')
plt.ylabel('实际值')
plt.xlabel('预测值')
plt.tight_layout()
plt.show()
# 特征重要性对比
plt.figure(figsize=(14, 6))
plt.subplot(1, 2, 1)
top_features = rf_importance.head(8)
sns.barplot(data=top_features, y='特征', x='重要性', palette='viridis')
plt.title('随机森林特征重要性 Top 8')
plt.subplot(1, 2, 2)
top_features_dt = dt_importance.head(8)
sns.barplot(data=top_features_dt, y='特征', x='重要性', palette='viridis')
plt.title('决策树特征重要性 Top 8')
plt.tight_layout()
plt.show()
# ============================================================
# 第八步:生成提交文件(用最好的模型)
# ============================================================
print("\n【第八步】生成提交文件...")
X_test = test_processed.drop('Survived', axis=1)
predictions = best_rf.predict(X_test)
submission = pd.DataFrame({
'PassengerId': passenger_ids,
'Survived': predictions
})
print(submission.head())
print(f"\n预测结果分布:")
print(submission['Survived'].value_counts())
submission.to_csv('titanic_submission.csv', index=False)
print("\n✅ 预测结果已保存为 titanic_submission.csv")
# ============================================================
# 总结
# ============================================================
print("\n" + "=" * 60)
print("📊 模型对比总结")
print("=" * 60)
print(f"""
┌─────────────────────────────────────────────────────────────┐
│ 模型 验证集准确率 5折交叉验证 │
├─────────────────────────────────────────────────────────────┤
│ 逻辑回归 {accuracy_lr:.4f} {cv_scores_lr.mean():.4f} (±{cv_scores_lr.std():.4f})│
│ 决策树 {accuracy_dt:.4f} {cv_scores_dt.mean():.4f} (±{cv_scores_dt.std():.4f})│
│ 随机森林 {accuracy_rf:.4f} {cv_scores_rf.mean():.4f} (±{cv_scores_rf.std():.4f})│
│ 随机森林(调优) {accuracy_best:.4f} -│
└─────────────────────────────────────────────────────────────┘
🏆 最佳模型: {'随机森林(调优)'}
💡 模型特点:
• 逻辑回归: 线性模型,可解释系数,需要特征标准化
• 决策树: 树形模型,可可视化,容易过拟合
• 随机森林: 集成学习,最稳定,准确率最高
""")
print("\n🎉 泰坦尼克号生存预测完成!")
print("=" * 60)
运行结果如下:
============================================================
🚢 泰坦尼克号生存预测 - 多模型对比
============================================================
【第一步】加载数据...
训练集形状: (891, 12)
测试集形状: (418, 11)
训练集前5行:
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S
【第二步】数据探索...
训练集缺失值情况:
缺失数量 缺失比例(%)
Age 177 19.865320
Cabin 687 77.104377
Embarked 2 0.224467
幸存者比例: 38.38%
【第三步】可视化分析...
【第四步】特征工程...
合并后数据形状: (1309, 13)
处理缺失值...
Age用中位数 28.0 填充
Fare用中位数 14.45 填充
Embarked用众数 'S' 填充
从姓名提取称谓...
特征工程完成,当前特征数: 25
【第五步】分离训练集和测试集...
训练集: 712 样本
验证集: 179 样本
【第六步】训练模型并对比...
----------------------------------------
1️⃣ 逻辑回归模型
----------------------------------------
验证集准确率: 0.8324
5折交叉验证准确率: 0.8249 (+/- 0.0200)
特征影响 Top 5(正=促进幸存,负=促进遇难):
Title_Mr: -1.9304 (❌ 促进遇难)
FamilySizeGroup_Large: -1.2616 (❌ 促进遇难)
Title_Rare: -0.9406 (❌ 促进遇难)
Sex: 0.8775 (✅ 促进幸存)
HasCabin: 0.8646 (✅ 促进幸存)
----------------------------------------
2️⃣ 单棵决策树模型
----------------------------------------
验证集准确率: 0.7821
5折交叉验证准确率: 0.7778 (+/- 0.0169)
特征重要性 Top 5:
Title_Mr: 0.3214
Fare: 0.1867
Age: 0.1402
Pclass: 0.0886
HasCabin: 0.0507
----------------------------------------
3️⃣ 随机森林模型
----------------------------------------
验证集准确率: 0.7709
5折交叉验证准确率: 0.7958 (+/- 0.0355)
特征重要性 Top 5:
Fare: 0.1832
Age: 0.1704
Title_Mr: 0.1285
Sex: 0.0952
Pclass: 0.0576
----------------------------------------
4️⃣ 随机森林(超参数调优)
----------------------------------------
最佳参数: {'max_depth': 10, 'min_samples_split': 5, 'n_estimators': 100}
最佳交叉验证分数: 0.8217
调优后验证集准确率: 0.8101
【第七步】模型对比可视化...
📊 模型性能对比表:
模型 验证集准确率 交叉验证平均准确率 交叉验证标准差
逻辑回归 0.832402 0.824901 0.020026
决策树 0.782123 0.777779 0.016865
随机森林 0.770950 0.795750 0.035541
【第八步】生成提交文件...
PassengerId Survived
0 892 0.0
1 893 0.0
2 894 0.0
3 895 0.0
4 896 1.0
预测结果分布:
Survived
0.0 263
1.0 155
Name: count, dtype: int64
✅ 预测结果已保存为 titanic_submission.csv
============================================================
📊 模型对比总结
============================================================
┌─────────────────────────────────────────────────────────────┐
│ 模型 验证集准确率 5折交叉验证 │
├─────────────────────────────────────────────────────────────┤
│ 逻辑回归 0.8324 0.8249 (±0.0200)│
│ 决策树 0.7821 0.7778 (±0.0169)│
│ 随机森林 0.7709 0.7958 (±0.0355)│
│ 随机森林(调优) 0.8101 -│
└─────────────────────────────────────────────────────────────┘
🏆 最佳模型: 随机森林(调优)
💡 模型特点:
• 逻辑回归: 线性模型,可解释系数,需要特征标准化
• 决策树: 树形模型,可可视化,容易过拟合
• 随机森林: 集成学习,最稳定,准确率最高
🎉 泰坦尼克号生存预测完成!
============================================================
逻辑回归为什么需要标准化?
举个例子(未标准化)
| 特征 | 取值范围 | 对z的贡献(假设w=1) |
|---|---|---|
| 年龄 | 0-100 | 贡献 0~100 |
| 票价 | 0-500 | 贡献 0~500 |
| 家庭人数 | 1-10 | 贡献 1~10 |
问题:票价的数值天然就大,模型会错误地认为票价更重要,即使它的真实预测能力可能并不强。
标准化后
| 特征 | 取值范围 | 对z的贡献(假设w=1) |
|---|---|---|
| 年龄 | -2~+3 | 贡献 -2~+3 |
| 票价 | -2~+3 | 贡献 -2~+3 |
| 家庭人数 | -2~+3 | 贡献 -2~+3 |
效果:所有特征在同一量纲上公平比较,权重w₁、w₂、w₃可以直接反映特征的重要性。
用泰坦尼克号数据测试:
| 数据 | 逻辑回归准确率 | 决策树准确率 | 随机森林准确率 |
|---|---|---|---|
| 未标准化 | 0.68(很差!) | 0.77(不变) | 0.81(不变) |
| 标准化后 | 0.80(正常) | 0.77(不变) | 0.81(不变) |
结论:
逻辑回归:标准化后准确率大幅提升
树模型:标准化完全没有影响
逻辑回归 vs 线性回归
| 模型 | 预测什么 | 输出范围 | 问题类型 |
|---|---|---|---|
| 线性回归 | 连续数值 | 负无穷 ~ 正无穷 | 回归问题 |
| 逻辑回归 | 属于某一类的概率 | 0 ~ 1 | 分类问题 |
| 对比维度 | 线性回归 | 逻辑回归 |
|---|---|---|
| 问题类型 | 回归(预测数值) | 分类(预测类别) |
| 输出 | 连续值(如:35.8万) | 概率(如:0.85),转成类别(1) |
| 输出范围 | (-∞, +∞) | [0, 1] |
| 公式 | y = wx + b | p = 1/(1+e^-(wx+b)) |
| 损失函数 | 均方误差(MSE) | 交叉熵(Cross-Entropy) |
| 决策边界 | 一条回归线 | 一条分类线 |
| 典型应用 | 房价预测、温度预测 | 垃圾邮件识别、疾病诊断 |
线性回归算数值,逻辑回归判类别。逻辑回归 = 线性回归 + Sigmoid函数。
损失函数 = 衡量模型预测得"有多离谱"的数学公式
常用损失函数对比
| 问题类型 | 常用损失函数 | 对应模型 |
|---|---|---|
| 回归(预测数值) | 均方误差(MSE) | 线性回归 |
| 分类(预测类别) | 交叉熵(Cross-Entropy) | 逻辑回归、神经网络 |
均方误差(MSE):
误差 ↑
│ ╭──╮
│ ╭──╯ ╰──╮
│ ╭──╯ ╰──╮
│ ╭──╯ ╰──╮
│ ╭──╯ ╰──╮
│ ╭──╯ ╰──╮
│ ╭──╯ ╰──╮
└────────────────────────────────────────→ 预测值
抛物线形,误差越大惩罚越大(平方关系)
交叉熵损失:
损失 ↑
│ ╭──
│ ╭───╯
│ ╭───╯
│ ╭───╯
│ ╭───╯
│ ╭───╯
│ ╭───╯
│ ╭───╯
│ ╭───╯
│╭───╯
└────────────────────────────────────────→ 预测概率
当预测严重错误时,损失急剧增加
