Kaggle實戰之Titanic: Machine Learning from Disaster


最近埋頭苦讀,啃機器學習的算法和編程,真是非(xiang)常(dang)歡(lan)樂(sou)呢~ 於是開始自我膨脹躍躍欲試。
嗯,那就從Kaggle的playground開始吧,找了個經典而又浪漫的愛情故事—泰坦尼克,應該能引起我的興趣好好挖掘吧~

"You jump! I jump! ",『Jack and Rose』的故事想必大家都很熟悉。愛情故事很動人,但是泰坦尼克號在與冰山相撞后沉沒后,2224名乘客中有1502人死亡。盡管幸存者有一些運氣因素,但是是否獲救其實並非隨機,而是基於一些背景有rank先后的,比如女人,孩子和上流社會。在這個挑戰中,我們需要根據乘客的個人信息和存活情況,運用機器學習工具來預測哪些乘客在悲劇中幸存下來。這是一個典型的二分類問題。
Titanic: Machine Learning from Disaster

1. 首先從整體觀察數據情況

  • 可用.info().describe()查看數據的基本信息和統計特征
#-*- coding: utf-8 -*-
import numpy as np
import pandas as pd
from pandas import DataFrame,Series
data_train = pd.read_csv("train.csv")    #讀取訓練數據
data_train.columns                       #展示數據數據的表頭
data_train.info()
data_train.describe()
<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
PassengerId Survived Pclass Age SibSp Parch Fare
count 891.000000 891.000000 891.000000 714.000000 891.000000 891.000000 891.000000
mean 446.000000 0.383838 2.308642 29.699118 0.523008 0.381594 32.204208
std 257.353842 0.486592 0.836071 14.526497 1.102743 0.806057 49.693429
min 1.000000 0.000000 1.000000 0.420000 0.000000 0.000000 0.000000
25% 223.500000 0.000000 2.000000 20.125000 0.000000 0.000000 7.910400
50% 446.000000 0.000000 3.000000 28.000000 0.000000 0.000000 14.454200
75% 668.500000 1.000000 3.000000 38.000000 1.000000 0.000000 31.000000
max 891.000000 1.000000 3.000000 80.000000 8.000000 6.000000 512.329200

我們可以看到數據中大概有以下這些字段

PassengerId => 乘客ID
Pclass => 乘客艙級(1/2/3等艙位)
Name => 乘客姓名
Sex => 性別
Age => 年齡
SibSp => 堂兄弟/妹個數
Parch => 父母與小孩個數
Ticket => 船票信息
Fare => 票價
Cabin => 艙位編號
Embarked => 登船港口(3個港口)
Survived => 幸存情況(1為幸存)


其中,我們可以觀察到

  • 乘客艙級,性別,登船港口,幸存情況,堂兄妹個數、父母與小孩個數是離散屬性
  • 年齡,票價為連續值屬性
  • 乘客ID, 乘客姓名,船票信息,艙位編號等屬性具有唯一值
  • 年齡、艙位編號 有缺失值

2. 數據可視化,分析屬性特征


可做數據分析的圖標類型有:

  • 線型圖
  • 柱形圖
  • 直方圖
  • 散點圖
  • 密度圖
  • 箱型圖

其中:

  • 離散屬性,可用柱狀圖觀察屬性每種類別的分布情況
  • 年齡,票價為連續值屬性,可用散點、線性或者密度圖來描述分布情況
  • 堂兄妹個數、父母與小孩個數, 可試驗
  • 乘客ID, 乘客姓名,船票信息,艙位編號等信息,可找源數據觀察數據特征,價值有待挖掘

首先分析幾個離散屬性本身的特征情況

#coding:utf-8
import matplotlib.pyplot as plt
plt.rc('figure',figsize=(8,6))
plt.rcParams['font.sans-serif']=['SimHei'] #用來正常顯示中文標簽
plt.rcParams['axes.unicode_minus']=False #用來正常顯示負號
fig = plt.figure()
fig.set(alpha=0.2)  # 設定圖表顏色alpha參數

