kaggle入門之戰(zhàn)——Titanic

機(jī)器學(xué)習(xí)的理論部分大致過了一遍了,下一步要理論聯(lián)系實(shí)踐了。Kaggle是一個很好的練手場,這個數(shù)據(jù)挖掘比賽平臺最寶貴的資源有兩個:

  • 各種真實(shí)場景產(chǎn)生的數(shù)據(jù)集。這些數(shù)據(jù)集并不像著名的MNIST、CIFAR-10等數(shù)據(jù)集一樣整齊和完整,它們當(dāng)中有缺失值,有難以處理的特征,也有離群值和噪聲,這更接近實(shí)際應(yīng)用中的情況,可以鍛煉我們數(shù)據(jù)預(yù)處理、數(shù)據(jù)清洗、特征工程的能力。
  • 各路大牛在線分享經(jīng)驗(yàn),討論從數(shù)據(jù)中發(fā)現(xiàn)的模式。這其實(shí)才是Kaggle最吸引人的地方,因?yàn)檫@些數(shù)據(jù)比賽并沒有官方的最好答案,在這里一切以最后的score說話,很多高分team處理數(shù)據(jù)的方法是很值得學(xué)習(xí)的。

和多數(shù)小白一樣,在面對Kaggle眾多比賽無從下手的時候,我選擇從最簡單的Titanic預(yù)測生還者比賽入門。

1、數(shù)據(jù)清洗和預(yù)處理

數(shù)據(jù)的質(zhì)量決定模型能達(dá)到的上界。這話說的無比正確。機(jī)器學(xué)習(xí)的算法模型是用來挖掘數(shù)據(jù)中潛在模式的,但若是數(shù)據(jù)太過雜亂,潛在的模式就很難找到,更糟的情況是,我們所收集的數(shù)據(jù)的特征和我們想預(yù)測的標(biāo)簽之間并沒有太大關(guān)聯(lián),這時候這個特征就像噪音一樣只會干擾我們的模型做出準(zhǔn)確的預(yù)測。

綜上所述,我們要對拿到手的數(shù)據(jù)集進(jìn)行分析,并看看各個特征到底會不會顯著影響到我們要預(yù)測的標(biāo)簽值。

首先我們導(dǎo)入機(jī)器學(xué)習(xí)和數(shù)據(jù)挖掘中最常用也最強(qiáng)大的幾個庫——numpy、pandas、matplotlib。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

第一步是使用pd.read_csv()方法導(dǎo)入數(shù)據(jù):

train = pd.read_csv(r'C:\Users\Sidney Nash\Desktop\data\train.csv')
test = pd.read_csv(r'C:\Users\Sidney Nash\Desktop\data\test.csv')

第二步是展示數(shù)據(jù)和觀察數(shù)據(jù)(由于數(shù)據(jù)量大,一般用head()展示前五行):

train.head()

可以看到,每個乘客有12個屬性,其中PassengerId在這里只起到索引作用,而Survived是我們要預(yù)測的目標(biāo),因此實(shí)際上我們需要處理的特征一共有10個,因此我們可以先去掉PassengerId和Survived,并對余下特征進(jìn)行觀察:

drop_features=['PassengerId','Survived']
train_drop=train.drop(drop_features,axis=1)
train_drop.head()

我們看到,這些特征的類型各不相同,有整數(shù)型的、浮點(diǎn)數(shù)型的,也有字符型的,我們先審視一下各特征的類型:

train_drop.dtypes.sort_values()

out:
Pclass        int64
SibSp         int64
Parch         int64
Age         float64
Fare        float64
Name         object
Sex          object
Ticket       object
Cabin        object
Embarked     object
dtype: object

然后按把同類型的放在一起:

train_drop.select_dtypes(include='int64').head()
train_drop.select_dtypes(include='float64').head()
train_drop.select_dtypes(include='object').head()

知道了各類型的特征都有哪些,下一步我們要看看特征中是否有缺失值:

train.isnull().sum()[lambda x: x>0]

out:
Age         177
Cabin       687
Embarked      2
dtype: int64

可以看到,Age和Cabin兩個特征的缺失值數(shù)量較多,于是我們接下來重點(diǎn)關(guān)注這兩個特征的缺失值補(bǔ)全就行了,是這樣嗎?NoNoNo,這只是訓(xùn)練集train,我們不能想當(dāng)然地認(rèn)為test中的情形和train相同,我們看一下test中的情況:

test.isnull().sum()[lambda x: x>0]

out:
Age       86
Fare       1
Cabin    327
dtype: int64

