最近,因为感觉好久没有写代码,面试的时候表现也不佳,所以打算拿Kaggle练练手,积累一些项目经验。因为Titanic是Kaggle的第一个项目,所以决定从这个开始入手。
以下是基本流程:
- 观察并预处理数据
- 建立baseline模型
- 根据baseline模型的结果增加新的特征并预处理数据
- 建立改进后的模型,并采用不同模型通过投票来决定最后结果
观察并预处理数据
- 观察数据主要包括观察数据的类型,确认是连续型变量还是离散型变量,了解各个特征的数量分布、不同特征与存活率之间的关系,根据观察的结果选择想要采用的特征
- 预处理数据主要包括找到缺失值并根据需要进行处理(删除对应record或者进行估计),利用one-hot来将处理离散型变量,对连续型变量则进行scaling来加快模型收敛。
- 用同样的方式对test数据集进行预处理
观察数据
输入如下命令:
1 | train.head(5) |
以上命令分别打印出训练数据集的前五行,以及其他相关数据信息包括数据缺失情况、数据类型、数据分布等等。
经过观察,可以确认:
- 主要的数据缺失集中在Age和Cabin这两个特征
- 特征中既有连续变量也有离散变量
- 离散变量中有部分变量的频数非常低,比如Cabin、Ticket以及Name,其中最高频数只有7
根据以上观察,频数非常低的三个离散变量暂时被放弃,以后模型改进再做考虑。
接下来需要观察各个特征与存活率之间的关系,例如年龄与生存人数之间的关系:
1 | grid = sns.FacetGrid(train, col = 'Survived') |
其余图片可在具体notebook中找到。
从数据和图表中可以明显看出部分特征与存活率之间有明显关系,比如Pclass这个特征,Pclass=1的存活率是最高的,随着船舱等级下降(Pclass数值从1变化到3),存活率也在下降,符合事实(有钱人更有几率存活下来)。再比如Sex这个特征,女性的存活率为74.2%,而男性的存活率仅为18.9%,这一点也符合事实,通过这些观察,可以确认这些特征的确有其实际意义也的确与存活率相关。
预处理数据
数据的预处理主要包括三个部分:
- 处理缺失数据
- 对离散变量进行处理
- 对连续变量进行处理
处理缺失数据
存在却是数据的主要包括三个特征:‘Age’、’Cabin’以及’Embarked’,其中’Cabin’暂时不作为特征处理,因此可以略过不提。‘Embarked’缺失两个record,本身是离散型变量,因此我用众数来作为这两个缺失值的估计值。’Age’缺失177个record,本身为连续型变量,用随机森林回归模型来估计这些缺失值。
‘Embarked’变量
1 | # fill the missing value with the mode value |
‘Age’变量
1 | from sklearn.ensemble import RandomForestRegressor |
需要同时返回预处理的train数据集和回归模型是因为我们还需要用这个回顾模型去对test数据集进行预处理。
处理离散变量
采用one-hot对如下变量进行处理:
- Pclass
- Sex
- Embarked
1 | def Transform_Feature(df): |
处理连续变量
对如下连续变量进行scaling:
1 | import sklearn.preprocessing as preprocessing |
最后,用同样方法对test数据集进行预处理。
Baseline模型
baseline模型使用logistic regression模型,代码如下:
1 | from sklearn.linear_model import LogisticRegression |
该模型的预测结果提交Kaggle后,准确率为75.598%。
分析Baseline模型的预测结果
观察各个特征的在模型中的系数
1 | pd.DataFrame({'features': list(train_data.columns)[1:], 'coef': list(clf.coef_.T)}) |
从结果来看,虽然部分特征的系数略显奇怪,但是基本上每个特征都与生存率有关。比如’Sex_female’的系数为1.787,而’Sex_male’的系数为-0.863,这表明女性会有大概率获救,而男性则大概率遇难。
观察bad case
将训练集分割成training set和validation set,然后在预测完validation set后,找到其中预测失误的bad case,打印出来观察特征。
1 | # split the data |
(这份bad case我毛都没看出来= =)
添加新的特征
参考了一些别人的思路,我决定加上一下特征:
- Title
- Family Szie
- IsAlone
- AgeBand
第一项可以在Name中找到,Title在一定程度上反映了社会地位以及性别、年龄,因此有一定的价值,第二项的话是因为SibSp和Parch都与存活率相关,那么两者的结合,理论上也应该相关,而且在这种灾难面前,理论上每个人都会以家族为单位尽可能展开自救或救人。第三项的原因与第二项相同,可以算是第二项的一个特例吧。第四项是对年龄进行离散化,因为实际上幼年和老年获救的概率理论上都应该比壮年高,因此离散化到几个区间可能会更好的预测结果。
以下是相关代码:
1 | # Title |
在添加完这几新的feature之后同样要进行数据的预处理,此处掠过不提(具体代码在这里)。
使用改进后的模型进行预测
此处使用的模型包括以下几种:
- Logistic Regression
- Random Forest
- Knn
- SVM
- Decision Tree
这几个模型的训练与预测过程与之前相同,此外需要作出learning curve,以防止有模型过拟合。
以logistic regression的代码为例:
1 | # build the dataset from the complete dataset |
这里可以看到其他几个模型各自的learning curve,从这些learning curve,可以看出random forest和decision tree都存在过拟合的问题, 而knn、logistic regression以及knn的learning curve相对理想,
所以我们用knn、logistic regression以及svm来组成一个投票的模型,三个模型各自独立预测,然后根据预测结果,共同决定结果。代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22# lr model
lr_clf = LogisticRegression(penalty='l1')
lr_clf.fit(X_train, Y_train)
Y_test = lr_clf.predict(X_test)
lr_result = pd.DataFrame({'PassengerId': test['PassengerId'].as_matrix(), 'Survived': Y_test.astype(np.int32)})
# knn model
knn_clf = KNeighborsClassifier(n_neighbors=5)
knn_clf.fit(X_train, Y_train)
Y_test = knn_clf.predict(X_test)
knn_result = pd.DataFrame({'PassengerId': test['PassengerId'].as_matrix(), 'Survived': Y_test.astype(np.int32)})
# svm model
svm_clf = svm.SVC()
svm_clf.fit(X_train, Y_train)
Y_test = svm_clf.predict(X_test)
svm_result = pd.DataFrame({'PassengerId': test['PassengerId'].as_matrix(), 'Survived': Y_test.astype(np.int32)})
# voting predict
result = pd.DataFrame({'PassengerId': test['PassengerId'].as_matrix()})
result['Survived_Vote'] = lr_result['Survived'] + knn_result['Survived'] + svm_result['Survived'] + rf_result['Survived'] + dt_result['Survived']
result['Survived'] = result['Survived_Vote'].apply(lambda x: 1 if x > 2 else 0)
result.drop(['Survived_Vote'], inplace=True, axis=1)
result.to_csv("../input/voting.csv", index=False)
预测结果为78.47%,比之前的baseline模型要好。
但是值得注意的是,svm模型独立的预测结果准确率为78.95%,也就是说svm的准确率甚至更高,而且svm的learning curve也非常理想。
总结
暂时就先做到这里了,以后有空再往里面加点东西,做点修改吧,这个模型的预测准确率并不高,只有78%左右,究其原因的话,可能在特征工程那一步还是做得有点草率,根据其他一些人的分析和我自己事后的一些思考,有一部分被我丢弃的特征事实上是有非常重要的意义的,比如:
- Name的长度,虽然并不清楚原因,但是Name的长度与存活率似乎正相关
- Cabin的有无,有无Cabin也严重影响存活率,可以理解为是只有活下来的人,才能准确记录自己的Cabin吧
- Fare可以类似于Age做一个离散化
- 按照Name中的姓氏做一个家族的分类,作为一个新的特征,因为同一个家族应该存活率接近
这个项目的完整ipython notebook已被我传到github上,网页版也可以在这里找到。
参考文献:
机器学习系列(3)_逻辑回归应用之Kaggle泰坦尼克之灾
Titanic Data Science Solutions