fig.add_subplot(2,2,1)
data_train.Survived.value_counts().plot(kind="bar")
plt.ylabel(u"人數")
plt.title(u"獲救情況(1為獲救)")

fig.add_subplot(2,2,2)
data_train.Pclass.value_counts().plot(kind="bar")
plt.ylabel(u"人數")
plt.title(u"艙級情況")

fig.add_subplot(2,2,3)
data_train.Sex.value_counts().plot(kind="bar")
plt.ylabel(u"人數")
plt.title(u"性別情況")

fig.add_subplot(2,2,4)
data_train.Embarked.value_counts().plot(kind="bar")
plt.ylabel(u"人數")
plt.title(u"登船港口情況")
Text(0.5,1,'登船港口情況')

由上圖可以得到以下信息:

  1. 三個艙位人數: 3級 > 1級 > 2級 ,3級的人數基本是其他兩個艙級總和。
  2. 獲救人數大概只有30%
  3. 乘客中男性比例大於女性
  4. 三個登船港口人數:S > C > G ,其中S 登船港口人數最多

分析離散屬性與最終目標值的關系

1.不同乘客等級的獲救情況

  • 如圖可以看出乘客等級的獲救概率為: 1 > 2 > 3
Survived0=data_train.Pclass[data_train.Survived==0]
Survived1=data_train.Pclass[data_train.Survived==1]
df=DataFrame({u'不獲救':Survived0.value_counts(),u'獲救':Survived1.value_counts()})
df.plot(kind='bar',stacked=True)
plt.xlabel(u"乘客等級") 
plt.ylabel(u"人數")
plt.title(u"各乘客等級的獲救情況")
plt.show()

2.不同性別的獲救情況

  • 如圖可以看到女性的獲救概率比男性的獲救概率要高
Survived0=data_train.Sex[data_train.Survived==0]
Survived1=data_train.Sex[data_train.Survived==1]
df=DataFrame({u'不獲救':Survived0.value_counts(),u'獲救':Survived1.value_counts()})
df.plot(kind='bar',stacked=True)
plt.xlabel(u"性別") 
plt.ylabel(u"人數")
plt.title(u"不同性別的獲救情況")
plt.show()

3.不同登船港口的情況

  • 可以看出港口的獲救情況:C>S>Q
Survived0=data_train.Embarked [data_train.Survived==0]
Survived1=data_train.Embarked [data_train.Survived==1]
df=DataFrame({u'不獲救':Survived0.value_counts(),u'獲救':Survived1.value_counts()})
df.plot(kind='bar',stacked=True)
plt.xlabel(u"登船港口") 
plt.ylabel(u"人數")
plt.title(u"不同登船港口的獲救情況")
plt.show()

分析兩個連續值 年齡和票價 本身屬性的特征

  • 一般用直方圖、密度圖 (兩者反應的信息是相似的)
  • 如圖可以看出:乘客年齡的分布主要是集中在20-40歲之間,同時,票價集中在0-50之間,由部分土豪消費較高
import pandas as pda
from pandas import DataFrame,Series
# fig,axes=plt.subplots(1,1)
fig=plt.figure()
fig.add_subplot(2,1,1)
# data_train.Age.plot(kind='kde')
data_train.Age.hist()
plt.title(u"年齡分布情況")
fig.add_subplot(2,1,2)
# data_train.Fare.plot(kind='kde')
data_train.Fare.hist()
plt.title(u"票價分布情況")
Text(0.5,1,'票價分布情況')

兩個連續值與最終獲救情況的關系

  • 可根據最終分類將數值分為幾組,比較直方圖或者密度圖的差異
  • 可用散點圖、箱型圖
Survived0=data_train.Age[data_train.Survived==0]
Survived1=data_train.Age[data_train.Survived==1]
df=DataFrame({u'不獲救':Survived0,u'獲救':Survived1})
df.plot(kind='kde')
plt.xlabel(u"年齡") 
plt.ylabel(u"核密度")
plt.title(u"不同獲救情況的年齡分布密度圖")
plt.show()

