Kaggle数据科学的入门项目:Titanic
写在前面
基本步骤
- 数据准备和探索
- 数据清洗
- 特征选择
- 模型训练
- 模型验证
- 提交结果
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()
通过对数据的观察,可以总结出如下数据字典:
变量名 | 变量解释 | 数据解释 |
---|---|---|
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. 数据清洗
通过初步探索发现,在Age
、Cabin
、Fare
和Embarked
特征上都有数据缺失,我们需要处理缺失值才能进行下一步的分析。
处理缺失数据一般有两种方法:滤除缺失数据和填充缺失数据。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_
可以看出,原来的Embarked
和Sex
两列均根据变量取值拆成了若干列:
['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示范文件的格式,也就是包含两列PassengerID
和Survived
数据的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()
很显然,有Cabin记录的乘客的获救率远远高于没有Cabin记录的乘客,可以考虑将Cabin转化成二元属性用于模型训练。
还有一些属性
Parch
、SibSp
简单的查看了一下,参考价值不大,暂且不用了吧。分析到这里就可以进行下一步的预处理了。
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
属性的格式是名
+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分为
Normal
、Middle
和Royal
三类。
# 先将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.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拆为了Normal
、Middle
heRoyal
三列。
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_1
、pclass_2
和pclass_3
3列。
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
属性,现在特征数量已经很多了,就利用奥卡姆剃刀把它剃掉吧~~
经过这一系列的处理后,现在的数据变成了这样:
由原来的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
各种特征排列组合寻求最优解是一个需要耐心和细心的活儿,可能也需要一点点的灵感和新的视角
暂时告一段落啦,接下来会用模型融合来试一试~~