泰坦尼克號沉船事件是機器學習領域最經典的入門項目之一。Kaggle 上的 Titanic: Machine Learning from Disaster 競賽,被無數人稱為“機器學習的 Hello World”。
一、數據導入與清洗:讓數據從 “雜亂” 變 “干凈”
機器學習模型就像 “挑食的孩子”,只吃 “數值型、無缺失” 的數據。但原始數據往往充滿 “漏洞”,所以第一步必須先做數據清洗。
1.1 第一步:導入工具庫與加載數據
# 導入必備庫
import pandas as pd
import numpy as np# 加載訓練集(帶生存標簽)和測試集(需預測生存)
df_train = pd.read_csv('train.csv') # 891條數據,含Survived列(1=幸存,0=遇難)
df_test = pd.read_csv('test.csv') # 418條數據,無Survived列
1.2 第二步:檢查數據 “漏洞”—— 找缺失值
數據缺失是最常見的問題,我們先寫一個函數,直觀展示哪些列有缺失:
def show_missing(df, name="DataFrame"):# 統計每列缺失值數量missing = df.isnull().sum()# 只保留有缺失的列missing = missing[missing > 0]if len(missing) == 0:print(f"? {name} 中無缺失值")else:print(f"?? {name} 中存在缺失值:")print(missing)# 檢查訓練集和測試集
show_missing(df_train, "訓練集")
show_missing(df_test, "測試集")
運行結果:
?? 訓練集中存在缺失值:
Age 177 # 177個乘客年齡缺失
Cabin 687 # 687個乘客船艙號缺失(占比77%)
Embarked 2 # 2個乘客登船港口缺失?? 測試集中存在缺失值:
Age 86 # 86個年齡缺失
Fare 1 # 1個票價缺失
Cabin 327 # 327個船艙號缺失
看到這些缺失值,不能直接刪數據(會浪費信息),需要針對性處理。
1.3 第三步:缺失值處理 —— 用 “合理邏輯” 補全數據
缺失值處理的核心原則:基于業務邏輯選擇填充方式,避免 “隨便填數”。
(1)年齡(Age):按 “頭銜” 智能填充
直接用 “全體年齡中位數” 填充不合理(比如 “兒童” 和 “老人” 年齡差異大)。但姓名中藏著 “頭銜”(如 Mr = 先生、Mrs = 夫人、Miss = 小姐、Master = 少爺),不同頭銜的年齡分布很規律 —— 這是關鍵突破口!
# 1. 從姓名中提取頭銜
def extract_title(name):if pd.isna(name): # 極端情況:姓名為空(實際數據中沒有)return 'Unknown'# 姓名格式如“Braund, Mr. Owen Harris”,按逗號和句號分割取“Mr”title = name.split(',')[1].split('.')[0].strip()# 只保留常見頭銜,少見的(如 Sir、Lady)歸為“Other”if title not in ['Mr', 'Mrs', 'Miss', 'Master']:return 'Other'return title# 給訓練集和測試集添加“頭銜”列
df_train['Title'] = df_train['Name'].apply(extract_title)
df_test['Title'] = df_test['Name'].apply(extract_title)# 2. 按頭銜計算訓練集的年齡中位數(用訓練集數據,避免“數據泄露”)
title_age_median = df_train.groupby('Title')['Age'].median()
print("按頭銜分組的年齡中位數:")
print(title_age_median)# 3. 按頭銜填充缺失年齡
for title in title_age_median.index:# 填充訓練集df_train.loc[(df_train['Age'].isnull()) & (df_train['Title'] == title), 'Age'] = title_age_median[title]# 填充測試集(用訓練集的中位數,保證數據一致性)df_test.loc[(df_test['Age'].isnull()) & (df_test['Title'] == title), 'Age'] = title_age_median[title]
運行結果:
按頭銜分組的年齡中位數:
Title
Master 3.5 # 少爺(兒童):3.5歲
Miss 21.0 # 小姐(青年女性):21歲
Mr 32.0 # 先生(成年男性):32歲
Mrs 36.0 # 夫人(成年女性):36歲
Other 42.0 # 其他頭銜(如醫生、軍官):42歲
用對應頭銜的年齡中位數填充,比 “全體中位數” 精準得多!
(2)登船港口(Embarked):用 “眾數” 填充
Embarked
是分類變量(S = 南安普頓、C = 瑟堡、Q = 昆士敦),缺失值用 “出現次數最多的值(眾數)” 填充 —— 因為 “多數人的選擇更可能接近真實情況”。
# 計算訓練集Embarked的眾數(mode()[0]取第一個眾數,避免返回Series)
embarked_mode = df_train['Embarked'].mode()[0]
print(f"訓練集登船港口眾數:{embarked_mode}")# 填充缺失值
df_train['Embarked'] = df_train['Embarked'].fillna(embarked_mode)
df_test['Embarked'] = df_test['Embarked'].fillna(embarked_mode)
運行結果:訓練集登船港口眾數:S
(大部分乘客從 S 港口登船)。
(3)票價(Fare):用 “中位數” 填充
Fare
是數值變量,但可能有極端值(比如頭等艙票價極高),用 “中位數” 填充比 “平均值” 更穩健(不受極端值影響)。這里用sklearn
的SimpleImputer
工具,方便后續復用。
from sklearn.impute import SimpleImputer# 初始化填充器,策略為“中位數”
imputer = SimpleImputer(strategy='median')# 訓練集:先fit(計算中位數),再transform(填充)
df_train['Fare'] = imputer.fit_transform(df_train[['Fare']])
# 測試集:只transform(用訓練集的中位數,避免數據泄露)
df_test['Fare'] = imputer.transform(df_test[['Fare']])print(f"訓練集票價中位數(用于填充):{imputer.statistics_[0]:.2f}")
運行結果:訓練集票價中位數(用于填充):14.45
(用這個值補全測試集的 1 個缺失票價)。
(4)船艙(Cabin):簡化為 “艙位等級”
Cabin
缺失率高達 77%,直接填充意義不大,但完全刪除又可惜 —— 我們可以提取船艙號首字母(如 C85→C,代表 C 區艙位),把 “具體艙號” 簡化為 “艙位等級”,缺失值標記為 'U'(Unknown)。
# 提取Cabin首字母,缺失值填'U'
df_train['Cabin_Class'] = df_train['Cabin'].str[0].fillna('U')
df_test['Cabin_Class'] = df_test['Cabin'].str[0].fillna('U')# 刪除無用列:原始Cabin(已替換為Cabin_Class)、Title(已用于填充年齡)
df_train.drop(['Cabin', 'Title'], axis=1, inplace=True)
df_test.drop(['Cabin', 'Title'], axis=1, inplace=True)# === 修復:對 Cabin_Class 進行獨熱編碼 ===
df_train = pd.get_dummies(df_train, columns=['Cabin_Class'], prefix='Cabin')
df_test = pd.get_dummies(df_test, columns=['Cabin_Class'], prefix='Cabin')# 確保測試集列與訓練集對齊(除了 Survived)
X_columns = [col for col in df_train.columns if col != 'Survived']
df_test = df_test.reindex(columns=X_columns, fill_value=0)# 驗證:現在還有沒有缺失值?
print("\n處理后的數據缺失情況:")
show_missing(df_train, "訓練集")
show_missing(df_test, "測試集")
運行結果:
? 訓練集 中無缺失值
? 測試集 中無缺失值
數據終于 “干凈” 了!接下來進入核心環節:特征工程。
二、特征工程:讓模型 “看懂” 數據
原始數據中的文本(如 “male/female”)、零散信息(如 “SibSp+Parch”),模型無法直接理解。特征工程的目標是:把原始數據轉化為 “有預測力的特征”。
2.1 類別編碼:把文本轉成數字
模型只認數字,需要把分類變量(如性別、登船港口)轉成數值。
# 1. 性別編碼:male→0,female→1(簡單映射,只有兩個類別)
df_train['Sex'] = df_train['Sex'].map({'male': 0, 'female': 1})
df_test['Sex'] = df_test['Sex'].map({'male': 0, 'female': 1})# 2. 登船港口編碼:S→0,C→1,Q→2(按登船人數排序,不影響模型)
embarked_mapping = {'S': 0, 'C': 1, 'Q': 2}
df_train['Embarked'] = df_train['Embarked'].map(embarked_mapping)
df_test['Embarked'] = df_test['Embarked'].map(embarked_mapping)# 3.艙位等級編碼
cabin_mapping = {'U': 0, 'A': 1, 'B': 1, 'C': 1, 'D': 1, 'E': 1, 'F': 1, 'G': 1, 'T': 0}
# 將高級艙位設為1,未知和其他為0
df_train['Cabin_Class_Encoded'] = df_train['Cabin_Class'].map(cabin_mapping)
df_test['Cabin_Class_Encoded'] = df_test['Cabin_Class'].map(cabin_mapping)
2.2 創造新特征:挖掘隱藏規律
好的特征能讓模型性能 “翻倍”。我們基于 “泰坦尼克號逃生邏輯”(如 “家庭團聚影響逃生”“兒童優先救援”),創造 3 個關鍵新特征:
# 1. 家庭規模:兄弟姐妹數(SibSp)+ 父母子女數(Parch)+ 自己(1)
df_train['FamilySize'] = df_train['SibSp'] + df_train['Parch'] + 1
df_test['FamilySize'] = df_test['SibSp'] + df_test['Parch'] + 1# 2. 是否單身:家庭規模=1→1(單身),否則→0
df_train['IsAlone'] = (df_train['FamilySize'] == 1).astype(int)
df_test['IsAlone'] = (df_test['FamilySize'] == 1).astype(int)# 3. 年齡分組:將連續年齡離散化(捕捉“兒童優先、老人弱勢”的規律)
df_train['AgeGroup'] = pd.cut(df_train['Age'], bins=[0, 12, 18, 35, 60, 100], # 兒童(0-12)、青少年(12-18)、青年(18-35)、中年(35-60)、老年(60+)labels=[0, 1, 2, 3, 4] # 用數字標記
).astype(int)
df_test['AgeGroup'] = pd.cut(df_test['Age'], bins=[0, 12, 18, 35, 60, 100], labels=[0, 1, 2, 3, 4]
).astype(int)# 4. 票價分組:將連續票價分成4檔(捕捉“富人優先”的規律)
df_train['FareGroup'] = pd.qcut(df_train['Fare'], 4, # 按四分位數分成4組(每組人數相近)labels=[0, 1, 2, 3]
).astype(int)
df_test['FareGroup'] = pd.qcut(df_test['Fare'], 4, labels=[0, 1, 2, 3]
).astype(int)
2.3 刪除無用列:減少模型干擾
有些列對預測毫無幫助(如姓名、船票號,每個值都是唯一的),需要刪除:
# 要刪除的列:Name(姓名)、Ticket(船票號)
columns_to_drop = ['Name', 'Ticket','Cabin_Class']
df_train = df_train.drop(columns=columns_to_drop)
df_train = df_train.drop(columns_to_drop, axis=1, errors='ignore') # errors='ignore':列不存在也不報錯
df_test = df_test.drop(columns_to_drop, axis=1, errors='ignore')# 查看最終特征
final_features = [col for col in df_train.columns if col != 'Survived' and col != 'PassengerId']
print("最終特征:", final_features)
print("特征數量:", len(final_features))
運行結果:
最終特征: ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked', 'Cabin_Class_Encoded', 'FamilySize', 'IsAlone', 'AgeGroup', 'FareGroup']
特征數量: 12
特征工程完成!接下來準備訓練模型。
三、模型訓練與評估:找到 “最會預測” 的模型
我們先嘗試 4 種常見的機器學習模型,用交叉驗證評估它們的性能,選出表現最好的那個。
3.1 準備訓練數據
先把 “特征” 和 “標簽” 分開(訓練集才有標簽Survived
):
# 訓練集:X=特征(排除PassengerId和Survived),y=標簽(Survived)
X = df_train.drop(['PassengerId', 'Survived'], axis=1)
y = df_train['Survived']# 測試集:X_test=特征(排除PassengerId,無Survived)
X_test = df_test.drop(['PassengerId'], axis=1, errors='ignore')
3.2 對比 4 種模型性能
用cross_val_score
做 5 折交叉驗證(把訓練集分成 5 份,輪流用 4 份訓練、1 份驗證,結果更可靠):
# 導入要測試的模型和評估工具
from sklearn.ensemble import RandomForestClassifier # 隨機森林
from sklearn.svm import SVC # 支持向量機
from sklearn.neighbors import KNeighborsClassifier # K近鄰
from sklearn.tree import DecisionTreeClassifier # 決策樹
from sklearn.model_selection import cross_val_score # 交叉驗證# 定義要測試的模型字典
models = {'隨機森林': RandomForestClassifier(random_state=42), # random_state=42:結果可復現'支持向量機': SVC(random_state=42),'K近鄰': KNeighborsClassifier(),'決策樹': DecisionTreeClassifier(random_state=42)
}# 循環測試每個模型
print("各模型5折交叉驗證準確率:")
for name, model in models.items():# 交叉驗證:cv=5(5折),scoring='accuracy'(用準確率評估)scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')# 輸出結果:均值(平均準確率)± 2倍標準差(波動范圍)print(f"{name}:{scores.mean():.4f} ± {scores.std()*2:.4f}")
運行結果:
各模型5折交叉驗證準確率:
隨機森林:0.8092 ± 0.0503
支持向量機:0.6690 ± 0.0911
K近鄰:0.7105 ± 0.0795
決策樹:0.7890 ± 0.0678
結論:隨機森林表現最好,選擇它作為基礎模型繼續優化。
四、超參數調優:讓模型 “更上一層樓”
隨機森林的性能很大程度上依賴超參數(比如樹的數量、樹的深度等),默認參數只是 “及格線”。我們用 “先粗搜再精調” 的策略,既能避免盲目嘗試,又能精準找到最優參數組合。
4.1 第一步:隨機搜索(RandomizedSearchCV)—— 大范圍快速篩選
隨機搜索的核心是 “在大參數空間里隨機抽樣嘗試”,比 “逐個遍歷所有組合” 更高效,適合先鎖定 “最優參數的大致范圍”。
from sklearn.model_selection import RandomizedSearchCV
import numpy as np# 1. 初始化基礎隨機森林模型
rf_base = RandomForestClassifier(random_state=42)# 2. 定義要搜索的大參數空間
# 每個參數給出多個可能值,覆蓋常見合理范圍
param_dist = {'n_estimators': [100, 200, 300, 400], # 樹的數量(越多不一定越好,需平衡速度)'max_depth': [8, 10, 12, 15, None], # 樹的最大深度(None表示不限制,可能過擬合)'min_samples_split': [2, 5, 10, 15], # 節點分裂所需最小樣本數(越大越保守)'min_samples_leaf': [1, 2, 4, 8], # 葉子節點最小樣本數(越大越保守)'max_features': ['sqrt', 'log2', None] # 每棵樹使用的特征數(sqrt=根號下總特征數)
}# 3. 初始化隨機搜索
random_search = RandomizedSearchCV(estimator=rf_base, # 基礎模型param_distributions=param_dist, # 參數空間n_iter=50, # 隨機抽樣50組參數嘗試(越多越準,但越慢)cv=5, # 5折交叉驗證(保證結果可靠)scoring='accuracy', # 評估指標:準確率n_jobs=-1, # 用所有CPU核心加速(-1=自動適配)random_state=42, # 固定隨機種子,結果可復現verbose=1 # 顯示搜索進度(1=簡潔輸出,2=詳細輸出)
)# 4. 開始搜索(用訓練集數據)
print("=== 開始隨機搜索超參數 ===")
random_search.fit(X, y)# 5. 查看隨機搜索結果
print(f"\n隨機搜索最佳參數:{random_search.best_params_}")
print(f"隨機搜索最佳交叉驗證準確率:{random_search.best_score_:.4f}")
運行結果示例:
=== 開始隨機搜索超參數 ===
Fitting 5 folds for each of 50 candidates, totalling 250 fits隨機搜索最佳參數:{'n_estimators': 100, 'min_samples_split': 10, 'min_samples_leaf': 4, 'max_features': None, 'max_depth': 8}
隨機搜索最佳交叉驗證準確率:0.8328
可以看到:隨機搜索把準確率從默認的 80.92% 提升到了 83.28%,同時鎖定了最優參數的大致范圍(比如樹的數量 100、最大深度 8)。
4.2 第二步:網格搜索(GridSearchCV)—— 小范圍精細優化
在隨機搜索找到的 “大致范圍” 基礎上,網格搜索會 “逐個遍歷所有組合”,精準找到最優值。相當于先 “用漁網撈魚”,再 “用鑷子夾魚”。
from sklearn.model_selection import GridSearchCV# 1. 基于隨機搜索的最佳參數,縮小參數范圍(只在最優值附近微調)
best_params_random = random_search.best_params_
param_grid = {'n_estimators': [best_params_random['n_estimators']-99,best_params_random['n_estimators'],best_params_random['n_estimators']+100], # 000、100、200'max_depth': [best_params_random['max_depth']-2,best_params_random['max_depth'],best_params_random['max_depth']+2], # 6、8、10'min_samples_split': [best_params_random['min_samples_split']-1,best_params_random['min_samples_split'],best_params_random['min_samples_split']+1], # 9、10、11'min_samples_leaf': [best_params_random['min_samples_split']-1,best_params_random['min_samples_leaf'],best_params_random['min_samples_leaf']+1], # 3、4、5'max_features': [best_params_random['max_features']] # 固定為隨機搜索的最佳值(sqrt)
}# 2. 初始化網格搜索
grid_search = GridSearchCV(estimator=RandomForestClassifier(random_state=42),param_grid=param_grid, # 精細參數范圍cv=5, # 5折交叉驗證scoring='accuracy',n_jobs=-1,verbose=1
)# 3. 開始精細搜索
print("\n=== 開始網格搜索精細調優 ===")
grid_search.fit(X, y)# 4. 查看網格搜索結果
print(f"\n網格搜索最佳參數:{grid_search.best_params_}")
print(f"網格搜索最佳交叉驗證準確率:{grid_search.best_score_:.4f}")# 5. 獲取最終最優模型(用最佳參數訓練好的模型)
best_model = grid_search.best_estimator_
print(f"\n? 最終最優模型已保存(準確率:{grid_search.best_score_:.4f})")
運行結果示例:
=== 開始網格搜索精細調優 ===
Fitting 5 folds for each of 81 candidates, totalling 405 fits網格搜索最佳參數:{'max_depth': 8, 'max_features': None, 'min_samples_leaf': 4, 'min_samples_split': 9, 'n_estimators': 100}
網格搜索最佳交叉驗證準確率:0.8350? 最終最優模型已保存(準確率:0.8350)
經過精細調優,準確率提升幅度較小(0.8328—0.8350)!
五、數據分析與可視化
5.1 特征重要性
import matplotlib.pyplot as plt# 設置中文字體(避免亂碼)
plt.rcParams['font.sans-serif'] = ['WenQuanYi Zen Hei']
plt.rcParams['axes.unicode_minus'] = False# 1. 獲取特征重要性和特征名稱
feature_importance = best_model.feature_importances_
feature_names = X.columns# 2. 按重要性排序(從高到低)
sorted_idx = np.argsort(feature_importance)[::-1]
sorted_importance = feature_importance[sorted_idx]
sorted_names = feature_names[sorted_idx]# 3. 繪制特征重要性柱狀圖
plt.figure(figsize=(10, 6))
bars = plt.bar(range(len(sorted_importance)), sorted_importance, color='#1f77b4')# 添加數值標簽(顯示每個特征的重要性占比)
for i, bar in enumerate(bars):height = bar.get_height()plt.text(bar.get_x() + bar.get_width()/2., height + 0.005,f'{height:.3f}', ha='center', va='bottom', fontsize=9)# 設置圖表標題和標簽
plt.title('隨機森林模型:特征重要性排名', fontsize=14, pad=20)
plt.xlabel('特征', fontsize=12)
plt.ylabel('特征重要性', fontsize=12)
plt.xticks(range(len(sorted_names)), sorted_names, rotation=45, ha='right') # 旋轉標簽避免重疊
plt.tight_layout() # 自動調整布局,防止標簽被截斷
plt.show()
排名前 3 的特征是:
- Sex(性別):重要性最高—— 符合歷史事實 “女性優先救援”;
- AgeGroup(年齡分組)?或?Age(年齡):重要性第二 —— 兒童和老人的生存率有差異;
- Fare(票價)?或?FareGroup(票價分組):重要性第三 —— 高票價乘客(頭等艙)更易獲救。
六、生成提交文件:在 Kaggle 上看排名
最后一步,用我們的最優模型預測測試集的 “生存狀態”,生成符合 Kaggle 要求的 CSV 文件并提交。
6.1 預測測試集并生成文件
Kaggle 要求提交文件包含兩列:PassengerId
(測試集乘客 ID)和Survived
(預測的生存狀態,0 = 遇難,1 = 幸存)。
# 1. 用最優模型預測測試集
test_predictions = best_model.predict(X_test)# 2. 生成提交DataFrame
submission = pd.DataFrame({'PassengerId': df_test['PassengerId'], # 測試集乘客ID(必須和原始一致)'Survived': test_predictions.astype(int) # 預測結果轉整數(0/1)
})# 3. 保存為CSV文件(index=False:不保存行索引,否則Kaggle會報錯)
submission.to_csv('titanic_submission_best.csv', index=False)
文件格式正確,接下來就是提交到 Kaggle。
6.2 Kaggle 提交步驟(手把手教)
- 打開 Kaggle 泰坦尼克競賽頁面:Titanic - Machine Learning from Disaster;
- 點擊頁面上方的?“Submit Predictions”?按鈕(如果沒看到,先點擊 “Join Competition” 接受規則);
- 在彈出的頁面中,點擊?“Upload Submission File”,選擇我們生成的
titanic_submission_best.csv
; - 點擊?“Make Submission”,等待 Kaggle 計算得分;
- 提交完成后,點擊頁面上方的?“Leaderboard”,就能看到自己的排名和公共分數(Public Score)。
注:截圖的評分不一定和實際一致,練習時用的代碼和文章用的代碼細節上有一點差異,由于每天有提交次數上限,最后版本的代碼已無提交次數。
七、總結:從項目到能力的提升
泰坦尼克號生存預測項目雖然看似簡單,但它覆蓋了機器學習的完整流程——從數據清洗、特征工程、模型訓練、超參數調優、可視化到最后的結果提交。對初學者而言,關鍵在于理解“每一步為什么要做”,而不是單純追求排行榜上的名次。
7.1 核心收獲
-
數據清洗的原則:處理缺失值時,不能隨意填充,而應根據業務邏輯來決定如何填補。例如,在本項目中,我們依據乘客頭銜來推測年齡,而非直接使用均值或中位數。
-
特征工程的核心:并不是創造盡可能多的特征,而是要提煉出能夠真正提供預測價值的信息。比如,通過家庭規模(FamilySize)和年齡分組(AgeGroup)等特征,我們可以捕捉到更多影響生存率的因素。
-
模型選擇的邏輯:在初步階段,應當對比多種基礎模型(如隨機森林、決策樹等),從中挑選表現最優者進行進一步優化。這有助于理解不同算法的適用場景及其局限性。
-
調參的技巧:采用先隨機搜索后網格搜索的方法,可以在保證一定精度的前提下提高效率。隨機搜索用于粗略地確定參數范圍,而網格搜索則針對選定的較窄范圍進行精細調整。
-
性價比的權衡:盡管隨機搜索和網格搜索都能找到較好的參數組合,但在面對大規模數據集時,考慮到計算成本,有時需要做出妥協。評估哪種方法在特定情況下更為高效是至關重要的。
7.2 進階方向
若想在此基礎上進一步提升你的技能或項目成績,可以考慮以下幾個方向:
-
深入探索高級特征工程:嘗試更復雜的特征交叉與組合,或者利用領域知識創造新的特征變量。
-
集成學習方法:研究并實踐不同的集成學習策略,如Bagging、Boosting等,以期獲得更加穩定且準確的模型性能。
-
應用深度學習模型:對于某些問題,尤其是那些具有大量復雜非線性關系的數據集,深度學習模型可能提供更好的解決方案。