欢迎来到小象学院实践课!在此文件中,有些示例代码已经提供给你,但你还需要实现更多的功能让项目成功运行。除非有明确要求,你无须修改任何已给出的代码。以'练习'开始的标题表示接下来的代码部分中有你必须要实现的功能。每一部分都会有详细的指导,需要实现的部分也会在注释中以'TODO'标出。请仔细阅读所有的提示!
除了实现代码外,你还必须回答一些与项目和你的实现有关的问题。每一个需要你回答的问题都会以'问题 X'为标题。请仔细阅读每个问题,并且在问题后的'回答'文字框中写出完整的答案。我们将根据你对问题的回答和撰写代码所实现的功能来对你提交的项目进行评分。
提示:Code 和 Markdown 区域可通过Shift + Enter快捷键运行。此外,Markdown可以通过双击进入编辑模式
泰坦尼克号的沉没是历史上最臭名昭着的沉船之一。 1912年4月15日,在她的第一次航行中,泰坦尼克号在与冰山相撞后沉没,在2224名乘客和机组人员中造成1502人死亡。造成海难失事的原因之一是乘客和机组人员没有足够的救生艇。 尽管幸存生存下来有一些运气因素,但有些人比其他人更容易生存,比如女人,孩子和上流社会。
在这个挑战中,我们要求您完成对哪些人可能存活的分析,我们要求您运用机器学习工具来预测哪些乘客幸免于悲剧。
数据集出自Kaggle. (https://www.kaggle.com/c/titanic) 在完成作业后,同学们可以将自己的结果在该页面提交来检验效果。
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score
from sklearn.preprocessing import MinMaxScaler
from sklearn.decomposition import PCA
from sklearn.svm import SVC
from sklearn.model_selection import GridSearchCV
import xgboost as xgb
from xgboost.sklearn import XGBClassifier
from sklearn.ensemble import GradientBoostingClassifier
import warnings
warnings.filterwarnings('ignore')
# train.csv中为训练数据,用于训练和效果检测
train = pd.read_csv('./all/train.csv')
y = train['Survived']
# test.csv为已经去除Survived列的数据,使用训练好的模型对其进行拟合,将结果提交即可完成Kaggle任务
test = pd.read_csv('./all/test.csv')
# 查看一下训练和测试数据大小分别是多少
print(train.shape)
print(test.shape)
查看一下训练数据:
train.head()
可以看到我们有如下几个特征:
数据特征数不多,接下来我们尝试构建一个函数,通过绘制柱形图展示出类别型特征与目标特征之间的关系。
import matplotlib.pyplot as plt
# 定义画布尺寸
plt.figure(figsize=(10, 4))
def showSurvivalRate(df, column_name):
"""
通过柱形图展示数据特征与目标特征之间的关系。
横坐标为希望考察的特征属性,纵坐标为生存率。
输入参数:
df:需要考察的数据,存储类型为DataFrame
column_name: 需要考察的属性名字。
"""
# TODO
# 构建一个dataframe类型的结果result_df。
# result_df由两列组成,一列由参数df中的名为column_name的列的可能取值组成;另一列,名为'Survival Rate',是该取值所对应的存活率。
# 比如当输入column_name为'Pclass'时,计算出的result_df结果为(第一列列名与传入的列名column_name相同,第二列列名为'Survival Rate'):
# Pclass Survival Rate
# 1 0.629630
# 2 0.472826
# 3 0.242363
# 表示不同等级仓位旅客的存活率
count_val = df[column_name].value_counts()
survive_rate = []
for ind in count_val.index:
n_all = count_val[ind]
n_survive = df[df[column_name]==ind].Survived.sum()
survive_rate.append(n_survive/n_all)
results_df = pd.DataFrame({column_name: count_val.index, 'Survival Rate': survive_rate})
# 绘制柱形图
results_df.plot(x=column_name, y=['Survival Rate'], kind='bar', title=column_name, legend=False)
plt.show()
接下来我们来看一下各个特征对于最终存活率的影响吧。 我们先从类别型属性开始:
# 获取所有数据列的名字
columns = train.columns.values.tolist()
# 排除目标列
columns.remove('Survived')
# 去除连续特征Age和Fare
columns.remove('Age')
columns.remove('Fare')
# 从直觉判断上,我们认为名字、PassengerId(用户ID)、Ticket(票号)这些是每个人都不同的随机属性,与结果关系不大,先将它们也排除
columns.remove('PassengerId')
columns.remove('Ticket')
columns.remove('Name')
# 调用showSurvivalRate绘制柱形图,观察各属性与Survived的关系
for item in columns:
showSurvivalRate(train, item)
留意绘制的图形,绝大部分特征不同取值所对应的生存率都有明显的差别,将它们作为预测Survived的依据应该比较可靠。
唯一例外的,我们关注一下属性Cabin(仓位号码)。可能对豪华邮轮的仓位不太熟悉,我们一开始以为仓位号码是每位乘客都不一样,这样它将与PassengerId一样,不具备可预测目标属性的区分度。
问题: 从图中,你是否能判断出Cabin属性不是每位乘客都不相同?理由是?
回答: 能够,因为在该图中存在一些值既不是0,也不是1.
我们对Cabin中生存率大于零小于一的那些乘客做下观察:
# TODO
# 将不同Cabin所对应的生存率计算出来,并存储在DataFrame类型的变量rate中
def cal_survive_rate(df, column_name):
count_val = df[column_name].value_counts()
survive_rate = []
for ind in count_val.index:
n_all = count_val[ind]
n_survive = df[df[column_name]==ind].Survived.sum()
survive_rate.append(n_survive/n_all)
results_df = pd.DataFrame({column_name: count_val.index, 'Survival Rate': survive_rate})
return results_df
rate = cal_survive_rate(train, "Cabin")
# TODO
# 遍历rate中的元素,将Survived大于零小于1的项打印出来
rate[(rate["Survival Rate"]>0) & (rate["Survival Rate"]<1)]
观察下输出结果,我们打印一下Cabin为E44的那些乘客的信息:
# TODO
# 打印一下Cabin为G6的那些乘客的信息
train[train.Cabin=='E44']
首先我们了解了同个仓位中可以有多名乘客,然后,通过他们的Name,不难判断这是一对夫妻。
从这里,我们得到启示:
在test集数据中,Cabin应是另外一批值,直接使用Cabin作为训练属性没什么意义。
从E44这个例子看,Name这个字段应该还是有意义的,它第一个逗号之后的title(例子中是Mrs., Mr.)可能也是可以考虑的影响是否生存的因素。
接下来,我们考察名字中的title和存活率之间的关系:
# TODO
# 实现一个函数get_title,将title(名字第一个逗号后以点结尾的部分)提取出来
import re
def get_title(name):
pattern = re.compile(r'\, ([a-z]|[A-Z].+?)\.')
return pattern.search(name)
# 显示Title与存活率的关系
title_df = pd.DataFrame()
title_df['Survived'] = train['Survived']
title_df['Title'] = train['Name'].apply(lambda x: get_title(x).group(1) if get_title(x)!=None else "not find")
showSurvivalRate(title_df, 'Title')
对于Title,还有些情况需要我们考虑,先看下Title的取值:
title_df['Title'].value_counts()
重新定义函数get_title_optimize,对于那些出现次数少于10的title统一称为Misc,大于10次的保留原来函数get_title相同的逻辑:
# TODO
# 定义函数get_title_optimize,将title(名字第一个逗号后以点结尾的部分)提取出来。
# 同时,如果title在输入数据集name中出现的次数小于10,统一称为Misc
def get_title_optimize(name):
pattern = re.compile(r'\, ([a-z]|[A-Z].+?)\.')
name_cp = pd.Series([0]*len(name))
for i in range(len(name_cp)):
search = pattern.search(name[i])
if search == None:
name_cp[i] = 'not find'
else:
name_cp[i] = search.group(1)
title_counts = name_cp.value_counts()
return name_cp.apply(lambda x: 'Misc' if title_counts[x]<=10 else x)
# 显示Title与存活率的关系
title_df = pd.DataFrame()
title_df['Survived'] = train['Survived']
title_df['Title'] = get_title_optimize(train['Name'])
showSurvivalRate(title_df, 'Title')
至此,总结以上的探索结论,我们定义一个类别属性处理函数。函数所要进行的操作是:
生成一个新列,名为Title,该列值通过调用函数get_title_optimize得到(Title是将Name中的第一个逗号之后的,点之前的部分提取出来,如果在整个列中,某个Title出现总数小于10,将其命名为Misc)。
去除无关的属性列'PassengerId'、'Ticket'、'Name'、'Cabin'。
对各属性进行独热编码。
def handle_categorical_feature(data):
"""
类别型属性处理函数。
完成功能:
1. 生成一个新列,名为Title,该列值通过调用函数get_title_optimize得到(Title是将Name中的第一个逗号之后的,点之前的部分提取出来,如果在整个列中,某个Title出现总数小于10,将其命名为Misc)。
2. 去除无关的属性列'PassengerId'、'Ticket'、'Name'、'Cabin'。
3. 对各属性进行独热编码。
所有的操作不改变输入参数data原先所指向数据结构的内容。
输入参数:
data:需要处理的DataFrame类型数据
输出:
经过如上处理之后的DataFrame数据(新生成的数据结构,不改变输出变量data的内容)。
"""
# TODO
# 定义类别属性处理函数handle_categorical_feature的以上逻辑
data['Title'] = get_title_optimize(data['Name'])
data = data.drop(columns=['PassengerId', 'Ticket', 'Name', 'Cabin'])
enc_columns = []
for col in data.columns:
if data[col].dtype == 'O':
enc_columns.append(col)
coded_data = pd.get_dummies(data[enc_columns ])
#data.drop(columns=enc_columns, inplaec=True)
unenc_columns = data.columns.difference(enc_columns)
data_done = pd.concat([data[unenc_columns], coded_data], axis=1)
return data_done
数据中的数值型属性是'Age'和'Fare',考察一下它们的分布:
# 绘制Age的取值分布图
train['Age'].plot(kind = 'kde')
# 绘制Fare的分布图
train['Fare'].plot(kind = 'kde')
根据对取值分布的观察,定义一个函数handle_number_feature,完成功能:
对输入dataframe中的'Age'及'Fare'列进行正态标准化处理(使用StandardScaler),生成新的列数据分别命名为'Age_scal'、'Fare_scal'。
将标准化处理之后的结果列与输入的数据拼接,并将原有的Age和Fare列删除。
from sklearn.preprocessing import StandardScaler
def handle_number_feature(data):
"""
数值型属性处理函数。
完成功能:
1. 对输入dataframe中的'Age'及'Fare'列进行正态标准化处理(使用StandardScaler),生成新的列数据分别命名为'Age_scal'、'Fare_scal'。
2. 将标准化处理之后的结果列与输入的数据拼接,并将原有的Age和Fare列删除。
所有的操作不改变输入参数data原先所指向数据结构的内容。
输入参数:
data:需要处理的DataFrame类型数据
输出:
经过如上处理之后的DataFrame数据(新生成的数据结构,不改变输出变量data的内容)。
"""
# TODO
# 完成以上所述数值型属性的处理函数逻辑
ss = StandardScaler()
ss_data = ss.fit_transform(data[['Age', 'Fare']])
data_std = pd.DataFrame(ss_data, columns=['Age_scal', 'Fare_scal'])
data.drop(columns=['Age', 'Fare'], inplace=True)
data_done = pd.concat([data_std, data], axis=1)
return data_done
使用以上已经定义好的处理方法,我们对所有不同类别的数据进行处理。我们可以预期,当数据经过转换之后,使用train训练出来的模型,当输入test数据集之后,得到的结果便是我们希望获得的。
如果我们对train数据集进行了一系列操作,这些操作同样要作用于test数据集,才可以获得正确的结果。所以,在对训练属性进行处理的时候,我们将test数据集也一并加进来。
# 将train中的目标列 Survived删除
train.drop(['Survived'], axis = 1, inplace = True)
# 将train与test做拼接,方便对数据整体进行处理
data = pd.concat([train, test], axis = 0, ignore_index = True)
# 查看一下train的数据完整性
train.isnull().any()
# 查看一下合并数据data的完整性
data.isnull().any()
这里将test引入到train中组成data进行整体考虑,我们可以看到在test中的空值列比train更多了,所以将他们整体考虑以做空值处理,期望能得到更好的效果。
关注不完整的列'Age'、'Cabin'、'Embarked'、'Fare'。 根据之前我们的讨论,'Cabin'列将会被删除,所以对于剩下的'Age'、'Embarked'、'Fare'三列进行处理。
# TODO
# 绘制'Age'列的箱线图
data['Age'].plot.box()
# TODO
# 绘制'Fare'列的箱线图
data['Fare'].plot.box()
Fare列中存在大量的异常值,我们采用中位数对该列做空值填充。对于Age列,我们使用平均值做填充。
from sklearn.preprocessing import Imputer
# TODO
# 使用中位数填充data的Fare列中的空值,结果重新赋值给data
median_imput = Imputer(missing_values='NaN', strategy='median', copy=True)
median_data = data['Fare'].values.reshape(-1, 1)
Fare_fill = median_imput.fit_transform(median_data)
data['Fare'] = Fare_fill
# TODO
# 使用平均值填充data的Age列中的空值,结果重新赋值给
mean_imput = Imputer(missing_values='NaN', strategy='mean', copy=True)
mean_data = data['Age'].values.reshape(-1, 1)
Age_fill = mean_imput.fit_transform(mean_data)
data['Age'] = Age_fill
data.head()
对于类别型属性'Embarked',我们使用属性值中的最频繁出现值(模式mode)做填充。
data = data.fillna({'Embarked':data['Embarked'].mode()[0]})
验证一下数据完整性状况:
data.isnull().any()
调用我们之前定义的handle_categorical_feature和handle_number_feature对数据进行处理:
data_1 = handle_categorical_feature(data)
data_handled = handle_number_feature(data_1)
data_handled.head()
在数据预处理的最后,我们将数据重新划分。
把原来属于train数据集的内容划分到变量train_X中,同时将剩下行划分到test_X中以备后面模型训练使用
# 将需要提交的没有标签的数据分离出来
train_X = data_handled.iloc[0:len(train)]
test_X = data_handled.iloc[len(train):]
X_train, X_test, y_train, y_test = train_test_split(train_X, y, test_size=0.2, random_state=1)
使用网格搜索(GridSearchCV)调整模型的重要参数,并进行训练。在调参之前,需要同学们利用搜索引擎以及通过学习课程内容来熟悉各参数的意义。推荐调节参数有max_depth,learning_rate,n_estimators,min_child_weight等。请注意复杂的条件组合可能会耗费较长的训练时间。
def train_test_model(X_train, y_train, X_test, y_test, model_name, model, param_range):
print('训练{}中'.format(model_name))
clf = GridSearchCV(estimator = model,
param_grid = param_range,
cv = 6,
scoring = 'roc_auc',
refit = True, verbose = 1, n_jobs = 4)
clf.fit(X_train, y_train)
train_score = clf.score(X_train, y_train)
test_score = clf.score(X_test, y_test)
print('train score:{:.3f}'.format(train_score))
print('test score:{:.3f}'.format(test_score))
print('parameter:{}'.format(clf.best_params_))
print('###########################################')
return clf
# TODO
# 对参数进行调整来得到最优结果
model_name_param_dict = {
'XG': (XGBClassifier(),{'max_depth': [3,5,7,9], 'learning_rate': [0.1, 0.03, 0.01, 0.003],
'n_estimators': range(100, 200, 5)})}
for model_name, (model, param_range) in model_name_param_dict.items():
gscv = train_test_model(X_train, y_train, X_test, y_test,
model_name, model, param_range)
问题: 在参数的调整中,如果learning_rate的值过小会导致什么问题呢?
回答:
在获得满意的效果之后,就可以执行以下代码啦。它会把结果输出为Kaggle要求的格式,文件名为result.csv,同学们可以将这个文件提交到 https://www.kaggle.com/c/titanic 就可以看到自己的分数了!
id=test['PassengerId']
id = id.as_matrix()
result=list(zip(id,gscv.predict(test_X)))
result=np.array(result)
df = pd.DataFrame(result, columns=['PassengerId','Survived'])
df.to_csv('result.csv',index=False)
同学们可以将自己提交结果截图下来,复制到下面的文本框当中:
图片: 截图不方便,评分:0.79
Kaggle是个开放的竞赛平台,以上对数据的处理,模型参数的选做都非最终答案。如果希望得到更高的分数,我们需要对数据进行更多的处理,对模型进行更细致的调优。
在数据处理方面,可以参考的思路包括但不限于:
对数据进行分段,提高模型的泛化能力。比如将数据中的Parch和SibSp合并考虑,将它们相加结果FamilySize进行分段离散化,我们可以构建FamilySize与Survival Rate的关系图,判断将其取值范围划分为若干段的离散化数据(比如FamilySize < 2 映射为一段,2到5之间为一段,5以上为一段),放入训练数据中进行模型拟合。再比如对Age也可以进行分段划分。
对数据尝试进行PCA等降维处理,留下数据中最主要的方差解释,降低噪声以提高泛化能力。
归一化方法以及空值处理方法我们还有其它的可选方式,比如空值处理中,用整体数据的平均年龄可能不太合适,我们可以考虑根据Title计算每种Title的平均年龄,在空值填充时对不同Title使用不同的填充方式(毕竟Miss.跟Mrs.一般来说年龄是有差异的)。
在模型方面,可以尝试更多的参数组合,也可以考虑使用XGBoost的 Learning API,调整除了以上作业中的其它更多参数(比如lambda等)来测试模型效果。
如果进行了调整并得到更好的效果,在下面贴出你的代码和排名图片吧:
# TODO
# 模型优化代码
Kaggle结果图片: