【python】Kaggle入門:titanic 的特徵提取與特徵分析

目   錄

0、概述

1、Anaconda的準備

2、導入必需包和數據集

3、數據分析

3.1 數據概覽

3.2 數據初步分析

3.2.1 Pclass 客艙等級

3.2.2 Sex 性別

3.2.3 Age 年齡

3.2.4 SibSp 兄弟數量

3.2.5 Parch 父母與子女數量

3.2.6 Fare 票價

3.3 數據深入分析

3.3.1 PassengerId 乘客序號

3.3.2 Name 乘客姓名

3.3.3 Ticket 船票編號

3.3.4 Cabin 房間號

3.3.5 Embarked 上船的港口編號

3.3.6 SibSp+Parch 親人數量

4、總結


0、概述

kaggle是一個數據挖掘網站,上面有許多各類數據挖掘問題與競賽。對於數據挖掘專業的學生來說十分友好。

對我來說,無論是利用python進行數據分析還是pandas還是numpy都太枯燥了,因此選擇用這個入門問題來學習一些有關數據挖掘的基本技能。

主要參考該網址的第二部分。對於大多數有一些pandas基礎的人,直接看這個就可以了,我是初次接觸這些,會把其中遇到的所有問題都寫下來,供自己和大家查詢。

1、Anaconda的準備

我這邊有好幾個虛擬環境,爲了防止環境互相影響,決定對該問題新建一個環境。

參考網址使用Jupyter Notebook,所以在該環境裏面要安裝notebook。

安裝好這些必須包以後,別忘了裝nb_conda這個包,有了這個包才能隨意在notebook裏面切環境,否則只有一個默認環境沒法切。如下:

不裝nb_conda就只有第一個環境。

2、導入必需包和數據集

代碼如下:

%matplotlib inline #notebook中的魔法函數
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore') #無視所有代碼警告

train=pd.read_csv(r'F:\kaggle\titanic\train.csv') #讀取訓練集
test=pd.read_csv(r'F:\kaggle\titanic\test.csv') #讀取測試集
PassengerId=test['PassengerId'] #讀取測試集中PassengerId這一列
all_data=pd.concat([train,test],ignore_index=True) #將訓練集和測試集合在一起
train.head()

上述代碼的效果如下:

可以看到train已經成功保存了測試集中的數據。下面開始分析代碼:

第一行的魔法函數的作用很簡單,就是當我們使用plt.plot畫一個函數之後,正常應該加一句plt.show纔會顯示圖像,有了這句之後就不用再多寫一句show了,plot之後直接顯示圖像。

之後是引入包,pandas和numpy不需要解釋,seaborn是一個用於畫圖的庫。

然後是warning的一句,這一句是隱藏所有的解釋器的warning。

接下來進入正題:利用pandas的read_csv可以將csv文件的內容寫入變量,作爲一個類似二維數組的結構,稱之爲DataFrame。

如何對dataframe進行訪問呢?

首先來看對列訪問:

有三種形式,按行訪問、按列訪問、指定某元素訪問。

對於按行訪問:

比如說上面的第0行:

第一種:

test.iloc[0]

等價於訪問“第0行”,iloc的特點是其訪問的是從前往後數第幾行,當然最開始的那行列名不算。

第二種:

test.loc[0]

等價於訪問“序號那一列中名爲0的行”,loc的特點是找左邊第一列中名字與參數對得上的那一行並返回。

這倆效果都一樣:

返回的都是Series。

按行訪問還有一種類似切片的方式:

test[0:1]

效果如下:

相當於切下第0行,返回類型爲DataFrame。

對於按列訪問:

比如說對上文中的passergerId來說:

第一種:

test['PassengerId']

返回的效果如下:

其類型爲Series,pandas中的一維數據結構,可以看做是字典,是序號到元素的映射。

第二種:

test[['PassengerId']]

返回的效果如下:

其類型爲DataFrame,pandas中的二維數據結構。

