优秀的编程知识分享平台

网站首页 > 技术文章 正文

Kaggle练习赛---Titanic的分析与整理

nanyue 2024-11-24 19:43:10 技术文章 3 ℃

文章来源:公众号【机器学习炼丹术】

前言

开始Kaggle练习,最先进行的当然是Kaggle的“Hello World”---“Titanic: Machine Learning from Disaster",即预测哪些泰坦尼克的旅客幸存。自己最开始写的很乱,包括数据处理、分析、建模等。参考了Kaggle的一篇[1]再整理一遍,清晰很多。实验并没有得到一个很好的结果,但是规范了整个流程,因此写下笔记 。

本文约6.2k字,预计阅读15分钟

主要流程

竞赛解决方案的流程主要分为以下7个步骤:

  1. 问题的定义;
  2. 获取训练集和测试集;
  3. 整理、准备、清洗数据;
  4. 分析、探索数据;
  5. 建模、预测和解决问题;
  6. 可视化、报告和呈现问题解决步骤和最终解决方案;
  7. 提交结果;

当然这个只是一般的步骤,3、4可以进行交叉交换,可视化也可以应用到多个阶段。

问题定义

泰坦尼克数据集的机器学习研究可以说是Kaggle的“Hello world”。问题的直观定义很简单:使用机器学习创建一个模型,预测哪些乘客在泰坦尼克号沉船中幸存。

其他的信息条件:

1912年4月15日,被广泛认为“永不沉没”的皇家邮轮“泰坦尼克号”在处女航中撞上一座冰山后沉没。不幸的是,船上没有足够的救生艇,导致2224名乘客和船员中有1502人死亡。(存活率为:32.46%)

虽然在生存中有一些运气因素,但似乎有些群体的人比其他人更有可能生存下来。

获取数据集

数据集的获取很简单,使用pandas包即可。

import pandas as pd
train_df = pd.read_csv('../input/titanic/train.csv')
test_df = pd.read_csv('../input/titanic/test.csv')

分析数据

数据的描述(即特征)

赛题中Data部分给出了数据特征,也可以通过pandas进行查看:

train_df.columns.values
Out:
  array(['PassengerId', 'Survived', 'Pclass', 'Name', 'Sex', 'Age', 'SibSp',
       'Parch', 'Ticket', 'Fare', 'Cabin', 'Embarked'], dtype=object)

结合赛题描述,我们可以对上述特征进行归类:

  1. 分类特征:这里指的是可以对数据集按照该特征可以明确的划分为一系列相似的样本。上述属于分类特征的有:
  2. Pclass(阶级)、Survived(是否存活)、Sex(性别)、Embarked(出发地);
  3. 数值型特征:值会随着样本的变化而变化,可以是连续的、离散的或基于时间序列的,这里有:
  4. Age(年龄)、Fare(票价)、SibSp(船上兄弟姐妹数量)、Parch(船上父母孩子数量)【注:后两者属于数值型特征是因为当不知道数据集时无法确定特征的取值范围,是随着样本而变化】
  5. 其他特征:很难找到有效的的规律,这里还剩下:
  6. Name(姓名)、Ticket(船票号)、Cabin(船舱号);

具体数据

在对训练集的首尾数据进行查看:

train_df.head()
train_df.tail()

通过数据中的信息,可以进行得到以下结论:

  1. Name特征中,虽然是字符串类型,但包含了一些性别特征,如(Mr、Mrs、Miss);
  2. Ticket特征,包含了数字和字母的组合,我们很难从中找出一些规律;
  3. 我们发现,Cabin特征中存在缺失值,我们需要进一步进行判断,并查看其他特征是否也有缺失;
  4. Passengerld特征为序号,对模型建立应该并无影响;

数据结构

首先最为关键的是数据缺失问题,Pandas可以通过很多种方式进行判断每个特征是否存在缺失值。最简单的是直接查看数据集的全局信息。

train_df.info()
print('-' * 40)
test_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  891 non-null    int64  
 1   Survived     891 non-null    int64  
 2   Pclass       891 non-null    int64  
 3   Name         891 non-null    object 
 4   Sex          891 non-null    object 
 5   Age          714 non-null    float64
 6   SibSp        891 non-null    int64  
 7   Parch        891 non-null    int64  
 8   Ticket       891 non-null    object 
 9   Fare         891 non-null    float64
 10  Cabin        204 non-null    object 
 11  Embarked     889 non-null    object 
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB
________________________________________
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 418 entries, 0 to 417
Data columns (total 11 columns):
 #   Column       Non-Null Count  Dtype  
---  ------       --------------  -----  
 0   PassengerId  418 non-null    int64  
 1   Pclass       418 non-null    int64  
 2   Name         418 non-null    object 
 3   Sex          418 non-null    object 
 4   Age          332 non-null    float64
 5   SibSp        418 non-null    int64  
 6   Parch        418 non-null    int64  
 7   Ticket       418 non-null    object 
 8   Fare         417 non-null    float64
 9   Cabin        91 non-null     object 
 10  Embarked     418 non-null    object 
dtypes: float64(2), int64(4), object(5)
memory usage: 36.0+ KB

通过上述数据集的信息我们可以分析:

  1. 数据集中有7个为数值型,5个为object类型;
  2. 训练集共891个样本,Age特征缺失177(即19.87%),Cabin特征缺失687(即77.10%),Embarked特征缺失2(即0.22%);
  3. 测试集共418个样本,Age特征缺失86(即20.57%),Cabin特征缺失326(即67.78%),Fare特征缺失1(即0.21%);

数据的分布

可以通过Pandas包的describe方法快速查看整个数据的分布情况【这里只观察训练集】:

train_df.describe()

结果如图所示:

我们可以得到以下结论:

  1. 在训练集中,存活率为38.38%;
  2. 社会经济地位(SES)是低的人占的比例最大;
  3. 船上大多数没有和父母孩子一起(至少75%);
  4. 船上有兄弟姐妹的关系的旅客为25%~50%;
  5. 年老的乘客很少;
  6. 票价分布不均匀,差异很大,极少数人花费超过了500美元;

上述缺少了部分特征(数据类型不为数值),查看数据分布,可以添加参数include=['O'],即包含object类型:

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

可以发现:

  1. 游客出发的港口有3个;
  2. 船舱有部分重复(147个不相同),某些游客可能共用一个船舱;
  3. 票据特征有一定的重复率(22%);

单个特征与标签的相关性

上述分析的是数据集整体的结构与分布,接下来我们将探索单个特征与是否存活的相关性,这是非常有意义的。这里我们指分析不含有缺失值的特征:Pclass、Sex、SibSp、Parch。

(1)Pclass:

train_df[['Pclass', 'Survived']].groupby('Pclass', as_index=False).mean().sort_values(by='Survived', ascending=False)
  Pclass Survived
0   1   0.629630
1   2   0.472826
2   3   0.242363

结论:社会地位高的人存活率最高。

(2)Sex:

train_df[['Sex', 'Survived']].groupby('Sex', as_index=False).mean().sort_values(by='Survived', ascending=False)
    Sex   Survived
0   female  0.742038
1   male   0.188908

结论:女性比男性的存活率高。

(3)SibSP:

train_df[['SibSp', 'Survived']].groupby('SibSp', as_index=False).mean().sort_values(by='Survived', ascending=False)
 SibSp Survived
1  1  0.535885
2  2  0.464286
0  0  0.345395
3  3  0.250000
4  4  0.166667
5  5  0.000000
6  8  0.000000

结论:有少数(1~3)或无兄弟姐妹或的旅客存活率较高。

(4)Parch:

rain_df[['Parch', 'Survived']].groupby('Parch', as_index=False).mean().sort_values(by='Survived', ascending=False)
 Parch Survived
3  3  0.600000
1  1  0.550847
2  2  0.500000
0  0  0.343658
5  5  0.200000
4  4  0.000000
6  6  0.000000

结论:与上述结论类似。

数据可视化

(1)首先我们可视化年龄与存活的关系:

import matplotlib.pyplot as plt
plt.figure(dpi=100)
plt.hist(train_df['Age'], label='All', bins=20)
plt.hist(train_df.loc[train_df.Survived == 1, 'Age'], label='Survived', bins=20)
plt.xlabel('Age')
plt.ylabel('People Number')
plt.legend()

如图所示:

结论:模型建模应该考虑年龄;0~8岁的小孩和75~80的老人存活率偏高;需要对年龄进行分段处理,并填充缺失值

(2)对上述直方图我们再加入Pclass特征:

plt.clf()
fig, axes = plt.subplots(1, 3, dpi=100)
plt.subplots_adjust(left=0, bottom=None, right=2.5, top=None, wspace=None, hspace=None)
def subplot(Pclass, index):
    ax_i = axes[index]
    ax_i.hist(train_df.loc[train_df.Pclass == Pclass, 'Age'], label='All', bins=20)
    ax_i.hist(train_df.loc[(train_df.Survived == 1) & (train_df.Pclass == Pclass), 'Age'], label='Survived', bins=20)
    ax_i.set_xlabel('age')
    ax_i.set_title('Pclass=' + str(Pclass))
    ax_i.set_ylabel('People Number')
for i in range(3):
    subplot(i+1, i)
plt.legend()

如图:

结论:Pclass=1的旅客存活率最高,Pclass=2其次,Pclass=3最低;Pclass=2和3中年龄为0~10的旅客存活率较高;故Pclass是一个重要的特征,建模需要加入

(3)可视化出发地、性别、社会地位(Pclass)与存活率的关系:

import seaborn as sns
grid = sns.FacetGrid(train_df, row='Embarked', height=2.2, aspect=1.6)
grid.map(sns.pointplot, 'Pclass', 'Survived', 'Sex', palette='deep')
grid.add_legend()

如图【用了seaborn】:

结论:在三个出发港口中,港口C男性各个阶级的存活率远高于其他两个,且比女性高;对于社会地位高的男性在不同的港口出发,存活率也各不相同;故性别(Sex)与出发地(Embarked)都是重要的特征,建模需要加入

(4)可视化票价与存活率的关系:

plt.figure(dpi=100)
plt.hist(train_df['Fare'], label='All', bins=50)
plt.hist(train_df.loc[train_df.Survived == 1, 'Fare'], label='Survived', bins=50)
plt.xlabel('Fare')
plt.ylabel('People Number')
plt.legend()

如图:

结论:票价高的旅客存活率更高;由于票价分布差异较大,因此可以对该特征进行分段处理;故应将Fare加入进行建模

总结

经过上述对11个特征(除去Survived作为标签)进行数据分析,结论如下:

  1. PassengerId为ID,Ticket特征很难找到规律,Cabin缺失太多,因此都需要进行删除;
  2. Name特征可以提取部分性别、已婚未婚信息、身份等【Name特征本身需要删除】;
  3. Sex、Pclass、Age、Embarked、Fare是非常重要的特征,且特征之间存在着一定的关系;
  4. SibSp、Parch特征关于存活率的分布类似,且两者相加能反应一个家族的人员情况。可以尝试相加;
  5. Age、Embarked、Fare存在着缺失数据,需要对其进行填充;
  6. Sex、Emabrked需要转化为分类的数值型特征;
  7. Age、Fare根据之前的分析进行合理的分段;

清洗、整理数据

删除无效特征

经上述分析,删除Passengerld【可进行保存】、Ticket、Cabin特征:

data_id = []
train_df = train_df.drop(['Ticket', 'Cabin'], axis=1)
test_df = test_df.drop(['Ticket', 'Cabin'], axis=1)
data_id.append(train_df.pop('PassengerId'))
data_id.append(test_df.pop('PassengerId'))
train_df.shape, test_df.shape
# ((891, 9), (418, 8))

提取有效的特征

关于Name特征,一般称呼上"."前面表示一种身份,例如“Miss”、“Dr“等,可以将其用正则表达式进行提取:

train_df['Title'] = train_df.Name.str.extract('([A-Za-z]+)\.', expand=False)
test_df['Title'] = test_df.Name.str.extract('([A-Za-z]+)\.', expand=False)
pd.crosstab(train_df['Title'], train_df['Sex'])
Sex female male
Title  
Capt   0 1
Col    0 2
Countess 1 0
Don    0 1
Dr    1 6
Jonkheer 0 1
Lady   1 0
Major   0 2
Master  0 40
Miss   182 0
Mlle   2 0
Mme    1 0
Mr    0 517
Mrs    125 0
Ms    1 0
Rev    0 6
Sir    0 1

我们发现某些类别特别少,所占比例不均匀,且“Mlle”、“Ms”都是“Miss”,“Mme”应该为“Mrs”,可以对其进行替换,对于数量少的可以进行整合为一个类别“Rare”。

combine = [train_df, test_df]
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')
    
train_df[['Title', 'Survived']].groupby(['Title'], as_index=False).mean()
  Title Survived
0 Master 0.575000
1 Miss  0.702703
2 Mr   0.156673
3 Mrs   0.793651
4 Rare  0.347826

再对所有的类别进行映射:

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)

删除Name特征:

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

转换特征

由于性别是字符串表示,我们可以将其用数值型代表:

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

缺失值填充

1、Age

最简单的填充方式就是取所有旅客的均值和标准差之间进行随机选择。但是,之前我们分析,Age和Pclass、Gender存在一定的关联。因此,我们可以选择取Pclass和Gender匹配下的Age的均值与标准差的随机选择。

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()
            age_mean = guess_df.mean()
            age_std = guess_df.std()
            age_guess = np.random.uniform(age_mean - age_std, age_mean + age_std)
            
            dataset.loc[ (dataset.Age.isnull()) & (dataset.Sex == i) & (dataset.Pclass == j+1),\
                    'Age'] = age_guess
    dataset['Age'] = dataset['Age'].astype(int)

由于Age为连续性数值,根据分析,我们可以将其进行分段【10段加以区分】:

train_df['AgeBand'] = pd.cut(train_df['Age'], 10)
train_df[['AgeBand', 'Survived']].groupby(['AgeBand'], as_index=False).mean().sort_values(by='AgeBand', ascending=True)
  AgeBand Survived
0 (-0.08, 8.0] 0.666667
1 (8.0, 16.0]  0.413043
2 (16.0, 24.0] 0.264706
3 (24.0, 32.0] 0.429245
4 (32.0, 40.0] 0.463768
5 (40.0, 48.0] 0.325843
6 (48.0, 56.0] 0.466667
7 (56.0, 64.0] 0.375000
8 (64.0, 72.0] 0.000000
9 (72.0, 80.0] 0.500000

将其转化为分类数值型:

train_df['Age'] = pd.cut(train_df['Age'], bins=10, labels=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
test_df['Age'] = pd.cut(test_df['Age'], bins=10, labels=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
train_df['Age'] = train_df['Age'].astype('int64')
test_df['Age'] = test_df['Age'].astype('int64')
combine = [train_df, test_df]

2、Embarked

由于训练集中只有两个缺失值,因此我们采用最简单的最常用填充:

for dataset in combine:
    dataset['Embarked'] = dataset['Embarked'].fillna(dataset.Embarked.dropna().mode()[0])

然后再将其转化为分类数值型特征:

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

3、Fare

测试集中有一个样本缺失Fare特征,我们简单的填充为中位数:

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

将不同的Fare进行分段处理,和Age处理方式不同的是,由于票价差距过大,我们不能采用等分,且票价基本和Pclass对应,因此这里使用pd.qcut划分为3份:

train_df['Fare'] = pd.qcut(train_df['Fare'], 3, labels=[0, 1, 2])
test_df['Fare'] = pd.qcut(test_df['Fare'], 3, labels=[0, 1, 2])
train_df['Fare'] = train_df['Fare'].astype('int64')
test_df['Fare'] = test_df['Fare'].astype('int64')
combine = [train_df, test_df]

特征组合(特征工程)

1、将SibSp与Parch组合,得到Family特征

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

模型预测

模型完成的是分类问题,因此有很多个机器学习模型可供选择:

  • Logistic Regression;
  • KNN;
  • Naive Bayes Classifier;
  • Decision Tree;
  • Support Vector Machine;
  • MLP;

首先对数据进行处理:

X_train = np.array(train_df.drop("Survived", axis=1))
Y_train = np.array(train_df["Survived"])
X_test  = np.array(test_df)
X_train.shape, Y_train.shape, X_test.shape

导入模型包:

from sklearn.model_selection import KFold
from sklearn.tree import DecisionTreeClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.naive_bayes import GaussianNB
from sklearn.neighbors import KNeighborsClassifier
from sklearn.svm import SVC
from sklearn.neural_network import MLPClassifier

模型训练,采用五折交叉验证:

def train(clf):
    kf = KFold(n_splits=5)
    score = list()
    for train_index, test_index in kf.split(range(len(X_train))):
        train_X, test_X = X_train[train_index], X_train[test_index]
        train_y, test_y = Y_train[train_index], Y_train[test_index]
        clf.fit(train_X, train_y)
        score.append(clf.score(test_X, test_y))
    avg_score = np.mean(score))
    print(avg_score)
    return avg_score

1、Logistic 回归

clf = LogisticRegression()
lr = train(clf)
0.8047078023978406

我们可以查看模型中各个特征的权重,权重的绝对值越大,说明对模型的影响也越大:

def correlation(clf):
    coeff_df = pd.DataFrame(train_df.columns.delete(0), columns=['Feature'])
    coeff_df["Correlation"] = pd.Series(clf.coef_[0])
    print(coeff_df.sort_values(by='Correlation', ascending=False))
    
correlation(clf)
      Feature  Correlation
1         Sex     2.119560
7       Title     0.486584
5        Fare     0.318108
6    Embarked     0.118401
4       Parch    -0.027359
3       SibSp    -0.202700
8  FamilySize    -0.228518
2         Age    -0.291479
0      Pclass    -0.958525

我们发现Sex与Pclass特征的影响最大,我们通过特征工程构造的FamilySize也比原来的特征作用更大;【这里我们可以调整特征,例如删除一些影响小的特征,如Parch等;

2、决策树(CART)

clf = DecisionTreeClassifier()
cart = train(clf)
0.8249199673592367

3、KNN

clf = KNeighborsClassifier(n_neighbors=3)
knn = train(clf)
0.8114368212918208

4、朴素贝叶斯

clf = GaussianNB()
nb = train(clf)
0.7834348126294646

5、SVM

clf = SVC(kernel='rbf')
svm = train(clf)
0.8271420500910175

6、MLP

clf = MLPClassifier(max_iter=1000, learning_rate='adaptive')
mlp = train(clf)
0.812591802146758

排序:

models = pd.DataFrame({
    'Model': ['Logistic Regression', 'Decision Tree', 'KNN', 
              'Naive Bayes', 'Support Vector Machines', 'MLP', 
              ],
    'Score': [lr, cart, knn, nb, svm, mlp]})
models.sort_values(by='Score', ascending=False)

结果:

    Model        Score
4 Support Vector Machines  0.827142
1 Decision Tree       0.824920
5 MLP            0.812592
2 KNN            0.811437
0 Logistic Regression    0.806955
3 Naive Bayes        0.783435

提交结果

我们使用SVM模型进行预测(这里简单考虑,不做模型融合,其他的特征工程暂不考虑)

clf = SVC(kernel='rbf')
svm = train(clf)
pred = clf.predict(X_test)
pred = np.c_[data_id[1], pred]
pred_df = pd.DataFrame(pred, columns=['PassengerId', 'Survived'])
pred_df.to_csv('/kaggle/working/Submission.csv', index=False)

最后预测的结果为:0.78947(5116名/结果并不好,还需继续进行调整)

参考

[1] https://www.kaggle.com/startupsci/titanic-data-science-solutions




最近发表
标签列表