果然還是有些差別的,雖然差別很微小,但依然可能對模型結(jié)果產(chǎn)生影響。

實(shí)際上,pandas中有更為方便的方法來列出缺失值以及各特征的一些統(tǒng)計(jì)信息,那就是info()和describe():

train.info()

out:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
PassengerId    891 non-null int64
Survived       891 non-null int64
Pclass         891 non-null int64
Name           891 non-null object
Sex            891 non-null object
Age            714 non-null float64
SibSp          891 non-null int64
Parch          891 non-null int64
Ticket         891 non-null object
Fare           891 non-null float64
Cabin          204 non-null object
Embarked       889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.6+ KB
train.describe()

當(dāng)然這里我們只列出了train的統(tǒng)計(jì)信息,對test也是同理。進(jìn)行到這里,我們發(fā)現(xiàn)每次都對train和test進(jìn)行同樣的操作很麻煩,后續(xù)我們添加新特征或刪改原特征的時候更是如此,為什么不把train和test合并起來統(tǒng)一操作呢?下面我們把兩者合并(注意,合并的時候不能讓各個記錄重新排序,否則我們難以再將train和test拆分開來):

titanic=pd.concat([train, test], sort=False)
#這里記錄train中的記錄個數(shù)是為了后面將train和test拆分開來
len_train=train.shape[0]

現(xiàn)在我們查看合并后的titanic的信息:

titanic.info()

out:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 12 columns):
PassengerId    1309 non-null int64
Survived       891 non-null float64
Pclass         1309 non-null int64
Name           1309 non-null object
Sex            1309 non-null object
Age            1046 non-null float64
SibSp          1309 non-null int64
Parch          1309 non-null int64
Ticket         1309 non-null object
Fare           1308 non-null float64
Cabin          295 non-null object
Embarked       1307 non-null object
dtypes: float64(3), int64(4), object(5)
memory usage: 132.9+ KB
len_train==891

out:
True

我們可以看到Age、Fare、Cabin和Embarked四個特征均有缺失值(注意這里的Survived并沒有缺失值,因?yàn)橹挥衪rain有標(biāo)簽Survived,test的Survived是我們的訓(xùn)練目標(biāo))。

處理缺失值一般有兩種方式,第一種就是舍棄包含較多缺失值的特征,這個做法對我們這個小規(guī)模的數(shù)據(jù)集來說不太適用,因?yàn)槲覀儽緛碚莆盏男畔⒕筒欢?,若再舍棄一些信息,可能會造成模型效果不佳。第二種是進(jìn)行缺失值補(bǔ)全,一般根據(jù)實(shí)際情況將缺失值補(bǔ)全為0、特征列均值或中位數(shù),或者將缺失值劃入一個新的類別特殊處理。

在進(jìn)行特征值補(bǔ)全之前,我們先處理一個雖然沒有缺失值但很難利用的特征——Name。

每個人的名字都是獨(dú)一無二的,名字和幸存與否看起來并沒有直接關(guān)聯(lián),那怎么利用這個特征呢?有用的信息其實(shí)隱藏在稱呼當(dāng)中,比如我們猜測上救生艇的時候是女士優(yōu)先,那么稱呼為Mrs和Miss的就比稱呼為Mr的更可能幸存。于是我們從Name特征中其稱呼并建立新特征列Title。

titanic['Title'] = titanic.Name.apply(lambda name: name.split(',')[1].split('.')[0].strip())

看一下效果:

titanic.head()

ok,我們已經(jīng)把稱呼提取出來了,下面我們來看看稱呼的種類和數(shù)量:

titanic.Title.value_counts()

out:
Mr              757
Miss            260
Mrs             197
Master           61
Rev               8
Dr                8
Col               4
Mlle              2
Ms                2
Major             2
Capt              1
Lady              1
Jonkheer          1
Don               1
Dona              1
the Countess      1
Mme               1
Sir               1
Name: Title, dtype: int64

emmm,大類似乎只有四個:Mr、Miss、Mrs、Master,其余稱呼非常少,因此我們將其余稱呼都?xì)w為一類——Rare。

List=titanic.Title.value_counts().index[4:].tolist()
mapping={}
for s in List:
    mapping[s]='Rare'
titanic['Title']=titanic['Title'].map(lambda x: mapping[x] if x in mapping else x)
titanic.Title.value_counts()

out:
Mr        757
Miss      260
Mrs       197
Master     61
Rare       34
Name: Title, dtype: int64