訪問某個指定元素:

可以混合使用上面的方法,畢竟返回的不是DataFrame就是Series,直接繼續訪問就好:

例如:

test.loc[0]['Name']

訪問第0行中名爲name的列。

又如:

test['Name'][0]

訪問name這列中的第0個。

扯遠了。接着看這裏的代碼。下一行就是按列訪問名爲PassengerId這一列,返回的是Series。

然後將測試集和訓練集黏在一起,用的是concat函數。

最後訪問訓練集的前五行,用head,後五行則用tail。

3、數據分析

3.1 數據概覽

我們既然已經將數據集導入了,那就來看看裏面都是什麼吧。

回想起我在數據挖掘課上剛學的那點三腳貓功夫:

列名就是每個人的屬性咯,這樣看來:

PassengerId爲乘客序號,序數屬性,沒任何用——八成就是一個撈上來的順序問題;

Survived爲是否生還,二元屬性,只有0和1,這是最重要的屬性,是我們最後要預測的屬性;

Pclass爲客艙等級,數值屬性,取值1,2,3,看上去應該挺重要的,是不是客艙等級越高就越容易生還?;

Name爲乘客名字,標稱屬性,取值爲人名,我覺得很難從中提取出什麼屬性,或許沒名字代表必死?;

Sex爲性別,二元屬性,取值0,1,看來那個時候對於性別劃分還沒那麼複雜;

Age爲年齡,數值屬性,取值整數,同樣重要,老弱先行麼;

SibSp爲兄弟數量,數值屬性;

Parch爲父母與子女數量,數值屬性,其值0,1,2,3等;

Ticket爲船票編號,標稱屬性,和乘客名字差不多;

Fare爲票價,數值屬性,但是不是離散而是連續,應該和客艙等級有一定的聯繫,效果也差不多;

Cabin爲房間號,標稱屬性,可能房間號有一定的規律;

Embarked上船的港口編號,標稱屬性。

知道了這些都是啥,下面來看看數據的缺失吧。很簡單,使用如下指令:

train.info()

效果如下:

這命令主要就是看有多少非空屬性,可以看出來,Age缺了一些、Cabin缺了很多。那麼我們在用age建模的時候就需要好好想一想了,是自己填值還是爲空的全置爲0作爲特殊的一類呢?

3.2 數據初步分析

這一步主要看各屬性與是否生還的關係。我們先來篩出來大概率和是否生還有關的屬性吧:

Pclass、Sex、Age、Sibsp、Parch、Fare。

使用命令

train['Survived'].value_counts()

來查看在Survived屬性中各值的數量:

八成0就是沒救回來的,1就是救回來的。先在問題就變成了看其餘屬性中,某個屬性哪些取值對應Survived中的0,哪些對應1。

3.2.1 Pclass 客艙等級

直接畫柱狀圖來看:

sns.barplot(x="Pclass", y="Survived", data=train)

對於barplot函數,Seaborn會對”Pclass“列中的數值進行歸類後按照estimator參數的方法(默認爲平均值)計算相應的值,計算出來的值就作爲條形圖所顯示的值(條形圖上的誤差棒則表示各類的數值相對於條形圖所顯示的值的誤差)。如下:

可以看出來,Pclass值越小,Survived值越高。

怎麼理解這個平均值呢?舉個例子吧,Pclass=1一共10個人,有6個獲救,那麼就有6個Survived爲1,總和爲6,平均值爲6/10=0.6。其他也一樣。看來有錢的明顯更容易獲救。

3.2.2 Sex 性別

柱狀圖走起:

可以看出來,女性獲救的比例是男性的四倍還多= =。

3.2.3 Age 年齡

額,可能這個用柱狀圖不太合適,不過先試試吧:

這效果太差了,什麼都看不出。得換一種圖。

我們來想一想我們需要什麼:我們需要年齡與生還與否的關係,也就是說,給我一個年齡,我能估計出他更可能死還是被救。怎麼辦呢?用密度圖。代碼如下:

