机器学习项目实战(二): Kaggle Titanic(二)


泰坦尼克号幸存预测(二)

本次的泰坦尼克号项目是在机器学习项目实战(一): Kaggle Titanic的基础上所进行的二次开发,这次采用了一些全新的思路来解决泰坦尼克号幸存预测问题。因此,将项目的过程总结出来,以便给其他人提供参考。

本文的运行环境是 Jupyter notebook

一、导入所需要的包

首先得将项目中所需要的模块下载下来,并导入到系统中来:

import os
import numpy as np
import pandas as pd

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.tree import DecisionTreeClassifier
from sklearn.ensemble import VotingClassifier

二、理解数据

Kaggle 所提供的数据下载下来,导入到系统中,并对数据进行展示理解:

1.导入数据

train_df = pd.read_csv('./datasets/titanic/train.csv')
test_df = pd.read_csv('./datasets/titanic/test.csv')
data = [train_df, test_df]

2.查看数据

首先查看下所提供的训练数据,将数据的前5行和最后5行打印出来:

train_df.head()

训练数据前五行数据

train_df.tail()

训练数据后五行数据

查看训练数据和测试数据的具体情况:

train_df.info()
print('-'*40)
test_df.info()
<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
----------------------------------------
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 418 entries, 0 to 417
Data columns (total 11 columns):
PassengerId    418 non-null int64
Pclass         418 non-null int64
Name           418 non-null object
Sex            418 non-null object
Age            332 non-null float64
SibSp          418 non-null int64
Parch          418 non-null int64
Ticket         418 non-null object
Fare           417 non-null float64
Cabin          91 non-null object
Embarked       418 non-null object
dtypes: float64(2), int64(4), object(5)
memory usage: 36.0+ KB

可以看到,数据集中存在一定的缺失信息。

接下来查看下样本数据的特征值取值情况:

train_df.describe()

数字样本取值

字符串类型属性的取值情况:

train_df.describe(include=['O'])

数字样本取值

3.查看不同的属性和生存率之间的关系

既然,项目是要求我们进行幸存预测。因此,我们就得明确哪些属性会影响到最后的生存情况。

查看船舱等级与存活率之间的关系:

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

船舱等级与存活率之间的关系

可以看到,很明显住在高级船舱的乘客的存活率更大。

查看性别与存活率之间的关系:

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

性别与存活率之间的关系

很明显,女人比较容易在灾害中存活下来。因为大家基本都是让女人和小孩先走。

查看船上兄弟姐妹及配偶的个数与存活率之间的关系

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

兄弟姐妹及配偶的个数与存活率之间的关系

查看船上父母或子女的个数与存活率之间的关系

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

父母或子女的个数与存活率之间的关系

可以看出,假如有亲属在船上的话生存率回比单独一人要低,这也应该是大家在灾难发生时第一时间会想到自己的家人的原因。

接下来为了更加方便的查看一些属性和生存率之间的关系,将对应的属性和生存率之间的关系用曲线的方式展示出来。

查看性别阶级与存活率之间的关系:

sns.pointplot(x="Sex", y="Survived", hue="Pclass", data=train_df);

性别阶级与存活率之间的关系

再来查看下年龄和存活率之间的关系:

sns.boxplot(x='Survived', y='Age', hue='Sex', data=train_df)

年龄和存活率之间的关系

最后再来看一下登船港口和存活率之间的关系:

sns.pointplot(x="Embarked", y="Survived", hue="Sex", data=train_df);

登船港口和存活率之间的关系

三、数据清洗

在对数据有个大概的认识后,便可以开始对数据进行清洗操作了。

首先删除多余的数据,这些数据因为在训练模型的过程中不会起到任何作用或者因为这些属性中的样本值缺失太严重,必须删除。

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

1.乘客名称

我们都知道,国外的人名中会将身份信息包含在名字中,比如:Master、Mr、Miss等。而这些称号便可以在一定程度上表示一个人的身份。因此,数据清洗的第一步我们便可以考虑将 Name 属性进行处理。

首先使用正则表达式将名字中的称号提取出来:

for dataset in data:
    dataset['Title'] = dataset.Name.str.extract(' ([A-Za-z]+)\.', expand=False)

接下来将提取好的称号中的一些比较奇怪的都变为 Rare:

for dataset in data:
    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_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5}
for dataset in data:
    dataset['Title'] = dataset['Title'].map(title_mapping)
    dataset['Title'] = dataset['Title'].fillna(0)

最后将原先的 Name 字段的属性删除:

train_df = train_df.drop(['Name',], axis=1)
test_df = test_df.drop(['Name'], axis=1)
data = [train_df, test_df]

2.性别

因为性别数据没有缺失,因此直接将其表示成数字形式即可:

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

3.年龄

接下来是年龄属性,因为年龄属性是有缺失的数据值的。因此首先得将缺失的数据集补全。这里是根据之前查看数据时发现年龄数据与性别以及船舱等级之间又很强的关系。因此,可以通过对应的 Sex 以及 Pclass 来推测出年龄的缺失值:

guess_ages = np.zeros((2,3))

# 对数据集中的每一个数据都做相同的操作
for dataset in data:
    # 开始迭代性别和阶级来计算出6个对应的年龄取值
    for i in range(0, 2):
        for j in range(0, 3):
            guess_df = dataset[(dataset['Sex'] == i) & \
                                  (dataset['Pclass'] == j+1)]['Age'].dropna()
            # 取6个数值中的中值
            age_guess = guess_df.median()

            # 随机年龄浮动转换为最接近的.5岁
            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]
    
    # 将年龄字段转为int类型
    dataset['Age'] = dataset['Age'].astype(int)

为了达到更好的预测效果,可以将年龄字段划分为不同的年龄区间,这样便可以实现更简单有效的表示数据:

train_df['AgeBand'] = pd.cut(train_df['Age'], 5)

for dataset in data:    
    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 = train_df.drop(['AgeBand'], axis=1)
data = [train_df, test_df]

4.Age*Class

因为年龄和船舱等级之间的关系密切,因此可以将两个字段组合起来形成一个新的字段 Age*Class:

for dataset in data:
    dataset['Age*Class'] = dataset.Age * dataset.Pclass

5.IsAlone

根据 Parch 和 SibSp 属性可以创建一个新的属性 FamilySize,用来表示每个人在船上的亲人数量。

for dataset in data:
    # 为了防止没有亲戚和配偶在船上,最后的结果加1
    dataset['FamilySize'] = dataset['SibSp'] + dataset['Parch'] + 1

有了 FamilySize 这个属性之后便可以根据这个属性来创建一个用来表示是否是一个人在船上的属性:IsAlone。

for dataset in data:
    dataset['IsAlone'] = 0
    dataset.loc[dataset['FamilySize'] == 1, 'IsAlone'] = 1

之后将一些过程变量属性删除掉:

train_df = train_df.drop(['Parch', 'SibSp', 'FamilySize'], axis=1)
test_df = test_df.drop(['Parch', 'SibSp', 'FamilySize'], axis=1)
data = [train_df, test_df]

6.Embarked

Embarked 表示的是登船港口,由于这个属性存在缺失值,因此我们首先得将其补全。

查看登船港口最多的地方:

freq_port = train_df.Embarked.dropna().mode()[0]
freq_port

结果是 “S”,因此,便使用出现最多的港口来填充:

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

最后将字符串转变为数字类型:

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

7.Fare

船票价格属性也有缺失值,这里使用中值进行填充:

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

接下来将船票价格划分为不同的区间,简化表示属性:

train_df['FareBand'] = pd.qcut(train_df['Fare'], 4)

for dataset in data:
    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]

到此为止,数据清洗结束。查看下数据清洗的结果:

训练集

数据集

四、构建与评估模型

在训练模型之前,首先得将训练模型的数据集准备好。因为该项目是属于监督学习类的问题。因此,对于训练集来说,得分为对应的 X 和 y 。测试集也就是原先 Kaggle 所提供的数据:

X_train = train_df.drop("Survived", axis=1)
y_train = train_df["Survived"]
X_test  = test_df.drop("PassengerId", axis=1).copy()

之后便可以进行创建模型,这里使用一些常见的模型:

# 逻辑回归
log_clf = LogisticRegression(solver="liblinear")

# 支持向量机
svm_clf = SVC(gamma="auto", probability=True)

# 线性支持向量机
lsvm_clf = LinearSVC()

# k相邻
knn_clf = KNeighborsClassifier(n_neighbors = 3)

# 决策树
dt_clf = DecisionTreeClassifier()

# 随机森林
rnf_clf = RandomForestClassifier(n_estimators=500, n_jobs=-1)

之后使用集成学习中的投票分类器来聚合这些模型:

voting_clf = VotingClassifier(
    estimators=[('svc', svm_clf), ('knn', knn_clf), ('dt', dt_clf), ('rnf', rnf_clf)],
#     voting='hard'
    voting='soft'
)
voting_clf.fit(X_train, y_train)

这里选择的是软投票。

之后查看下各个模型在训练集上的准确性如何:

for clf in (log_clf, svm_clf, lsvm_clf, knn_clf, dt_clf, rnf_clf, voting_clf):
    clf.fit(X_train, y_train)
    y_pred = clf.predict(X_test)
    acc = round(clf.score(X_train, y_train) * 100, 2)
    print(clf.__class__.__name__, acc)
  • LogisticRegression 81.26
  • SVC 83.5
  • LinearSVC 79.46
  • KNeighborsClassifier 84.06
  • DecisionTreeClassifier 86.64
  • RandomForestClassifier 86.64
  • VotingClassifier 86.2

五、方案实施

最后便可以将训练好的模型来进行预测

submission = pd.DataFrame({
        "PassengerId": test_df["PassengerId"],
        "Survived": y_pred
    })
submission.to_csv('./datasets/titanic/submission.csv', index=False)

将所生成的文件提交到 Kaggle 上看看自己的得分吧。

六、项目代码

项目所创建的完整代码地址:https://github.com/wangxin1248/ml_project/blob/master/01-Titanic/Titanic-v2.ipynb