這樣我們就把稱呼劃分成了5大類,下一步我們可以根據(jù)稱呼來對Age的缺失值進(jìn)行補(bǔ)全了,這是因?yàn)镸iss用于未婚女子,通常其年齡比較小,Mrs則表示太太,夫人,一般年齡較大,因此利用稱呼中隱含的信息去推測其年齡是合理的。下面我們根據(jù)Title進(jìn)行分組并對Age進(jìn)行補(bǔ)全。

grouped=titanic.groupby(['Title'])
median=grouped.Age.median()
median

out:
Title
Master     4.0
Miss      22.0
Mr        29.0
Mrs       35.5
Rare      44.5
Name: Age, dtype: float64

可以看到,不同稱呼的乘客其年齡的中位數(shù)有顯著差異,因此我們只需要按稱呼對缺失值進(jìn)行補(bǔ)全即可,這里使用中位數(shù)(平均數(shù)也是可以的,在這個問題當(dāng)中兩者差異不大,而中位數(shù)看起來更整潔一些)。

def newage (cols):
    age=cols[0]
    title=cols[1]
    if pd.isnull(age):
        return median[title]
    return age

titanic.Age=titanic[['Age','Title']].apply(newage,axis=1)
titanic.info()

out:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 13 columns):
PassengerId    1309 non-null int64
Survived       891 non-null float64
Pclass         1309 non-null int64
Name           1309 non-null object
Sex            1309 non-null object
Age            1309 non-null float64
SibSp          1309 non-null int64
Parch          1309 non-null int64
Ticket         1309 non-null object
Fare           1308 non-null float64
Cabin          295 non-null object
Embarked       1307 non-null object
Title          1309 non-null object
dtypes: float64(3), int64(4), object(6)
memory usage: 183.2+ KB

可以看到,Age的缺失值已經(jīng)被我們補(bǔ)全了。下面我們對Cabin、Embarked以及Fare進(jìn)行補(bǔ)全。

首先Cabin代表的是艙位號,這個信息似乎并不能由其它的特征推出來(我本來想著ticket這個特征會有些幫助,但是我發(fā)現(xiàn)這個船票號似乎并沒有什么規(guī)律),那么我們可以想一想為什么艙位號的缺失值這么多呢?會不會是因?yàn)橹挥行掖嬲卟拍軠?zhǔn)確地登記自己的艙位號呢?也就是說,會不會艙位號的有無才是關(guān)鍵所在呢?為了驗(yàn)證這個想法,我們畫出幸存與艙位的關(guān)系圖(注意這里只能畫出train中has_Cabin和Survived的關(guān)系):

titanic['has_Cabin'].loc[~titanic.Cabin.isnull()]=1
titanic['has_Cabin'].loc[titanic.Cabin.isnull()]=0
pd.crosstab(titanic.has_Cabin[:len_train],train.Survived).plot.bar(stacked=True)

不難看出,艙位號缺失的乘客的幸存率遠(yuǎn)遠(yuǎn)低于有艙位號的乘客,這就驗(yàn)證了我之前的猜想。于是我們把Cabin的缺失值劃為單獨(dú)的一類。

titanic.Cabin = titanic.Cabin.fillna('U')
titanic[:10]

然后我們看Embarked特征,這個特征表示在那個港口登船,只有兩個缺失值,對結(jié)果影響不大,所以直接把缺失值補(bǔ)全為登船港口人數(shù)最多的港口(這其實(shí)應(yīng)用的是先驗(yàn)概率最大原則)。

most_embarked = titanic.Embarked.value_counts().index[0]
titanic.Embarked=titanic.Embarked.fillna(most_embarked)

最后我們補(bǔ)全Fare,這個只有一個缺失值,對結(jié)果影響不大,所以直接用票價Fare的中位數(shù)補(bǔ)全。

titanic.Fare = titanic.Fare.fillna(titanic.Fare.median())

至此我們的缺失值補(bǔ)全就完成了,check一下:

titanic.info()

out:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 13 columns):
PassengerId    1309 non-null int64
Survived       891 non-null float64
Pclass         1309 non-null int64
Name           1309 non-null object
Sex            1309 non-null object
Age            1309 non-null float64
SibSp          1309 non-null int64
Parch          1309 non-null int64
Ticket         1309 non-null object
Fare           1309 non-null float64
Cabin          1309 non-null object
Embarked       1309 non-null object
Title          1309 non-null object
dtypes: float64(3), int64(4), object(6)
memory usage: 183.2+ KB

ok,下一步我們進(jìn)行特征工程對補(bǔ)全后的特征做進(jìn)一步處理。

2、特征工程

首先我們處理Cabin特征,原因很簡單,因?yàn)樗男问奖容^復(fù)雜(字母加數(shù)字),不方便直接使用。