facet = sns.FacetGrid(train, hue="Survived",aspect=2)
facet.map(sns.kdeplot,'Age',shade= True)
facet.set(xlim=(0, 100))
facet.add_legend()
plt.xlabel('Age') 
plt.ylabel('density') 

有點難看懂。主要使用的函數爲FacetGrid,直觀來講,可以把這個函數畫出來的圖像看成是多個y軸的圖像,最簡單的2y軸圖像就是左側一個y軸,右側一個y軸,相信大家都看過。這種圖像可以很輕鬆點看出來在同一個條件(X的值相同)下,不同y值的大小關係;也可以看出來在一個趨勢下(X由小到大,相當於由左到右)不同y值的趨勢。對於本組數據,兩個y軸分別爲Survived取值0或1,x軸爲年齡,完美。

如何畫呢?

首先調用FacetGrid函數,第一個參數必須是DataFrame格式;第二個參數hue爲第一個參數中的列名,官方文檔中解釋爲“It can also represent levels of a third varaible with the ``hue``parameter”,即“多個y軸”這一功能通過hue實現;第三個參數aspect爲一個比值,用於調整x軸的單位長度。這個函數會返回一個FacetGrid對象,包含數據集和用於畫圖的變量。對於這些變量的使用,需要函數FacetGrid.map。

正好,下面就用這個函數了,官方文檔如下描述“Apply a plotting function to each facet's subset of the data”,將函數圖像應用於數據的每一個子集。第一個參數爲sns.kdeplot。wdnmd,這又是個啥?

這個函數就是用來實現“密度圖”的。kde全稱kernel density estimation,核密度估計,簡而言之,就是根據離散採樣估計整體的概率分佈。參見該鏈接。對於本例,“某一年齡中Survived中取值爲0或1的個數”就是一個採樣,對於全部年齡,都會有一個對應的採樣。我們可以想象一下這些採樣畫成直方圖,應該就是類似上面亂七八糟的樣子,但是有個問題:這個直方圖的縱軸的值與“該年齡生還的概率”有直接關係,意思是這個值可能爲1,但是這沒啥用——我們真正想要的是“某段年齡生還的概率”,因爲直方圖是離散的,雖然我們一定可以得到一個值,但是由於部分值的缺失、樣本數量太少等原因,直方圖不能很好的反映概率。我們就需要根據直方圖繪製概率密度圖——我年齡在[0,max]之間,那對應我生還就有一個概率。如何求呢,就用到kde了。kde的原理有些複雜,但我們要調用就很簡單了,直接sns.kdeplot即可。

那麼第二個參數值爲age就可以理解了,它就是kde需要的數據。第三個參數shade爲True表示將函數圖像與x軸部分變成實心的。

接下來的set、add、xlable、ylable用於調整圖像的x軸範圍、圖像名稱、x軸y軸名稱。

效果如下:

從這個圖我們就能看出來,15歲以下生還的概率更高,15~30死亡概率更高,之後差不多。說明老幼優先還是有點效果的。

3.2.4 SibSp 兄弟數量

同樣是數值變量,直接直方圖走起。

看來親屬在1~2個的乘客生還概率最高啊。

3.2.5 Parch 父母與子女數量

Parch爲數值變量,先barplot沒錯:

emmmmm,是值越高生還概率越低麼?存疑。

3.2.6 Fare 票價

票價是連續數值屬性,這就難受了,用barplot一定難看。我們用分區間數頻率的方法看一下吧。

這個有一點難,我們分步來做:

①、將區間劃分好

我們來看票價的最大值和最小值,用max和min函數:

最大值爲512,最小值爲0,差距太大了,先嚐試按0~600,每100爲一個區間吧。

區間儲存端點,格式爲list,因此代碼如下:

l=list(range(0,700,100))

(0,700)是左閉右開,所以是0~600,每隔100一個區間。

