机器学习: Kaggle Titanic入门比赛

203 阅读9分钟

Titanic生存预测

1912年4月15日,在泰坦尼克号的处女航中,这艘被广泛认为“永不沉没”的巨轮在与冰山相撞后沉没。不幸的是,船上并没有足够的救生艇,最终导致2224名乘客和船员中有1502人死亡。

Kaggle的Titanic入门比赛是以此事件为背景的机器学习比赛,需要通过Kaggle提供的乘客信息训练模型,进行对乘客是否生还的预测。

数据处理

1.引入依赖

首先我们在Kaggle中的Titanic比赛页面创建一个代码页面,初始状态Kaggle会为我们引入numpy与pandas。

numpy是主要用于科学计算的库,pandas是主要用于数据处理的库。

我们还需要引入matplotlib,这是一个绘图库,用于之后的数据分析过程。

# This Python 3 environment comes with many helpful analytics libraries installed
# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python
# For example, here's several helpful packages to load

import numpy as np # linear algebra
import pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)
from matplotlib import pyplot as plt # 引入图表库

2.引入数据

在代码页面中kaggle为我们提供了本次任务所需要的数据集,在右方的data区域可以复制到地址,使用pandas的read_csv()方法可以将文件读取为DataFrame对象方便之后的处理。主要使用的文件是train.csv与test.csv。同时使用DataFrame的head()方法查看一下数据的情况。

# 引入数据
titanic_train = pd.read_csv('../input/titanic/train.csv')
titanic_test = pd.read_csv('../input/titanic/test.csv')
titanic_train.head()

其中每一条数据就是一位乘客的信息,可以看出数据中有PClass, Name, Sex等特征,下一步就要对这些特征进行分析处理。

3.分析数据

首先使用info()方法查看训练集的统计数据。

# 查看训练测试数据信息
titanic_train.info()

这样就可以看出每个特征值的类型,而且Age, Cabin有大量缺失值,Embarked有少量缺失值。

在了解了这些信息我们就可以开始逐个分析数据了。由于PassageId显然对预测是无意义的,Survived为数据的标签,故从Pclass开始分析。

Pclass

该特征是旅客等级,类比高铁的一二等座。数据类型为整型数据,取值为[1, 2, 3],数字越小等级越高。通过常识判断旅客等级越高旅客的社会地位、经济实力、人脉资源往往越高,故生还率可能相对等级低的旅客较高。通过柱形图的方式查看一下Pclass与Survived的关系。

titanic_train.groupby(['Pclass', 'Survived'])['Survived'].count().unstack().plot(kind = 'bar', stacked = 'True')

和猜测的差不多,随着等级的提升旅客的生还几率也在提升。故这个Pclass特征可以保留下来。

Name

该特征是旅客姓名,从整体来看很难联想到该属性与生还率有什么关系,而且也不好量化,不过该特征中的头衔可以被提取出来单独分析。头衔与旅客的社会地位有一定的联系,而社会地位可能会对生还率产生影响,同时Age特征有大量缺失值,利用同称呼的平均年龄进行填补可能精度更高。

通过正则表达式从Name提取新的特征Title.

# 提取Title
titanic_train['Title'] = titanic_train['Name'].str.extract('([A-Za-z]+)\.', expand = False)

Title

该特征是从Name中提取的旅客头衔,由于头衔众多有的头衔的样本实在是太少,所以先根据社会属性分组进行分析。

# 将Title映射到社会属性
titanic_train['Title'].replace(['Capt', 'Col', 'Major', 'Dr', 'Rev'],'Officer', inplace=True)
titanic_train['Title'].replace(['Don', 'Sir', 'the Countess', 'Countess', 'Dona', 'Lady'], 'Royalty', inplace=True)
titanic_train['Title'].replace(['Mme', 'Ms', 'Mrs'],'Mrs', inplace=True)
titanic_train['Title'].replace(['Mlle', 'Miss'], 'Miss', inplace=True)
titanic_train['Title'].replace(['Master','Jonkheer'],'Master', inplace=True)
titanic_train['Title'].replace(['Mr'], 'Mr', inplace=True)

查看一下Title与Survived关系。