Survived0=data_train.Fare[data_train.Survived==0]
Survived1=data_train.Fare[data_train.Survived==1]
df=DataFrame({u'不獲救':Survived0,u'獲救':Survived1})
df.plot(kind='kde',xlim=[-50,400])
plt.xlabel(u"票價") 
plt.ylabel(u"核密度")
plt.title(u"不同獲救情況的票價分布密度圖")
plt.show()

接下來可以分析組合多個屬性,得到有價值的信息

首先看看有重要影響的因素——艙級,分別與年齡、票價、港口的關系

Pclass1=data_train.Age[data_train.Pclass==1]
Pclass2=data_train.Age[data_train.Pclass==2]
Pclass3=data_train.Age[data_train.Pclass==3]
df=DataFrame({u'艙級1':Pclass1,u'艙級2':Pclass2,u'艙級3':Pclass3})
df.plot(kind='kde')
plt.xlabel(u"年齡") 
plt.ylabel(u"核密度")
plt.title(u"不同艙級的年齡分布密度圖")
plt.show()

Pclass1=data_train.Fare[data_train.Pclass==1]
Pclass2=data_train.Fare[data_train.Pclass==2]
Pclass3=data_train.Fare[data_train.Pclass==3]
df=DataFrame({u'艙級1':Pclass1,u'艙級2':Pclass2,u'艙級3':Pclass3})
df.plot(kind='kde',xlim=[-50,400])
plt.xlabel(u"票價") 
plt.ylabel(u"核密度")
plt.title(u"不同艙級的票價分布密度圖")
plt.show()

Pclass1=data_train.Embarked [data_train.Pclass==1]
Pclass2=data_train.Embarked [data_train.Pclass==2]
Pclass3=data_train.Embarked [data_train.Pclass==3]
df=DataFrame({u'艙級1':Pclass1.value_counts(),u'艙級2':Pclass2.value_counts(),u'艙級3':Pclass3.value_counts()})
df.plot(kind='bar',stacked=True)
plt.xlabel(u"港口") 
plt.ylabel(u"核密度")
plt.title(u"不同艙級的港口占比圖")
plt.show()

Embarked_C =data_train.Fare[data_train.Embarked =='C']
Embarked_Q =data_train.Fare[data_train.Embarked =='Q']
Embarked_S =data_train.Fare[data_train.Embarked =='S']
df=DataFrame({u'港口C':Embarked_C,u'港口Q':Embarked_Q,u'港口S':Embarked_S})
df.plot(kind='kde',xlim=[-50,400])
plt.xlabel(u"票價") 
plt.ylabel(u"核密度")
plt.title(u"不同港口的票價分布圖")
plt.show()

data_train.groupby(['SibSp','Survived']).count()
PassengerId Pclass Name Sex Age Parch Ticket Fare Cabin Embarked
SibSp Survived
0 0 398 398 398 398 296 398 398 398 49 398
1 210 210 210 210 175 210 210 210 77 208
1 0 97 97 97 97 86 97 97 97 17 97
1 112 112 112 112 97 112 112 112 52 112
2 0 15 15 15 15 14 15 15 15 1 15
1 13 13 13 13 11 13 13 13 5 13
3 0 12 12 12 12 8 12 12 12 1 12
1 4 4 4 4 4 4 4 4 2 4
4 0 15 15 15 15 15 15 15 15 0 15
1 3 3 3 3 3 3 3 3 0 3
5 0 5 5 5 5 5 5 5 5 0 5
8 0 7 7 7 7 0 7 7 7 0 7
Survived0=data_train.SibSp[data_train.Survived==0].value_counts()   # 通過布爾索引
Survived1=data_train.SibSp[data_train.Survived==1].value_counts()
Survived1=Series(Survived1,index=Survived0.index)
Survived1=Survived1.fillna(0)
num=Survived0+Survived1
Survived_rate=Survived1/num
Survived_rate=Survived_rate.sort_index()
plt.figure()
Survived_rate.plot()
plt.xlabel(u"兄弟/妹個數") 
plt.ylabel(u"存活率(%)")
plt.title(u"兄弟/妹個數對幸存率的影響")
plt.show()

