Kaggle入门之Titanic幸存者分析

最近开始学习机器学习的相关知识,学习需要实践,于是就打算在kaggle上做个项目。第一个kaggle项目就是Titanic,kaggle最简单,也是最经典的项目。我按照kaggle提供的教学文章做了一遍,断断续续得做了两个星期,但是学到了很多知识,感觉最重要的,是熟悉了这种数据竞赛的流程。

项目地址:Titanic: Machine Learning from Disaster
参考文章:Titanic Data Science Solution

1. 问题描述

已有的数据是Titanic号的乘客资料。要求通过分析这份资料,通过机器学习的方法,预测乘客的存活情况。

前期准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 数据整理、分析
import pandas as pd
import numpy as np
import random as rnd

# 可视化
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline

# 机器学习模型
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC, LinearSVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.linear_model import Perceptron
from sklearn.linear_model import SGDClassifier
from sklearn.tree import DecisionTreeClassifier

2. 获得数据

对于一些操作,可以把训练集和测试集合并处理,比如把数据集的标题转化为数值。

1
2
3
train_df = pd.read_csv('./input/train.csv')
test_df = pd.read_csv('./input/test.csv')
combine = [train_df, test_df]

3. 数据分析

数据集中有哪些特征?

1
print(train_df.columns.values)
特征缩写 特征含义
PassengerId 乘客编号
Survived 是否存活(1表示存活,0表示死亡)
Pclass 舱位等级(1-3表示舱位从高到低)
Name 姓名
Sex 性别
Age 年龄
SibSp 一同乘船的兄弟姐妹或配偶的人数
Parch 一同乘船的父母或子女的人数
Ticket 票号
Fare 票价
Cabin 舱位号
Embarked 出发地(C指Cherbourg,Q指Queenstown,S指Southampton)

数据类型

不同特征具有不同的数据类型

  • 分类型数据(categorical)
    分类型数据一般表示事物的某种特性。题目中分类型数据为:Survived,Sex。
  • 顺序型数据(ordinal)
    数据是有分类性质的,但衡量性质的数字也有实际意义。一般被归为分类型数据。本题中Pclass就是一个例子,数字1~3表示一等舱,二等舱,三等舱,舱位级别从高到低。
  • 数值型数据(numerical)
    数值型数据一般分为离散型和连续型。其中离散型数据一般通过计数得到,本题中出现的是SibSp,Parch。连续型数据一般表示特征的测量值,不能被计数,只能通过区间去衡量,本题中有Age,Fare。
  • 混合型数据(mixed)
    既包含数值,又包含字符。这种数据需要校正。本题中Ticker和Cabin这种数据就是混合型。
1
2
# preview the data
train_df.head()
1
train_df.tail()

哪些特征的数据可能包含错误或缺失?

在姓名这个特征中,可能包含称呼,名字,外号等,容易在录入的时候出现错误。这种数据需要校正。
在训练集中,舱位号,年龄,出发地都存在数据缺失。在测试集中,舱位号和年龄也有缺失。

1
2
3
train_df.info()
print('_'*40)
test_df.info()

样本中数值型数据的分布

  • 题目所给乘客总人数为2224,死亡人数为1502,幸存人数为722,存活率为32.4%。
  • 样本总数为891, 存活率为38.3%。
  • 大多数的乘客(>75%)没有和父母或孩子同行。
  • 将近30%的乘客有兄弟姐妹或配偶也在船上。
  • 不同乘客所购船票的票价差异很大,极少数人(1%)的票价高至$512。
  • 年龄在65~80岁之间的乘客只占不到总数的1%。
1
2
3
4
5
6
7
8
# 数值型数据
train_df.describe()

# survived的mean在另一层面上就是幸存者的比率。
# 通过调节 percentiles的参数,可以看到各项特征的参数
# 设置 percentiles=[.76, .77],查看parch的数值分布变化
# 设置 percentiles=[.68, .69],查看sibsp的数值分布变化
# 设置 percentiles=[.1, .2, .3, .4, .5, .6, .7, .8, .9, .99],查看age和fare的数值分布变化