titanic_train[['Title','Survived']].groupby(['Title']).mean().plot.bar()

可以看出不同社会属性的旅客的平均生汇率还是有显著区别的。

Sex

该特征是旅客的性别,当时由于救生艇数量远远不够所以逃生策略为“妇女和儿童优先”。这样的话Sex特征对于预测就非常重要了。从柱状图中也可以清晰的看出男性的总数量是要多于女性的,生还下来的男性是少于女性的,大部分的男性都遇难了。

titanic_train.groupby(['Sex', 'Survived'])['Survived'].count().unstack().plot(kind = 'bar', stacked = 'True')

Age

该特征旅客的年龄,由上可知儿童也是优先逃生的,所以年龄较小的乘客平均生还率显著提高。不过Age特征有大量的缺失值,所以先要进行填补。由于已经提取了Title特征,所以我们可以试着使用相同Title的旅客的平均年龄进行填补,因为相同头衔的旅客年龄差距可能相近。首先看这样做的是否合理,使用DataFrame的describe()方法查看一下整体Age数据的统计信息。

# 查看训练数据统计信息
titanic_train.describe()

image.png 可以看出Age的std(标准差)是14.52...,然后我们再看看通过Title分组之后的Age数据。

titanic_train[['Title','Age']].groupby(['Title']).describe()

image.png 可以看到标准差缩小了,故我们可以试着用Title来进行Age的分组填补。

填补之后由于年龄的取值非常多,为了避免一些异常值对于结果的影响将年龄分为几个区间进行模型的训练。再查看一下Age与Survived关系。

# 将年龄分组避免异常值影响
bins = [0, 12, 18, 65, 100]
titanic_train['Age_group'] = pd.cut(titanic_train['Age'], bins, labels = [1, 2, 3, 4])
titanic_train[['Age_group', 'Survived']].groupby(['Age_group']).mean().plot.bar()

可以看出年龄对生还率的影响是比较大的。

SibSp&Parch

这两个特征分别为旅客同代与非同代的直系亲属数量,观察一下两个特征与Survived关系后可知这两个特征都是数量适中的旅客的生还率较高。所以将其合并为一个Family属性表示家庭成员个数,再根据是否有家庭成员提取出一个Is_alone属性表示是否独自上船。

# 将亲属属性转化为家庭和是否一个人上船
titanic_train['Family'] = titanic_train["SibSp"] + titanic_train['Parch']
titanic_train['Is_alone'] = titanic_train['Family'].apply(lambda x:1 if x==0 else 0)
titanic_test['Family'] = titanic_test["SibSp"] + titanic_test['Parch']
titanic_test['Is_alone'] = titanic_test['Family'].apply(lambda x:1 if x==0 else 0)

观察一下两个新属性与Survived的关系。

titanic_train[['Family', 'Survived']].groupby('Family').mean().plot.bar(),
titanic_train[['Is_alone', 'Survived']].groupby('Is_alone').mean().plot.bar()

image.png 可以看出Family依然是数量适中的旅客生还几率大,而且非独自上船的旅客也更容易生还。

Ticket

该特征是旅客的票号,这个特征的值比较杂乱,我没有发现可以利用的点。所以我舍弃了这个特征。

Fare

该特征是旅客的旅费,可以想到旅费应该会和旅客的经济实力相关,而有钱的旅客更容易存活。和Age一样将Fare分组分析一下柱形图。

# 旅费特征生还率
bins = [0, 50, 100, 150, 200, 250, 300, 550]
titanic_train['Fare_group'] = pd.cut(titanic_test['Fare'], bins, labels = [1, 2, 3, 4, 5, 6, 7])
titanic_train[['Fare_group', 'Survived']].groupby(['Fare_group']).mean().plot.bar()

image.png 可以清晰看出和我们的预期一样,Fare也是对于预测非常重要的特征。

Cabin

该特征是旅客的房号,实际上这个特征应该是有参考意义的。房间与救生艇的距离和与撞击冰山损坏船体的位置的应该会对旅客的生还率造成影响。但是由于该特征的缺失值太多,而且我并没有想到很好的填补方式,盲目填补的话可能会造成大量的noise,所以还是将其舍去了。

Embarked