②、數據準備

簡而言之,就是我們準備區間化的數據。我們準備區間化兩組數據:第一組是被救上來的人的船票價格,第二組是沒被救上來的船票價格。都需要DataFrame格式。這就用到之前對DateFrame的選擇了,還用到了布爾下標:

s1=train[train['Survived']==1]['Fare']
s2=train[train['Survived']==0]['Fare']

注意,布爾下標不能直接是'Survived'==1,要把train加上。

這樣我們就分別得到了兩組數據。

③、數據區間化

這個有點難理解,第二步我們不是已經把數據準備好了麼,是兩列數據,那現在我有一列數據假如說是150,250,350,那區間話之後就變成了(100,200],(200,300],(300,350],這個區間的開閉是怎麼確定的呢?用函數的參數right=True控制。

也就是說,這相當於一個映射,把一個實數映射成一個空間。代碼如下:

qujian1=pd.cut(s1,l1,right=True)

效果如下:左邊是原數據,右邊是區間化後的數據。

       

④、區間計數

我們既然已經分好區間了,那麼下一步就是想要知道各區間都有多少人。使用如下代碼:

pinshu1=qujian1.value_counts(sort=False)

value_counts函數用於對元素出現的次數計數,默認是次數多的在前,次數少的在後,但我們不想要這個順序,我們需要這個計數保持原來的順序,即區間由小到大,因此要令其不排序。效果如下:

額,看起來效果不怎麼樣啊,大多數人都是在0~100這個區間。我們不如把區間調小,不然會丟失細節:

l1=list(range(0,100,10))
l2=list(range(100,700,100))
l1.extend(l2)

現在區間就變細了。效果如下:

還不錯。

⑤、計算各區間的獲救概率

這個就很簡單了:

s1=train[train['Survived']==1]['Fare']
l1=list(range(0,100,10))
l2=list(range(100,700,100))
l1.extend(l2)
qujian1=pd.cut(s1,l1,right=True)
pinshu1=qujian1.value_counts(sort=False)
s2=train[train['Survived']==0]['Fare']
l1=list(range(0,100,10))
l2=list(range(100,700,100))
l1.extend(l2)
qujian2=pd.cut(s2,l1,right=True)
pinshu2=qujian2.value_counts(sort=False)
pinlv1=pinshu1/(pinshu1+pinshu2)
pinlv1.plot(kind='bar')

先求各區間被救人數,再求各區間沒被救人數,然後就可以求出獲救綠,柱狀圖一畫ok。注意這裏可以直接用Series的plot方法,參數kind選擇bar即可。效果如下:

總體來說是富人得救率高啊。

3.3 數據深入分析

在3.2中,我們已經對Pclass、Sex、Age、Sibsp、Parch、Fare這六個屬性進行了分析,還剩PassengerId、Name、Ticket、Cabin、Embarked這些屬性沒有分析。因爲它們或者是序數屬性或者是標稱屬性,難以直接看出關係。所以留到這一節。

3.3.1 PassengerId 乘客序號

打擾了,這個是真沒用。

3.3.2 Name 乘客姓名

我們觀察乘客姓名,發現瞭如下規律:

所有乘客的姓名都是按“aaa,bb.cc”格式寫的,其中這個bb很有用,bb指的就是該乘客的稱呼,如“Mr”、“Miss”等。

這算廢物利用吧。

我們不如新建一列專門存這個稱呼。代碼如下:

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

好長,而且有之前沒見過的apply方法。解釋如下:

我們新建了一個匿名函數x,x的用途呢,是返回bb,怎麼做到的?

首先,對於一個名字,先用split(,)將其分成aa和bb.cc兩部分,選擇第1部分,也就是bb.cc,再用split(.)將其分成bb和cc兩部分,選擇第0項bb,然後用strip刪除前後的空格。

