最近数据挖掘老师给了我们这个案例,让我们做预测,不过我得到的数据集合kaggle上的数据集不太一样,如列名,上船地点的表示方式,如果要照着做的话还请自行调整,数据集和ipynb在最后的github仓库链接中。
现在这个项目差不多结束了,就来把我从数据挖掘什么都不懂到现在跑通整个流程的过程总结下,若能帮到各位最好不过。
目录
1.你最好能会
2.案例数据
3.什么是机器学习?
4.数据探索
5.数据预处理
6.初步建模
7.模型状态评估
8.模型优化
9.真正的‘高考’——测试集测试
10.参考
11.项目地址
1. 你最好能会
python基础
pandas基础(为python 的模块,需要安装后使用)
matplotlib基础(为python 的模块,需要安装后使用)
jupyter notebook使用(编写代码的工具)
什么是决策树
2. 案例数据
本例子基于一个泰坦尼克号乘客数据集,预测泰坦尼克号乘客的命运(是否生存),包含了将 近 80% 乘客的信息和生存状态,包含 1,309 个样本,每个样本包含 14 个属性。
3. 什么是机器学习?
通俗地来讲,机器学习就是让机器在很大的数据集中学习和总结规律,然后生成一个模型,当我们给他一部分数据时,他能根据模型推断出其他信息,这就是机器学习。
这么讲可能不太好理解,举个例子:
假设pipi美(左)给pop子(右)1个苹果,那么pop子会返回给她1个菠萝,pipi美(左)给pop子(右)2个苹果,pop子会返回给她2个菠萝.那么问,如果pipi美(左)给pop子(右)3个苹果,那么pop子会返回给她几个菠萝呢?
虽然不敢肯定,但是很大概率会返回3个菠萝。
如果把苹果定为输入,菠萝定为输出,那么有方程y=x+0
现在我们依据上面两个数据,得到了这个线性模型,我们也对数据做了预测(如果给3个苹果,得到3个菠萝),那么究竟对不对呢,不知道,只能说有一定概率正确,而且两个数据太少,得到的规律很不稳定,如果有很多数据都符合这个模型,那我们这个模型才说是好模型。
但是这是个很简单的情况,当我们的点出现这样的情况时,用普通的线性模型y=wx+b显然不能准确描述这些点,那么模型就要变得复杂了。
(这样用函数来描述很多点的行为称为拟合)
大多数时候,拟合不可能完全正确,可能有的点就是在外面,我们的工作就是找到适合的函数,去贴合这些数据点:
这时候人力就难以企及了,需要机器的帮助。近年来互联网数据大爆炸,数据的丰富度和覆盖面远远超出人工可以观察和总结的范畴,而机器学习的算法能指引计算机在海量数据中,挖掘出有用的价值,也使得无数学习者为之着迷。
3.1 机器学习能解决什么呢
-
分类问题
根据数据样本上抽取出的特征,判定其属于有限个类别中的哪一个。比如:
垃圾邮件识别(结果类别:1、垃圾邮件 2、正常邮件)
文本情感褒贬分析(结果类别:1、褒 2、贬)
图像内容识别识别(结果类别:1、喵星人 2、汪星人 3、人类 4、草泥马 5、都不是)。
而我们现在看这个项目就是个分类的问题,最后我们通过把现有的数据输入模型,得到是否存活的分类:0非存活,1存活。
回归问题
根据数据样本上抽取出的特征,预测一个连续值的结果。比如:
波士顿2个月后的房价聚类问题
根据数据样本上抽取出的特征,让样本抱抱团(相近/相关的样本在一团内)。比如:
google的新闻分类
用户群体划分
而分类与回归问题需要用已知结果的数据(根据之前的苹果菠萝兑换比例)做训练,属于监督学习
聚类的问题不需要已知标签,属于非监督学习。
3.2 机器学习的算法
绝大多数问题用典型机器学习的算法都能解决,粗略地列举一下这些方法如下:
处理分类问题的常用算法包括:
- 逻辑回归lr
- 支持向量机svm
- 随机森林
- 朴素贝叶斯
- 深度神经网络(视频、图片、语音等多媒体数据中使用)。
处理回归问题的常用算法包括:
- 线性回归
- 普通最小二乘回归
- 逐步回归多元自适应回归样条
处理聚类问题的常用算法包括
- K均值(K-means),基于密度聚类,LDA等等。
降维的常用算法包括:
- 主成分分析(PCA)
- 奇异值分解(SVD) 等。
推荐系统的常用算法:协同过滤算法
模型融合(model ensemble)和提升(boosting)的算法包括:
bagging,adaboost,GBDT,GBRT
其他很重要的算法包括:EM算法等等。
不用担心现在完全不懂,只要知道有这么些神奇的算法即可。这个案例我们用到的是随机森林算法。
3.3 训练集和测试集
可能我们在看别人的博客时,经常会看见这两个词汇,这是什么呢?简单来说,刚才我们得到的一个苹果换一个菠萝,两个苹果换两个菠萝就是训练集,用来训练模型。
而我们用测试集去测试这个模型好不好,假设测试集中输入三个苹果正确的答案就是三个菠萝,而我们计算机猜出来就是三个,那就对了,是个好模型。
因此我们用训练集训练模型,测试集评估模型的好坏。
下面对整个数据集划分成训练集和测试集,如果你已经有了分开的两个csv文件,可以略过这部分:
import pandas as pd
data_all=pd.read_csv('titanic3.csv')
# 随机抽取891条数据为训练集,然后从整个数据集中删除训练集,就获得了测试集
df_train=data_all.sample(n=891)
drop_index=[]
for indexs in df_train.index:
drop_index.append(indexs)
df_test=data_all.drop(drop_index,axis=0)
#输出两个DataFrame到csv
df_train.to_csv('data_train.csv')
df_test.to_csv('data_test.csv')
print('data_all: ',data_all.shape)
print('df_train: ',df_train.shape)
print('df_test: ',df_test.shape)
# out:数据尺寸
# data_all: (1309, 14)
# df_train: (891, 14)
# df_test: (418, 14)
4. 数据探索
拿到数据第一脸肯定是懵逼的,这么多数据让人头大,别怕,我们来捋一捋。
首先这是1309条数据,第一行是属性名称,啥意思呢,前面那个图解释了,包括了
船舱等级
,是否存活
,名字
,性别
,年龄
,船上兄弟姐妹数量
,船上父母数量
等等。
人对直接的数据时头晕的,但是对图表就容易理解了。利用pandas读取训练集csv,并用matplotlab把现在的数据情况大体展示:
首先导入库:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
4.1 读取data_train并查看NaN值(缺失值)
data_train=pd.read_csv('data_train.csv')
data_train.info()
可以看到
age
和cabin
的缺失值较多,boat
和body
,home.dest
缺失也很多,但是感觉和survived
关系不大,我们是要根据这些已知信息推断出是否生存的,既然关系不大,那后面可以考虑直接排除这几个属性,不作参考依据。4.2 初步分析
看下乘客的属性分布情况:
4.2.1 获救与不获救
data_train.survived.value_counts().plot(kind='bar')
plt.title('获救人数(1为获救)',fontname='SimHei')
plt.ylabel('人数',fontname='SimHei')
plt.grid(True,axis='y')
plt.show()
未获救的人数还是比获救的人少呀。
4.2.2 乘客仓位分布
data_train.pclass.value_counts().plot(kind='bar')
plt.title('乘客仓位分布',fontname='SimHei')
plt.ylabel('人数',fontname='SimHei')
plt.grid(True,axis='y')
plt.show()
三等舱的人比较多,也正常。
4.2.3 乘客性别
data_train.sex.value_counts().plot(kind='bar')
plt.title('乘客性别',fontname='SimHei')
plt.ylabel('人数',fontname='SimHei')
plt.grid(True,axis='y')
plt.show()
男性人数要远大于女性人数。
4.2.4 乘客上船地点分布
data_train.embarked.value_counts().plot(kind='bar')
plt.title('乘客上船地点分布',fontname='SimHei')
plt.ylabel('人数',fontname='SimHei')
plt.grid(True,axis='y')
plt.show()
s港的人数远大于另外两个港口,这个差异性有点大,后面看看和是否存活有关系。
4.2.5 乘客年龄和船舱等级分布
data_train.age[data_train.pclass=='1st'].plot(kind='kde')
data_train.age[data_train.pclass=='2nd'].plot(kind='kde')
data_train.age[data_train.pclass=='3rd'].plot(kind='kde')
plt.legend(('1st','2nd','3rd'))
plt.grid(True)
plt.title('乘客年龄和船舱等级分布',fontname='SimHei')
plt.ylabel('人数密度',fontname='SimHei')
plt.show()
这是仓位人数密度的分布,三等舱的多为20多岁的年轻人,一等舱多为40岁的人。
4.2.6 分析
现在我们对整个数据有了大致了解,那么什么因素会影响生存和死亡呢?
- 不同仓位的人因为地理位置或者财富权势,可能会影响生存
- 性别会影响生存几率(女人、小孩、老人虽然生理弱,但是救生艇机会优先让给他们)
- 上船地点是否会有影响呢?
4.3 下面看属性和获救之间的关系
比如你说男人比较强壮,存活率比女性大,是不是这样呢?用数据说话:
s=data_train.sex[data_train.survived== 1].value_counts()
ds=data_train.sex[data_train.survived== 0].value_counts()
a_s=pd.DataFrame({u'saved':s,u'dontsaved':ds})
a_s.plot.bar(stacked=True)
plt.xlabel('性别',fontname='SimHei')
plt.ylabel('人数',fontname='SimHei')
plt.title('性别和获救人数的分布',fontname='SimHei')
plt.grid(True,axis='y')
plt.show()
可以看到,虽然男性人数多于女性,但是男性的存活率却远低于女性,这可能是因为在逃生时,女士有优先逃生的机会,因此性别无疑是个影响是否获救的重要因素。
pclass船舱等级是否和获救有关呢?
s=data_train.pclass[data_train.survived== 1].value_counts()
ds=data_train.pclass[data_train.survived== 0].value_counts()
p_s=pd.DataFrame({u'saved':s,u'dontsaved':ds})
p_s.plot.bar(stacked=True)
plt.xlabel('客舱等级',fontname='SimHei')
plt.ylabel('人数',fontname='SimHei')
plt.title('客舱等级和获救人数的分布',fontname='SimHei')
plt.grid(True,axis='y')
plt.show()
三级舱的人死亡人数明显多于另外两个等级,这可能和不同仓位所处位置不同,离救生艇远近不同,因此客舱等级和生存相关性大。
上船地点
s=data_train.embarked[data_train.survived== 1].value_counts()
ds=data_train.embarked[data_train.survived== 0].value_counts()
e_s=pd.DataFrame({u'saved':s,u'dontsaved':ds})
e_s.plot.bar(stacked=True)
plt.xlabel('上船地点',fontname='SimHei')
plt.ylabel('人数',fontname='SimHei')
plt.title('上船地点和获救人数的分布',fontname='SimHei')
plt.grid(True,axis='y')
plt.show()
s港死亡率略大点,后面分析的时候可以试着加入该特征。
cabin
has_cabin=data_train.survived[pd.notnull(data_train.cabin)].value_counts()
donthas_cabin=data_train.survived[pd.isnull(data_train.cabin)].value_counts()
c_s=pd.DataFrame({u'has':has_cabin,u'donthave':donthas_cabin})
c_s.plot.bar(stacked=True)
plt.xlabel('有无cabin',fontname='SimHei')
plt.ylabel('人数',fontname='SimHei')
plt.title('有无cabin和获救人数的分布',fontname='SimHei')
plt.grid(True,axis='y')
plt.show()
cabin的缺失值太多了,这里由有无cabin划分,观察下来没有cabin的死亡率高于有cabin记录的。可能可以作为特征属性。
5. 数据预处理
数值化
由于模型大多只接受数值型数据,所以现在要对数据做数值化处理,比如pclass的1st,2nd,3rd要转换成1,2,3,sex转成1和0表示男女。
def process_data(df):
# 处理pclass
p_index=df['pclass'].unique().tolist()
df['pclass']=df['pclass'].apply(lambda x: p_index.index(x))
# 处理embarked
e_index=df['embarked'].unique().tolist()
df['embarked']=df['embarked'].apply(lambda x: e_index.index(x))
# 处理sex
df.loc[df['sex']=='male','sex']=1
df.loc[df['sex']=='female','sex']=0
return df
同时在函数中加上丢弃我们觉得无关的属性:
# 丢弃无用列
df=df.drop(['Unnamed: 0','boat','home.dest','ticket','body','name','fare'],axis=1)
把data_train放入函数中处理后,data_train看起来像这样:
嗯看上去很不错,但是age和cabin有NaN值,这是无法输入模型的,所以接下来需要进行数据清洗中的空值处理,当然你先去除空值再数值化也是没问题的。
去除空值
这里我们只关注两个属性,因为这两个属性和生存相关性比较大,age和cabin。因此如何处理这两个属性的缺失值有讲究。
对于age,首先我们采用的是0填充:
df.loc[df.age.isnull(),'age']=0
同时将其离散化,将连续的数值划分为指定区间的值,方便后面模型分类:
df.loc[df['age']<10,'age']=0
df.loc[(df['age']>=10)&(df['age']<20),'age']=1
df.loc[(df['age']>=20)&(df['age']<30),'age']=2
df.loc[(df['age']>=30)&(df['age']<40),'age']=3
df.loc[(df['age']>=40)&(df['age']<50),'age']=4
df.loc[(df['age']>=50)&(df['age']<60),'age']=5
df.loc[df['age']>=60,'age']=6
对于cabin,由于之前做数据观察时发现,有cabin记录的存活率要比没有记录的高,因此这里新增一列has_cabin,有记录则填充1,没有填充0:
# 处理cabin
df['has_cabin']=df['cabin'].apply(lambda x: 0 if pd.isnull(x) else 1)
我们分析、组合、拆分原来的属性而产生的新的属性,称为衍生变量。
衍生变量
我们对sibsp和parch做合并处理,新增一个family字段,为他们两的和:
# 合并sibsp和parch 到family
df['family']=df['sibsp']+df['parch']
最后我们的data_trian处理后像是这样:
其中我们需要的
pclass
,survived
,sex
,age
,embarked
,has_cabin
,family
都已经数值化和离散化了,看上去很不错,数据规整了很多。接下来就是丢到模型里去训练,先做出个初步模型看看。
6. 初步建模
这里我们选择随机森林算法建立机器学习模型,因为是分类的问题(对是否生存给出是或否的答案),所以用到sklearn.ensemble的RandomForestClassifier
RandomForestClassifier官方文档
RandomForestClassifier的中文介绍
建模整体代码:
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
sur_df=data_train[['pclass','survived','sex','age','embarked','has_cabin','family']]
X=sur_df.drop('survived',axis=1).values
y=sur_df['survived'].values
# 划分验证集(0.2)和训练集(0.8)
X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.2)
rfc_sur=RandomForestClassifier(n_estimators=100)
rfc_sur.fit(X_train,y_train)
print("train score:", rfc_sur.score(X_train, y_train))
print("test score:", rfc_sur.score(X_test, y_test))
解释
导入模块:
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
首先筛选出我们需要的属性:
sur_df=data_train[['pclass','survived','sex','age','embarked','has_cabin','family']]
接下来把数据拆分为无标签数据和有标签数据,X意为除去标签后的数据,y就是标签。这里标签就是survived
列。:
X=sur_df.drop('survived',axis=1).values
y=sur_df['survived'].values
划分验证数据集
由于我们需要知道训练出的模型是好是坏,所以从训练集里再单独划分出两个数据集,一个用于训练,一个用于验证模型好坏。
给如X,y后,将其分为X_train和y_train,X_test和y_test。
X_train意为除去标签后的训练数据,y_train就是训练集的标签。同样,X_test是验证集的除去标签后的数据,y_test是验证集的标签。
# 划分验证集(0.2)和训练集(0.8)
X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.2)
创建模型
然后是创建模型,参数这里给了n_estimators=100就是随机森林里决策树的数量是100,当决策树产生自己的结果后,对所有结果进行投票,多的那个就是最后结果。比如给出一个人的信息,60棵树给出生存结果,40棵给出死亡结果,那随机森林最后会给出生存的结果。
rfc_sur=RandomForestClassifier(n_estimators=100)
产生了模型接下来就是训练,把X_train,y_train丢到fit方法里,用于训练模型。
rfc_sur.fit(X_train,y_train)
成绩
用rfc.score(X,y)方法会把预测出的数据和标签数据(正确答案)做统计,自动给出得分:
print("train score:", rfc_sur.score(X_train, y_train))
print("test score:", rfc_sur.score(X_test, y_test))
目前训练成绩是0.86,测试成绩0.78。训练成绩比较高,这也是正常的,相当于考试考的都是平时做过的题目,成绩自然比做全新题目高一点。
7. 模型状态评估
学习曲线
这里使用学习曲线来观察数据在不同数据量时预测成绩的表现,代码在官方文档里已经给出,这里也贴出来:
from sklearn.model_selection import learning_curve
#绘制学习曲线,以确定模型的状况
def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None,
train_sizes=np.linspace(.1, 1.0, 5)):
"""
画出data在某模型上的learning curve.
参数解释
----------
estimator : 你用的分类器。
title : 表格的标题。
X : 输入的feature,numpy类型
y : 输入的target vector
ylim : tuple格式的(ymin, ymax), 设定图像中纵坐标的最低点和最高点
cv : 做cross-validation的时候,数据分成的份数,其中一份作为cv集,其余n-1份作为training(默认为3份)
"""
plt.figure()
train_sizes, train_scores, test_scores = learning_curve(
estimator, X, y, cv=5, n_jobs=1, train_sizes=train_sizes)
train_scores_mean = np.mean(train_scores, axis=1)
train_scores_std = np.std(train_scores, axis=1)
test_scores_mean = np.mean(test_scores, axis=1)
test_scores_std = np.std(test_scores, axis=1)
plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
train_scores_mean + train_scores_std, alpha=0.1,
color="r")
plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
test_scores_mean + test_scores_std, alpha=0.1, color="g")
plt.plot(train_sizes, train_scores_mean, 'o-', color="r",
label="Training score")
plt.plot(train_sizes, test_scores_mean, 'o-', color="g",
label="Cross-validation score")
plt.xlabel("Training examples")
plt.ylabel("Score")
plt.legend(loc="best")
plt.grid("on")
if ylim:
plt.ylim(ylim)
plt.title(title)
plt.show()
使用方法:
plot_learning_curve(rfc_sur,'learning curve',X,y,ylim=(0.6,1.01))
- rfc_sur为你的模型,未训练过的也可以,可以直接放
RandomForestClassifier(n_estimators=100)
- 第二个是你图表的标题
- X,y为拆分过后的训练集,看上文。
-
ylim为纵坐标的范围,用元组表示
可以看出,有点过拟合了,即在训练集上表现良好,但是验证集上一般般,两个曲线最后没有很好地收敛。
交叉验证
首先是我们能想到的简单交叉验证
从这个图可以看到,简单交叉验证就是每次在训练集中随机划出的训练集和验证集,验证集评估出来的就是模型成绩。
因此验证集的划分对模型成绩影响很大,每次划分的验证集不同,成绩不同,成绩的变化幅度会比较大,这就导致我们没法正确评估这个模型好不好。比如一个学生一次考99,一次考60,那这个学生到底是好学生还是差学生呢?一次考试显然是不够的证明的。
这里介绍下k折交叉验证方法
将原始数据分成K组(一般是均分),将每个子集数据分别做一次验证集,其余的K-1组子集数据作为训练集,这样会得到K个模型,用这K个模型最终的验证集的分类准确率的平均数作为此K-CV下分类器的性能指标。K一般大于等于2,实际操作时一般从3开始取,只有在原始数据集合数据量小的时候才会尝试取2。
K-CV可以有效的避免过拟合与欠拟合的发生,最后得到的结果也比较具有说服性。
代码,这里用了5折交叉验证(cv=5):
from sklearn.model_selection import cross_val_score
def cross_score(X,y,mod):
#简单看看打分情况
scores=cross_val_score(mod,X,y,cv=5)
print('交叉验证scores:',scores)
print('交叉验证mean:',scores.mean())
cross_score(X,y,rfc_sur)
细心的同学可能发现了,这和模型用简单验证得到的结果差不多呀,怎么回事?这是因为我们选用的随机森林算法在投票时已经降低了答案偏差的可能性,因此两个验证效果不会差很多。
结论
模型有点过拟合了。
8. 模型优化
首先是特征的优化。
age不再用0填充,而是用平均值上下方差范围内的随机值来填充更加有说服力。
而分组,考虑到小孩和老人会优先照顾,而年轻人和中年人区别不大,因此只分成三类,0-15岁为小孩,15-50岁,50岁以上为老人:
# 对age填充
age_mean=df['age'].mean()
age_std=df['age'].std()
age_nan_count=df.age.isnull().sum()
age_nan_list=np.random.randint(age_mean-age_std,age_mean+age_std,size=age_nan_count)
df.age[pd.isnull(df['age'])]=age_nan_list
df.age=df.age.astype('int')
# 分组
df.loc[df['age']<=15,'age']=0
df.loc[(df['age']>15)&(df['age']<50),'age']=1
df.loc[df['age']>=50,'age']=2
模型优化我们使用了超参数优化器:网格搜索交叉验证器GridSearchCV
这东西,说白了,就是对于你给出的参数列表,一个个去训练,然后交叉验证得分,最后给你个得分最高的模型,那么相对来讲这个模型就是最好的模型了。其中的超参数就是我们人为提前设定的参数,是相对模型在训练过程中自我产生的参数而言的。
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split,GridSearchCV
# 你选出的特征
sur_df=data_train[['pclass','survived','sex','age','has_cabin','family']]
X=sur_df.drop('survived',axis=1).values
y=sur_df['survived'].values
# 你的参数列表
params={'n_estimators': [6,9,20],
'max_features': ['log2', 'sqrt','auto'],
'max_depth': [2, 3, 5, 10],
'min_samples_split': [2, 3, 5],
'min_samples_leaf': [1, 5, 8]}
grid_cv=GridSearchCV(RandomForestClassifier(),params,cv=5)
grid_cv=grid_cv.fit(X,y) #训练
print('best score: {}\nbest params:{}'.format(grid_cv.best_score_,grid_cv.best_params_))
看到最后交叉验证的最好成绩到达了0.82
rfc_best=grid_cv.best_estimator_
plot_learning_curve(rfc_best,'learning curve',X,y,ylim=(0.6,1.01))
这是比较好的学习曲线,最后收敛。
9.真正的‘高考’——测试集测试
df_test=pd.read_csv('data_test.csv')
df_test=process_data(df_test)
sur_df=df_test[['pclass','survived','sex','age','has_cabin','family']]
X=sur_df.drop('survived',axis=1).values
y=sur_df['survived'].values
print('test score: ',rfc_best.score(X,y))
成绩在0.78,也就是说,你给我一个人的信息,我的判断78%的可能性是正确的,这无疑是比较理想的状态。
10. 参考
- 交叉验证
- Kaggle入门级赛题:泰坦尼克号生还者预测——数据挖掘篇
- 机器学习系列(3)_逻辑回归应用之Kaggle泰坦尼克之灾
- 机器学习系列(7)_机器学习路线图(附资料)
- 机器学习实战(三)——决策树
- 【机器学习】 随机森林(Random Forest)
11. 项目地址
https://github.com/chajiuqqq/kaggle_Titanic
联系我:chajiuqqq@outlook.com