【python】Kaggle入门:titanic 的数据清洗与模型训练

目     录

 

0、概述

1、数据清洗

1.1 缺失值填充

1.1.1 Age填充

1.1.2 Embarked填充

1.1.3 Fare填充

1.2 异常值处理

1.3 特征转换

2、建模和优化

2.1、参数优化

2.2 训练模型

2.3 交叉验证

3、预测

4、总结


0、概述

在上一篇文章中,我们对训练集的各个特征已经有了初步了解,并提取出了九个可用的特征:

Pclass、Age、SibSp+Parch、Fare、Sex、Name、Ticket、Cabin、Embarked。

本篇文章中,将继续跟着该网址的第三部分和第四部分,对数据进行清洗,并且进行模型训练。

1、数据清洗

数据清洗主要有两部分工作:第一部分为缺失值填充,第二部分为异常值处理。

1.1 缺失值填充

在前一篇文章我们说过,训练集和测试集的数据中,有部分数值为NaN,这部分数值我们要进行填充:

主要是Age、Embarked、Fare,Survived是我们要预测的,不用填。

1.1.1 Age填充

Age缺失263个,因此要填充263个。缺失量较大。不能用平均数、中位数、众数等直接填充,要利用算法进行预测。

这里选择采用随机森林算法。有关随机森林的知识,请参阅该网址。代码如下:

from sklearn.ensemble import RandomForestRegressor
age_df = all_data[['Age', 'Pclass','Sex','Title']]
age_df=pd.get_dummies(age_df)
known_age = age_df[age_df.Age.notnull()].as_matrix()
unknown_age = age_df[age_df.Age.isnull()].as_matrix()
y = known_age[:, 0]
X = known_age[:, 1:]
rfr = RandomForestRegressor(random_state=0, n_estimators=100, n_jobs=-1)
rfr.fit(X, y)
predictedAges = rfr.predict(unknown_age[:, 1::])
all_data.loc[ (all_data.Age.isnull()), 'Age' ] = predictedAges 

首先要安装sklearn这个库,这个库里面集成了许多常用的机器学习算法,有了这个我们就可以当一个入门的调参工程师了。但是conda不能直接用conda install sklearn安装这个库,在源里面找不到,要用如下这条命令:

conda install -c anaconda scikit-learn

才可以。

放假回家这移动网下点外网的东西简直欲仙欲死,恰逢国庆,飞机场又都被封,只能靠这辣鸡网度日了。

随机森林算法在这里我们是作为一个分类器使用的,既然是分类器,就有输入输出。输入由我们自己选定,这里选择了三个可用的特征:Sex、Pclass、Title。输出为Age。本质上就是用这三个输入来预测输出。

所以我们要先把所有数据中的这四个特征拿出来,保存在age_df这个矩阵中:

嗯,1309个数据全部导入。每个数据都有四个特征。

然后需要将上面这个矩阵向量化。如何向量化?就是将非数值的量转为数值量。比如说对于Sex,这一列是一个向量,每个向量元素有两个取值male和female,那么将其转换为数值量0和1,就变成两列:Sex_male和Sex_female。对于每个Sex的值,若是male,则新的Sex_male列对应的值为1,Sex_female的值为0。对于Title也一样,只不过要拓展成的列多一点。这样,一个元素的可能取值为n个值,就变成了n列向量,这n列向量的元素或者为0,或者为1。如下,是原矩阵经变换后得到的新矩阵:

这个新矩阵的最大优点为所有元素均为数字,可以直接输入使用。

然后我们要将整个数据集分为两部分:第一部分的Age已知,第二部分的Age未知。即第一部分为训练集,第二部分为要预测的数据。怎么把这两部分分开呢?用的就是第四行和第五行的代码:

known_age = age_df[age_df.Age.notnull()].as_matrix()
unknown_age = age_df[age_df.Age.isnull()].as_matrix()

我们知道,Age不存在的元素值为NaN,因此用notnull函数可以筛选出所有的Age已知的行,用isnull可以筛选出未知的行。注意无论是isnull还是notnull,返回的都是一个包含True和False对应的向量,然后用布尔下标的方法筛选出对应的行。这时返回的是一个DataFrame,我们要将这个DataFrame转换成一个数组,因此要调用as_matrix函数。如下图:

从DataFrame转换成了ndarray。

known_age的内容如下所示:

有点乱啊,不过可以看出来第一个是Age,然后是Pclass、Sex什么的。

这部分是训练集。训练集的输入和输出要分开。观察训练集known_age可知,训练集的输入为第二列及之后,输出应该为第一列。因此我们设置X为第二列及之后,y为第一列。

接下来是重头戏:随机森林的调用。sklearn库里面有两个随机森林,一个用于分类(RandomForestClassifier),一个用于回归(RandomForestRegressor),我们当然要用回归的随机森林了。

使用随机森林需要调用RandomForestRegressor函数,该函数的参数参见该网址

我们调整了三个参数:

random_state=0:此参数让结果容易复现。 一个确定的随机值将会产生相同的结果,在参数和训练数据不变的情况下。

n_estimators=100:在利用最大投票数或平均值来预测之前,你想要建立子树的数量。 较多的子树可以让模型有更好的性能,但同时让你的代码变慢。 你应该选择尽可能高的值,只要你的处理器能够承受的住,因为这使你的预测更好更稳定。

n_jobs=-1:这个参数告诉引擎有多少处理器是它可以使用。 “-1”意味着没有限制,而“1”值意味着它只能使用一个处理器。 

也就是说,我们将使用处理器的所有核心,建立一百棵子树的随机森林。随机值取值固定,这会让每次的结果都确定。

然后这样随机森林的架子就搭好了,下一步是训练模型。我们的架子用rfr来表示。训练模型就直接rfr.fit(X,y)即可。

利用rfr.predict进行预测,输入参数为unknown_age的第二列到最后一列。返回值为预测的年龄。然后填入年龄为空的那些行即可。

1.1.2 Embarked填充

由上文我们可以知道,Embarked就缺俩,因此我们可以很容易的把他们俩填上,不用像Age一样用复杂的算法。而是直接观察缺少Embarked的乘客的特征,在不缺失Embarked的乘客中去找与之最相似的乘客,把这些乘客的Embarked填入即可。

那么我们就要看缺少Embarked的乘客是什么样的:

特征还是很明显的:Fare都是80,Pclass都是1,Sex都是female,还都是妹子,甚至都是一家的——Ticket都一样。那我们来找一下Fare为80,Pclass为1的所有乘客的资料:

不行啊,满足这俩条件的就她们俩,那先看看Pclass为1的所有人,这就很多,323人。

现在我们想知道,Pclass为1的人,Embarked为不同值时,他们的Fare的中位数为多少,我们选择中位数离80最近的Embarked作为缺失值。

也就是说,我要对所有乘客进行分组,首先按Pclass分组,然后按Embarked分组,最后算出中位数。

为什么选Pclass和Embarked呢?因为看这些特征,和船票价格Fare直接相关的,只有两个:Pclass,你Pclass不同价格自然不一样;Embarked不同则坐船距离不同,价格也不一样;类似坐高铁,高铁票价不就和距离还有几等座有关系嘛。

如何实现呢?

all_data.groupby(by=["Pclass","Embarked"]).Fare.median()

首先调用groupby函数,这个函数很有用,它不会改变all_data的索引,但是其中每行的相对位置会发生变化,如何变呢?首先,Pclass相同的会在一起,然后,Embarked相同的会在一起。先不看后面的效果,只看groupby的效果如下:

然而失败了,它返回的类型不是DataFrame,因此不能直接输出。

只好看整个函数的返回了:

所以,Pclass=1的乘客中,Fare为76的离80最近,这些乘客的Embarked为C,因此这俩缺失值就填为C。

all_data['Embarked'] = all_data['Embarked'].fillna('C')

1.1.3 Fare填充

由上文可知,Fare只有一个缺失值,因此和Embarked类似,先看这个缺失值的特征:

Pclass为3,Embarked为S,妥了,从上文看,满足这俩条件的Fare中位数为8.05元,填进去即可。

现在除了Survived以外都是1309个了。满足。

1.2 异常值处理

在进行异常值处理之前,我们先来讨论一个问题:什么是异常值?

异常值就是不符合普遍规律的值。我们的代码是遵循普遍规律的,对于不符合普遍规律的样本不能够很好的处理。

对于本题来说,异常值可以简单地定义如下:

既然我们都知道妇女儿童容易获救,成年男性不易获救,那我看一下所有值,没获救的妇女儿童和获救的成年男性不就是异常值了么。

好像有点太武断了,比如说独自一人出行的夫人小姐,她们相对于一个大家庭中的夫人小姐肯定相对弱势,获救机率没他们高。因此更强的异常定义应该是:在多人组成的家庭中,“妇女儿童没获救”和“成年男性获救”的为异常值。

先来看看这两种是不是真的异常:

怎么看是不是“多人组成的家庭”呢?根据我们上一篇对name的分析,这个aa一样的人,应该就是一个家庭的。实际上这个aa的实际名字应该为surname。我们找出所有乘客的surname,把相同的作为一组来看:

all_data['Surname']=all_data['Name'].apply(lambda x:x.split(',')[0].strip())

效果如下:

surname这一列得到了所有乘客的aa值,那么我想看某个乘客的家庭共有多少成员,然后对于某个乘客,就可以知道他属于一个多大家庭的成员了。因此需要建新的一列FamilyGroup,用来存储每个乘客的家庭有多少人:

Surname_Count = dict(all_data['Surname'].value_counts())
all_data['FamilyGroup'] = all_data['Surname'].apply(lambda x:Surname_Count[x])

效果如下:

来看看有多少个大家庭吧:

单身的有637个,二人家庭有266个,三人家庭有189个......那么下一步就很简单了:找出所有处于多人家庭的“妇女儿童”和“成年男性”。

Female_Child_Group=all_data.loc[(all_data['FamilyGroup']>=2) & ((all_data['Age']<=12) | (all_data['Sex']=='female'))]
Male_Adult_Group=all_data.loc[(all_data['FamilyGroup']>=2) & (all_data['Age']>12) & (all_data['Sex']=='male')]

儿童定义为小于12岁,成年男性定义为大于12岁(男性真的惨)。分别保存在两个组中。

接下来就是家庭划分了:按surname将两个组的成员划分为不同的组,然后看各组中的生还率。先看妇女儿童把:

Female_Child=pd.DataFrame(Female_Child_Group.groupby('Surname')['Survived'].mean().value_counts())
Female_Child.columns=['GroupCount']
Female_Child

先来看第一行,新建一个DataFrame,每行的值为“妇女儿童按surname分组,计算各组生还的平均值,查完得到平均值的计数”。有点难理解,举例来说吧:对于每个value_counts,有一个对应的value,value值为1,那么生还平均值为1,value_counts为100,说明生还平均值为1的有100个。生还平均值为1代表什么呢?代表该家庭的所有妇女儿童全部获救。也就是说,全部获救的家庭有100个。这太绕了:

看来全部获救的家庭有115个,全灭的家庭有31个,其余的都很少。

再看成年男性:

Male_Adult=pd.DataFrame(Male_Adult_Group.groupby('Surname')['Survived'].mean().value_counts())
Male_Adult.columns=['GroupCount']
Male_Adult

全部获救的家庭有20个,全灭的有122个。。。差距太大了。

这样我们就可以回答最开始的问题了:多人组成的家庭中,妇女儿童全灭或成年男性都获救的家庭,占比均很少,可以认为是异常。我们称前者的异常组成遇难组,后者的异常组成幸存组。来看看遇难组怎么生成的吧:

Female_Child_Group=Female_Child_Group.groupby('Surname')['Survived'].mean()
Dead_List=set(Female_Child_Group[Female_Child_Group.apply(lambda x:x==0)].index)
print(Dead_List)

首先通过groupby生成一个数据结构,其序号为Surname,值为Survived的mean,这建立了一个家庭与遇难平均数的映射。然后建立一个遇难组,是一个set,其值为刚刚生成的数据结构中值为0的index。也就是说,遇难组存储了所有“妇女儿童全部遇难”的家庭。对于幸存组也一样:

下一步,我们要将测试集中的异常值修改为正常值。

这里我其实有点疑问:根据这个操作,kaggle的评分流程应该是这样:我自己通过算法得到test的结果,将结果发给服务器以得到分数;而我对test进行任何操作都没有关系。我之前则认为是测试集不能动,能动的只有train,也就是训练集。这里或许需要问问老师。

(PS:后来我又自己仔细想了想,这个问题抽象出来是这样:

对于一个分类问题,样本均具有A、B、C、d、e五个特征,分类依据A、B、C三个特征。然后我发现由于某种原因,d无法加入到影响分类的特征中;但是分类不依据的d特征中,d=1时A的特征的表现与d=0时A的特征的表现相反,而且d=0占大多数,那么,对于测试集中的样本,我不妨对d=1的所有样本的A全部取反,从而有利于预测。因为kaggle仅看最后的csv结果,因此我对test进行什么处理都是可以的)

注意,这里的遇难组和幸存组都是根据train来的,不要看是根据all_data来的就认为该组来自train和test,因为test没有survived,它怎么能知道是否幸存呢?我们这一步实际上是根据train对test做了一个修正,使其的结果更准确。

改为正常值的代码如下:

train=all_data.loc[all_data['Survived'].notnull()]
test=all_data.loc[all_data['Survived'].isnull()]
test.loc[(test['Surname'].apply(lambda x:x in Dead_List)),'Sex'] = 'male'
test.loc[(test['Surname'].apply(lambda x:x in Dead_List)),'Age'] = 60
test.loc[(test['Surname'].apply(lambda x:x in Dead_List)),'Title'] = 'Mr'
test.loc[(test['Surname'].apply(lambda x:x in Survived_List)),'Sex'] = 'female'
test.loc[(test['Surname'].apply(lambda x:x in Survived_List)),'Age'] = 5
test.loc[(test['Surname'].apply(lambda x:x in Survived_List)),'Title'] = 'Miss'

我们将测试集中的所有幸存组的所有成员全改为了女性,将遇难组中的所有成员改为了男性。

1.3 特征转换

在该部分,我们要划分测试集和训练集,选取特征,以及在测试集上划分输入和输出的验证。

代码如下:

all_data=all_data[['Survived','Pclass','Sex','Age','Fare','Embarked','Title','FamilyMem','RoomNum','TicketGroup']]
all_data=pd.get_dummies(all_data)
train=all_data[all_data['Survived'].notnull()]
test=all_data[all_data['Survived'].isnull()].drop('Survived',axis=1)
X = train.as_matrix()[:,1:]
y = train.as_matrix()[:,0]

唯一要注意的就是要用dummies将矩阵数量化。

2、建模和优化

我们选择使用随机森林模型对题目进行求解。

2.1、参数优化

从上文可以知道,随机森林的参数较少,我们仅对两个参数进行优化:n_estimators和max_depth,前者为森林中树的数量,后者为树的深度。对于参数优化,我们有一个很方便的函数可以使用:GridSearchCV,该函数的使用参见该网址。代码如下:

from sklearn.pipeline import Pipeline
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV
from sklearn.feature_selection import SelectKBest

pipe=Pipeline([('select',SelectKBest(k=20)), 
               ('classify', RandomForestClassifier(random_state = 10, max_features = 'sqrt'))])

param_test = {'classify__n_estimators':list(range(20,50,2)), 
              'classify__max_depth':list(range(3,60,3))}
gsearch = GridSearchCV(estimator = pipe, param_grid = param_test, scoring='roc_auc', cv=10)
gsearch.fit(X,y)
print(gsearch.best_params_, gsearch.best_score_)

首先新建一个pipeline,这个pipeline可以看做是一个流水线,整个机器学习的流程就由这个pipeline规定:第一步做什么,第二步做什么。。。。。。最后一步确定整个问题的性质——比如说本题就是classify,分类问题。第一步是选择特征,这里是选择最好的20个特征,函数的解释见该网址,我们使用的是默认函数,设置k=20。默认函数只适用于分类问题——我们刚好是分类。第二步是确定功能为classify,后面调用了随机森林方法,参数random_state=10表示控制随机参数为10,这样我们最后得到的参数才会和参考网址的结果一样,否则无法得到相同的结果,具体解释参见该网址;参数max_features='sqrt'表示我们使用RF划分时考虑的最大特征数为\sqrt{n}

注意到我们使用RF时,要考虑两类参数,第一类是森林的框架参数:有多少棵树啊;第二类是每棵树的参数:随机值啊,最大特征数啊,树高啊,上面只考虑了部分参数:随机值和最大特征数,这些直接确定了,没有经过调参,下面要输入需要调参的参数。我们需要指定一些值,然后网格搜索就会分别遍历这些值,比如说这里我们对n_estimators和Max_depth分别指定了一些值,那么网格搜索则会对这些值全部配对,最后找出表现最好的配对。

之后就是用GridSearch来遍历了,GridSearch参数参见该网址

输出效果如图,跑了小一分钟:

正是由于我们确定了随机过程,才能得到和原作者一样的结果。两个超参数这就选择好了。

2.2 训练模型

既然我们已经确定好参数的值,就可以吧这些值带入随机森林中进行预测了:

from sklearn.pipeline import make_pipeline
select = SelectKBest(k = 20)
clf = RandomForestClassifier(random_state = 10, warm_start = True, 
                                  n_estimators = 26,
                                  max_depth = 6, 
                                  max_features = 'sqrt')
pipeline = make_pipeline(select, clf)
pipeline.fit(X, y)

首先仍然是新建一个pipeline,这里使用了另外一种建立pipeline的方法,因为函数参数太多了,直接扔进去太长了不好看。

第一个参数是特征选择,仍然是20个,第二个参数为随机森林函数,random_state不用去管了;warm_start为热启动参数,默认为False,如果为True,则下一次训练是以追加树的形式进行(重新使用上一次的调用作为初始化);下面三个参数则是我们在上面搜索得到的。之后开始训练即可。

PS:看上去第一步的参数优化的都是超参数,第二步才会对RF本身的参数进行优化啊。

效果如下:

惊了,算的很快。但是看上去也没训练什么参数啊。不太清楚调用fit是干嘛用了。查阅文献后发现fit的确会确定参数。

2.3 交叉验证

代码如下:

from sklearn import cross_validation, metrics
cv_score = cross_validation.cross_val_score(pipeline, X, y, cv= 10)
print("CV Score : Mean - %.7g | Std - %.7g " % (np.mean(cv_score), np.std(cv_score)))

看上去很简单,只需要调用一个cross_validation.cross_val_score函数即可,它可以直接用train作为验证集,不用我们自己去花纹,用cv这个参数来控制即可。

注意新版的sklearn的cross_validation被废弃,改用model_selection即可,代码如下:

from sklearn import model_selection, metrics
cv_score = model_selection.cross_val_score(pipeline, X, y, cv= 10)
print("CV Score : Mean - %.7g | Std - %.7g " % (np.mean(cv_score), np.std(cv_score)))

效果如下:

3、预测

既然我们的模型已经建好了,那下一步就可以开始对test进行预测咯:

predictions = pipeline.predict(test)
submission = pd.DataFrame({"PassengerId": PassengerId, "Survived": predictions.astype(np.int32)})
submission.to_csv(r"h:\kaggle\submission1.csv", index=False)

预测出来就是一个csv文件,就不截图了。

4、总结

由于我还没有系统的学习这些算法的具体使用,因此相对于上一篇文章,本篇文章略显仓促,由于个人能力有限,没能对RF、GridSearch、CV等函数的原理作充分说明,仅是简略的说明了一下它们的参数的含义。这还需要在以后的生活中多加练习。

在数据清洗环节,我学习了对于缺失数据如何进行填充,若缺失较多,可以利用RF进行回归分析;若缺失较少,则利用特征类似的样本进行填充即可;还学习了对于异常点的处理:对异常点可以进行一定的修正使得预测结果更为理想。

在建模和优化环节,我学习了完整的一套建模流程:选择模型,建pipeline,给超参数赋值,利用GridSearch选择最优超参数,利用fit训练模型,利用cv进行交叉验证,最后利用Predict进行预测。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章