样本中分类型数据的分布

  • 样本中每个人名字是唯一的,没有重复的。
  • 样本中有577位男性,314位女性。
  • 舱位号的值存在重复,这说明有一些乘客共用一个船舱。
  • 出发地有三个,其中从Southampton出发的乘客最多。
  • 票号大约有22%的重复。
1
2
# 分类型数据
train_df.describe(include=['O'])

一些假设

  • 相关性(Correlating)
    我们想知道每个特征和是否存活的关系。这个工作需要在早期进行,把这些特征应用到预测模型中。

  • 完整性(Completing)
    1. 年龄特征需要补全,因为题目告诉了它影响了乘客是否存活。
    2. 出发地特征也需要补全,因为它可能和存活率或其他特征有关。

  • 校正(Correcting)
    1. 票号特征可以去掉,因为它存在很高的重复率(22%),而且它可能和存活率没有关系。
    2. 舱位号特征也可以去掉,因为它的数据大部分缺失,而且有很多空白值。
    3. 乘客编号和姓名也可以去掉,因为这类数据和存活率并没有直接联系。

  • 新增特征(Creating)
    1. 我们可能会新增一个特征叫做家庭成员,根据SibSp和Parch来计算这个乘客有多少个家人也一同登船。
    2. 我们可能会根据乘客姓名,提取乘客的title,并新建特征。
    3. 我们可能会把年龄特征改为年龄带,从而把数值型特征转变为顺序型特征。
    4. 我们可能会新增一个特征,来表示船票票价的区间。

  • 分类(Classifying)
    1. 女性乘客更可能存活。(Sex = female)
    2. 儿童更可能存活。(Age < ?)
    3. 上层阶级更可能存活。(Pclass = 1)

数据透视

为了确认我们的发现和假设,可以快速的分析一些特征和存活率的关系通过数据透视的方法(感觉这个翻译有点奇怪)。这种方法需要特征没有空值。满足条件的特征有Sex,Pclass,SibSp,Parch这四个。

  • Sex
    可以看到,女性乘客中,有74%的乘客最终存活。于是我们验证了classifying #1,女性乘客更容易存活。
1
train_df[["Sex", "Survived"]].groupby(['Sex'], as_index=False).mean().sort_values(by='Survived', ascending=False)
  • Pclass
    可以看到,在一等舱的乘客中存活率达到了62.9%(>50%),这验证了classifying #3,上层阶级更容易存活。
1
train_df[['Pclass', 'Survived']].groupby(['Pclass'], as_index=False).mean().sort_values(by='Survived', ascending=False)
  • SibSp 和 Parch
    把SibSp特征和Parch特征的数据结合起来,新建一个特征表示家庭成员的情况。
1
train_df[["SibSp", "Survived"]].groupby(['SibSp'], as_index=False).mean().sort_values(by='Survived', ascending=False)
1
train_df[["Parch", "Survived"]].groupby(['Parch'], as_index=False).mean().sort_values(by='Survived', ascending=False)

4. 数据可视化

数值型特征与存活率

分析年龄与存活率的关系。
直方图的x轴表示乘客的数量。

观察

  • 婴儿(Age <= 4)有很高的存活率。
  • 最老的乘客(Age = 80)活下来了。
  • 很大一批年龄在15~25之间的乘客没有活下来。
  • 大多数乘客的年龄在15~35之间。

