一、问题复述
泰坦尼克号是一艘英国皇家邮轮,在当时是全世界最大的海上船舶。1912年4月,该邮轮在首航中碰撞上冰山后沉没。造成船上2224名人员中1514人罹难。
现在根据乘客的船舱等级、性别、年龄等信息,对其是否获救进行判定。我们一共有1309名乘客的信息,其中891名乘客信息作为训练集,另外418名乘客信息作为测试集。
先查看数据的总体情况:
# -*- coding: utf-8 -*-
import pandas as pd
import numpy as np
from pandas import Series,DataFrame
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
pd.set_option('display.width', 2000, 'display.max_rows', None,'display.max_columns', None) # 设置数据显示
trd=pd.read_csv("../data/train.csv") # 读取训练数据
tsd=pd.read_csv("../data/test.csv") # 读取测试数据
trd.info() # 读取训练数据列信息
tsd.info() # 读取测试数据列信息
print(trd.describe()) # 显示训练数据特征
print(tsd.describe()) # 显示测试数据特征
可看到包含如下属性:
PassengerId(乘客编号),训练集:1-891,测试集:892-1309;
Survived(是否获救),是用1表示,否用0表示,只训练集中有该项属性;
Pclass(船舱等级),分为1、2、3级;
Name(乘客姓名);
Sex(乘客性别),female,male;
Age(乘客年龄),训练集:714名乘客有该项属性,177名乘客缺失,测试集:332名乘客有该项属性,86名乘客缺失;
SibSp(兄弟姐妹\配偶个数);
Parch (父母\子女个数);
Ticket (船票信息),每名乘客均不同,由数字编号,字母等组成,十分杂乱;
Fare(船票价格);
Cabin(船舱编号) , 由单个大写字母+数字组成,训练集:204名乘客有该项属性,687名乘客缺失;测试集:91名乘客有该项属性,241名乘客缺失。
Embarked(登船口),分别有C、S、Q三个登船口,训练集中两名乘客缺失该项信息。
二、数据初步分析
每位乘客的信息中,优先考虑数据质量相对较高的数值属性、标称属性等。对于PassengerId、Name、Ticket这3项暂时不做分析,另外8项属性,首先独立地分析每个属性对乘客获救与否的影响。
# -*- coding: utf-8 -*-
import pandas as pd
import numpy as np
from pandas import Series,DataFrame
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif']=['SimHei'] #用来正常显示中文标签
plt.rcParams['axes.unicode_minus']=False #用来正常显示负号
pd.set_option('display.width', 2000, 'display.max_rows', None,'display.max_columns', None) # 设置数据显示
trd=pd.read_csv("../data/train.csv") # 读取数据
trd.info() # 读取列信息
# print(trd.describe()) # 显示特征值
# 两个Series,将一个索引处有值另一个为NaN的地方填充为0
def func1(Series1,Series2):
for i in Series1.index:
if i not in Series2.index:
Series2[i]=0
for i in Series2.index:
if i not in Series1.index:
Series2[1] = 0
return Series1,Series2
# begin -*- 6.2属性与获救结果的关联统计 -*-
fig=plt.figure(figsize=(12,6)) # 定义图并设置画板尺寸
fig.set(alpha=0.2) # 设定图表颜色alpha参数
# fig.tight_layout() # 调整整体空白
plt.subplots_adjust(left=0.08,right=0.94,wspace =0.36, hspace =0.5) # 调整子图间距
#1 各船舱等级的获救情况
ax1=fig.add_subplot(241)
ax1.set(title=u"各船舱等级乘客获救情况",xlabel=u"船舱等级",ylabel=u"人数")
ax1.set_title(u"各船舱等级乘客获救情况",fontdict={'fontsize':10}) # 设置标题字体大小
ax1.axis([0,4,0,600])
S0_Pclass= trd.Pclass[trd.Survived == 0].value_counts()
S1_Pclass= trd.Pclass[trd.Survived == 1].value_counts()
plt.xticks(rotation=90)
dfp1=pd.DataFrame({u'未获救':S0_Pclass, u'获救':S1_Pclass}).plot(ax=ax1,kind='bar', stacked=True,rot=1)
for i in S0_Pclass.index: # 添加列标签
plt.text(i-1.16,S0_Pclass[i]+S1_Pclass[i]+12,"{:.2f}".format(S1_Pclass[i]/(S0_Pclass[i]+S1_Pclass[i])))
#2 各船舱号乘客获救情况
ax2=fig.add_subplot(242)
ax2.set(title="各船舱号乘客获救情况",xlabel=u"船舱号",ylabel=u"人数")
ax2.set_title(u"各船舱号乘客获救情况",fontdict={'fontsize':10}) # 设置标题字体大小
ax2.axis([0,8,0,800])
trd2=trd.copy()
count=0
for i in trd2.Cabin.fillna("N").values:
trd2.Cabin[count]=i[0]
count+=1
S0_Cabin=trd2.Cabin[trd2.Survived==0].value_counts()
S1_Cabin=trd2.Cabin[trd2.Survived==1].value_counts()
dfp2=pd.DataFrame({"未获救":S0_Cabin,"获救":S1_Cabin}).plot(ax=ax2,kind="bar",stacked=True,rot=1)
S0_Cabin,S1_Cabin=func1(S0_Cabin,S1_Cabin)
S0_Cabin,S1_Cabin=S0_Cabin.sort_index(),S1_Cabin.sort_index()
count2=-0.5
for i in S0_Cabin.index:
# print(i,S0_Cabin.index,S0_Cabin[i])
# print(ax2.get_xticks())
plt.text(count2,S0_Cabin[i]+S1_Cabin[i]+16,"{:.1f}".format(S1_Cabin[i]/(S0_Cabin[i]+S1_Cabin[i])))
count2+=1
#3 各登船口的获救情况
ax3=fig.add_subplot(243)
ax3.set(title=u"各登船口乘客获救情况",xlabel=u"登船口",ylabel=u"人数")
ax3.set_title(u"各登船口乘客获救情况",fontdict={'fontsize':10}) # 设置标题字体大小
ax3.axis([0,3,0,800])
S0_Embarked= trd.Embarked[trd.Survived == 0].value_counts()
S1_Embarked= trd.Embarked[trd.Survived == 1].value_counts()
dfp2=pd.DataFrame({u'未获救':S0_Embarked, u'获救':S1_Embarked}).plot(ax=ax3,kind='bar', stacked=True,rot=1)
c=0
for i in S0_Embarked.index: # 添加列标签
plt.text(c-0.2,S0_Embarked[i]+S1_Embarked[i]+20,"{:.2f}"\
.format(S1_Embarked[i]/(S0_Embarked[i]+S1_Embarked[i])))
c+=1
#4 各船票价格乘客的获救情况
ax4=fig.add_subplot(244)
ax4.set(title="各船票价格乘客的获救情况",xlabel=u"票价",ylabel=u"获救率")
ax4.set_title(u"各船票价格乘客获救情况",fontdict={'fontsize':10}) # 设置标题字体大小
ax4.axis([0,300,0,1])
x=np.array(sorted(trd.Fare[trd.Fare.notnull()]))
y=[]
for i in x:
y.append(trd.Fare[trd.Fare < i][trd.Survived == 1].count()/trd.Fare[trd.Fare < i].count())
y=np.array(y)
plt.plot(x,y,"--",linewidth=0.6)
# ax4.set_xticks([]) # 不显示x轴刻度
#5 各性别的获救情况
ax5=fig.add_subplot(245)
ax5.set(title=u"不同性别乘客获救情况",xlabel=u"性别",ylabel=u"人数")
ax5.set_title(u"不同性别乘客获救情况",fontdict={'fontsize':10}) # 设置标题字体大小
ax5.axis([0,5,0,700])
S0_Sex=trd.Sex[trd.Survived==0].value_counts()
S1_Sex=trd.Sex[trd.Survived==1].value_counts()
dfp3=pd.DataFrame({u'未获救':S0_Sex, u'获救':S1_Sex}).plot(ax=ax5,kind='bar', stacked=True,rot=0)
c=1
for i in S0_Sex.index: # 添加列标签
plt.text(c-0.15,S0_Sex[i]+S1_Sex[i]+16,"{:.2f}".format(S1_Sex[i]/(S0_Sex[i]+S1_Sex[i])))
c-=1
#6 各年龄乘客的获救情况
ax6=fig.add_subplot(246)
ax6.set(title="各年龄乘客获救情况",xlabel=u"乘客年龄",ylabel=u"获救率")
ax6.set_title(u"各年龄乘客获救情况",fontdict={'fontsize':10}) # 设置标题字体大小
x6=np.array(sorted(trd.Age[trd.Age.notnull()]))
# print(x6)
y6=[]
for i6 in x6:
y6.append(trd.Age[trd.Age
得到如下图所示结果,对8个子图逐一进行解释和分析(子图编号按照从左至右,先行后列排序)。
子图1,船舱不同等级乘客获救情况。共有3个等级,图上标签表示存活率。由图可知,船舱等级为1、2、3的乘客获救率分别为0.64、0.47、0.24。因此,船舱等级是一个较显著的影响因素。
子图2,由于各乘客船舱号是大写字母加数字,且大部分乘客缺失该项属性,尝试以船舱号首字母将其分类,并以N表示该项缺失。由子图2可知,缺失该项属性的乘客存活率为0.3,其它乘客存活率在0.5-0.8之间,且未缺失该项属性的乘客每类样本量均较小。因此在后续分析中,该项属性以是否缺失作为分类标准。
子图3,从S、C、Q登船口登船的乘客获救率分别为0.34、0.55、0.39。
子图4,票价-存活率的概率分布,即横坐标为票价,纵坐标为低于该票价的乘客的存活率。可以看出,票价越高,获救率越大。
子图5,按照乘客性别考查获救率,可以看出女性乘客获救率0.74,明显高于男性0.19的获救概率。是一个较显著的影响因素。
子图6,年龄-存活率的概率分布,即横坐标为年龄,纵坐标为小于该年龄的乘客的存活率。可以看出,年龄越小,获救率越大。
子图7,按照同登船的兄弟姐妹\配偶个数考查,该属性值为0、1、2的乘客获救率分别为0.35、0.54、0.46,其它取值的乘客样本量较小,且获救率较低,可以归为一类。
子图8,按照同登船的父母\子女个数考查,该属性值为0、1、2的乘客获救率分别为0.34、0.55、0.50,其它取值的乘客样本量较小,且获救率较低,可以归为一类。
三、数据预处理
通过以上分析,我们大致了解了各属性对乘客获救与否的影响,现对各属性作如下预处理:
船舱号:缺失该项属性标记为0,未缺失标记为1
登船口:缺失、C、S、Q分别标记为0、1、2、3
船票价格: 规范化(按照比例映射到[0,1]区间内)
性别:female标记为0,male标记为1
年龄:利用随机森林和其它属性填补缺失数据,再对其规范化(按照比例映射到[0,1]区间内)
登船兄弟姐妹\配偶人数:大于等于3个统一记为3,其余不变
登船父母\子女人数:大于等于3个统一记为3,其余不变
# 数据数值化
def data_sd(trd):
trd.loc[(trd.Cabin.notnull()), 'Cabin'] = 1
trd.loc[(trd.Cabin.isnull()), 'Cabin'] = 0
trd.loc[(trd['SibSp']>=3), 'SibSp'] = 3
trd.loc[(trd['Parch']>=3),'Parch'] = 3
trd.Sex[trd.Sex=="female"]=0
trd.Sex[trd.Sex=="male"]=1
trd.Embarked[trd.Embarked=="C"]=0
trd.Embarked[trd.Embarked=="S"]=1
trd.Embarked[trd.Embarked=="Q"]=2
trd.Embarked[trd.Embarked.isnull()]=3
data_sd(trd) # 训练数据数值化
data_sd(tsd) # 测试数据数值化
# 随机森林填补缺失的年龄属性
def set_missing_ages(df):
df1= df[['Age', 'Pclass', 'Fare', "Embarked",'Cabin','Parch', 'SibSp']][df.Fare.notnull()] # 提取特征较显著的几个属性数据
y = df1[df1.Age.notnull()].values[:, 0] # 提取有年龄乘客的年龄数据
x = df1[df1.Age.notnull()].values[:, 1:] # 提取有年龄乘客的其它属性数据
rfr = RandomForestRegressor(random_state=0, n_estimators=2000, n_jobs=-1) # 定义随机森林
rfr.fit(x, y) # 进行训练
predictedAges = rfr.predict(df1[df1.Age.isnull()].values[:, 1:]) # 进行预测。
df.loc[(df.Age.isnull()), 'Age'] = predictedAges # 用得到的预测结果填补原缺失数据
return df, rfr
trd, rfr = set_missing_ages(trd) # 调用年龄填补函数
trd.Age=trd.Age.astype(np.int32) # 年龄数据换为整数
tsd, rfr = set_missing_ages(tsd) # 调用年龄填补函数
tsd.Age=tsd.Age.astype(np.int32) # 年龄数据换为整数
# 年龄数据规范化
import sklearn.preprocessing as prc
def data_asd(trd):
mmsc= prc.MinMaxScaler(feature_range=(0, 1)) # 年龄数据规范区间(0,1)
T=np.array([trd.Age]).transpose() # 年龄数据加维、数组化、取转置。才能顺利进行规范化操作。
trd_d=mmsc.fit_transform(T).transpose()[0] # 数据规范化,转置回来,取一维。
trd["Age_mmsc"]=trd_d # 规范化的年龄数据拼接到原数据
data_asd(trd)
data_asd(tsd)
# 票价数据规范化
def data_fsd(trd):
trd.Fare[trd.Fare.isnull()]=trd.Fare.mean() # 空缺票价填充为平均值
mmsc= prc.MinMaxScaler(feature_range=(0, 1)) # 票价数据规范区间(0,1)
T=np.array([trd.Fare]).transpose() # 票价数据加维、数组化、取转置。才能顺利进行规范化操作。
trd_d=mmsc.fit_transform(T).transpose()[0] # 数据规范化,转置回来,取一维。
trd["Fare_mmsc"]=trd_d # 规范化的票价数据拼接到原数据
data_fsd(trd)
data_fsd(tsd)
四、建模及结果输出
对于8个属性,一共可以有$c^1_8+c^2_8+...+c^8_8=255$种特征组合。对每种特征组合,我们用训练集进行交叉验证,并在指定标准差范围内,选取出平均分最高的特征组合。
采用k-邻近算法、逻辑回归、SVM、决策树等方法进行建模,下面为k-邻近算法代码,其余方法代码框架与其类似:
# k-邻近算法
score=[] # 记录评分的列表
temp0=[] # 记录当前选取的特征组合的评分
temp1=0 # 记录当前选取组合的平均分数
temp2=0 # 记录当前选取组合的分数标准差
z=["Pclass","Sex","Embarked","Age_mmsc","Cabin","Fare_mmsc",'SibSp','Parch'] # 用于生成特征组合的完整属性列表
for j in range(1,9):
for i in itertools.combinations(z, j): # 取包含j个属性的特征组合
i=list(i)
# 交叉验证库,将训练集进行切分交叉验证取平均
from sklearn import cross_validation
from sklearn.model_selection import cross_val_score
knc_kf=KNeighborsClassifier() # 定义一个k-邻近分类器
x =trd[i]
y =trd["Survived"]
score=cross_val_score(knc_kf, x, y, cv=5) # k为5的交叉验证分数列表
if (score.mean() > temp1 and score.std() < 0.016): # 特征组合选取条件,在指定标准差范围内,平均分最大
temp0 = score
temp1 = score.mean()
temp2 = score.std()
dict = {temp1: i} # 字典,key为平均分数,value为当前选取的特征组合
c =dict[temp1] # 最终选取的特征组合,用于建模
# K-邻近算法建模
knc1 = KNeighborsClassifier() # 定义一个K-邻近分类器
x_trd = trd[c]
y_trd = trd["Survived"]
knc1.fit(x_trd, y_trd) # 训练模型
x_tsd = tsd[c]
y_tsd = knc1.predict(x_tsd) # 进行预测
result = pd.DataFrame({'PassengerId': tsd['PassengerId'].values, 'Survived': y_tsd.astype(np.int32)}) # 预测结果改为要求的格式
result.to_csv("../result/result_knc.csv", index=False) # 输出结果
在提交的结果中k-邻近算法得分相对较高,相应特征组合为["Pclass","Sex","Embarked","Age_mmsc"]。最后进行模型融合,方法为在k-邻近算法基础上,用另外几种算法结果进行优化,最终得到的分数为0.78947,
项目完整代码:https://github.com/windsayno/Titanic