完整代碼見kaggle kernel 或 GitHub
比賽頁面:https://www.kaggle.com/c/titanic
Titanic大概是kaggle上最受歡迎的項目了,有7000多支隊伍參加,多年來誕生了無數關於該比賽的經驗分享。正是由於前人們的無私奉獻,我才能無痛完成本篇。
事實上kaggle上的很多kernel都聚焦於某個特定的層面(比如提取某個不為人知的特征、使用超復雜的算法、專做EDA畫圖之類的),當然因為這些作者本身大都是大神級別的,所以平日里喜歡鑽研一些奇淫巧技。而我目前階段更加注重一些整體流程化的方面,因此這篇提供了一個端到端的解決方案。
關於Titanic,這里先貼一段比賽介紹:
The sinking of the RMS Titanic is one of the most infamous shipwrecks in history. On April 15, 1912, during her maiden voyage, the Titanic sank after colliding with an iceberg, killing 1502 out of 2224 passengers and crew. This sensational tragedy shocked the international community and led to better safety regulations for ships.
One of the reasons that the shipwreck led to such loss of life was that there were not enough lifeboats for the passengers and crew. Although there was some element of luck involved in surviving the sinking, some groups of people were more likely to survive than others, such as women, children, and the upper-class.
In this challenge, we ask you to complete the analysis of what sorts of people were likely to survive. In particular, we ask you to apply the tools of machine learning to predict which passengers survived the tragedy.
主要是讓參賽選手根據訓練集中的乘客數據和存活情況進行建模,進而使用模型預測測試集中的乘客是否會存活。乘客特征總共有11個,以下列出。當然也可以根據情況自己生成新特征,這就是特征工程(feature engineering)要做的事情了。
- PassengerId => 乘客ID
- Pclass => 客艙等級(1/2/3等艙位)
- Name => 乘客姓名
- Sex => 性別
- Age => 年齡
- SibSp => 兄弟姐妹數/配偶數
- Parch => 父母數/子女數
- Ticket => 船票編號
- Fare => 船票價格
- Cabin => 客艙號
- Embarked => 登船港口
總的來說Titanic和其他比賽比起來數據量算是很小的了,訓練集合測試集加起來總共891+418=1309個。因為數據少,所以很容易過擬合(overfitting),一些算法如GradientBoostingTree的樹的數量就不能太多,需要在調參的時候多加注意。
下面我先列出目錄,然后挑幾個關鍵的點說明一下:
- 數據清洗(Data Cleaning)
- 探索性可視化(Exploratory Visualization)
- 特征工程(Feature Engineering)
- 基本建模&評估(Basic Modeling & Evaluation)
- 參數調整(Hyperparameters Tuning)
- 集成方法(Ensemble Methods)
數據清洗(Data Cleaning)
1 full.isnull().sum()
首先來看缺失數據,上圖顯示Age,Cabin,Embarked,Fare這些變量存在缺失值(Survived是預測值)。其中Embarked和Fare的缺失值較少,可以直接用眾數和中位數插補。
Cabin的缺失值較多,可以考慮比較有Cabin數據和無Cabin數據的乘客存活情況。
1 pd.pivot_table(full,index=['Cabin'],values=['Survived']).plot.bar(figsize=(8,5)) 2 plt.title('Survival Rate')
上面一張圖顯示在有Cabin數據的乘客的存活率遠高於無Cabin數據的乘客,所以我們可以將Cabin的有無數據作為一個特征。
Age的缺失值有263個,網上有人說直接根據其他變量用回歸模型預測Age的缺失值,我把訓練集分成兩份測試了一下,效果並不好,可能是因為Age和其他變量沒有很強的相關性,從下面這張相關系數圖也能看得出來。
所以這里采用的的方法是先根據‘Name’提取‘Title’,再用‘Title’的中位數對‘Age‘進行插補:
1 full['Title']=full['Name'].apply(lambda x: x.split(',')[1].split('.')[0].strip()) 2 full.Title.value_counts()
Title中的Master主要代表little boy,然而卻沒有代表little girl的Title,由於小孩的生存率往往較高,所以有必要找出哪些是little girl,再填補Age的缺失值。
先假設little girl都沒結婚(一般情況下該假設都成立),所以little girl肯定都包含在Miss里面。little boy(Master)的年齡最大值為14歲,所以相應的可以設定年齡小於等於14歲的Miss為little girl。對於年齡缺失的Miss,可以用(Parch!=0)來判定是否為little girl,因為little girl往往是家長陪同上船,不會一個人去。
以下代碼創建了“Girl”的Title,並以Title各自的中位數填補Age缺失值。
1 def girl(aa): 2 if (aa.Age!=999)&(aa.Title=='Miss')&(aa.Age<=14): 3 return 'Girl' 4 elif (aa.Age==999)&(aa.Title=='Miss')&(aa.Parch!=0): 5 return 'Girl' 6 else: 7 return aa.Title 8 9 full['Title']=full.apply(girl,axis=1) 10 11 Tit=['Mr','Miss','Mrs','Master','Girl','Rareman','Rarewoman'] 12 for i in Tit: 13 full.loc[(full.Age==999)&(full.Title==i),'Age']=full.loc[full.Title==i,'Age'].median()
至此,數據中已無缺失值。
探索性可視化(Exploratory Visualization)
普遍認為泰坦尼克號中女人的存活率遠高於男人,如下圖所示:
1 pd.crosstab(full.Sex,full.Survived).plot.bar(stacked=True,figsize=(8,5),color=['#4169E1','#FF00FF']) 2 plt.xticks(rotation=0,size='large') 3 plt.legend(bbox_to_anchor=(0.55,0.9))