我們猜測Cabin特征中的開頭字母應(yīng)該是艙位等級,于是我們把Cabin特征中的數(shù)字去掉以方便分析:

titanic['Cabin'] = titanic.Cabin.apply(lambda cabin: cabin[0])
titanic.Cabin.value_counts()

out:
U    1014
C      94
B      65
D      46
E      41
A      22
F      21
G       5
T       1

艙位等級A~G沒有問題,但是T是什么艙呢……我個人感覺這個應(yīng)該是登記錯誤吧,看了眼票價并不是很貴,于是手動把它標(biāo)記成G好了……

titanic['Cabin'].loc[titanic.Cabin=='T']='G'
titanic.Cabin.value_counts()

out:
U    1014
C      94
B      65
D      46
E      41
A      22
F      21
G       6
Name: Cabin, dtype: int64

我們現(xiàn)在看看這樣劃分是否對預(yù)測有用。

pd.crosstab(titanic.Cabin[:len_train],train.Survived).plot.bar(stacked=True)

看起來似乎A~F的幸存率都不低,不過各艙位之間還是有差別的。

接下來我們對其它特征進(jìn)行篩選或組合,標(biāo)準(zhǔn)就是和幸存率相關(guān)性大。

我們先看Parch(直系親人)和SibSp(旁系親人)兩個特征。

pd.crosstab(titanic.Parch[:len_train],train.Survived).plot.bar(stacked=True)
pd.crosstab(titanic.SibSp[:len_train],train.Survived).plot.bar(stacked=True)

看起來這兩個特征對幸存率的影響差不多啊……那我們把這兩個特征整合成一個特征好了,Parch+SibSp+1就是家庭大小,我們創(chuàng)建新特征FamilySize。

titanic['FamilySize'] = titanic.Parch + titanic.SibSp + 1
pd.crosstab(titanic.FamilySize[:len_train],train.Survived).plot.bar(stacked=True)

可以看到,孤身一人的死亡率極高……兩口之家三口之家和四口之家的存活概率都比較高,家庭規(guī)模再大一些的死亡率又有所回升,這個結(jié)果也在情理之中。

于是我們添加FamilySize這個特征,并去掉SibSp和Parch兩個特征。

titanic=titanic.drop(['SibSp','Parch'],axis=1)

接下來我們看Sex特征。

pd.crosstab(titanic.Sex[:len_train],train.Survived).plot.bar(stacked=True)

女性的幸存率比男性要大很多,看來lady first的紳士風(fēng)度并不只是說說而已。

接下來我們看Pclass特征。

pd.crosstab(titanic.Pclass[:len_train],train.Survived).plot.bar(stacked=True)

不出所料,階級高的人存活率要遠(yuǎn)大于階級低的人。

接下來是Title。

pd.crosstab(titanic.Title[:len_train],train.Survived).plot.bar(stacked=True)

直觀上看,這個特征其實(shí)和Sex包含的信息差不多啊,都是男性幸存率低女性幸存率高。那么它還包含更多有用信息嗎,我們把Sex和Title組合起來看看。

pd.crosstab([titanic.Title[:len_train],titanic.Sex[:len_train]],train.Survived).plot.bar(stacked=True)

可以看到,相比Sex,Title確實(shí)提供了更多信息,比如雖然男性的幸存率低,但是男孩(Master)的幸存率卻達(dá)到了將近50%。而Title在Sex的補(bǔ)充下也呈現(xiàn)出新的信息,比如雖然Rare的幸存率雖然并不太高,但Rare中的女性幸存率卻很高。

綜上,我們要保留Title特征。

那么Age特征呢?首先將其當(dāng)作連續(xù)特征是不好處理的,所以我們應(yīng)劃分年齡段來觀察:

pd.crosstab(pd.cut(titanic.Age,8)[:len_train],train.Survived).plot.bar(stacked=True)

看起來只有年齡很小的孩子受到了優(yōu)待,年輕人死亡率最高,而年紀(jì)稍長者存活率較高,老者存活率最低。

我們按照年齡段將Age轉(zhuǎn)化為類別特征:

titanic.Age=pd.cut(titanic.Age,8,labels=False)

接下來看Embarked特征。

pd.crosstab(titanic.Embarked[:len_train],train.Survived).plot.bar(stacked=True)

可以看到C港口上船的乘客幸存率最高,其它兩個港口幸存率較低。原因不詳。

目前為止,我們得到的特征長這樣:

我們看到,形式比較復(fù)雜的特征還有Name、Ticket和Fare三個,Name的信息我們認(rèn)為已經(jīng)被Title涵蓋了,因此直接舍棄。