data_train.groupby(['SibSp','Survived']).count()
PassengerId Pclass Name Sex Age Parch Ticket Fare Cabin Embarked
SibSp Survived
0 0 398 398 398 398 296 398 398 398 49 398
1 210 210 210 210 175 210 210 210 77 208
1 0 97 97 97 97 86 97 97 97 17 97
1 112 112 112 112 97 112 112 112 52 112
2 0 15 15 15 15 14 15 15 15 1 15
1 13 13 13 13 11 13 13 13 5 13
3 0 12 12 12 12 8 12 12 12 1 12
1 4 4 4 4 4 4 4 4 2 4
4 0 15 15 15 15 15 15 15 15 0 15
1 3 3 3 3 3 3 3 3 0 3
5 0 5 5 5 5 5 5 5 5 0 5
8 0 7 7 7 7 0 7 7 7 0 7
Survived0=data_train.Parch[data_train.Survived==0].value_counts()
Survived1=data_train.Parch[data_train.Survived==1].value_counts()
Survived1=Series(Survived1,index=Survived0.index)
Survived1=Survived1.fillna(0)
num=Survived0+Survived1
Survived_rate=Survived1/num
Survived_rate=Survived_rate.sort_index()
plt.figure()
Survived_rate.plot()
plt.xlabel(u"父母/小孩個數") 
plt.ylabel(u"幸存率(%)")
plt.title(u"父母/小孩對幸存率的影響")
plt.show()

Survived_noCabin=data_train.Survived[data_train.Cabin.isnull()]
Survived_haveCabin=data_train.Survived[data_train.Cabin.notnull()]
df=DataFrame({'無':Survived_noCabin.value_counts(),'有':Survived_haveCabin.value_counts()}).transpose()
df.plot(kind='bar', stacked=True)
plt.title(u"按Cabin有無看獲救情況")
plt.xlabel(u"Cabin有無") 
plt.ylabel(u"人數")
plt.show()

3.缺失值處理

通常遇到缺值的情況,我們會有幾種常見的處理方式

  1. 如果缺值的樣比例極高,直接舍棄,因為作為特征可能反倒帶入noise,影響結果
  2. 如果缺值的樣本適中,而該屬性是離散值(比如說類目屬性),那就把NaN作為一個新類別,加到類別特征中
  3. 如果缺值的樣本適中,而該屬性為連續值,有時候我們會考慮給定一個step(比如這里的age,我們可以考慮每隔2/3歲為一個步長),然后把它離散化,之后把NaN作為一個type加到屬性類目中。
  4. 有些情況下,缺失的值個數並不是特別多,那我們也可以試着根據已有的值,使用模型擬合一下數據,補充上。

在第1步的源數據分析中,我們知道Age 和Cabin 有缺失值

其中 :

  • Cabin 字段缺失太多,暫時按Cabin有無數據,將這個屬性處理成Yes和No兩種類
  • Age 字段 通過模型擬合,補充數據
# Cabin 的缺失值填充函數
def set_Cabin_type(df):
#     df.Cabin[df.Cabin.notnull()]='YES'
#     df.Cabin[df.Cabin.isnull()]='NO'
    df.loc[(df.Cabin.notnull()),'Cabin']='YES'
    df.loc[(df.Cabin.isnull()),'Cabin']='NO'
    return df
data_train=set_Cabin_type(data_train)
# 使用 RandomForestClassifier 填補缺失的年齡屬性
from sklearn.ensemble import RandomForestRegressor
def set_missing_ages(df):
    # 把已有的數值型特征取出來丟進Random Forest Regressor中,因為邏輯回歸算法輸入都需要數值型特征
    age_df = df[['Age','Fare', 'Parch', 'SibSp', 'Pclass']]
    age_known=age_df.loc[age_df.Age.notnull()].values   # 要用sklearn的模型,輸入參數要把Series和DataFrame 轉成nparray 
    age_unknown=age_df.loc[age_df.Age.isnull()].values
    #將有值的年齡樣本 訓練數據,然后用訓練好的模型用於 預測 缺失的年齡樣本的值
    #訓練數據的特征集合
    X=age_known[:,1:]
    #訓練數據的目標
    y=age_known[:,0]
    
    #構建隨機森林回歸模型器
    rfr = RandomForestRegressor(random_state=0, n_estimators=2000, n_jobs=-1)
    # 擬合數據,訓練模型
    rfr.fit(X,y)
    # 用訓練好的模型 預測 有缺失值的年齡屬性
    predictedAges=rfr.predict(age_unknown[:,1:])
    # 得到的預測結果,去填補缺失數據
    df.loc[df.Age.isnull(),'Age']=predictedAges
    return df,rfr

