顺利注册完kaggle之后,终于可以开始上手撸项目啦!
先从大名鼎鼎的泰坦尼克号开始吧!
尽管网上有很多大神进行了“入门级别”的代码分享讲解,但我看了一轮仍然觉得对新手不够友好。
愿此文能给新手入门一些帮助。
声明在前:
我的代码有参考某些大神的帖子,在文末会贴上作者及链接;
我的代码和文章仅做学习研究分享,如需转载请注明作者(笨小孩)和出处
https://blog.csdn.net/CC_Cynthia/article/details/104278690;
转载请知会作者;
此文仅供非商业用途,谢谢。
那么闲话不多说,上干货。
我在这里使用的是Jupyter Notebook,Python语言
首先先导入需要使用到的包
%matplotlib inline
#使得图像即时显示
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
#引用warinings 是为了美观的看代码及运行结果,不然Jupyter会爆红提示版本更新后用法过时问题
然后导入数据
train = pd.read_csv('C:/Users/笨小孩/Desktop/泰坦尼克/train.csv')
test= pd.read_csv('C:/Users/笨小孩/Desktop/泰坦尼克/test.csv')
PassengerId=test['PassengerId']
all_data= pd.concat([train, test], ignore_index=True)
all_data
这里有2个细节:
1.导入数据的时候,我的是windows系统,所以引用路径需要使用“/”而非ios的“\”,大家可以根据自己的系统而做出调整,路径直接去文件夹上方复制粘贴。见下图。
2.函数pd.concat( )是拼接函数
代码all_data= pd.concat([train, test], ignore_index=True)的含义是,将train, test合并拼接,参数ignore_index=True 是test的原来索引洗去,强制赋予新的索引。我们来看一下,如果没有这个参数会怎么样。
我们发现其索引并未改变,沿用了原来的索引
现在我们来观察下表头,或许直接打开csv文件观察更直观一些。
可以看到,表头分别有Passenger(唯一标识),Survived(是否获救),Pclass(舱位等级),Name(名字),Sex(性别),Age(年龄),SibSp(同上船的兄弟姐妹+配偶),Parch(同上船的父母子女),Ticket(票号),Fare(船票价格),Cabin(船舱),Embarked(上船地点)。随便滚动下鼠标就会发现,Age, Cabin存在缺失值,但是具体的,我们需要调用函数来看。
在这里,我们可以调用pandas的info( )或者是pandas的describe( )来看,下面对比下优劣。
使用describe( )
我们就会发现貌似少了一些特征的描述,比如Name, Sex, Embarked…因为这些特征都是字符串,无法进行简单的数字统计,比如mean, std等等,所以被略过了。
我们再用info( )观察下
这时候就能直观的看出来Age, Cabin, Embarked存在缺失值。但是比起describe少了简单的数据统计。
单看数据是无法建立起特征之间的有效联系的。所以我们需要运用统计学及可视化来了解数据之间的相关性,挖掘出潜藏在数字背后的关联,必要时可以创造一个新的特征。
这里说明一下,barplot是条形图,传入的参数用于设定x轴,y轴,还有数据源。
极有可能高等级的船舱更接近逃生出口,易于逃生。
年龄不是简单的分类值,而是回归数值,所以这里用到了Seaborn的FacetGrid(结构化多绘图网格)。在我们想要可视化变量的分布或者多个变量之间的关系时,使用FacetGrid非常有用。
我们来看:
facet=sns.FacetGrid(train, hue=‘Survived’, aspect=2)
FacetGrid( )里面设置了参数,data=train, hue(颜色显示为)Survived, aspect是长宽比,这里设置为2;
facet.map(sns.kdeplot, ‘Age’, shade=True)
这里的kdeplot是核密度估计图,通过这个图我们可以比较直观的看出数据样本本身的分布特征,我们对Age进行处理。shade若为True,则在kde曲线下面的区域进行阴影处理。除此之外还可以设置参数color,g代表绿色,r 代表红色,y代表黄色;
facet.set(xlim=(0, train[‘Age’].max()))
是对图像x轴的数据范围进行设定,从0到Age的最大值;
facet.add_legend()
plt.xlabel(‘Age’)
plt.ylabel(‘Density’)
这一段代码很明了,设置图例,横坐标是Age,纵坐标是Density;
从上图可以看出来,大约在12岁左侧,生还率有明显的差异,密度图非交叉面积很大。
举一反三,我们也可以通过上述代码对票价进行分析:
这个陡峭的曲线告诉我们,票价越低,越不容易生还。
现在特征还剩Name, Tickect, Cabin,SlibSp,Parch没有进行可视化观察。先观察下Name,似乎有身份职业的提示信息。
我们调用函数再仔细看看:
all_data['Title']=all_data['Name'].apply(lambda x:x.split(',')[1].split('.')[0].strip())
all_data['Title'].value_counts()
Out[13]:
Mr 757
Miss 260
Mrs 197
Master 61
Dr 8
Rev 8
Col 4
Mlle 2
Major 2
Ms 2
Capt 1
Dona 1
Jonkheer 1
Mme 1
Sir 1
Don 1
the Countess 1
Lady 1
Name: Title, dtype: int64
all_data[‘Title’]=all_data[‘Name’].apply(lambda x:x.split(’,’)[1].split(’.’)[0].strip())
这段代码表示,我们基于all_data(train+test的总数据集)创建一个新的特征【Title】,【Title】是从Name中提取出来的,apply(lambda x:x.split(’,’)[1].split(’.’)[0].strip()),我们截取【,】之后的第一个字母,到【.】之前的字母,并将选取的字符串去除空格。
仔细观察下提取出来的抬头称呼,可以进行大致分类,比如像Lady, Jonkheer这些典型的贵族称呼。
对于抬头的划分,目前只能基于我的英语知识储备+感性直觉进行,如果有更加可信的分类方法,烦请告知。
第一类Mr(先生)
第二类Mrs(已婚或未知婚姻状况)Mme, Ms, Mrs
第三类Miss(未婚女子)Mlle, Miss
划分完最容易识别的三类之后,我们来看剩下的:
‘Master’(表示某一专业领域的专家,也可能是学院的老师);
‘Rev’(这个是reverend的缩写,就是牧师);
‘Dr’(医生或者博士);
‘Col’(上校的缩写);
‘Major’(少校);
‘Jonkheer’(年轻贵族的称呼)
‘Lady’(贵族女性)
‘the Countess’(女公爵);
‘Sir’(爵士);
‘Don’(也是一种贵族称呼)
‘Dona’(Don的夫人)
‘Capt’(上尉)
直观感受下就可以分类了。
第四类Nobility(贵族),Jonkheer, Lady, the Countess, Sir, Don, Dona
第五类Offiecer(军官), Cpl, Major, Capt
第六类Master(某一领域专家),Master, Dr, Rev
Title_Dict={}
Title_Dict.update(dict.fromkeys(['Mr'], 'Mr'))
Title_Dict.update(dict.fromkeys(['Mme', 'Ms', 'Mrs'], 'Mrs'))
Title_Dict.update(dict.fromkeys(['Mlle', 'Miss'], 'Miss'))
Title_Dict.update(dict.fromkeys(['Jonkheer','Lady','the Countess','Sir','Don', 'Dona'], 'Nobility'))
Title_Dict.update(dict.fromkeys(['Capt', 'Col', 'Major'], 'Officer'))
Title_Dict.update(dict.fromkeys(['Master','Dr','Rev'], 'Master'))
先建立一个Title_Dict的空字典,然后我们使用dict.fromkeys往字典里面添加键值,在这个函数里第一个参数是键,第二个参数是值。比如Title_Dict.update(dict.fromkeys([‘Mme’, ‘Ms’, ‘Mrs’], ‘Mrs’))这行代码中,Mme, Ms, Mrs都是键(key),对应的是Mrs(Value)。
下面来可视化一下
怎么感觉什么头衔都没有的普普通通Mr最惨啊,存活率好低,不到2成。Master和Officer存活率虽然相对于Mr高,但是仍然低于5成。
我们来分析下代码:
之前我们已经赋值过all_data[‘Title’],即下面这些提取出来的Title字符串。
all_data[‘Title’]=all_data[‘Title’].map(Title_Dict),这一步实现了什么,我们取all_data的前20行看下。
all_data.head(20)
结果发现Title都被赋予了Title_Dict的value值,这样就可以为分类统计做好准备。
我们再看Cabin,似乎存在很多缺失值,但是如果直接弃之不用又有些可惜。
调用个函数观察下
all_data['Cabin'].value_counts()
Out[17]:
C23 C25 C27 6
G6 5
B57 B59 B63 B66 5
B96 B98 4
D 4
..
C50 1
B4 1
A14 1
A11 1
B39 1
Name: Cabin, Length: 186, dtype: int64
我们来处理下看看。先将缺失值补为Unknown,然后提取首字母。
似乎船舱类别不一样也是对存活率有影响的。
最后看下Ticket……感觉一团糟,没有头绪,我决定舍弃。有的大神对同票号的人数进行了统计。最后得出同行人数对于存活率的影响,这个思路很很厉害,不过对于小白的我,有些复杂,我就跳过啦。
一家人就要整整齐齐在一起,数据也是。这里加多了个1是因为还有乘客自己,我们得到一个新的特征——Family_size(家庭大小)。
但是类别太多,无法有效分析,我们需要进一步细化。仔细看来,family_size=2,3,4可以归为高生存率(超过5成),family_size=1,5,6,7为低生存率(低于5成),其他生存率为0。这么一想,我们可以封装个函数,函数里面使用if分类。代码如下:
def Family_type(s):
if (s>=2)&(s<=4):
#高生存率
return 2
elif ((s>4)&(s<=7)|(s==1)):
#低于40%的生存率
return 1
else:
#无生存率
return 0
all_data['Family_type']=all_data['Family_size'].map(Family_type)
sns.barplot(x='Family_type',y='Survived',data=all_data)
到这里,我们终于可以进行数据清洗了。
我们先来观察下all_data的缺失值:
一共三个Age, Embarked, Fare
再来粗略看下统计数据
all_data['Age']=all_data['Age'].fillna(all_data['Age'].mean())
Fare虽然缺失值只有1个,可以简单粗暴的使用mean填充,但是不同的路程和舱位等级票价肯定是不一样的,直接使用mean值就不科学。我们调取这条缺失数据看一下
我们可以看到登船地点是S,舱位等级是3等舱
代码如下
fare=all_data[(all_data['Embarked']=='S')&(all_data['Pclass']==3)].Fare.mean()
all_data['Fare']=all_data['Fare'].fillna(fare)
好了,现在就剩下Embarked这个没处理了。
我们当然可以采用登船人数多的那一个口岸作为填充数。先调取数据看一下。
all_data['Embarked'].value_counts()
Out[26]:
S 914
C 270
Q 123
Name: Embarked, dtype: int64
S港口登船的人数最多,所以我们可以选择填‘S’,但是我们可以效仿Fare的处理方式,先观察下数据。
与登船口岸数据相关的就是Fare(票价),除了距离票价也受到舱位等级影响。这里可以看到票价80,1等舱。
我们可以看一下大概在哪个区间:
这样一看,S最接近,好的,就是S了。
all_data['Embarked']=all_data['Embarked'].fillna('S')
再来看下数据状态,很好,缺失值处理完成。
all_data.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 1309 entries, 0 to 417
Data columns (total 16 columns):
Age 1309 non-null float64
Cabin 1309 non-null object
Embarked 1309 non-null object
Fare 1309 non-null float64
Name 1309 non-null object
Parch 1309 non-null int64
PassengerId 1309 non-null int64
Pclass 1309 non-null int64
Sex 1309 non-null object
SibSp 1309 non-null int64
Survived 891 non-null float64
Ticket 1309 non-null object
Title 1309 non-null object
Cabin_type 1309 non-null object
Family_size 1309 non-null int64
Family_type 1309 non-null int64
dtypes: float64(3), int64(6), object(7)
memory usage: 213.9+ KB
接下来就是数据的类型转换了,我们将一些为字符串形式的特征转换为数字。
all_data=all_data[['Survived','Pclass','Sex','Age','Fare','Embarked','Title','Cabin_type','Family_type']]
all_data=pd.get_dummies(all_data)
我们来看下代码
我们并不需要所有特征,这里选取我们新增的特征和原有的关键特征:
[‘Survived’,‘Pclass’,‘Sex’,‘Age’,‘Fare’,‘Embarked’,‘Title’,‘Cabin_type’,‘Family_type’]。
接下来看着一条,all_data=pd.get_dummies(all_data)。
pd.get_dummies( )是一个很简单粗暴实用的函数。我们很多时候拿到的数据特征是字符串,需要转化为虚拟变量,就是讲不能定量处理的变量量化,比如我们拿到的Sex原始数据是Female和Male,无法有效进行处理,所以可以将Female=0,Male=1来处理。通过dummies的处理之后数据就成了这个样子。
数据按特征类别分出了列,符合该项特征的为1,不符合为0。而要得到这个结果,并不复杂。我们在pd.get_dummies( )里面输入data这个参数行。但是大家可能已经想到了,dummies明显的缺点就是,当类别数量很多的时候,我们的特征空间会变得非常大。
然后将all_data类型转化好的数据分为X_train和X_test(我们要预测的418名乘客数据),并且将数据转化为矩阵。调用一下就了然了。
train=all_data[all_data['Survived'].notnull()]
X_test=all_data[all_data['Survived'].isnull()].drop('Survived',axis=1)
X_train=train.as_matrix()[:,1:]
X_train[0]
Out[32]:
array([ 3. , 22. , 7.25, 2. , 0. , 1. , 0. , 0. , 1. ,
0. , 0. , 1. , 0. , 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 0. , 0. , 1. ])
Y_train是与X_train对应的样本特征,即Survived这一栏,我们调用部分Y_train看一下,都是数字0与1。
Y_train=train.as_matrix()[:,0]
Y_train[:15]
Out[31]:
array([0., 1., 1., 1., 0., 0., 0., 0., 1., 1., 1., 1., 0., 0., 0.])
在使用算法模型之前,我们看下shape有个形象的认识。
X_train.shape,Y_train.shape,X_test.shape
Out[34]:
((891, 24), (891,), (418, 24))
好的,接下来咱们就可以开始套用算法模型啦哈哈哈哈,主要使用4种算法:逻辑回归,k邻近算法,随机森林,决策树。别问我为啥不用别的,因为我跟它们不太熟。
先粗略看下:
#Logistic Regression
from sklearn.linear_model import LogisticRegression
logreg=LogisticRegression()
logreg.fit(X_train,Y_train)
Y_pred=logreg.predict(X_test)
log_score=logreg.score(X_train,Y_train)
log_score
Out[35]:
0.8260381593714927
#k近邻算法
from sklearn.neighbors import KNeighborsClassifier
knn=KNeighborsClassifier(n_neighbors=3)
knn.fit(X_train,Y_train)
Y_pred=knn.predict(X_test)
knn_score=knn.score(X_train,Y_train)
knn_score
Out[36]:
0.8395061728395061
#随机森林
from sklearn.ensemble import RandomForestClassifier
random_forest= RandomForestClassifier(n_estimators=300)
random_forest.fit(X_train,Y_train)
Y_pred=random_forest.predict(X_test)
rf_score=random_forest.score(X_train,Y_train)
rf_score
Out[37]:
0.9876543209876543
#决策树
from sklearn.tree import DecisionTreeClassifier
decision_tree=DecisionTreeClassifier()
decision_tree.fit(X_train,Y_train)
Y_pred=decision_tree.predict(X_test)
dt_score=decision_tree.score(X_train,Y_train)
dt_score
Out[38]:
0.9876543209876543
哇,得分都挺高的啊,看到这里是不是很激动,别急,我们需要进行交叉验证。
给大家演示调整参数的两种办法,交叉验证手写循环和网格搜索交叉验证。
在进行交叉验证之前,先分割数据
#分割训练数据集
from sklearn.model_selection import train_test_split
from sklearn.model_selection import cross_val_score
a_train,a_test, b_train, b_test=train_test_split(X_train, Y_train, test_size=0.4, random_state=1)
在这里,我们将Train(X_train, Y_train)分成训练集(a_train, b_train)和测试集(a_test, b_test),测试集站40%,随机种子固定为1。到这里有的同学可能有些懵了,别怕,我画了张图,一目了然。
我们先看一下只使用a_train,b_train时knn交叉验证得出的score
首先得到的是有3个元素的元组,这代表着进行了3次交叉验证,我们取平均值,最后得分71.34%左右。
接下来使用手动for循环找到最优参数。
best_score, best_p, best_k=0, 0, 0
for k in range(1,11):
for p in range(1,11):
knn_clf=KNeighborsClassifier(weights='distance',n_neighbors=k,p=p)
scores=cross_val_score(knn_clf,a_train,b_train)
score=np.mean(scores)
if score>best_score:
best_score,best_p,best_k =score, p, k
print('best k=',best_k)
print('best p=',best_p)
print('best score=',best_score)
>>
best k= 7
best p= 1
best score= 0.7696629213483147
将最优参数带回knn算法,此时加上a_test,b_test数据集,得到71.42,与之前的分数有所不同,但是经过新数据测试的得分可信度更高。
best_knn=KNeighborsClassifier(weights='distance',n_neighbors=7, p=1,n_jobs=-1)
best_knn.fit(a_train,b_train)
best_knn.score(a_test,b_test)
Out[41]:
0.7170868347338936
接下来对随机森林和决策树进行网格搜索最优参数,代码如下
#随机森林交叉验证
cross_val_score(random_forest,a_train,b_train).mean()
Out[43]:
0.8464419475655429
from sklearn.model_selection import GridSearchCV
param_grid={
'n_estimators':[i for i in range(1,50)],
'max_leaf_nodes':[i for i in range(2,50)],
'max_depth':[i for i in range(2,10)]
}
random_forest_clf=RandomForestClassifier(random_state=1)
grid_search=GridSearchCV(random_forest_clf,param_grid, n_jobs=-1)
grid_search.fit(a_train,b_train)
grid_search.best_estimator_
Out[44]:RandomForestClassifier(bootstrap=True, class_weight=None, criterion='gini',
max_depth=9, max_features='auto', max_leaf_nodes=17,
min_impurity_decrease=0.0, min_impurity_split=None,
min_samples_leaf=1, min_samples_split=2,
min_weight_fraction_leaf=0.0, n_estimators=8,
n_jobs=None, oob_score=False, random_state=1, verbose=0,
warm_start=False)
best_random_forest=RandomForestClassifier(n_estimators=8,max_leaf_nodes=17,max_depth=9,random_state=1)
best_random_forest.fit(a_train,b_train)
best_random_forest.score(a_test,b_test)
Out[45]:
0.7955182072829131
这里注意固定一下随机种子。
#决策树交叉验证
cross_val_score(decision_tree,a_train,b_train)
cross_val_score(decision_tree,a_train,b_train).mean()
Out[46]:
0.7902621722846442
param_grid={
'max_depth':[i for i in range(1,20)],
}
decision_tree_clf=DecisionTreeClassifier(random_state=1)
grid_search=GridSearchCV(decision_tree_clf,param_grid, n_jobs=-1,verbose=2)
grid_search.fit(a_train, b_train)
grid_search.best_estimator_
Out[47]:
DecisionTreeClassifier(class_weight=None, criterion='gini', max_depth=5,
max_features=None, max_leaf_nodes=None,
min_impurity_decrease=0.0, min_impurity_split=None,
min_samples_leaf=1, min_samples_split=2,
min_weight_fraction_leaf=0.0, presort=False,
random_state=1, splitter='best')
best_decision_tree=DecisionTreeClassifier(random_state=1,max_depth=5)
best_decision_tree.fit(a_train, b_train)
best_decision_tree.score(a_test,b_test)
Out[48]:
0.7955182072829131
对于逻辑回归的优化需要使用梯度算法,对于我有些复杂,在这里,我们就简单的使用交叉分析看下分数。
现在,翻到前面看看,是不是得分下降了很多啊,这才是比较可信的得分。
我们使用了4个算法,究竟哪个才是最适合呢,我这里选择集成算法的Soft Voting,即将所有模型预测样本为某一类别的概率的平均值作为标准,概率最高的对应的类型为最终的预测结果。简单形象的说就是把4个算法宝宝集合在一起开会,对418名乘客存活率进行预测。究竟得分多少呢?期待的搓搓手。
#集成学习soft voting
from sklearn.ensemble import VotingClassifier
voting_clf=VotingClassifier(estimators=[
('logreg_voting',LogisticRegression()),
('knn_voting',KNeighborsClassifier(weights='distance',n_neighbors=7,p=1)),
('random_forest_voting',RandomForestClassifier(n_estimators=8,max_leaf_nodes=17,max_depth=9,random_state=1)),
( 'decision_tree_voting',DecisionTreeClassifier(max_depth=5,random_state=1))
], voting='soft')
voting_clf.fit(a_train, b_train)
voting_clf.score(a_test,b_test)
Out[49]:
0.8067226890756303
#存储预测结果
predictions=voting_clf.predict(X_test)
submission = pd.DataFrame({'PassengerId':PassengerId,'Survived':predictions.astype(np.int32)})
submission.to_csv('C:/Users/笨小孩/Desktop/泰坦尼克/submissional.csv',index=False)
因为predicions的矩阵是浮点数,所以我们需要转化为int32
最终存储结果就是这样的啦。
到这里,这个case就告一段落了,再继续就是不断优化代码提升排名了。
在刚开始做这个项目的时候,颇有一种无处下手的感觉,参考了很多人的代码(我都在文末贴上了网址,感兴趣的朋友可以去看看)之后,才有了一些思路。可惜的是,很多优秀的算法和代码因为现阶段掌握的知识有限,没能实现。在这里,只能提供给大家笨小孩这略显简陋稚嫩的代码。
下面分享下我对泰坦尼克项目的一些思考:
结束之前推广下我的个人微信公众号:BXH_data,欢迎大家与我沟通交流,共同进步!
最后的最后,用一句话结尾吧:业精于勤荒于嬉,行成于思毁于随!与君共勉。
参考文献
[1]炼己者.kaggle入门–泰坦尼克号之灾(手把手教你) https://www.jianshu.com/p/e79a8c41cb1a,2018.07.26.
[2]起名困难症用户.Kaggle平台Titanic生存率预测项目(TOP3%) https://zhuanlan.zhihu.com/p/50194676,2019-3-2.
[3]大树先生.Kaggle Titanic 生存预测 – 详细流程吐血梳理 https://zhuanlan.zhihu.com/p/31743196,2018-1-15.