泰坦尼克号生存预测是Kaggle平台上最经典的入门竞赛项目之一。这个项目之所以经久不衰,是因为它完美融合了现实世界的复杂性和机器学习的基础要素。1912年那场著名的海难中,船上2224名乘客和船员中只有710人幸存,生存率仅为32%。这个数据集包含了乘客的年龄、性别、舱位等级等关键信息,让我们能够用现代数据分析技术还原历史事件中的生存规律。
我刚开始接触这个项目时,最惊讶的是原始数据的"脏乱"程度。训练集有891条记录,测试集418条,共12个特征列。但仔细检查就会发现:年龄列缺失263个值,船舱号缺失超过77%,就连船票价格都缺失1条记录。这种数据质量在真实业务场景中非常典型,也正因如此,它成为了学习数据清洗和特征工程的绝佳案例。
面对缺失数据,我通常会考虑三种处理方式。对于数值型特征如Age和Fare,均值填充是最快捷的选择:
python复制full['Age'] = full['Age'].fillna(full['Age'].mean())
full['Fare'] = full['Fare'].fillna(full['Fare'].median()) # 使用中位数更抗异常值
但实践中发现,年龄用分组均值填充效果更好。比如按头衔(Mr/Mrs/Miss等)分组计算平均年龄,再填充对应分组的缺失值。这比整体均值更能保留原始分布特征。
对于分类变量Embarked(登船港口),直接取众数填充:
python复制full['Embarked'] = full['Embarked'].fillna(full['Embarked'].mode()[0])
最棘手的是Cabin(船舱号)缺失率高达77%。我的解决方案是提取首字母作为舱位等级标识(如A/B/C等),缺失值标记为'U'。后来验证这个新建的CabinType特征确实对预测有帮助。
数据清洗时容易忽略异常值检测。通过箱线图分析,我发现Fare列存在极端高值:
python复制import seaborn as sns
sns.boxplot(x=full['Fare'])
处理方法是使用分位数截断:
python复制upper_bound = full['Fare'].quantile(0.99)
full['Fare'] = full['Fare'].clip(upper=upper_bound)
原始数据中Name字段的格式如"Braund, Mr. Owen Harris"。通过分析发现,头衔不仅反映性别,更隐含社会地位信息。我优化后的提取逻辑包括:
python复制title_map = {
"Capt": "Officer", "Col": "Officer", "Major": "Officer",
"Jonkheer": "Royalty", "Don": "Royalty", "Sir": "Royalty",
"Dr": "Officer", "Rev": "Officer", "Countess": "Royalty",
"Dona": "Royalty", "Mme": "Mrs", "Mlle": "Miss",
"Ms": "Mrs", "Mr": "Mr", "Mrs": "Mrs",
"Miss": "Miss", "Master": "Master", "Lady": "Royalty"
}
原始数据有SibSp(兄弟姐妹/配偶数)和Parch(父母/子女数)。我创建了三个新特征:
python复制full['FamilySize'] = full['SibSp'] + full['Parch'] + 1
full['IsAlone'] = (full['FamilySize'] == 1).astype(int)
full['FamilyCat'] = pd.cut(full['FamilySize'],
bins=[0,1,4,20],
labels=['Single','Small','Large'])
我测试了6种经典算法,使用5折交叉验证确保结果可靠:
| 模型 | 平均准确率 | 训练时间(s) |
|---|---|---|
| 逻辑回归 | 0.821 | 0.15 |
| 随机森林 | 0.830 | 0.85 |
| SVM | 0.835 | 1.23 |
| GBDT | 0.842 | 1.56 |
| KNN | 0.798 | 0.07 |
| 朴素贝叶斯 | 0.781 | 0.03 |
从结果看,梯度提升决策树(GBDT)表现最好,但逻辑回归在速度与精度间取得了更好平衡。
通过网格搜索寻找最优参数组合:
python复制from sklearn.model_selection import GridSearchCV
param_grid = {
'n_estimators': [100, 200, 300],
'max_depth': [5, 8, 10],
'min_samples_split': [2, 5, 10]
}
rf = RandomForestClassifier()
grid_search = GridSearchCV(rf, param_grid, cv=5)
grid_search.fit(X_train, y_train)
print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳得分: {grid_search.best_score_:.4f}")
调优后随机森林的准确率从0.83提升到了0.848,关键参数显示:
在第三次尝试这个项目时,我发现几个容易踩的坑:
数据泄露:在填充缺失年龄时,如果使用全体数据(含测试集)的均值,会导致模型评估失真。正确做法是仅用训练集统计量填充。
类别不平衡:原始数据中幸存者占比仅38%,建议采用以下方法:
特征相关性陷阱:初期我发现SibSp与生存率呈负相关(-0.035),就移除了该特征。后来创建FamilySize后才发现,正相关隐藏在非线性关系中——独自出行和超大家庭生存率都较低,中等家庭生存率最高。
这个项目最宝贵的收获是:好的特征工程往往比模型选择更重要。仅用原始特征时,最优模型准确率仅0.78;而经过深度特征工程后,即使简单逻辑回归也能达到0.82+。建议初学者在模型调优前,至少投入60%时间在数据理解和特征构建上。