这次数据分析的案例是,经典的数据分析案例——泰坦尼克号生还预测。本案例的分析思路包括以下三个部分:
- 数据集描述与来源展示
- 数据分析过程
- 明确分析问题、理解数据
- 数据清洗
- 数据探索性分析
- 数据建模与分析
- 模型选择与结果输出
- 数据分析总结
数据集描述与来源
这次的数据是Kaggle官方提供的Titanic:Machine Learning from Disaster。Titanic : Machine Learning from Disaster kaggle平台官方对这份数据集的定位是机器学习的入门级比赛项目(Get_started),是适合新接触机器学习的数据分析人员进行操作实践并了解机器学习基础知识的一个分析项目。
当然Titanic也是我接触数据分析实践的第一个项目,在项目中我有学习到数据分析的整体思路与框架,也学到了一些数据分析操作,如:对数据集进行清洗,数据特征提取、数据分析建模等。也通过kaggle社区,补充了模型优化与参数调整部分知识。Titanic确实适合入门分析者进行分析实践,本次的案例分析,是我在Kaggle上提交过数据集,并在社区内去找高精确率的分析方法学习后,独立完成的。
Titanic提供了两组数据,分别是“训练集”与“测试集”数据,虽然是称为训练集和测试集数据,但测试集数据其实是需要进行预测的数据集,其中的“生存状况”列数据是确实状态需要补足用于提交分析分析结果。两份数据都包含了10个数据字段。具体字段及其含义如图1。
数据分析过程
数据分析过程包括五部分:明确分析问题理解数据、数据清洗、数据探索性分析、数据建模与分析、模型优化与可视化展示
一、明确分析问题、理解数据
1.1明确分析问题
Titanic的分析问题,其实已经给定了,是一个二分类问题,需要使用机器学习知识搭建一个分类器,通过对数据集的学习,能够基于测试集数据给出生还预测。即,告知一部分的乘客信息(性别、年龄、座位等级、同乘的父母子女与兄弟姊妹等)与生还状态,能否基于对此信息进行学习与建模,用以预测另一批乘客的生还状态。
1.2理解数据
第一步还是先导入数据分析中需要用到的模块
#导入基础模块
再将数据集下载至jupyter 操作的Titanic项目文件夹下,用python打开训练集与测试集数据,为了方便数据清洗步骤操作,现将两份数据归为统一列表中,并输出两个数据集的结构信息,字段信息,粗略浏览数据信息。
#导入数据
test数据集的数据结构为:(418,11)。有418条记录,包含11个数据字段。
训练集的数据结构为:(891,12),891条数据记录,12个数据字段,其中相对于test多出的字段为Survival也就是生还状况,需要预测的数据。
首先需要划分出数据类型
- 离散型的数据有:Pclass、Sex、Embarkded、PassengerId、SibSp、Parch;
- 连续性的数据有:Age、Fare;
- 文字形式信息有:Name、Ticket、Cabin。
根据数据内容信息,进行数据理解
- 我们认为PassengerId为乘客编号其信息量较少不适合作为数据分析的特征,但其编号唯一可将其设置为数据集的Index;
- Pclass、Sex、Embarkded等信息需要构建虚拟变量,可以用one-hot编码构建特征加入模型分析中;
- Age信息代表具体年龄,但年龄对于生还来说,我们认为不同年龄区间的取值对结果的贡献是不一样的而不是具体的年龄数值,因此需要进行离散化;
- SibSp、Parch特征都代表了同行的人员数量,我认为两个特征对于生还的影响可能是不同向的因此暂时不将特征合并看探索性分析的结果,Fare也留到探索性分析再进行相关处理;
- 而Name特征根据仔细观察,名字中含有乘客的头衔,如Master其包含了乘客社会地位信息很可能影响结果预测,因此需要提取出头衔特征;
- Ticket毫无规律无法找到有用的信息可以在数据清洗阶段剔除。
最后对比两个数据集中的缺失值信息。
- train数据集中,Age字段缺失177条记录,Embarked缺失2条记录,Cabin大量缺失数据仅有204条记录。
- test数据集中,Age缺失86条记录,Fare缺失1条记录,而Cabin同样大量缺失数据仅有91条记录。
最后说明下提交文件的要求,提交文件预览结果只有两列,其中第一列为PessengerId,第二列为Survival信息,1或0,int64类型数据。
二、数据清洗
2.1缺失值处理
根据数据理解的信息,需要处理的缺失值为train中的Age、Embarked、Cabin,test中的Age、Fare、Cabin。为了增加容错,此处用test与train的copy集进行清洗。
#缺失值处理
#数据备份并装入清洗列表中
data_cleaner = []
data_test = test.copy()
data_train = train.copy()
data_cleaner.append(data_train)
data_cleaner.append(data_test)
#查看数据集中的数据分布状况,提供填充值选择思路
for data in data_cleaner:
data[['Age']].hist()
print(data[['Age']].mean())
两个数据集中的Age数据都近似呈正态分布,输出的年龄均值分别为29.70,30.27。从以上分布直方图中看一看出包含平均值的年龄区间内样本量最多而两侧样本量则随着与平均值的差距的绝对值的增加,其样本量也在逐渐减少,因此对于Age数据的缺失值,优先用平均值填充。
#填充缺失值
填充后数据集中 Age数据字段分别891条和418条,不存在缺失值。
#同样查看test中的Fare
test[['Fare']].hist()
print(test[['Fare']].mean())
Fare的数据分布左倾,主要因为存在大量的离群值,大多数数据处于0-100之间,而最大值则是出现在了500左右。因此对于此类数据我们填充中位数保证数据的严谨。
#填充中位数
data_test[['Fare']] = data_test[['Fare']].fillna(data_test['Fare'].median())
data_test[['Fare']].info()
Fare字段418条记录,不存在缺失值。
#查看Embarked 字段
print(data_train[['Embarked']].head(20))
#Embarked字段主要有S、C、Q三种取值,我们用众数填充
data_train[['Embarked']] = data_train[['Embarked']].fillna(data_train['Embarked'].mode())
data_train[['Embarked']].info()
Embarked字段891条记录,不存在缺失值。
#查看Cabin字段数据
for data in data_cleaner:
print(data[['Cabin']].head(50))
Cabin数据大多数都存在缺失,而有的又存在多个房间,数据较为杂乱,为了防止填充值影响最终结果先为Cabin填充为‘Unknown’。
#填充Unknown
for data in data_cleaner:
data[['Cabin']] = data_test[['Cabin']].fillna('Unknown')
data[['Cabin']].info()
train集891条记录、test集418条记录,无缺失值。
2.2异常值处理
#查看特征的分布状况
for data in data_cleaner:
print(data.describe())
异常值主要出现在连续型特征中,
#使用matplotlib 画箱型图
#先看Age字段箱型图
plt.figure(figsize = (10,6))
plt.subplot(121)
data_train[['Age']].boxplot()
plt.title('train Age boxplot')
plt.subplot(122)
data_test[['Age']].boxplot()
plt.title('test Age boxplot')
plt.grid(alpha = 0.3)
plt.show()
图中看出,Age数据存在很多在上界与下界之外的数据记录,但这些记录并不违反常识,年龄都在0岁以上和85岁以下,都是在正常的年龄分为内。
#为了保证数据的可靠性,我们还是查看小于Age下界的记录数据
for data in data_cleaner:
IQR = data[['Age']].quantile(0.75)-data[['Age']].quantile(0.25)
DB = data[['Age']].quantile(0.25) - 1.5*IQR
print(data.loc[data['Age']< DB ,:])
对比数据记录,不存在异常值。
除去Age外,还有Fare特征需要排除异常值,Fare在之前的直方图中存在很多的离群值,因此也同样画出箱型图,并查看异常值。
#Fare数据箱型图与异常值查询
图看出Fare大多数数据在0-50,少部分在150左右,更少的在250,还有4个在500+,从横向看离群点的值都在较近的分布区间,另外根据观察数据发现Fare较高的人都是预订了多个房间,因此尽管有大量数据超过上界Fare的取值扔是合理的,(就像天价演唱会门票,如果也用箱型图,相信也是类似的分布状态。)
这部分通过图表与查询验证 数据中不存在异常值。
2.3数据预处理
2.3.1离散化
Age与Fare存在很多值在上界与下界之外,数据分布较分散,为了提升模型拟合程度,将此部分信息进行离散化,分到多个数值区间,减少数值计算能提升模型运算的速度,也能提升模型的拟合度。因此对Age与Fare进行离散化。普通离散化常用qcut与cut函数进行自动的离散化,但通过散点图我们从Fare分布,能看出数据大致类别,因此用列表[0,50,100,200,300,600]对Fare进行离散化。
#为了观察Age是否分布也存在一定规律,决定画出Age散点图
# Age散点图
i = 0
plt.figure(figsize=(10,6))
namelist = ['train','test']
for data in data_cleaner:
i += 1
plt.subplot(1,2,i)
plt.scatter(data['PassengerId'],data['Age'])
plt.title('{} scatter'.format(namelist[i-1]))
plt.grid(alpha = 0.3)
plt.show()
Age数据分布没有比较明显的类别信息,因此用qcut对数据进行自动离散化分类。
#离散化
for data in data_cleaner:
data['Agebin'] = pd.qcut(data['Age'],4)
data['Farebin'] = pd.cut(data['Fare'],[0,50,100,200,300,600],labels=['A','B','C','D','E'])
2.3.2删除不需要的数据
上文提到数据集中的不必要字段为:Ticket,其对分析结果没有作用需要删除
#删除 Ticket
for data in data_cleaner:
data = data.drop(columns=['Ticket'])
2.4特征提取
此部分需要对离散型数据(Agebin、Farebin、Embarked、Sex、Pclass)进行onehot编码,提取Name数据中信息字段,对SibSp和Parch进行特征提取,Cabin数据房间号类别提取。
2.4.1 Name数据字段特征提取
#Name字段的常规结果为:'名字,头衔.姓氏',有效特征为头衔
for data in data_cleaner:
data['Head'] = data['Name'].str.split(',').apply(lambda x:x[1].split('.')[0])
#查询Head包含的值
for data in data_cleaner:
print(data['Head'].value_counts())
#存在量很少的统计值,将其替换成‘N’
for data in data_cleaner:
H_values = data['Head'].value_counts()
for i in H_values[H_values<10].index:
data.loc[data['Head'].str.contains(i),'Head'] = 'N'
2.4.2 放弃 SibSp、Parch数据字段
我认为是否有父母子女同乘是会导致过拟合的特征数据,。例如,有年迈的父母与有壮年的子女,同样是计数1但对模型的影响结果,方向是相反的,而且这种作用相反的例子不止一种,因为父母可能有年迈的可能有中年的有青年的,子女有婴儿或青壮年,因此这类数据如果用于分析,其对结果的影响,取决于不同作用样本量的期望值,这类数据或许与结果有相关关系,但是在没有较好的控制变量的情况下是不存在因果关系的。因此,除非样本集过于特殊,否则两项放入模型中会导致模型过拟合。兄弟姐妹同行人数,我认为也是这种情况。因此先不将两个特征放入模型。
2.4.3将Cabin房间类型取出
for data in data_cleaner:
data['Cabin_c'] = data['Cabin'].str[0:1]
2.4.4 Onehot编码构建虚拟变量
#进行onehot编码,但数据编码后放入模型会很麻烦,因此建立列表进行管理
x_dummy_columns = ['Agebin','Farebin','Head','Embarked','Sex','Cabin_c','Pclass']
data_train_dummy = pd.get_dummies(data_train[x_dummy_columns])
data_test_dummy = pd.get_dummies(data_test[x_dummy_columns])
三、数据探索性分析
探索性分析中需要用到的数据为train集,因探索性分析需要Survival数据用于衡量各特征值对Survival数据的影响。
3.1相关性分析
#相关性分析
data_train.set_index(['PassengerId'])
# 设置标签
lable = preprocessing.LabelEncoder()
data_train['Sex_Code'] = lable.fit_transform(data_train['Sex'])
data_train['Agebin_Code'] = lable.fit_transform(data_train['Agebin'])
data_train['Farebin_Code'] = lable.fit_transform(data_train['Farebin'])
data_train['Head_Code'] = lable.fit_transform(data_train['Head'])
data_train['Embarked_Code'] = lable.fit_transform(data_train['Embarked'])
print(data_train.corr()[['Survived']].sort_values(by = 'Survived',ascending= False))
3.2可视化
为了更清晰展示各变量对Survived的影响进行可视化的展示
#Survived hist
plt.figure(figsize=(18,12))
#Fare
plt.subplot(221)
plt.hist(x = [data_train.loc[data_train['Survived'] == 0,'Fare'],data_train.loc[data_train['Survived'] == 1,'Fare']],label = ['died','Survived'],color = ['red','green'])
plt.grid(alpha = 0.3)
plt.title('Fare Survived hist')
plt.xlabel('Fare')
plt.ylabel('Survived')
plt.legend()
#Age
plt.subplot(222)
plt.hist(x = [data_train.loc[data_train['Survived'] == 0,'Age'],data_train.loc[data_train['Survived'] == 1,'Age']],label = ['died','Survived'],color = ['red','green'])
plt.grid(alpha = 0.3)
plt.title('Age Survived hist')
plt.xlabel('Age')
plt.ylabel('Survived')
plt.legend()
#Pclass
plt.subplot(223)
plt.hist(x = [data_train.loc[data_train['Survived'] == 0,'Pclass'],data_train.loc[data_train['Survived'] == 1,'Pclass']],label = ['died','Survived'],color = ['red','green'])
plt.grid(alpha = 0.3)
plt.title('Pclass Survived hist')
plt.xlabel('Pclass')
plt.ylabel('Survived')
plt.legend()
#Embarked
plt.subplot(224)
plt.hist(x = [data_train.loc[data_train['Survived'] == 0,'Embarked'],data_train.loc[data_train['Survived'] == 1,'Embarked']],label = ['died','Survived'],color = ['red','green'])
plt.grid(alpha = 0.3)
plt.title('Embarked Survived hist')
plt.xlabel('Embarked')
plt.ylabel('Survived')
plt.legend()
plt.show()
对于探索性分析的目的主旨,理解还不够充分,缺少系统,结构化的思路进行深入探索,这部分就不过多陈述,等有一定探索性分析思路后再回来补充。
四、数据建模与分析
数据建模与分析部分需要将案例用到的机器学习算法定义出来,并用拆分后的训练集去训练我们的算法分类器,并在最后用测试集对分类器进行评估(如,精确率,召回率,roc曲线等)
4.1模型选择
4.1.1模型建立
#将需要使用的模型及需要网格搜索的参数提前设定
# LogisticRegression
LR_clf = LogisticRegression(max_iter= 3000)
param_dict1 = {
'penalty' : ['l1','l2'],
'C':[0.1,0.5,1]
}
# DecisionTreeClassifier
DT_clf = DecisionTreeClassifier(random_state=0)
param_dict2 = {
'max_depth':[3,4,5],
'min_samples_leaf' : [1,2]
}
# MultinomialNB
NBC_clf = MultinomialNB()
para_dict3 ={
'alpha' : [1.0]
}
# SVC
SVC_clf = SVC()
param_dict4 = {
'C':[2,2.5,3],
'kernel':['rbf','linear','poly']
}
# RandomForestClassifier
RF_clf = RandomForestClassifier()
param_dict5 = {
'n_estimators' :range(80,200,4),
'max_features':[2,4,6,8]
}
# # XGBClassifier (带入网格搜索,优化参数时,一直报错:feature_names must be string,没找到合理的原因,先注释掉了)
# XGB_clf = XGBClassifier()
# param_dict6 = {
# 'n_estimators':[50,100,200],
# 'max_depth':[2,5,8],
# 'learning_rate':np.linspace(0.01,2,20),
# 'binary':['hinge']
# }
# KNeighborsClassifier
KNN_clf = KNeighborsClassifier()
param_dict6 = {
'n_neighbors':[5]
}
4.2模型分析
用网格搜索进行参数优化和选择,并储存准确率,模型与参数
Model_list = [LR_clf,DT_clf,NBC_clf,SVC_clf,RF_clf,KNN_clf]
param_list = [param_dict1,param_dict2,para_dict3,param_dict4,param_dict5,param_dict6]
estimator = []
params = []
score = []
scoring = make_scorer(accuracy_score)
for i in range(6):
clf_R = Model_list[i]
parameters = param_list[i]
grid = GridSearchCV(clf_R,parameters,cv = 5,scoring= scoring,n_jobs = 4)
grid.fit(data_train_dummy,Y)
temp = grid.best_estimator_
estimator.append(temp)
temp = grid.best_params_
params.append(temp)
temp = grid.best_score_
score.append(temp)
五、模型选择与结果输出
5.1模型选择与预测
for i in range(6):
model = Model_list[i]
model.fit(data_train_dummy,Y)
y_predict = model.predict(data_train_dummy)
roc = roc_auc_score(y_true=Y,y_score=y_predict)
recall = recall_score(y_true=Y,y_pred=y_predict)
print('算法模型:',estimator[i],'n')
print('模型的roc:',roc,'n')
print('模型的召回率:',recall,'n')
print('模型测试精确率:',score[i],'n')
print('-'*20)
5.2结果输出
对比各参数我们选择SVM模型作为最终模型对测试集数据进行预测。
CLF = Model_list[3]
results= CLF.predict(data_test_dummy)
results = pd.DataFrame(results,columns = ['Survived'])
test = pd.concat([test,results],axis = 1)
results = test[['PassengerId','Survived']]
results = results.set_index(['PassengerId'])
results.to_csv('./Results.csv')
最后将模型估计生存结果,最后将结果输出到csv文件并提交到Kaggle平台。
数据分析总结
开篇有提到Titanic是我做过的第一份数据分析项目,第一次提交的结果也如上图,准确率仅有74.88%,第一次的分析操作中,只是用简单的LR模型进行拟合,并且数据清洗过程也存在很多不合理的地方。这次距离上次分析经过了2个月的时间,期间我丰富了自己机器算法知识(用到了更多的算法模型),并自主学习了,模型估计过程中的数据集挑选(交叉验证)、参数优化(网格搜索)的相关操作。这次准确率虽然只上涨到78.229%,但kaggle平台的项目排名却从89%到了22%,可见预测模型的优化是高投入低产出的过程,但在工作业务中,对模型的优化调整又是必不可少。因此,数据分析学习需要我们投入大量的时间精力去完善自己的分析思路(数据理解、特征筛选等)与算法操作(理论背景、python操作)。
在此还是老规矩,总结一下本次案例的不足与需要进一步学习的方面:
- 需要精通pandas模块操作,在数据清洗附近因代码错误耽误时间;
- 探索性分析中,可视化图表展现不够,需要学习更多的可视化图表以及理解探索性分析的思路与框架,能为数据模型分析提供分析思路;
- 在算法模型创建中,XGBoost分类器的建立错误仍未解决,需要找到代码的错误点,并学会分类器的使用;
- 准确率78%,还能进一步的提升,提升方向:特征提取与模型优化能力。