该特征是旅客的登船港口,有三个地点分别是南安普顿(S)、瑟堡(C)和皇后镇(Q)。先观察一下Embarked与Survived关系。

titanic_train.groupby(['Embarked', 'Survived'])['Survived'].count().unstack().plot(kind = 'bar', stacked = 'True')

image.png 可以看出第一站登船的旅客最多,且生还率也最高。这可能和这些旅客中的部分会在后两站下船,或与登船时分配的甲板位置有关。所以在预测时也要考虑Embarked特征。

4.特征处理

因为一些特征的值是文本的形式,往往这样的特征需要转换为数值型的特征才能进行建模分析。一般有两种处理方式:一种是将其值转换为数字变量,像1,2,3,4,但是这样对于一些并没有大小关系的特征并不是太好,例如港口特征的取值如果转换为1,2,3其实并不是太合理。还有一种方式就是one-hot编码,以港口举例,one-hot方式会将港口特征分成三个特征,对应三个值,这三个特征采用0,1的方式表示具体的值,0,1的取值也有利于提高计算性能。我们可以使用pandas的get_dummies()方法对Sex、Embarked、Title特征完成这个操作。

# 对文本特征转换为数值特征
for column in ['Sex', 'Embarked', 'Title']:
    x = pd.get_dummies(titanic_train[column], prefix = column)
    titanic_train = pd.concat([titanic_train, x], axis = 1)
titanic_train.head()

image.png 最后挑选出需要使用的特征设置为train_X,Survived设置为train_y。

# 将特征和标签分开划分数据集
feature_columns = ['Pclass', 'Family', 'Parch', 'Fare_group', 'Age_group', 'Sex_female', 'Sex_male', 'Embarked_C', 'Embarked_Q', 'Embarked_S', 'Title_Master', 'Title_Miss', 'Title_Mr', 'Title_Mrs', 'Title_Officer', 'Title_Royalty']
train_X = titanic_train[feature_columns]
train_y = titanic_train['Survived']

数据建模

本次预测我使用的是随机森林模型。首先引入该模型,并且在预测之前也要对test数据进行数据处理的一系列操作。

# 引入模型算法
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(max_depth=77, min_samples_split = 40, n_estimators = 283).fit(train_X, train_y)

在查阅一些相关博客后了解到可以使用交叉验证求出一个较好的参数。

from bayes_opt import BayesianOptimization
from sklearn.model_selection import cross_val_score
def rbt_optimization(cv_splits):
    def function(m_p, m_s_s, n_s):
        return cross_val_score(
            RandomForestClassifier(
                max_depth = max(0,int(m_p)), random_state = 0, min_samples_split = max(0,int(m_s_s)), n_estimators = max(0,int(n_e))),  
                X=train_X, 
                y=train_y, 
                cv=cv_splits,
                scoring="roc_auc",
                n_jobs=-1).mean()
    parameters = {
        "m_p": (1, 150),
        "m_s_s": (2, 40),
        "n_e": (10, 700)}
    return function, parameters
    
def bayesian_optimization(dataset, function, parameters):
    train_X, train_y = dataset
    n_iterations = 100
    gp_params = {"alpha": 1e-4}
    BO = BayesianOptimization(function, parameters)
    BO.maximize(n_iter=n_iterations, **gp_params)
    return BO.max
    
dataset = [train_X,train_y]
f, p = rbt_optimization(2)
result = bayesian_optimization(dataset, f, p)

最后我使用的参数为max_depth=77, min_samples_split = 40, n_estimators = 283。 再将预测的结果写入submission.csv提交即可完成整个流程。

# 使用随机森林模型进行预测
predict_y = rf.predict(test_X)
# 写入sub
passenger_id = titanic_test['PassengerId']
sub = {'PassengerId': passenger_id, 'Survived': predict_y}
submission = pd.DataFrame(sub)
submission.to_csv('submission.csv', index = False)

最后得分:

image.png 排名为:

image.png

总结

作为研一入学前的预习,在这个项目学到了挺多基础的机器学习知识。由于对于数据处理方法与各个机器学习算法的不够熟悉,所以整体完成的还是比较粗糙的。开学深入学习后再优化一下代码,提高一下分数。