那爲什麼要用apply呢?簡潔,而且對於參數在元組或字典中的函數,用apply調用可以按序將參數傳入函數,這就相當於很多次循環,對all_data的Name這列的每一個元素,都傳入了x這個函數作爲參數,返回作爲title這列的元素值。

這樣我們就得到了名爲title的一列。

看看都有什麼吧:

757個Mr,260個Miss......

類型不多,可以從這裏入手看一看。

但還是有點多啊,能不能再縮小一點呢?比如說把Lady、Mlle、Mme全看成Miss,畢竟這幾個稱呼的人太少了;Sir也可以歸類到Mr裏。

這樣看來,我需要一個多對一的字典啊。這個多對一的字典怎麼生成呢?代碼如下:

Title_Dict = {}
Title_Dict.update(dict.fromkeys(['Capt', 'Col', 'Major', 'Dr', 'Rev'], 'Officer'))
Title_Dict.update(dict.fromkeys(['Don', 'Sir', 'the Countess', 'Dona', 'Lady'], 'Royalty'))
Title_Dict.update(dict.fromkeys(['Mme', 'Ms', 'Mrs'], 'Mrs'))
Title_Dict.update(dict.fromkeys(['Mlle', 'Miss'], 'Miss'))
Title_Dict.update(dict.fromkeys(['Mr'], 'Mr'))
Title_Dict.update(dict.fromkeys(['Master','Jonkheer'], 'Master'))

主要用了update和fromkeys兩個函數。

fromkey以第一個參數作爲鍵,第二個參數作爲值生成一個新字典,由於第一個參數可以是一個list,所以這可以形成一個多對一的字典。

update的參數是一個字典,會將參數字典中的鍵值對更新到原字典裏面。

於是我們就有了一個多個多對一的字典。

如何利用這個字典將title這一列數據歸類呢?很簡單,用DataFrame的map函數,傳入一個字典就自動歸類了:

all_data['Title'] = all_data['Title'].map(Title_Dict)

效果如下:

嗯,被壓縮的很少了。

然後來看一看吧,直接barplot:

。。。。。。Mr果真是最慘的,但是Royalty和Master好想獲救率很高啊。這就是我們通過name額外得到的信息。

3.3.3 Ticket 船票編號

我原本以爲船票是一人一張,因此編號也是一人一個——實際上不是這樣,有聯票的存在。也就是說,幾個人共用一張票。那麼,自己用一張票和一家人用一張聯票,生存率會有差距麼?應該有,親人肯定優先救親人吧。

我們來看一下一張聯票都能幾個人用:

all_data['Ticket'].value_counts()

返回值爲Series,類似一個字典。

看得出來,買編號爲CA. 2343的船票這家是個大戶人家,有11個人在這船上。如下:

嘿,還真是一家人,都是Sage家的。訓練集的全滅。

1601和CA 2144也不少,都有8個人。

現在來看一下我們想知道什麼:同一張船票對應的人越多,獲救率是不是越高?

這得怎麼看呢?分類麼?把聯票中上船人數相同的歸爲一類,看他們的生還率高低——比如說1601和CA 2144就是一類這樣。那該怎麼做呢?

我們來一個映射吧,船票名字映射到聯票人數——新建一列爲TicketGroup,保存爲聯票人數,代碼如下:

Ticket_Count = all_data['Ticket'].value_counts()
all_data['TicketGroup'] = all_data['Ticket'].apply(lambda x:Ticket_Count[x])
sns.barplot(x='TicketGroup', y='Survived', data=all_data)

首先建立一個字典,鍵名爲船票編號,鍵值爲聯票人數。注意Series直接可以當字典用。然後新建一列,匿名函數把鍵名轉爲鍵值。這裏的確看出來匿名函數好用了,不用循環,十分簡單。然後畫圖看一下:

這是人越多獲救率越低啊。

3.3.4 Cabin 房間號

房間號類似船票編號,也是一個房間可能住好幾個人的——所以我想到也可以用類似船票的處理方式來處理房間號。可是有一個大問題,房間號的缺失率太高了,大月只有百分之20%的人的房間號不是NaN。

對於不是NaN的值,都有一個特點,或者是一間房間,或者是多間房間,都是一個大寫字母加一個數字——字母不同會不會導致獲救率不同呢?可以一試:

all_data['Cabin'] = all_data['Cabin'].fillna('Z')
all_data['RoomNum']=all_data['Cabin'].apply(lambda x:x[0])

先把所有的NaN全置爲Z,利用fillna函數可以很輕易的完成這個。然後新建一列RoomNum,記錄房間號的首字母。匿名函數太好用了。

然後出圖:

看來沒有房間號記錄的苦命人生還率很低啊。房間號爲E、D、B的人獲救概率是苦命人的二倍多。

那麼我們可以將生存率相仿的分爲一組,從而減少參數的取值:

all_data['Cabin'] = all_data['Cabin'].fillna('Z')
all_data['RoomNum']=all_data['Cabin'].apply(lambda x:x[0])
def Cabin_Label(x):
    if ((x=='E')|(x=='D')|(x=='B')):
        return 2
    elif ((x=='C')|(x=='G')|(x=='A')|(x=='F')):
        return 1
    else:
        return 0
all_data['RoomNum']=all_data['RoomNum'].apply(Cabin_Label)
sns.barplot(x='RoomNum',y='Survived',data=all_data)

這樣就把參數減少到三個:

3.3.5 Embarked 上船的港口編號

這個乍一看好像屁用沒有。實際呢?需要我們處理一下。

嗯,所有人都從三個港口上船:S、C和Q。其中S上船的人最多。

看上去C上船的人生還率高一點。或者是這裏妹子多?

3.3.6 SibSp+Parch 親人數量

在上一節,我們分別分析了每個人的SibSp和Parch,但是實際上,這兩者是可加的,和爲該乘客的親人數量。這個值比SibSp或Parch各自能更好的反映親人數量與生還率的關係。先新建一行FamilyMem保存它們的和。

train['FamilyMem']=train['SibSp']+train['Parch']
sns.barplot(x='FamilyMem',y='Survived',data=train)

效果如下:

果然,不是孤兒的,親人越少的生還率越高。我想起來原來看烈火英雄的時候:“非獨生子向前一步走”。大概是一個意思。

由此我們可以吧生存率分爲三類:

1~3人的、0和4~6人的,6人以上的。這三類分別具有相似的生還率。因此可以分爲一類。

可以通過這一方法減少FamilyMem的取值:

all_data['FamilyMem']=all_data['SibSp']+all_data['Parch']
def Fam_Label(x):
    if (x>=1)&(x<=3):
        return 2
    elif (x==1)|((x>=4)&(x<=6)):
        return 1
    else:
        return 0
all_data['FamilyMem']=all_data['FamilyMem'].apply(Fam_Label)
sns.barplot(x='FamilyMem',y='Survived',data=all_data)

注意if的條件要各自用括號括起來。

效果如下:

明顯標號爲2的家庭生還率更高啊。

4、總結

通過學習該網址這一博客,瞭解了許多數據挖掘中特徵處理的知識。對於數據的不同特徵與最終結果的關係有了一個整體的認識,對於下一步數據的具體處理提供幫助。對於各類數據,總結如下:

特徵類型 特徵名稱 處理方式
離散數值特徵 Pclass barplot
Age FacetGrid+kdeplot
SibSp barplot
Parch barplot
SibSp+Parch barplot
連續數值特徵 Fare pd.cut
二元特徵 Sex barplot
標稱特徵 Name 格式提取+barplot
Ticket 數值映射+barplot
Cabin 格式提取+barplot
Embarked barplot
序數特徵 PassengerId pass

由此可以總結出二元分類問題的數據分析流程:

另外,對於某些具有關聯的特徵,可以經過處理之後合成新的特徵。

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章