Kaggle|数据科学Hello World: Titanic生存概率预测

Kaggle数据科学的入门项目:Titanic

写在前面

基本步骤

  1. 数据准备和探索
  2. 数据清洗
  3. 特征选择
  4. 模型训练
  5. 模型验证
  6. 提交结果

1. 数据准备和探索

1.1 明确数据来源和要研究的问题

Titanic是世界十大灾难之一,一部经典的电影让沉船的惨烈和凄美的爱情故事都留在了大众心中。同时,灾难的伤亡数据一直成为了数据科学领域的研究热门。
这次的研究的目的是试图根据Titanic乘客的数据,如性别、年龄、舱位、客舱编号等,来预测乘客最终是获救生还,还是不幸遇难。
显然,这是一个标准的二分类问题,预测类别为:生还和遇难。
先从Titanic Data下载数据集,一共有3个文件:

文件 内容
train.csv 训练数据集,包含特征信息和存活与否的标签,用来建模
test.csv 测试数据集,只包含特征信息,用来检测模型的准确度
gender_submission.csv 提交文档模板,假设所有女乘客都生还

1.2 数据探索

先简单查看一下数据,读取训练数据后,用head()函数来查看前5行数据:

#matplotlib inline

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd
import warnings
warnings.filterwarnings('ignore')

train = pd.read_csv('train.csv', index_col=0)
test = pd.read_csv('test.csv', index_col=0)

train.head()
train数据中的前5项

通过对数据的观察,可以总结出如下数据字典:

变量名 变量解释 数据解释
PassengerId 乘客编号 唯一编号
Survived 乘客是否生还 0=未生还,1=生还
Pclass 乘客所在舱位 1=一等舱,2=二等舱,3=三等舱
Name 乘客姓名
Sex 乘客性别 male,female
SibSp 乘客的兄弟姐妹和配偶数量
Parch 乘客的父母和子女数量
Ticket 船票编号
Fare 票价
Cabin 乘客所在船舱号
Embarked 乘客登船港口 C = Cherbourg, Q = Queenstown, S = Southampton

由此可见,除了确定乘客的唯一编号PassengerId外,一共有10个可以研究的特征变量。

train.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

可以看到各个数据字段的缺失情况和数据类型。
同时也不忘查看一下测试数据,test.info()运行结果如下:

<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

2. 数据清洗

通过初步探索发现,在AgeCabinFareEmbarked特征上都有数据缺失,我们需要处理缺失值才能进行下一步的分析。
处理缺失数据一般有两种方法:滤除缺失数据填充缺失数据。Titanic数据集特征只有10个,显然不能舍弃数据缺失的特征,所以需要填充缺失数据。
填充数据的时,一般会根据实际情况,将数据补0,或以该特征数据的均值或中位数来填充,或将缺失数据划为新的类型来处理。

2.1 补齐Embarked字段

Embarked为乘客登船港口,首先观察一下字段数据情况。

#查看Embarked字段数据
train['Embarked'].value_counts()

结果如下:

S    644
C    168
Q     77
Name: Embarked, dtype: int64

由于港口数据类型是字符型,而且数据量比较少,我们可以用众数来填补空缺数据,即港口数最多的'S'

train['Embarked'].fillna('S', inplace=True)
test['Embarked'].fillna('S', inplace=True)

2.1 补齐Age和Fare字段

Age和Fare字段简单地采用均值来填补

train['Age'].fillna(train['Age'].mean(), inplace=True)
test['Age'].fillna(test['Age'].mean(), inplace=True)

train['Fare'].fillna(train['Fare'].mean(), inplace=True)
test['Fare'].fillna(test['Fare'].mean(), inplace=True)

3. 特征选择

选取数据中有价值的特征,提取train和test中的特征向量

features = ['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Fare', 'Embarked']
train_features = train[features]
train_labels = train['Survived']

test_features = test[features]

由于数据中Embarked和Sex字段都是字符型数据,这样不利于分析,可以用sklearn中的特征提取,将符号化数据抽取成不同的特征向量:

from sklearn.feature_extraction import DictVectorizer

dvec = DictVectorizer(sparse=False)
train_features = dvec.fit_transform(train_features.to_dict(orient='record'))

print(dvec.feature_names_)

查看feature_names_可以看出,原来的EmbarkedSex两列均根据变量取值拆成了若干列:

['Age', 'Embarked=C', 'Embarked=Q', 'Embarked=S', 'Fare', 'Parch', 'Pclass', 'Sex=female', 'Sex=male', 'SibSp']

4. 模型训练

选择sklearn中的决策树模型,使用ID3算法构造决策树:

from sklearn.tree import DecisionTreeClassifier
clf = DecisionTreeClassifier(criterion='entropy')
clf.fit(train_features, train_labels)

决策树根据train数据fit过,可以用来预测test数据了:

test_features = dvec.transform(test_features.to_dict(orient='record'))
pred_labels = clf.predict(test_features)

5. 模型验证

因为没有测试数据的正确结果,计算决策树在训练数据上的准确率:

acc = round(clf.score(train_features, train_labels), 6)
print(u'accuracy = %.4lf' % acc)

运行结果:

accuracy = 0.9820

6. 提交结果

最终提交的结果要符合kaggle示范文件的格式,也就是包含两列PassengerIDSurvived数据的csv文件。

passengers = test['PassengerId']
survived= pd.Series(data=pred_labels, name='Survived')
result = pd.concat([passengers, survived], axis=1)
result.to_csv('submission.csv', index=False)

保存好文件后,在kaggle比赛页面提交,提交成功后会计算出此次的分数:
最终准确率是0.746╮(╯▽╰)╭

数据分析版hello world到此结束,如何提高准确率提升名次,把文中偷懒和简化的部分做得更细致,大概就可以提升不少了吧,日后再刷~~

---------------------------------------更新的分割线----------------------------------------
很显然之前简单用决策树模型得出的准确率74.6%是远远不够的,不足的地方有:

  • 数据探索不够深入,没有挖出数据更深层的隐含信息
  • 数据清洗不到位,对数据的宏观把握不足,对数据的认识和感知还不够,所以进而数据的清洗也没有找到更合适的方法
  • 数据模型单一且简单,应该选择更合适的模型
  • 模型验证方法很显然不对,与实际准确率相去甚远

接下来就来逐个点优化吧~~~

7. 数据再探索

7.1 数据基本分布

先从宏观上了解一下数据,看看几个主要特征上的分布情况,然后再进一步分析的切入点:

fig = plt.figure(figsize=(17,10), dpi=200)

#查看获救人数和未获救人数分别有多少
plt.subplot2grid((2,3),(0,0))
train_data.Survived.value_counts().plot(kind='bar',color=['lightblue','pink'])
plt.ylabel(u'人数')
plt.title(u'获救情况(1为获救)')

# 查看各舱位乘客人数
plt.subplot2grid((2,3),(0,1))
train_data.Pclass.value_counts().plot(kind='bar',color=['lightblue','pink','palegreen'])
plt.ylabel(u'人数')
plt.title(u'乘客等级分布')

# 查看获获救和未获救乘客的年龄分布
plt.subplot2grid((2,3),(0,2))
plt.scatter(train_data.Survived, train_data.Age, color='skyblue')
plt.ylabel(u'年龄')
plt.grid(b=True, which='major', axis='y')
plt.title(u'按年龄看获救分布(1为获救)')

# 查看各舱位等级乘客的年龄分布
plt.subplot2grid((2,3),(1,0), colspan=2)
train_data.Age[train_data.Pclass==1].plot(kind='kde')
train_data.Age[train_data.Pclass==2].plot(kind='kde')
train_data.Age[train_data.Pclass==3].plot(kind='kde')
plt.xlabel(u'年龄')
plt.ylabel(u'密度')
plt.title(u'各等级的乘客年龄分布')
plt.legend((u'头等舱',u'2等舱',u'3等舱'), loc='best')

# 查看各登船港口的获救人数
plt.subplot2grid((2,3),(1,2))
train_data.Embarked.value_counts().plot(kind='bar',color=['lightblue','pink','palegreen'])
plt.title(u'各登船口岸上岸人数')
plt.ylabel(u'人数')

plt.show()
数据基本分布

图表果然清晰多了,从图中不难看出:

  • 泰坦尼克号沉船是个大灾难,获救的人只是一小部分,为逝者默哀一分钟
  • 3等舱的乘客人数远远大于其他两个船舱的乘客
  • 获救和未获救的人年龄分布都很广
  • 各舱位乘客的年龄分布,3等舱的乘客集中分布在20左右,头等舱乘客40岁左右最多,很符合社会财富分布
  • S港口的登陆乘客最多,远多于C、Q港口
    大概了解了数据分布后可以大胆假设一下:
  • 乘客的财富地位也许会影响到最终是否被获救,所以与财富地位相关的因素如:舱位等级、姓名Title等都可能是相关特征
  • 乘客的登船港口和地点相关,各地区之间居民收入和身份地位也许也不同
    接下来就进行相关性分析,探讨一下到底哪些因素会影响到是否获救。

7.2 各特征与是否获救的关联统计

7.2.1 性别与获救情况
# 按性别查看获救情况
survived_M = train_data.Survived[train_data.Sex=='male'].value_counts()
survived_F = train_data.Survived[train_data.Sex=='female'].value_counts()

df = pd.DataFrame({u'男性':survived_M, u'女性':survived_F})
df.plot(kind='bar', stacked=True, color=['lightblue','pink'])

plt.title(u'按性别查看获救情况')
plt.xlabel(u'性别')
plt.ylabel(u'人数')

plt.show()
根据性别查看获救情况

果然,lady first不仅仅是一句口号,获救的乘客中还是女性居多,牺牲的乘客中男性占了绝大多数。

7.2.2 客舱等级与获救情况
# 按客舱等级查看获救情况
Survived_0 = train_data.Pclass[train_data.Survived == 0].value_counts()
Survived_1 = train_data.Pclass[train_data.Survived == 1].value_counts()

df = pd.DataFrame({u'获救':Survived_1,u'未获救':Survived_0})
df.plot(kind='bar', stacked=True, color=['#92ff92','#ff9292'])
plt.title(u'各乘客等级的获救情况')
plt.ylabel(u'人数')
plt.xlabel(u'乘客等级')

plt.show()
按客舱等级查看获救情况

1等舱和2等舱的获救比例远远大于3等舱,乘客的钱不是白花的呀,万恶的资本主义~~

7.2.3 登船港口与获救情况
# 查看各港口的获救情况
survived_0 = train_data.Embarked[train_data.Survived == 0].value_counts()
survived_1 = train_data.Embarked[train_data.Survived == 1].value_counts()

df = pd.DataFrame({u'获救':survived_0,u'未获救':survived_1})
df.plot(kind='bar', stacked=True, color=['#92ff92','#ff9292'])

plt.title(u'各港口乘客的获救情况')
plt.ylabel(u'人数')
plt.xlabel(u'港口')

plt.show()
各港口的获救情况

S港口的乘客人数众多,获救率似乎也低一些,C港口获救率比稍微高一些,港口这个特征暂且留着吧

7.2.4 船舱号与获救情况

有个特征Cabin缺失严重,补足数据也不太方便,我们可以考虑或许有没有Cabin字段会和生还率相关呢?

# 根据有没有Cabin属性创建一个新的CabinBool属性
train_data['CabinBool'] = (train_data['Cabin'].notnull().astype('int'))

cabin_1 = train_data['CabinBool'][train_data['Survived']==1].value_counts(normalize=True)
cabin_0 = train_data['CabinBool'][train_data['Survived']==0].value_counts(normalize=True)

df = pd.DataFrame({'获救':[cabin_1[0], cabin_0[0]],'未获救':[cabin_1[1], cabin_0[1]]})
df.plot.bar(stacked=True, color=['#92ff92','#ff9292'])

plt.show()

0代表没有Cabin值,1代表有Cabin值

很显然,有Cabin记录的乘客的获救率远远高于没有Cabin记录的乘客,可以考虑将Cabin转化成二元属性用于模型训练。
还有一些属性ParchSibSp简单的查看了一下,参考价值不大,暂且不用了吧。
分析到这里就可以进行下一步的预处理了。

8. 数据预处理

为了方便起见,我们将原始的训练数据和测试数据合并后一并处理。

y_train = train_data.pop('Survived')
data_all = pd.concat((train_data, test_data), axis=0)

8.1 提取姓名Title

先查看一下Name属性的格式

print(data_all['Name'].head(20))

Name属性前20项

可以看出来,Name属性的格式是+Title+,名和姓对我们都不重要,只要把Title提取出来就行了。

title = pd.DataFrame()
title['Title'] = data_all['Name'].map(lambda name:name.split(',')[1].split('.')[0].strip())
print(title['Title'].value_counts())

Title种类

可以大致把Title分为NormalMiddleRoyal三类。

# 先将Title归类
title_dict = pd.DataFrame()
title_dict['Normal'] = ['Mr', 'Miss', 'Mrs', 'Ms', 'Mme','Mlle']
title_dict['Middle']=['Capt', 'Dr', 'Rev', 'Col', 'Master', 'Major']
title_dict['Royal']=['the Countess', 'Sir', 'Lady', 'Don','Jonkheer', 'Dona']
#构造title_map
title_map = {}
for index,row in title_dict.iteritems():
    for title_ in row:
        title_map[title_] = index
        
print(title_map)
title和等级的对应关系

捋请了Title的分类后,就可以对原始数据进行改造了:

title['Title'] = title.Title.map(title_map)
# 将title进行one-hot encoding
title = pd.get_dummies(title.Title)
#拼接到data上
data_all = pd.concat((data_all, title), axis=1)
data_all.pop('Name')
print(data_all.head())

这时数据变成了13列,丢弃了Name,将title拆为了NormalMiddleheRoyal三列。

8.2 处理Cabin属性

根据是否有Cabin将原字符型属性转为binary属性:

data_all['Cabin'] = (data_all.Cabin.notnull().astype('int'))

8.3 处理Pclass属性

根据上面的分析,Pclass和是否获救的相关性很大,Pclass也是字符型属性,所以也很适合用one-hot encoding处理:

data_all.Pclass = data_all.Pclass.astype(str)
data_all.Pclass = data_all.Pclass.map(lambda pclass:'pclass_'+pclass)
pclass = pd.get_dummies(data_all.Pclass)
data_all = pd.concat((data_all, pclass), axis=1)
data_all.pop('Pclass')

Pclass被拆成了pclass_1pclass_2pclass_33列。

8.4 处理其他属性

先填充缺失值,用均值填充Age

data_all.Age.fillna(data_all.Age.mean(),inplace=True)

填充Embarked属性,这是个字符型属性,用出现概率最高的S来填充,再用one-hot encoding处理:

data_all.Embarked.fillna('S', inplace=True)
embarked = pd.get_dummies(data_all.Embarked)
data_all = pd.concat((data_all, embarked), axis=1)
data_all.pop('Embarked')

test数据中有一个Fare缺失,也用均值来填充:

data_all.Fare.fillna(data_all.Fare.mean(),inplace=True)

Sex属性拆成二元属性:

sex = pd.get_dummies(data_all.Sex)
data_all = pd.concat((data_all, sex),axis=1)
data_all.pop('Sex')

还有一个比较迷的Ticket属性,现在特征数量已经很多了,就利用奥卡姆剃刀把它剃掉吧~~
经过这一系列的处理后,现在的数据变成了这样:

清洗后的data info

由原来的10个特征变成了17个特征,并且全都是数值型的。
最后,别忘了把训练数据和测试数据拆开:

train_d = data_all.loc[train_data.index]
test_d = data_all.loc[test_data.index]
print(train_d.shape, test_d.shape)

拆开后shape分别是(891, 16)和(418, 16),符合原始数据的大小。

9. 模型训练

这次准备使用sklearn里的逻辑回归模型,同样也是一个很适合分类问题的模型。
先用sklearn试试最简单的baseline模型效果咋样:

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score

lr_clf = LogisticRegression(C=1.0, penalty='none', tol=1e-6)
lr_clf.fit(train_features, y_train)

lr_pred = lr_clf.predict(test_features)

corss_score = np.mean(cross_val_score(lr_clf, train_features, y_train, cv=10))
print(u'cross score = %.4lf' % corss_score)

cross validation 的准确率是 0.7834,似乎也没比决策树优秀多少。
下面就要开始进一轮的模型优化了~~

9.1 模型系数关联分析

查看一下目前这个LR模型中各特征的关联系数:

pd.DataFrame({"columns":list(train_d.columns)[0:], "coef":list(lr_clf.coef_.T)})
关联系数

这个关联系数表示的是每个特征的在逻辑回归模型中的模型参数,逻辑回归模型会通过sigmod函数将这个参数映射到0~1之间。
coef大于0时,说明特征和结果是正相关,小于0时是负相关。
从上面的关联系数表可以看出:

  • female和是否获救正相关,male则是负相关,很符合数据给人的印象
  • Age有一点点负相关,说明年龄越小越容易获救
  • Cabin这个字段竟然很有助于获救,说明有登记船舱信息的都是比较容易获救的
  • 登船港口S、Q、C竟然呈现出截然不同的表现,很出乎意料,不知道是不是有点弄错了o(╥﹏╥)o
  • Parch的关联性竟然这么高,还有SibSp也不错,可以考虑多挖掘挖掘

9.2 模型优化

根据上表,找出几个可以尝试的模型调优的几个点:

  • 增加Child属性,年龄小于12的添加Child属性
  • Pclass似乎没有利用起来,吧Pclass和港口组合成新的特征
  • 增加一个Family属性,把Parch和SibSp还有自己加起来,看看家庭人数的影响

经过一系列处理后,目前最好成绩是0.78947
各种特征排列组合寻求最优解是一个需要耐心和细心的活儿,可能也需要一点点的灵感和新的视角
暂时告一段落啦,接下来会用模型融合来试一试~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345