下圖顯示年齡與存活人數的關系,可以看出小於5歲的小孩存活率很高。
客艙等級(Pclass)自然也與存活率有很大關系,下圖顯示1號倉的存活情況最好,3號倉最差。
1 fig,axes=plt.subplots(2,3,figsize=(15,8)) 2 Sex1=['male','female'] 3 for i,ax in zip(Sex1,axes): 4 for j,pp in zip(range(1,4),ax): 5 PclassSex=full[(full.Sex==i)&(full.Pclass==j)]['Survived'].value_counts().sort_index(ascending=False) 6 pp.bar(range(len(PclassSex)),PclassSex,label=(i,'Class'+str(j))) 7 pp.set_xticks((0,1)) 8 pp.set_xticklabels(('Survived','Dead')) 9 pp.legend(bbox_to_anchor=(0.6,1.1))
特征工程(Feature Engineering)
我將‘Title‘、’Pclass‘,’Parch‘三個變量結合起來畫了這張圖,以平均存活率的降序排列,然后以80%存活率和50%存活率來划分等級(1,2,3),產生新的’MPPS‘特征。
1 TPP.plot(kind='bar',figsize=(16,10)) 2 plt.xticks(rotation=40) 3 plt.axhline(0.8,color='#BA55D3') 4 plt.axhline(0.5,color='#BA55D3') 5 plt.annotate('80% survival rate',xy=(30,0.81),xytext=(32,0.85),arrowprops=dict(facecolor='#BA55D3',shrink=0.05)) 6 plt.annotate('50% survival rate',xy=(32,0.51),xytext=(34,0.54),arrowprops=dict(facecolor='#BA55D3',shrink=0.05))
基本建模&評估(Basic Modeling & Evaluation)
選擇了7個算法,分別做交叉驗證(cross-validation)來評估效果:
- K近鄰(k-Nearest Neighbors)
- 邏輯回歸(Logistic Regression)
- 朴素貝葉斯分類器(Naive Bayes classifier)
- 決策樹(Decision Tree)
- 隨機森林(Random Forest)
- 梯度提升樹(Gradient Boosting Decision Tree)
- 支持向量機(Support Vector Machine)
由於K近鄰和支持向量機對數據的scale敏感,所以先進行標准化(standard-scaling):
1 from sklearn.preprocessing import StandardScaler 2 scaler=StandardScaler() 3 X_scaled=scaler.fit(X).transform(X) 4 test_X_scaled=scaler.fit(X).transform(test_X)
最后的評估結果如下: 邏輯回歸,梯度提升樹和支持向量機的效果相對較好。
1 # used scaled data 2 names=['KNN','LR','NB','Tree','RF','GDBT','SVM'] 3 for name, model in zip(names,models): 4 score=cross_val_score(model,X_scaled,y,cv=5) 5 print("{}:{},{}".format(name,score.mean(),score))
接下來可以挑選一個模型進行錯誤分析,提取該模型中錯分類的觀測值,尋找其中規律進而提取新的特征,以圖提高整體准確率。
用sklearn中的KFold將訓練集分為10份,分別提取10份數據中錯分類觀測值的索引,最后再整合到一塊。
1 # extract the indices of misclassified observations 2 rr=[] 3 for train_index, val_index in kf.split(X): 4 pred=model.fit(X.ix[train_index],y[train_index]).predict(X.ix[val_index]) 5 rr.append(y[val_index][pred!=y[val_index]].index.values) 6 7 # combine all the indices 8 whole_index=np.concatenate(rr) 9 len(whole_index)
先查看錯分類觀測值的整體情況:
下面通過分組分析可發現:錯分類的觀測值中男性存活率高達83%,女性的存活率則均不到50%,這與我們之前認為的女性存活率遠高於男性不符,可見不論在男性和女性中都存在一些特例,而模型並沒有從現有特征中學習到這些。
通過進一步分析我最后新加了個名為”MPPS”的特征。
1 full.loc[(full.Title=='Mr')&(full.Pclass==1)&(full.Parch==0)&((full.SibSp==0)|(full.SibSp==1)),'MPPS']=1 2 full.loc[(full.Title=='Mr')&(full.Pclass!=1)&(full.Parch==0)&(full.SibSp==0),'MPPS']=2 3 full.loc[(full.Title=='Miss')&(full.Pclass==3)&(full.Parch==0)&(full.SibSp==0),'MPPS']=3 4 full.MPPS.fillna(4,inplace=True)
參數調整(Hyperparameters tuning)
這部分沒什么好說的,選定幾個參數用grid search死命調就是了~
1 param_grid={'n_estimators':[100,120,140,160],'learning_rate':[0.05,0.08,0.1,0.12],'max_depth':[3,4]} 2 grid_search=GridSearchCV(GradientBoostingClassifier(),param_grid,cv=5) 3 4 grid_search.fit(X_scaled,y) 5 6 grid_search.best_params_,grid_search.best_score_
({'learning_rate': 0.12, 'max_depth': 4, 'n_estimators': 100}, 0.85072951739618408)
通過調參,Gradient Boosting Decision Tree能達到85%的交叉驗證准確率,迄今為止最高。
集成方法(Ensemble Methods)
我用了三種集成方法:Bagging、VotingClassifier、Stacking。
調參過的單個算法和Bagging以及VotingClassifier的總體比較如下:
1 names=['KNN','LR','NB','CART','RF','GBT','SVM','VC_hard','VC_soft','VCW_hard','VCW_soft','Bagging'] 2 for name,model in zip(names,models): 3 score=cross_val_score(model,X_scaled,y,cv=5) 4 print("{}: {},{}".format(name,score.mean(),score))
scikit-learn中目前沒有stacking的實現方法,所以我參照了這兩篇文章中的實現方法:
https://dnc1994.com/2016/04/rank-10-percent-in-first-kaggle-competition/
https://www.kaggle.com/arthurtok/introduction-to-ensembling-stacking-in-python
我用了邏輯回歸、K近鄰、支持向量機、梯度提升樹作為第一層模型,隨機森林作為第二層模型。
1 from sklearn.model_selection import StratifiedKFold 2 n_train=train.shape[0] 3 n_test=test.shape[0] 4 kf=StratifiedKFold(n_splits=5,random_state=1,shuffle=True) 5 6 def get_oof(clf,X,y,test_X): 7 oof_train=np.zeros((n_train,)) 8 oof_test_mean=np.zeros((n_test,)) 9 oof_test_single=np.empty((5,n_test)) 10 for i, (train_index,val_index) in enumerate(kf.split(X,y)): 11 kf_X_train=X[train_index] 12 kf_y_train=y[train_index] 13 kf_X_val=X[val_index] 14 15 clf.fit(kf_X_train,kf_y_train) 16 17 oof_train[val_index]=clf.predict(kf_X_val) 18 oof_test_single[i,:]=clf.predict(test_X) 19 oof_test_mean=oof_test_single.mean(axis=0) 20 return oof_train.reshape(-1,1), oof_test_mean.reshape(-1,1) 21 22 LR_train,LR_test=get_oof(LogisticRegression(C=0.06),X_scaled,y,test_X_scaled) 23 KNN_train,KNN_test=get_oof(KNeighborsClassifier(n_neighbors=8),X_scaled,y,test_X_scaled) 24 SVM_train,SVM_test=get_oof(SVC(C=4,gamma=0.015),X_scaled,y,test_X_scaled) 25 GBDT_train,GBDT_test=get_oof(GradientBoostingClassifier(n_estimators=120,learning_rate=0.12,max_depth=4),X_scaled,y,test_X_scaled) 26 27 stack_score=cross_val_score(RandomForestClassifier(n_estimators=1000),X_stack,y_stack,cv=5) 28 # cross-validation score of stacking 29 stack_score.mean(),stack_score
Stacking的最終結果:
0.84069254167070062, array([ 0.84916201, 0.79888268, 0.85393258, 0.83707865, 0.86440678]))
總的來說根據交叉驗證的結果,集成算法並沒有比單個算法提升太多,原因可能是:
- 開頭所說Titanic這個數據集太小,模型沒有得到充分的訓練
- 集成方法中子模型的相關性太強
- 集成方法可能本身也需要調參
- 我實現的方法錯了???
最后是提交結果:
1 pred=RandomForestClassifier(n_estimators=500).fit(X_stack,y_stack).predict(X_test_stack) 2 tt=pd.DataFrame({'PassengerId':test.PassengerId,'Survived':pred}) 3 tt.to_csv('submission.csv',index=False)