比賽頁面:https://www.kaggle.com/c/titanicpython
Titanic大概是kaggle上最受歡迎的項目了,有7000多支隊伍參加,多年來誕生了無數關於該比賽的經驗分享。正是因爲前人們的無私奉獻,我才能無痛完成本篇。git
事實上kaggle上的不少kernel都聚焦於某個特定的層面(好比提取某個鮮爲人知的特徵、使用超複雜的算法、專作EDA畫圖之類的),固然由於這些做者自己大都是大神級別的,因此平日裏喜歡鑽研一些奇淫巧技。而我目前階段更加註重一些總體流程化的方面,所以這篇提供了一個端到端的解決方案。github
關於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.app
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.dom
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.ide
主要是讓參賽選手根據訓練集中的乘客數據和存活狀況進行建模,進而使用模型預測測試集中的乘客是否會存活。乘客特徵總共有11個,如下列出。固然也能夠根據狀況本身生成新特徵,這就是特徵工程(feature engineering)要作的事情了。學習
總的來講Titanic和其餘比賽比起來數據量算是很小的了,訓練集合測試集加起來總共891+418=1309個。由於數據少,因此很容易過擬合(overfitting),一些算法如GradientBoostingTree的樹的數量就不能太多,須要在調參的時候多加註意。測試
下面我先列出目錄,而後挑幾個關鍵的點說明一下:this
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()
至此,數據中已完好失值。
廣泛認爲泰坦尼克號中女人的存活率遠高於男人,以下圖所示:
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))
我將‘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))
選擇了7個算法,分別作交叉驗證(cross-validation)來評估效果:
因爲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)
這部分沒什麼好說的,選定幾個參數用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%的交叉驗證準確率,迄今爲止最高。
我用了三種集成方法: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]))
總的來講根據交叉驗證的結果,集成算法並無比單個算法提高太多,緣由多是:
最後是提交結果:
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)