titanic=titanic.drop('Name',axis=1)

Ticket我不知道其中含義,而且我認(rèn)為其中的重要信息應(yīng)該已經(jīng)包含在Cabin當(dāng)中了,于是我選擇舍棄Ticket這個特征。

titanic=titanic.drop('Ticket',axis=1)

最后,我們看Fare特征,我個人認(rèn)為這個船票價格并不太重要,因?yàn)槠湫畔?yīng)該已經(jīng)包含在Pclass、Cabin以及Embarked當(dāng)中了,但我猜想這個票價可能和位置有關(guān)(靠窗啊臥鋪啊之類),蘊(yùn)含了我們舍棄的Ticket的部分信息,因此我決定保留這個特征,并對其進(jìn)行和Age類似的離散化操作。

pd.crosstab(pd.qcut(titanic.Fare,4)[:len_train],train.Survived).plot.bar(stacked=True)

果然票價越高幸存率越高。

我們按照Fare段將Fare轉(zhuǎn)化為類別特征:

titanic.Fare=pd.cut(titanic.Fare,4,labels=False)

我們看一下現(xiàn)在的特征形式:

titanic.head()

基本上完成了,最后我們只需把Sex、Cabin、Embarked和Title特征轉(zhuǎn)化成int類型即可:

titanic.Sex=titanic.Sex.map({'male':1,'female':0})
titanic.Cabin=titanic.Cabin.map({'A':0,'B':1,'C':2,'D':3,'E':4,'F':5,'G':6,'U':7})
titanic.Embarked=titanic.Embarked.map({'C':0,'Q':1,'S':2})
titanic.Title=titanic.Title.map({'Mr':0,'Miss':1,'Mrs':2,'Master':3,'Rare':4})

至此特征工程就完成了:

3、訓(xùn)練模型

訓(xùn)練模型階段我們只需要用sklearn庫即可完成大部分操作,因?yàn)椴恢篮畏N模型是最適合我們的數(shù)據(jù)的,因此我們可以嘗試多種模型,最終選出表現(xiàn)較好的模型進(jìn)行集成,得到最終的分類器。

首先我們還原train和test數(shù)據(jù)。

train=titanic[:len_train]
test=titanic[len_train:]

然后我們可以給由這兩個數(shù)據(jù)集產(chǎn)生相應(yīng)的X和y了:

X_train=train.loc[:, 'Pclass':]
y_train=train['Survived']

X_test=test.loc[:, 'Pclass':]

然后我們嘗試一下不同的模型,看看哪個效果比較好,這里考慮LR、SVM和DecisionTree。

from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
from sklearn.tree import DecisionTreeClassifier

log_reg=LogisticRegression()
log_reg.fit(X_train, y_train)
svm_clf = SVC()
svm_clf.fit(X_train, y_train)
tree_clf=DecisionTreeClassifier()
tree_clf.fit(X_train, y_train)
print(log_reg.score(X_train,y_train))
print(svm_clf.score(X_train,y_train))
print(tree_clf.score(X_train,y_train))

out:
0.8181818181818182
0.8462401795735129
0.8933782267115601

可以看到?jīng)Q策樹模型表現(xiàn)更好一些,而決策樹的集成是隨機(jī)森林,因此我們直接使用隨機(jī)森林來擬合數(shù)據(jù)。

from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV

RF=RandomForestClassifier(random_state=1)
PRF=[{'n_estimators':[10,100],'max_depth':[3,6],'criterion':['gini','entropy']}]
GSRF=GridSearchCV(estimator=RF, param_grid=PRF, scoring='accuracy',cv=2)
scores_rf=cross_val_score(GSRF,X_train,y_train,scoring='accuracy',cv=5)
model=GSRF.fit(X_train, y_train)
pred=model.predict(X_test)
output=pd.DataFrame({'PassengerId':test['PassengerId'],'Survived':pred})
output.to_csv('C:/logfile/submission.csv', index=False)

最終提交得分為0.7945,top 21%。對于入門之戰(zhàn)來說結(jié)果已經(jīng)不錯了。后來又參考了網(wǎng)上大牛的做法,相比上面的模型,將Age的缺失值補(bǔ)全分得更為細(xì)致了一些(結(jié)合Title和Sex的信息進(jìn)行補(bǔ)全而不僅僅是Title),并且采用了超參數(shù)調(diào)優(yōu)的SVM模型,最終達(dá)到了0.8133的得分,排名瞬間變成top 7%??磥碛泻芏嗳硕伎ㄔ诹?.8左右的精度上。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容