data_train,rfr=set_missing_ages(data_train)    
    

4.類目型特征因子化

因為邏輯回歸建模時,需要輸入的特征都是數值型特征,我們通常會先對類目型的特征進行轉化

  • 當類目型特征值 與數值大小有關聯關系時,比如大小S,M,L,可以映射為數值型,比如分別映射為 1,2,3。
  • 當類目型特征值 僅僅表示類別,沒有大小關系,則需要使用 因子化/one-hot編碼,使用pandas的.get_dummies函數。

類目型特征有:Cabin、Embarked、Sex、Pclass

dummies_Cabin = pd.get_dummies(data_train.Cabin,prefix='Cabin') #ptefix 是前綴,因子花之后的字段名為 前綴_類名

dummies_Embarked = pd.get_dummies(data_train['Embarked'], prefix= 'Embarked')

dummies_Sex = pd.get_dummies(data_train['Sex'], prefix= 'Sex')

dummies_Pclass = pd.get_dummies(data_train['Pclass'], prefix= 'Pclass')

df = pd.concat([data_train, dummies_Cabin, dummies_Embarked, dummies_Sex, dummies_Pclass], axis=1) #將啞編碼的內容拼接到data_train后
df.drop(['Pclass', 'Name', 'Sex', 'Ticket', 'Cabin', 'Embarked'], axis=1, inplace=True)  # 把編碼前的字段刪除

5.標准化和歸一化

對於量綱差異太大的數值型特征,我們需要將其標准化或歸一化到一個范圍,不然會對建模產生很大的影響(比如邏輯回歸和梯度下降)

  • 其中有 preprocessing 中的MinMaxScaler類、MinMaxScaler類、Normalizer類等

需要做 scaleing的 特征有: Age、Fare

# 這里用StandardScaler類對
from sklearn.preprocessing import StandardScaler
# 創建一個定標器
scaler=StandardScaler()
# 擬合數據 
#---fit和transform為兩個動作,可用fit_transform 合並完成
#df['Age_Scale']=scaler.fit_transform(df.Age.values.reshape(-1,1))  # 若為單個特征,需要reshape為(-1,1)

#--但是由於test和train 需要用同一個fit出來的參數,所以需要記錄fit參數,用於test數據的標准化,因此分開計算
Age_Scale_parame=scaler.fit(df.Age.values.reshape(-1,1))
#df['Age_Scale']=scaler.transform(df.Age.values.reshape(-1,1))
df['Age_Scale']=scaler.fit_transform(df.Age.values.reshape(-1,1),Age_Scale_parame)

Fare_Scale_parame=scaler.fit(df.Fare.values.reshape(-1,1))
df['Fare_Scale']=scaler.fit_transform(df.Age.values.reshape(-1,1),Fare_Scale_parame)
df.drop(['Age', 'Fare'], axis=1, inplace=True)

到這里,我們的數據預處理就結束了。

6.訓練模型

接下來,我們用邏輯回歸算法將訓練數據進行訓練模型

from sklearn.linear_model import LogisticRegression
df_train=df.filter(regex='Survived|Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*').values #用正則表達式把需要的字段過濾出來
# 訓練特征
df_train_feature=df_train[:,1:]
#訓練目標
df_train_label=df_train[:,0]
#構建邏輯回歸分類器
clf=LogisticRegression(C=1.0, penalty='l1', tol=1e-6)
#擬合數據
clf.fit(df_train_feature,df_train_label)
clf
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l1', random_state=None, solver='liblinear', tol=1e-06,
          verbose=0, warm_start=False)

