泰坦尼克号数据介绍与分析
数据介绍
作为当前机器学习最出名的几个入门kaggle项目,背景就无需多做介绍了。数据出处如下:
https://www.kaggle.com/c/titanic/data
数据中相关数据标签含义如下:
从这里,我们大致了解了关于这份数据的存在形式,于是我们便可以进行python的使用与分析了,在此之前,导入基本我们需要使用的第三方库与数据:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
train=pd.read_csv(r'E:\python\泰坦尼克号\train.csv')
test=pd.read_csv(r'E:\python\泰坦尼克号\test.csv')
数据初步认识
当我们导入数据后,需要对一些维度进行简单统计或者可视化,进而来加深对于数据的认识。只有对数据的认识越深,那么后面的特征工程的构建也就越轻松。
1.首先观察数据的基本展现形式:
train.head()
test.head()
test文件与train文件相比,就是少了’Survived’列,其它结构一样。而‘Survived’作为训练数据的标签。
2.观察train和test的数据基本信息
train.info()
'''
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
PassengerId 891 non-null int64
Survived 891 non-null int64
Pclass 891 non-null int64
Name 891 non-null object
Sex 891 non-null object
Age 714 non-null float64
SibSp 891 non-null int64
Parch 891 non-null int64
Ticket 891 non-null object
Fare 891 non-null float64
Cabin 204 non-null object
Embarked 889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.6+ KB
'''
test.info()
'''
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 418 entries, 0 to 417
Data columns (total 11 columns):
PassengerId 418 non-null int64
Pclass 418 non-null int64
Name 418 non-null object
Sex 418 non-null object
Age 332 non-null float64
SibSp 418 non-null int64
Parch 418 non-null int64
Ticket 418 non-null object
Fare 417 non-null float64
Cabin 91 non-null object
Embarked 418 non-null object
dtypes: float64(2), int64(4), object(5)
memory usage: 36.0+ KB
'''
看到了数据的变量类型,我们可以针对每个特征的数据结构给它们归一归类,这也有益于我们加深理解数据,除了使后面的计算更方便,更能帮助我们选择合适的图形做可视化,我们可以进一步进行如下分析:
1.离散型特征: 离散型可以理解为一系列相似的样本,具有同样的数值或者名词,在特定范围有规律性的表示,在这个问题中,离散型的变量有:Survived,Sex 和 Embarked。基于序列的有:Pclass
2.连续型特征: 连续型可以说是和离散型相反,呈现的是表面上看起来没有什么规律,没有特定范围,随着样本变化而变化,在这个问题中,连续型的数值特征有:Age,Fare。离散型数值有:SibSp,Parch
3.混合型特征: 在样本中,既不是明确的英文单词,也不是单纯的数值或字母,而是将其混合起来,形成了一种特定的代号,这种往往我们都是需要注意用正则或者用一些我们习惯的方式去处理与修正它。在这个问题中,Ticket是混合了数值型以及字母数值型的数据类型,Cabin是字母数值型数据
4.缺失、异常型特征: 数据样本有缺失值和异常值,这是很常见的问题,这个问题也是后面需要详细说明的。在这个问题中,Ticket是混合了数值型以及字母数值型的数据类型,Cabin是字母数值型数据
3.然后我们可以查看训练数据的具体分布情况:
train.describe(percentiles=[.1,.2,.3,.4,.5,.6,.7,.8,.9,.99])
我们可以从上表知道:
1.死亡率在60%~70%之间,因为Survice这一项从0变成了1
2.船上的父母以及子女数算比较少的,80%左右的人都算是单身或者说和女伴一起,这或许也是后来能让妇女和儿童先逃的一个因素,还是挺佩服他们的
3.船上贫富差距大,可以从Fare一项看出,虽然从Ticket一起看,会发现有团体票一说,但还是有差距的。
缺失、异常值处理
由于train和test具有相同的数据结构,我们把它们放在一起进行数据的预处理工作。
y=train['Survived']
df_train=train.drop(['Survived'],axis=1)
#合并时添加一个特征以便于后续分割
data=pd.concat([df_train.assign(is_train=1),test.assign(is_train=0)],axis=0)
data=data.reset_index(drop=True)
了解缺失值的情况:
data.isnull().sum()
'''
PassengerId 0
Pclass 0
Name 0
Sex 0
Age 263
SibSp 0
Parch 0
Ticket 0
Fare 1
Cabin 1014
Embarked 2
is_train 0
dtype: int64
'''
总共有 1309 个游客的数据,263 个 Age 缺失,1个Fare缺失,1014 个 Cabin 缺失,2 个 Embarked 缺失。在后面我们需要用不同的方法补充这些数据。
然后,我们查看特征类别分布是否平衡。类别平衡指分类样例不同类别的训练样例数目差别不大。当差别很大时,为类别不平衡。当类别不平衡的时候,例如正反比为 9:1,学习器将所有样本判别为正例的正确率都能达到 0.9。这时候,我们就需要使用 “再缩放”、“欠采样”、“过采样”、“阈值移动” 等方法。我们可以对Survice做图验证:
sns.countplot(x='Survived',data=train) #对不同值的 'Survived' 进行计数并绘图
这个结果说明样本比例还算均衡,也同样应征了上表中说明的幸存者在30%~40%之间,而这组数据本身就少,差距变化不大,我们可以认为是属于类别平衡问题。
接下来,删除无用的特征 PassengerId, Name。
data.drop(['PassengerId','Name'],axis=1,inplace=True)
data.columns
'''
Index(['Pclass', 'Sex', 'Age', 'SibSp', 'Parch', 'Ticket', 'Fare', 'Cabin',
'Embarked', 'is_train'],
dtype='object')
'''
下面对训练数据数值特征做一个相关系数矩阵图,从理论上来讲,相关系数越大,那么相关性就越强,图中的颜色越深。
g=sns.heatmap(train[['Survived','SibSp','Parch','Age','Fare','Pclass']].corr(),cmap='RdYlGn',annot=True)
从图中可以看出Age与Pclass,SibSp,Parch相关性比较大,后面缺失值处理会用到。
缺失值处理
缺失值有很多种处理方式,从直接剔除,到取平均值、中位数、众数,到线性回归,拉格朗日插值、三次样条插值、极大似然估计、KNN等等,这些都需要视具体情况具体分析。
我们首先观察年龄对存活率的变化情况:
Age0=train[(train['Survived']==0)&(train['Age'].notnull())]['Age']
Age1=train[(train['Survived']==1)&(train['Age'].notnull())]['Age']
g=sns.kdeplot(Age0,legend=True,shade=True,color='r',label='NotSurvived')
g=sns.kdeplot(Age1,legend=True,shade=True,color='b',label='Survived')
在前面,我们根据 heatmap构造的相关系数图中,Age 和 SibSp, Parch,Pclass 相关性高,我们再用箱型图直观感受下,以图形 Sex ~ Age,Pclass ~ Age 为例。
g=sns.catplot(x='Sex',y='Age',data=train,kind='box')
g=sns.catplot(x='Pclass',y='Age',data=train,kind='box')
上面两图说明男性和女性的年龄分布(指箱型图中的五条线,从上到下依次是最大值、四分位数、中位数、四分位数、最小值)基本一致,而购买不同等级票的人的年龄分布是不同的。所以,我们根据票的等级将数据分为不同的集合,再用缺失数据所在集合的平均值来进行填充,并检查填充后 Age ~ Survived 是否受到影响。
index=list(train[train['Age'].isnull()].index)
Age_mean=np.mean(train[train['Age'].notnull()]['Age'])
copy_data=train.copy()
for i in index:
filling_age=np.mean(copy_data[(copy_data['Pclass']==copy_data.iloc[i]['Pclass'])
&(copy_data['SibSp']==copy_data.iloc[i]['SibSp'])
&(copy_data['Parch']==copy_data.iloc[i]['Parch'])]['Age'])
if not np.isnan(filling_age):
train['Age'].iloc[i]=filling_age
else:
train['Age'].iloc[i]=Age_mean
Age0=train[train['Survived']==0]['Age']
Age1=train[train['Survived']==1]['Age']
g=sns.kdeplot(Age0,legend=True,shade=True,color='r',label='NotSurvived')
g=sns.kdeplot(Age1,legend=True,shade=True,color='b',label='Survived')
结果是基本与原数据一致,那么初步来看,是可以这样做的。
下面将合并数据集的Age进行缺失值填补:
index=list(data[data['Age'].isnull()].index)
Age_mean=np.mean(data[data['Age'].notnull()]['Age'])
copy_data=data.copy()
for i in index:
filling_age=np.mean(copy_data[(copy_data['Pclass']==copy_data.iloc[i]['Pclass'])
&(copy_data['SibSp']==copy_data.iloc[i]['SibSp'])
&(copy_data['Parch']==copy_data.iloc[i]['Parch'])]['Age'])
if not np.isnan(filling_age):
data['Age'].iloc[i]=filling_age
else:
data['Age'].iloc[i]=Age_mean
然后我们可以来处理关于cabin这个缺失项,这个不同于age,cabin可以看做是离散的缺失值,我们可以分析一下这个样本:
data[data['Cabin'].notnull()]['Cabin'].head(10)
'''
1 C85
3 C123
6 E46
10 G6
11 C103
21 D56
23 A6
27 C23 C25 C27
31 B78
52 D33
Name: Cabin, dtype: object
'''
然后我们可以将缺失值取做新变量名,比如说“U”:
# fillna() 填充 null 值
train['Cabin'].fillna('U',inplace=True)
# 使用 lambda 表达式定义匿名函数对 i 执行 list(i)[0]。map() 指对指定序列 data ['Cabin'] 进行映射,对每个元素执行 lambda
train['Cabin']=train['Cabin'].map(lambda i: list(i)[0])
# kind='bar' 绘制条形图,ci=False 不绘制概率曲线,order 设置横坐标次序
g = sns.factorplot(x='Cabin',y='Survived',data=train,ci=False,kind='bar',order=['A','B','C','D','E','F','G','U'])
这样看上去就比较合理了。
对合并数据集进行处理:
data['Cabin'].fillna('U',inplace=True)
data['Cabin']=data['Cabin'].map(lambda i:list(i)[0])
另外关于Embarked特征主要有三个值,分别为S,Q,C,对应了三个登船港口,缺失值用众数填充。Fare一个缺失值用平均值填充。
data['Embarked'].fillna(data['Embarked'].mode()[0],inplace=True)
data['Fare'].fillna(data['Fare'].mean(),inplace=True)
至此,缺失值处理完成。
异常值处理:
对于异常值的处理,方法也是多样的,可以设置一个初始阈值,超过那么就算做是异常,这也并没有什么问题,还有就是分布问题,同样会产生异常。有个经常用的离群值的处理原则:sigma原则,即3σ 原则。
而另一种就是关于数据分布问题了,比如说这里的Fare,我们可以看到它的分布情况:
g=sns.kdeplot(train[train['Survived']==0]['Fare'],shade=True,label='NotSurvived',color='r')
g=sns.kdeplot(train[train['Survived']==1]['Fare'],shade=True,label='Survived',color='b')
可以看到Fare存在明显的集中情况,但并不是标准正态分布,可以看做是偏态分布,或者说叫长尾分布。
Fare 属于右偏态分布,Python 提供了计算数据偏态系数的函数 skew(), 计算值越大,数据偏态越明显。使用 Log Transformation 后,我们看到计算值从 4.79 降到 0.44。
train['Fare']=train['Fare'].map(lambda i:np.log(i) if i>0 else 0)
g=sns.distplot(train['Fare'])
print('Skew Coefficient:%.2f' %(train['Fare'].skew())) # skew() 计算偏态系数
Skew Coefficient:0.44
对合并数据进行处理:
data['Fare']=data['Fare'].map(lambda i:np.log(i) if i>0 else 0)
对于性别,我们可以对其重新进行01编码,因为只有男性和女性:
data['Sex'].replace('male',0,inplace=True) #inplace=True 原位替换
data['Sex'].replace('female',1,inplace=True)
然后我们可以从上面的箱型图观察,其实还是有异常值的,可以考虑按照sigma原则排除,这里还是按照箱型图的定义来,针对于 [‘Age’, ‘Parch’, ‘SibSp’, ‘Fare’] :
from collections import Counter
def outlier_detect(n,df,features):
outlier_index=[]
for feature in features:
Q1=np.percentile(df[feature],25)
Q3=np.percentile(df[feature],75)
IQR=Q3-Q1
outlier_span=1.5*IQR
col=((df[df[feature]>Q3+outlier_span])|(df[df[feature]<Q1-outlier_span])).index
outlier_index.extend(col)
print('%s:%f (Q3+1.5*IQR),%f (Q1-1.5*IQR)'%(feature,Q3+1.5*IQR,Q1-1.5*IQR))
outlier_index=Counter(outlier_index)
outlier=list(i for i,j in outlier_index.items() if j>=n)
print('number of outliers:%d'%len(outlier))
print(df[features].loc[outlier])
return outlier
outlier=outlier_detect(3,train,['Age','Parch','SibSp','Fare'])
'''
离群值有四个:
Age:59.500000 (Q3+1.5*IQR),-0.500000 (Q1-1.5*IQR)
Parch:0.000000 (Q3+1.5*IQR),0.000000 (Q1-1.5*IQR)
SibSp:2.500000 (Q3+1.5*IQR),-1.500000 (Q1-1.5*IQR)
Fare:5.482703 (Q3+1.5*IQR),0.019461 (Q1-1.5*IQR)
number of outliers:4
Age Parch SibSp Fare
27 19.0 2 3 5.572154
88 23.0 2 3 5.572154
341 24.0 2 3 5.572154
438 64.0 4 1 5.572154
'''
然后删除上面四个值就好了:
data.drop(outlier,axis=0,inplace=True)
y.drop(outlier,axis=0,inplace=True)
下面对Embarked进行数值转换:
data['Embarked']=data['Embarked'].map(lambda i:i.strip())
data['Embarked'].replace({'S':0,'C':1,'Q':2},inplace=True)
对Cabin特征列进行OneHot编码:
data['Cabin']=data['Cabin'].map(lambda i:i.strip())
data=pd.get_dummies(data,columns=['Cabin'],prefix='C')
Ticket 特征值中的一串数字编号对我们没有意义,下面代码中,我们用正则表达式过滤掉这串数字,并使用 pandas的get_dummies 函数进行数值化(以 Ticket 特征值 作为新的特征,0,1 作为新的特征值),类似于onehot编码性质的方法。
Ticket=[]
import re
r=re.compile(r'\w*')#正则表达式,查找所有单词字符[a-z/A-Z/0-9]
for i in train['Ticket']:
sp=i.split(' ')#拆分空格前后字符串,返回列表
if len(sp)==1:
Ticket.append('U')#对于只有一串数字的 Ticket,Ticket 增加字符 'U'
else:
t=r.findall(sp[0])#查找所有单词字符,忽略符号,返回列表
Ticket.append(''.join(t))#将 t 中所有字符串合并
data['Ticket']=Ticket
data=pd.get_dummies(data,columns=['Ticket'],prefix='T')#get_dummies:如果DataFrame的某一列中含有k个不同的值,则可以派生出一个k列矩阵或DataFrame(其值全为1和0)
通过按此方法处理,发现Ticket列反映的数据有限,这种处理对结果并没有带来积极的影响。而直接踢出此列特征,结果表现更好:
data.drop(['Ticket'],axis=1,inplace=True)
至此,特征工程已经做完了。
提取出相关的训练数据及测试数据:
train_result=data[data['is_train']==1]
train_result.drop(['is_train'],axis=1,inplace=True)
train_result=train_result.reset_index(drop=True)
test_result=data[data['is_train']==0]
test_result.drop(['is_train'],axis=1,inplace=True)
test_result=test_result.reset_index(drop=True)
模型构建与评价
下面我们用 10 折交叉验证法(k=10)对两种常用的集成学习算法 AdaBoost 以及 Random Forest 进行评估。最后我们看到 Random Forest 比 Adaboost 效果稍好一些。
from sklearn.ensemble import AdaBoostClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score
y = y
X = train_result.values
classifiers = [AdaBoostClassifier(
random_state=2), RandomForestClassifier(random_state=2)]
for clf in classifiers:
score = cross_val_score(clf, X, y, cv=10, scoring='accuracy')#cv=10:10 折交叉验证法,scoring='accuracy':返回测试精度
print([np.mean(score)])#显示测试精度平均值
"""
[0.8084141981613892]
[0.8095122574055159]
"""
过拟合是学习器性能过好,把样本的一些特性当做了数据的一般性质,从而导致训练误差低但泛化误差高。学习曲线是判断过拟合的一种方式,同时可以判断学习器的表现。学习曲线包括训练误差(或精度)随样例数目的变化曲线与测试误差(或精度)随样例数目的变化曲线。
下面将以训练样例数目为横坐标,训练精度和测试精度为纵坐标绘制学习曲线,并分析 Random Forest 算法的性能。
from sklearn.model_selection import learning_curve
import matplotlib.pyplot as plt
# 定义函数 plot_learning_curve 绘制学习曲线。train_sizes 初始化为 array([ 0.1 , 0.325, 0.55 , 0.775, 1\. ]),cv 初始化为 10,以后调用函数时不再输入这两个变量
def plot_learning_curve(estimator, title, X, y, cv=10,
train_sizes=np.linspace(.1, 1.0, 5)):
plt.figure()
plt.title(title) # 设置图的 title
plt.xlabel('Training examples') # 横坐标
plt.ylabel('Score') # 纵坐标
train_sizes, train_scores, test_scores = learning_curve(estimator, X, y, cv=cv,
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.grid() # 设置背景的网格
plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
train_scores_mean + train_scores_std,
alpha=0.1, color='g') # 设置颜色
plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
test_scores_mean + test_scores_std,
alpha=0.1, color='r')
plt.plot(train_sizes, train_scores_mean, 'o-', color='g',
label='traning score') # 绘制训练精度曲线
plt.plot(train_sizes, test_scores_mean, 'o-', color='r',
label='testing score') # 绘制测试精度曲线
plt.legend(loc='best')
return plt
g=plot_learning_curve(RandomForestClassifier(n_estimators=100),'RFC',X,y) # 调用函数 plot_learning_curve 绘制随机森林学习器学习曲线
Random Forest 的学习曲线我们得到了,训练误差始终接近 0,而测试误差始终偏高,说明存在过拟合的问题。这个问题的产生是因为 Random Forest 算法使用决策树作为基学习器,而决策树的一些特性将造成较严重的过拟合。所以我们接下来需要调整随机森林参数,改善这种问题。
随机森林分类器调参
1.调参n_estimators
def para_tune(para,X,y):
clf=RandomForestClassifier(n_estimators=para,random_state=2)
score=np.mean(cross_val_score(clf,X,y,cv=10,scoring='accuracy'))
return score
def accurate_curve(para_range,X,y,title):
score=[]
for para in para_range:
score.append(para_tune(para,X,y))
plt.figure()
plt.title(title)
plt.xlabel('Paramters')
plt.ylabel('Score')
plt.grid()
plt.plot(para_range,score,'o-')
return plt
para_range=[2,5,80,100,150]
g=accurate_curve(para_range,X,y,'n_estimator tuning')
找到最优参数n_estimators=80
2.调参max_depth
def para_tune2(para,X,y):
clf=RandomForestClassifier(n_estimators=80,max_depth=para,random_state=2)
score=np.mean(cross_val_score(clf,X,y,cv=5,scoring='accuracy'))
return score
def accurate_curve2(para_range,X,y,title):
score=[]
for para in para_range:
score.append(para_tune2(para,X,y))
plt.figure()
plt.title(title)
plt.xlabel('Paramters')
plt.ylabel('Score')
plt.grid()
plt.plot(para_range,score,'o-')
return plt
para_range=[2,10,20,30,40]
g=accurate_curve2(para_range,X,y,'max_depth tuning')
最优max_depth=10
3.网格搜索其它参数
from sklearn.model_selection import GridSearchCV
clf=RandomForestClassifier(n_estimators=80,random_state=2)
para_grid={'max_depth':[10],'max_features':[5,10,15],'criterion':['gini','entropy'],
'min_samples_split':[2,5,10],'min_samples_leaf':[1,5,10]}
gs=GridSearchCV(clf,param_grid=para_grid,cv=3,scoring='accuracy')
gs.fit(X,y)
gs_best=gs.best_estimator_
gs.best_score_
'''
调参后最优得分:
0.8286358511837655
'''
g=plot_learning_curve(gs_best,'RFC',X,y)
我们发现,相比于上面,有效地缓解了过拟合。
由于这个项目本身测试数据规模也不够大,另外对于特征工程的构建,也可以继续进行挖掘,来测试下效果。本文只是整理下总体的一个处理流程,至于最终的处理结果,还可以用其它分类算法测试下效果,也可以运用多分类器进行模型的融合,可能会带来意想不到的效果。