作者:xiaoyu
微信公眾號:Python資料科學
知乎:python資料分析師
前情回顧
上一篇是資料探勘的前戲,主要目的是認識資料特徵、判斷特徵重要性、觀察資料異常,掌握資料間聯絡。本篇將繼續上一篇分析進行資料探勘建模部分。
上篇資料分析的連結:
【Kaggle入門級競賽top5%排名經驗分享】— 分析篇
資料預處理
資料預處理涉及的內容很多,也包括特徵工程,是任務量最大的一部分。為了讓大家更清晰的閱讀,以下先列出處理部分大致要用到的一些方法。
- 資料清洗:缺失值,異常值,一致性;
- 特徵編碼:
one-hot
和label coding
; - 特徵分箱:等頻,等距,聚類等;
- 衍生變數:可解釋性強,適合模型輸入;
- 特徵選擇:方差選擇,卡方選擇,正則化等;
1. 資料清洗
分析部分我們看到,存在缺失值的特徵有4個:Age,Cabin,Embarked,Fare。關於缺失值處理部分博主之前介紹過一些方法:【Python資料分析基礎】: 資料缺失值處理
下面開始對缺失值分別處理。
Fare缺失值處理
首先檢視一下Fare特徵缺失:
df[df[`Fare`].isnull()]
複製程式碼
發現只有一個缺失值,其實可以直接刪除,但是好多乘客都是以一個家庭來的,這其中會有很強的聯絡,並會給我們很好的線索,因此選擇不刪除。
繼續觀察一下這個缺失值乘客有什麼特點?如何利用我們之前的分析來處理?
- 特點1:Pclass為3,我們在分析部分知道Fare和Pclass社會等級有著緊密的關係,Pclass1的Fare相對較高,Fare最低的是Pclass3;
- 特點2:該乘客的Age大於60,且為男性;
這時我們可以使用相似特徵替換方法來填補缺失值,下面來找一下與缺失值具有相似特徵的其它樣本資料:
df.loc[(df[`Pclass`]==3)&(df[`Age`]>60)&(df[`Sex`]==`male`)]
複製程式碼
找到了與之相匹配的幾位其它乘客,我們就用這幾位乘客的Fare平均值來填補。
# 提取出Name中的Surname資訊
df[`surname`] = df["Name"].apply(lambda x: x.split(`,`)[0].lower())
fare_mean_estimated = df.loc[(df[`Pclass`]==3)&(df[`Age`]>60)&(df[`Sex`]==`male`)].Fare.mean()
df.loc[df[`surname`]==`storey`,`Fare`] = fare_mean_estimated
複製程式碼
Embarked特徵缺失值
同樣,觀察Embarked
的缺失值情況:
# Embarked缺失值處理
df[df[`Embarked`].isnull()]
複製程式碼
發現兩位都是女性
。上篇視覺化分析過,pclass1
且為女性的情況下,Q港口
幾乎為0,而C港口
最多,其次S港口
,下圖為分析篇的視覺化結果。
這裡採用出現最多的港口,也就是眾數C港口進行填補。
df[`Embarked`] = df[`Embarked`].fillna(`C`)
複製程式碼
Cabin特徵缺失值
Cain特徵有70%
的缺失值,較為嚴重,如果進行大量的填補會引入更多噪聲。因為缺失值也是一種值,這裡將Cabin缺失值視為一種特殊的值來處理,並根據Cabin首個字元衍生一個新的特徵CabinCat
。
df[`CabinCat`] = pd.Categorical.from_array(df.Cabin.fillna(`0`).apply(lambda x: x[0])).codes
複製程式碼
pandas的 Categorical.from_array()
用法。程式碼含義是用“0”
替換Cabin缺失值
,並將非缺失Cabin特徵值提取出第一個值以進行分類,比如A114就是A,C345就是C,如下:
[0, C, 0, C, 0, ..., 0, C, 0, 0, 0]
Length: 1309
Categories (9, object): [0, A, B, C, ..., E, F, G, T]
複製程式碼
用Categorical.from_array()
將Cabin分成了9組,最後通過codes量化為數字,通過視覺化觀察一下分組離散化後的結果:
fig, ax = plt.subplots(figsize=(10,5))
sns.countplot(x=`CabinCat`, hue=`Survived`,data=df)
plt.show()
複製程式碼
以上視覺化看到:Cabin缺失的乘客中,遇難人數是獲救人數2倍以上,而其它有Cabin資訊的乘客中,獲救人數都相對較多。因此說明Cabin缺失與否關係到了生還的概率。
Age特徵缺失值
Age有20%缺失值,缺失值較多,大量刪除會減少樣本資訊,由於它與Cabin不同,這裡將利用其它特徵進行預測填補Age,也就是擬合未知Age特徵值,會在後續進行處理。
資料一致性分析
當我們拿到資料後,我們要謹記一個道理:不要完全相信資料。即使不是異常值,也有可能是錯誤的資訊,那就是檢查資料的一致性。
本例中,我們通過兩個錯誤的修正來理解一下。
錯誤1:SibSp和Parch特徵存在不一致
df.loc[df[`surname`]==`abbott`,[`Name`,`Sex`,`Age`,`SibSp`,`Parch`]]
複製程式碼
為了方便閱讀,下面用序號來代替名字。
首先尋找到了船上姓 abbott
的所有人,即一家人。發現:392 乘客只有13歲,確有兩個孩子Parch=2
(理論上不太可能),而279乘客35歲,有一個孩子,還有一個兄弟姐妹,746有一個家長和一個兄弟姐妹。很明顯,資訊是錯誤的,279與392乘客的資訊寫反了。正確的資訊是一位母親帶著兩個孩子,所以改為:279乘客為SibSp=0,Parh=2
,392歲的乘客是:SibSp=1, Parh=1
。下面是修改程式碼:
df.loc[(df[`surname`]==`abbott`)&(df[`Age`]==35),`SibSp`] = 0
df.loc[(df[`surname`]==`abbott`)&(df[`Age`]==35),`Parch`] = 2
df.loc[(df[`surname`]==`abbott`)&(df[`Age`]==13),`SibSp`] = 1
df.loc[(df[`surname`]==`abbott`)&(df[`Age`]==13),`Parch`] = 1
複製程式碼
錯誤2:SibSp和Parch特徵存在不一致
df.loc[df[`surname`]==`ford`,[`Name`,`Sex`,`Age`,`SibSp`,`Parch`]]
複製程式碼
同理,ford
一家人也出現了一致性錯誤的問題,具體大家可自行分析。正確的是:一位母親帶著三個孩子,而最後一位乘客為測試集裡的樣本,推測很可能是父親。下面是修改程式碼:
df.loc[(df[`surname`]==`ford`)&(df[`Age`]==16),`SibSp`] = 3
df.loc[(df[`surname`]==`ford`)&(df[`Age`]==16),`Parch`] = 1
df.loc[(df[`surname`]==`ford`)&(df[`Age`]==9),`SibSp`] = 3
df.loc[(df[`surname`]==`ford`)&(df[`Age`]==9),`Parch`] = 1
df.loc[(df[`surname`]==`ford`)&(df[`Age`]==21),`SibSp`] = 3
df.loc[(df[`surname`]==`ford`)&(df[`Age`]==21),`Parch`] = 1
df.loc[(df[`surname`]==`ford`)&(df[`Age`]==48),`SibSp`] = 0
df.loc[(df[`surname`]==`ford`)&(df[`Age`]==48),`Parch`] = 4
df.loc[(df[`surname`]==`ford`)&(df[`Age`]==18),`SibSp`] = 3
df.loc[(df[`surname`]==`ford`)&(df[`Age`]==18),`Parch`] = 1
複製程式碼
2. 資料變換
衍生變數
分析部分沒提及到Name
特徵,因為每個人的名字都不一樣。但是一些人可能是群體行動,比如一家人一起,而一家人的surname
是一樣的,因此這時候就可以通過surname找到一個家庭群體。家庭群體有什麼用?我們後面會提到。
實際上,如果我們深入分析,Name特徵是非常重要的。試想一下乘客有沒有可能是和其他人一起上船的?是一家人?情侶?還是獨自一人?而這一群人生還的概率應該是存在共性的,比如:有一個5人之家,有4人死亡,可以推測第5個人極有可能死亡。
下面是對所有特徵進行衍生的新特徵變數。
# 從Name中提取Title資訊,因為同為男性,Mr.和 Master.的生還率是不一樣的
df["Title"] = df["Name"].apply(lambda x: re.search(` ([A-Za-z]+).`,x).group(1))
title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Dr": 5, "Rev": 6, "Major": 7, "Col": 7, "Mlle": 2, "Mme": 3,"Don": 9,"Dona": 9, "Lady": 10, "Countess": 10, "Jonkheer": 10, "Sir": 9, "Capt": 7, "Ms": 2}
# 量化Title資訊
df["TitleCat"] = df.loc[:,`Title`].map(title_mapping)
# SibSp和Parch特徵進行組合
df["FamilySize"] = df["SibSp"] + df["Parch"] + 1
# 根據FamilySize分佈進行分箱
df["FamilySize"] = pd.cut(df["FamilySize"], bins=[0,1,4,20], labels=[0,1,2])
# 從Name特徵衍生出Name的長度
df["NameLength"] = df["Name"].apply(lambda x: len(x))
# 量化Embarked特徵
df["Embarked"] = pd.Categorical.from_array(df.Embarked).codes
# 對Sex特徵進行獨熱編碼分組
df = pd.concat([df,pd.get_dummies(df[`Sex`])],axis=1)
複製程式碼
下面衍生特徵變數的說明:
- Title:從Name中提取Title資訊,因為同為男性,Mr.和 Master.的生還率是不一樣的;
- TitleCat:對映並量化Title資訊,雖然這個特徵可能會與Sex有共線性,但是我們先衍生出來,後進行篩選;
- FamilySize:視覺化分析部分看到SibSp和Parch分佈相似,固將SibSp和Parch特徵進行組合;
- NameLength:從Name特徵衍生出Name的長度,因為有的國家名字越短代表越顯貴;
- CabinCat:Cabin的分組資訊;
高階衍生變數
【1】人物衍生特徵
由於兒童的生還率較高,因此將所有乘客兒童單獨提取出來(這裡設定為18歲)。而對於成年人女性生還概率比較高,所以又非為成年女性和成年男性。程式碼如下:
# 婦女/兒童 男士標籤
child_age = 18
def get_person(passenger):
age, sex = passenger
if (age < child_age):
return `child`
elif (sex == `female`):
return `female_adult`
else:
return `male_adult`
df = pd.concat([df, pd.DataFrame(df[[`Age`, `Sex`]].apply(get_person, axis=1), columns=[`person`])],axis=1)
df = pd.concat([df,pd.get_dummies(df[`person`])],axis=1)
複製程式碼
【2】Ticket衍生特徵
下面基於Ticket
衍生出了幾個高階特徵變數,其含義:如果幾個人擁有相同的Ticket號碼,那麼意味著他門是一個小群體(一家人或情侶等),而又因為男性女性還概率本省存在差異,因此將分別衍生出幾個人物標籤特徵,即分群體情況下的男女生還特徵。以下是程式碼實現:
table_ticket = pd.DataFrame(df["Ticket"].value_counts())
table_ticket.rename(columns={`Ticket`:`Ticket_Numbers`}, inplace=True)
table_ticket[`Ticket_dead_women`] = df.Ticket[(df.female_adult == 1.0)
& (df.Survived == 0.0)
& ((df.Parch > 0) | (df.SibSp > 0))].value_counts()
table_ticket[`Ticket_dead_women`] = table_ticket[`Ticket_dead_women`].fillna(0)
table_ticket[`Ticket_dead_women`][table_ticket[`Ticket_dead_women`] > 0] = 1.0
table_ticket[`Ticket_surviving_men`] = df.Ticket[(df.male_adult == 1.0)
& (df.Survived == 1.0)
& ((df.Parch > 0) | (df.SibSp > 0))].value_counts()
table_ticket[`Ticket_surviving_men`] = table_ticket[`Ticket_surviving_men`].fillna(0)
table_ticket[`Ticket_surviving_men`][table_ticket[`Ticket_surviving_men`] > 0] = 1.0
# Ticket特徵量化
table_ticket["Ticket_Id"] = pd.Categorical.from_array(table_ticket.index).codes
table_ticket["Ticket_Id"][table_ticket["Ticket_Numbers"] < 3 ] = -1
# Ticket數量分箱
table_ticket["Ticket_Numbers"] = pd.cut(table_ticket["Ticket_Numbers"], bins=[0,1,4,20], labels=[0,1,2])
df = pd.merge(df, table_ticket, left_on="Ticket",right_index=True, how=`left`, sort=False)
複製程式碼
同理,基於衍生變數Surname
也可以衍生出高階特徵變數,以及Cabin的奇偶性衍生特徵
。
Age缺失值處理
前面說了將採用擬合的方法來填補Age
缺失值,那為什麼一定要在後面處理呢?
原因如下:
- 其它特徵還存在缺失值,放入擬合模型影響預測效果;
- 特徵保持原生符號,還沒有進行量化,無法輸入模型;
因為上面已經將所提問題解決,因此可以開始擬合Age缺失值
。這部分使用了隨機森林的ExtraTreesRegressor
模型進行擬合,程式碼如下:
from sklearn.ensemble import RandomForestClassifier, ExtraTreesRegressor
classers = [`Fare`,`Parch`,`Pclass`,`SibSp`,`TitleCat`,
`CabinCat`,`female`,`male`, `Embarked`, `FamilySize`, `NameLength`,`Ticket_Numbers`,`Ticket_Id`]
etr = ExtraTreesRegressor(n_estimators=200,random_state=0)
X_train = df[classers][df[`Age`].notnull()]
Y_train = df[`Age`][df[`Age`].notnull()]
X_test = df[classers][df[`Age`].isnull()]
etr.fit(X_train.as_matrix(),np.ravel(Y_train))
age_preds = etr.predict(X_test.as_matrix())
df[`Age`][df[`Age`].isnull()] = age_preds
複製程式碼
想繼續看一下擬合的結果是怎麼樣,可以通過視覺化來觀察:
# Age缺失值填補後的情況
X_test[`Age`] = pd.Series(age_preds)
f,ax=plt.subplots(figsize=(10,5))
sns.swarmplot(x=`Pclass`,y=`Age`,data=X_test)
plt.show()
複製程式碼
觀察:通過擬合得到的Age缺失值的視覺化展示,總體上看效果還可以,具體需要進一步排查。
3. 特徵選擇
過濾法—方差分析
這裡特徵採用 ANOVA
方差分析的 F值
來對各個特徵變數打分,打分的意義是:各個特徵變數對目標變數的影響權重。程式碼如下,使用了sklearn的feature_selection
:
from sklearn.feature_selection import SelectKBest, f_classif,chi2
target = data_train["Survived"].values
features= [`female`,`male`,`Age`,`male_adult`,`female_adult`, `child`,`TitleCat`,
`Pclass`,`Ticket_Id`,`NameLength`,`CabinType`,`CabinCat`, `SibSp`, `Parch`,
`Fare`,`Embarked`,`Surname_Numbers`,`Ticket_Numbers`,`FamilySize`,
`Ticket_dead_women`,`Ticket_surviving_men`,
`Surname_dead_women`,`Surname_surviving_men`]
train = df[0:891].copy()
test = df[891:].copy()
selector = SelectKBest(f_classif, k=len(features))
selector.fit(train[features], target)
scores = -np.log10(selector.pvalues_)
indices = np.argsort(scores)[::-1]
print("Features importance :")
for f in range(len(scores)):
print("%0.2f %s" % (scores[indices[f]],features[indices[f]]))
複製程式碼
此部分將之前訓練和測試合併的資料集分開,因為最後我們要對測試集進行預測。特徵選擇權重結果如下(可以通過視覺化的方法展示出來):
這裡分數越高代表特徵權重越大,當然我們可以規定相應的閾值來選擇權重大的特徵。
特徵相關性分析
除了對特徵權重選擇外,我們也要分析特徵相關性來篩選特徵。相關性大的特徵容易造成過擬合現象,因此需要進行剔除。最好的情況就是:所有特徵相關性很低,各自的方差或者說資訊量很高。
使用了seaborn的heatmap展示相關性,程式碼如下:
features_selected = features
# data_corr
df_corr = df[features_selected].copy()
colormap = plt.cm.RdBu
plt.figure(figsize=(20,20))
sns.heatmap(df_corr.corr(),linewidths=0.1,vmax=1.0, square=True, cmap=colormap, linecolor=`white`, annot=True)
plt.show()
複製程式碼
3 建模預測
建立模型
這是個明顯的監督分類問題,因此可選擇的模型演算法很多,或者模型融合等來提高準確度。這裡採用了整合學習的隨機森林RandomForest
模型。程式碼如下:
from sklearn import cross_validation
rfc = RandomForestClassifier(n_estimators=3000, min_samples_split=4, class_weight={0:0.745,1:0.255})
# rfc = AdaBoostClassifier(n_estimators=3000, learning_rate=0.1, random_state=1)
# 交叉驗證,建模隨機森林
kf = cross_validation.KFold(train.shape[0], n_folds=3, random_state=1)
scores = cross_validation.cross_val_score(rfc, train[features_selected], target, cv=kf)
print("Accuracy: %0.3f (+/- %0.2f) [%s]" % (scores.mean()*100, scores.std()*100, `RFC Cross Validation`))
rfc.fit(train[features_selected], target)
score = rfc.score(train[features_selected], target)
print("Accuracy: %0.3f [%s]" % (score*100, `RFC full test`))
importances = rfc.feature_importances_
indices = np.argsort(importances)[::-1]
for f in range(len(features_selected)):
print("%d. feature %d (%f) %s" % (f + 1, indices[f]+1, importances[indices[f]]*100, features_selected[indices[f]]))
複製程式碼
為防止過擬合,採用了K折交叉驗證
進行取樣。整合學習等高階模型有自帶的特徵打分方法,訓練資料後,我們可以通過feature_importances
得到特徵權重分數(當特徵特別多時,也可以作為初始的特徵篩選方法)。當然這也可以通過視覺化的方法展示出來。
輸入結果如下:
模型預測
# 預測目標值
rfc.fit(train[features_selected], target)
predictions = rfc.predict(test[features_selected])
複製程式碼
輸出檔案
# 輸出檔案
PassengerId =np.array(test["PassengerId"]).astype(int)
my_prediction = pd.DataFrame(predictions, PassengerId, columns = ["Survived"])
my_prediction.to_csv("my_prediction.csv", index_label = ["PassengerId"])
複製程式碼
最後,將預測結果輸出到excel表中。如果你到Kaggle
將輸出的資料提交,你應該得到的分數是:0.8188,也就是說你的準確率是0.8188。這個分數可以達到500/11000
的排名(top5%)。
4 總結
本篇分析了資料預處理以及建模的部分,完成了最後的生還者預測,有幾下幾點還需要提高的地方:
- 尋找更多衍生特徵,提高模型輸入質量;
- 嘗試多種模型,對比預測結果,或者可以使用高階模型融合,以及stacking二次融合優化來提高準確率;
- 嘗試多種方法在眾多特徵中篩選重要特徵;
- 對於一些模糊異常值進一步檢測和處理;
- 提高填補缺失值的準確度,減少資料中的噪音;
以上就是本次專案的全部內容,後續會繼續分享新資料分析挖掘專案,敬請期待。
參考:https://www.kaggle.com/francksylla/titanic-machine-learning-from-disaster/
關注微信公眾號:Python資料科學,發現更多精彩內容。