7.測試模型


我們對測試數據 做訓練數據相同的數據預處理,包括:

  • 缺失值填充:Age,Cabin,Fare
  • 類目特征因子化: Pclass、Sex、Cabin、Embarked
  • 歸一化 : Age、Fare
data_test = pd.read_csv("test.csv")
# 缺失值填充
data_test.loc[data_test.Fare.isnull(),'Fare']=0
data_test= set_Cabin_type(data_test)
age_data = data_test[['Age','Fare', 'Parch', 'SibSp', 'Pclass']]
age_test=age_data[age_data.Age.isnull()].values
# age的缺失值填充也用前訓練數據 的age值fit計算的模型,所以可以直接預測
predictedAges = rfr.predict(age_test[:,1:])
data_test.loc[data_test.Age.isnull(),'Age'] = predictedAges

# 類目特征因子化
dummies_Cabin = pd.get_dummies(data_test.Cabin, prefix= 'Cabin')
dummies_Sex = pd.get_dummies(data_test.Sex, prefix= 'Sex')
dummies_Pclass = pd.get_dummies(data_test.Pclass, prefix= 'Pclass')
dummies_Embarked = pd.get_dummies(data_test.Embarked, prefix= 'Embarked')

#歸一化也用訓練數據fit出來的參數進行轉化
data_test['Age_scaled'] = scaler.fit_transform(data_test['Age'].values.reshape(-1, 1), Age_Scale_parame)
data_test['Fare_scaled'] = scaler.fit_transform(data_test['Fare'].values.reshape(-1, 1), Fare_Scale_parame)

# 拼接處理后數據以及刪除處理前數據
df_test = pd.concat([data_test, dummies_Cabin, dummies_Embarked, dummies_Sex, dummies_Pclass], axis=1)
df_test.drop(['Pclass', 'Name', 'Sex', 'Ticket', 'Cabin', 'Embarked','Age','Fare'], axis=1, inplace=True)

將測試數據 用訓練數據訓練出來的模型 進行預測

df_test=df_test.values
# df_test = df_test.filter(regex='Age_.*|SibSp|Parch|Fare_.*|Cabin_.*|Embarked_.*|Sex_.*|Pclass_.*')  #可用正則表達式取刪選數據
predict_result=clf.predict(df_test[:,1:])
result = pd.DataFrame({'PassengerId':data_test['PassengerId'].values, 'Survived':predict_result.astype(np.int32)})
result.to_csv("logistic_regression_predictions.csv", index=False)
pd.read_csv("logistic_regression_predictions.csv")
PassengerId Survived
0 892 1
1 893 0
2 894 1
3 895 0
4 896 0
5 897 0
6 898 1
7 899 0
8 900 0
9 901 0
10 902 0
11 903 0
12 904 0
13 905 0
14 906 0
15 907 0
16 908 1
17 909 0
18 910 0
19 911 0
20 912 0
21 913 0
22 914 0
23 915 0
24 916 1
25 917 0
26 918 0
27 919 0
28 920 0
29 921 0
... ... ...
388 1280 1
389 1281 0
390 1282 0
391 1283 0
392 1284 0
393 1285 0
394 1286 0
395 1287 0
396 1288 1
397 1289 0
398 1290 0
399 1291 1
400 1292 1
401 1293 0
402 1294 0
403 1295 0
404 1296 0
405 1297 0
406 1298 0
407 1299 1
408 1300 1
409 1301 0
410 1302 1
411 1303 1
412 1304 0
413 1305 0
414 1306 0
415 1307 0
416 1308 0
417 1309 0

418 rows × 2 columns

把預測結果提交到Kaggle官網,得到准確率為:0.76555。

沒錯,效果還不夠理想,因為簡單分析過后出的一個baseline系統,前面數據探索出來的結論還沒用上呢,真正的挖掘工作現在才剛剛開始呢~~~

接下來將對模型狀態進行分析,並做一系列的優化工作。

未完待續...


本文參考了來自寒小陽的github:Kaggle_Titanic,有興趣的小伙伴可以看看。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM