目 录
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划分时考虑的最大特征数为。
注意到我们使用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进行预测。