决定
根据这个分析,我们应该:

  • 在预测模型中考虑年龄(Age)因素。(classifying #2)
  • 补全Age特征下数据的空白。(completing #1)
  • 把年龄分组。(creating #3)
1
2
g = sns.FacetGrid(train_df, col='Survived')
g.map(plt.hist, 'Age', bins=20)

顺序型特征与存活率

分析年龄,舱位等级和存活率的关系。

观察

  • 大多数乘客在三等舱(Pclass=3),其中大部分人没能幸存。(classifying #3) (原文写的2 写错了?)
  • 大多数的婴儿在二等舱和三等舱,而且大部分婴儿活了下来。(classifying #2)
  • 大部分一等舱的乘客活了下来。(classifying #3)
  • 三种舱位在各个年龄段的乘客中都有分布。

决定

  • 应当把Pclass列入预测模型中。
1
2
3
4
grid = sns.FacetGrid(train_df, col='Pclass', hue='Survived')
#grid = sns.FacetGrid(train_df, col='Survived', row='Pclass', size=2.2, aspect=1.6)
grid.map(plt.hist, 'Age', alpha=.5, bins=20)
grid.add_legend();

分类型特征与存活率

分析出发地,性别,舱位和存活率的关系。

观察

  • 女性乘客比男性乘客有更高的存活率。classifying #1
  • 只有从Cherbourg出发的乘客中,男性的存活率更高。这说明了Pclass和Embarked有联系,同样的,Pclass和Survived也有联系,但并不一定说明Embarked和Survived有直接联系。
  • 对于从C和Q地出发的男乘客中,乘坐三等舱的乘客的存活率比二等舱高。completing #2
  • 对于乘坐三等舱的乘客还有男乘客来说,他们的存活率在不同的出发地都不同。Ports of embarkation have varying survival rates for Pclass=3 and among male passengers.correlating #1

决定

  • 应当把Sex列入预测模型中。
  • 把出发地特征的数据补全。
1
2
3
4
grid = sns.FacetGrid(train_df, col='Embarked')
#grid = sns.FacetGrid(train_df, row='Embarked', size=2.2, aspect=1.6)
grid.map(sns.pointplot, 'Pclass', 'Survived', 'Sex', palette='deep')
grid.add_legend()

分类型、数值型特征与存活率

分析出发地,性别,票价和存活率的关系。

观察

  • 购买更贵票价的乘客有更高的存活率。creating #4
  • 出发地和存活率有关系。correlating #1,completing #2

决定

  • 应当把票价区间列入预测模型。
1
2
3
4
#grid = sns.FacetGrid(train_df, col='Embarked', hue='Survived', palette={0: 'k', 1: 'b'})
grid = sns.FacetGrid(train_df, row='Embarked', col='Survived', size=2.2, aspect=1.6)
grid.map(sns.barplot, 'Sex', 'Fare', alpha=.5, ci=None)
grid.add_legend()

5. 数据整理

去除无用特征

没有用处的数据的清楚能简化分析,加速模型的训练。根据correcting #1correcting #2,Ticket和Cabin应当去除。
注意训练集和测试集都要去除特征。

1
2
3
4
5
6
7
print("Before", train_df.shape, test_df.shape, combine[0].shape, combine[1].shape)

train_df = train_df.drop(['Ticket', 'Cabin'], axis=1)
test_df = test_df.drop(['Ticket', 'Cabin'], axis=1)
combine = [train_df, test_df]

"After", train_df.shape, test_df.shape, combine[0].shape, combine[1].shape

新增Title特征

通过正则表达式提取名字中的title。正则表达式(\w+\.)能匹配第一个以点为结尾的单词。

观察
当我们观察title,age和存活率的关系,有以下发现:

  • 不同title间的年龄有明显差异,title很好的划分了年龄区间。
  • Survival among Title Age bands varies slightly. (这句不太理解)
  • 某些特定的title都活下来了(Mme, Lady, Sir),有些都没活下来(Don, Rev, Jonkheer)。

决定

  • 把title特征列入预测模型。
1
2
3
4
5
# 提取title,并加入已有的数据集中
for dataset in combine:
dataset['Title'] = dataset.Name.str.extract(' ([A-Za-z]+)\.', expand=False)

pd.crosstab(train_df['Title'], train_df['Sex'])

把一些相似的title合并到一起,并把少见的title归到一个名为rare的title里。

1
2
3
4
5
6
7
8
9
10
for dataset in combine:
dataset['Title'] = dataset['Title'].replace(['Lady', 'Countess','Capt', 'Col',\
'Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare')

dataset['Title'] = dataset['Title'].replace('Mlle', 'Miss')
dataset['Title'] = dataset['Title'].replace('Ms', 'Miss')
dataset['Title'] = dataset['Title'].replace('Mme', 'Mrs')

# 训练集中每个title下的存活率
train_df[['Title', 'Survived']].groupby(['Title'], as_index=False).mean()

把分类型数据转换为顺序型数据。

1
2
3
4
5
6
title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5}
for dataset in combine:
dataset['Title'] = dataset['Title'].map(title_mapping)
dataset['Title'] = dataset['Title'].fillna(0)

train_df.head()

记得把name和PassengerID这两个特征去掉。correcting #3

1
2
3
4
train_df = train_df.drop(['Name', 'PassengerId'], axis=1)
test_df = test_df.drop(['Name'], axis=1)
combine = [train_df, test_df]
train_df.shape, test_df.shape

性别特征的数值化

性别特征是分类型特征,为了在模型中能运用,需要把它转换为数值型特征。(female = 1, male = 0)

1
2
3
4
for dataset in combine:
dataset['Sex'] = dataset['Sex'].map( {'female': 1, 'male': 0} ).astype(int)

train_df.head()

补全年龄特征

原文给出三种方法去补全数据:

  1. 根据原始数据,随机产生在均值和标准差之间的数值。
  2. 根据相关的特征猜出缺失的年龄。比如年龄与Pclass和Sex有关,那么对于一个已知Pclass和Sex的乘客,可以猜测他/她的年龄为所有这个Pclass和Sex的乘客年龄的中位数。
  3. 把方法1和2结合起来。对于所有该Pclass和Sex的乘客,计算他们年龄的均值与标准差,随机产生之间的数值作为猜测的年龄。

由于方法1和方法3会引入随机误差,每次的结果都不一样,所以这里采用方法2。

1
2
3
4
5
# 不同的Pclass和Sex一共有6种组合
# grid = sns.FacetGrid(train_df, col='Pclass', hue='Gender')
grid = sns.FacetGrid(train_df, row='Pclass', col='Sex', size=2.2, aspect=1.6)
grid.map(plt.hist, 'Age', alpha=.5, bins=20)
grid.add_legend()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 新建空数组,用来储存这6种组合下的中位数
guess_ages = np.zeros((2,3))

for dataset in combine:
for i in range(0, 2):
for j in range(0, 3):
guess_df = dataset[(dataset['Sex'] == i) & \
(dataset['Pclass'] == j+1)]['Age'].dropna()

# 方法3
# age_mean = guess_df.mean()
# age_std = guess_df.std()
# age_guess = rnd.uniform(age_mean - age_std, age_mean + age_std)

# 方法2
age_guess = guess_df.median()

# Convert random age float to nearest .5 age
guess_ages[i,j] = int( age_guess/0.5 + 0.5 ) * 0.5

for i in range(0, 2):
for j in range(0, 3):
dataset.loc[ (dataset.Age.isnull()) & (dataset.Sex == i) & (dataset.Pclass == j+1),\
'Age'] = guess_ages[i,j]

dataset['Age'] = dataset['Age'].astype(int)

train_df.head()

新建特征AgeBand,用来显示不同年龄区间下的存活率。

1
2
train_df['AgeBand'] = pd.cut(train_df['Age'], 5)
train_df[['AgeBand', 'Survived']].groupby(['AgeBand'], as_index=False).mean().sort_values(by='AgeBand', ascending=True)
1
2
3
4
5
6
7
8
9
# 根据AgeBand,把年龄换成顺序型数据。
for dataset in combine:
dataset.loc[ dataset['Age'] <= 16, 'Age'] = 0
dataset.loc[(dataset['Age'] > 16) & (dataset['Age'] <= 32), 'Age'] = 1
dataset.loc[(dataset['Age'] > 32) & (dataset['Age'] <= 48), 'Age'] = 2
dataset.loc[(dataset['Age'] > 48) & (dataset['Age'] <= 64), 'Age'] = 3
dataset.loc[ dataset['Age'] > 64, 'Age'] = 4
#相比原文做了修改
train_df.head()
1
2
3
4
# 去掉AgeBand特征
train_df = train_df.drop(['AgeBand'], axis=1)
combine = [train_df, test_df]
train_df.head()

创建IsAlone特征

通过把Parch和SibSp特征结合起来,创建FamilySize特征,来记录与该乘客同行的所有家庭成员的数量。

1
2
3
4
for dataset in combine:
dataset['FamilySize'] = dataset['SibSp'] + dataset['Parch'] + 1

train_df[['FamilySize', 'Survived']].groupby(['FamilySize'], as_index=False).mean().sort_values(by='Survived', ascending=False)

可以发现,不同FamilySize下乘客的存活率并没有明显关联。因此,尝试创建一个IsAlone特征,来区分独自乘船和有家人陪同乘船的乘客。

1
2
3
4
5
for dataset in combine:
dataset['IsAlone'] = 0
dataset.loc[dataset['FamilySize'] == 1, 'IsAlone'] = 1

train_df[['IsAlone', 'Survived']].groupby(['IsAlone'], as_index=False).mean()

可以看出,独自乘船和有家人陪同乘船的乘客,存活率有明显差异。因此,保留IsAlone特征,去除Parch、SibSp和FamilySize特征。

1
2
3
4
5
train_df = train_df.drop(['Parch', 'SibSp', 'FamilySize'], axis=1)
test_df = test_df.drop(['Parch', 'SibSp', 'FamilySize'], axis=1)
combine = [train_df, test_df]

train_df.head()

新建Age*Class特征

把Age和Pclass相乘,获得Age*Class特征。(不太理解为什么要加这个特征)

1
2
3
4
for dataset in combine:
dataset['Age*Class'] = dataset.Age * dataset.Pclass

train_df.loc[:, ['Age*Class', 'Age', 'Pclass']].head(10)

处理Embarked特征

训练集中有两组数据缺少Embarked特征,在这里简单的补全为Embarked的众数。

1
2
3
4
5
6
7
# S
freq_port = train_df.Embarked.dropna().mode()[0]

for dataset in combine:
dataset['Embarked'] = dataset['Embarked'].fillna(freq_port)

train_df[['Embarked', 'Survived']].groupby(['Embarked'], as_index=False).mean().sort_values(by='Survived', ascending=False)

Embarked特征为分类型特征,需要转化为数值型特征才能用到预测模型中。

1
2
3
4
for dataset in combine:
dataset['Embarked'] = dataset['Embarked'].map( {'S': 0, 'C': 1, 'Q': 2} ).astype(int)

train_df.head()

处理Fare特征

测试集中有一组数据的fare特征丢失,在这里简单的补全为fare特征的众数。

1
2
test_df['Fare'].fillna(test_df['Fare'].dropna().median(), inplace=True)
test_df.head()

类似于Age特征,为了方便计算,这里把Fare特征先转换为FareBand,再转换为顺序型特征。

1
2
train_df['FareBand'] = pd.qcut(train_df['Fare'], 4)
train_df[['FareBand', 'Survived']].groupby(['FareBand'], as_index=False).mean().sort_values(by='FareBand', ascending=True)
1
2
3
4
5
6
7
8
9
10
11
12
for dataset in combine:
dataset.loc[ dataset['Fare'] <= 7.91, 'Fare'] = 0
dataset.loc[(dataset['Fare'] > 7.91) & (dataset['Fare'] <= 14.454), 'Fare'] = 1
dataset.loc[(dataset['Fare'] > 14.454) & (dataset['Fare'] <= 31), 'Fare'] = 2
dataset.loc[ dataset['Fare'] > 31, 'Fare'] = 3
dataset['Fare'] = dataset['Fare'].astype(int)

train_df = train_df.drop(['FareBand'], axis=1)
combine = [train_df, test_df]

train_df.head(10)
#test_df.head(10)

6. 模型的建立、训练与预测

本题目是一个二元分类与回归型问题。适合该题目的预测模型有如下几种:

  • Logistic Regression(逻辑回归)
  • KNN or k-Nearest Neighbors(K近邻)
  • Support Vector Machines(支持向量机)
  • Naive Bayes classifier(朴素贝叶斯分类器)
  • Decision Tree(决策树)
  • Random Forrest(随机森林)
  • Perceptron(感知机)
  • Artificial neural network(人工神经网络)
  • RVM or Relevance Vector Machine(相关向量机)
1
2
3
4
X_train = train_df.drop("Survived", axis=1)
Y_train = train_df["Survived"]
X_test = test_df.drop("PassengerId", axis=1).copy()
X_train.shape, Y_train.shape, X_test.shape

Logistic Regression

1
2
3
4
5
logreg = LogisticRegression()
logreg.fit(X_train, Y_train)
Y_pred = logreg.predict(X_test)
acc_log = round(logreg.score(X_train, Y_train) * 100, 2)
acc_log

通过logistic Regression模型的参数,我们可以验证之前的假设。越大的参数值说明该特征对结果的影响越大。参数值的正负也表现出特征和结果的正负相关。

  • Sex参数有最大的数值,说明性别对存活率的影响最大。
  • 相应的,Pclass越大,存活率越低。
  • Age*Class对存活率有第二大的负相关。
  • Title有第二大的正相关。
1
2
3
4
5
coeff_df = pd.DataFrame(train_df.columns.delete(0))
coeff_df.columns = ['Feature']
coeff_df["Correlation"] = pd.Series(logreg.coef_[0])

coeff_df.sort_values(by='Correlation', ascending=False)

Support Vector Machines

1
2
3
4
5
svc = SVC()
svc.fit(X_train, Y_train)
Y_pred = svc.predict(X_test)
acc_svc = round(svc.score(X_train, Y_train) * 100, 2)
acc_svc

k-Nearest Neighbors

1
2
3
4
5
knn = KNeighborsClassifier(n_neighbors = 3)
knn.fit(X_train, Y_train)
Y_pred = knn.predict(X_test)
acc_knn = round(knn.score(X_train, Y_train) * 100, 2)
acc_knn

Naive Bayes Nlassifiers

1
2
3
4
5
gaussian = GaussianNB()
gaussian.fit(X_train, Y_train)
Y_pred = gaussian.predict(X_test)
acc_gaussian = round(gaussian.score(X_train, Y_train) * 100, 2)
acc_gaussian

Perceptron

1
2
3
4
5
perceptron = Perceptron()
perceptron.fit(X_train, Y_train)
Y_pred = perceptron.predict(X_test)
acc_perceptron = round(perceptron.score(X_train, Y_train) * 100, 2)
acc_perceptron

Linear SVC

1
2
3
4
5
linear_svc = LinearSVC()
linear_svc.fit(X_train, Y_train)
Y_pred = linear_svc.predict(X_test)
acc_linear_svc = round(linear_svc.score(X_train, Y_train) * 100, 2)
acc_linear_svc

Stochastic Gradient Descent

1
2
3
4
5
sgd = SGDClassifier()
sgd.fit(X_train, Y_train)
Y_pred = sgd.predict(X_test)
acc_sgd = round(sgd.score(X_train, Y_train) * 100, 2)
acc_sgd

Decision Tree

1
2
3
4
5
decision_tree = DecisionTreeClassifier()
decision_tree.fit(X_train, Y_train)
Y_pred = decision_tree.predict(X_test)
acc_decision_tree = round(decision_tree.score(X_train, Y_train) * 100, 2)
acc_decision_tree

Random Forest

1
2
3
4
5
6
random_forest = RandomForestClassifier(n_estimators=100)
random_forest.fit(X_train, Y_train)
Y_pred = random_forest.predict(X_test)
random_forest.score(X_train, Y_train)
acc_random_forest = round(random_forest.score(X_train, Y_train) * 100, 2)
acc_random_forest

Model evaluation

对各个模型进行排名。

1
2
3
4
5
6
7
8
9
models = pd.DataFrame({
'Model': ['Support Vector Machines', 'KNN', 'Logistic Regression',
'Random Forest', 'Naive Bayes', 'Perceptron',
'Stochastic Gradient Decent', 'Linear SVC',
'Decision Tree'],
'Score': [acc_svc, acc_knn, acc_log,
acc_random_forest, acc_gaussian, acc_perceptron,
acc_sgd, acc_linear_svc, acc_decision_tree]})
models.sort_values(by='Score', ascending=False)
1
2
3
4
5
6
# 储存结果
submission = pd.DataFrame({
"PassengerId": test_df["PassengerId"],
"Survived": Y_pred
})
submission.to_csv('./output/submission.csv', index=False)