泰坦尼克号生存者预测(细节篇)
本文是博主在看那篇kaggle的入门经典文章后(网址在这里:https://blog.csdn.net/han_xiaoyang/article/details/49797143),想把kaggle上大佬的代码分享在这里(因为大佬使用英文写的,我在这里用中文翻译一下,并对里面的部分语法点作做出解释,最后还会写一个概览性的博客),算是一种补充。建议读者先看那篇博客。
大佬的文章和源代码:https://www.kaggle.com/gunesevitan/titanic-advanced-feature-engineering-tutorial
数据的加载
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="darkgrid")
from sklearn.ensemble import RandomForestClassifier
from sklearn.preprocessing import OneHotEncoder, LabelEncoder, StandardScaler
from sklearn.metrics import roc_curve, auc
from sklearn.model_selection import StratifiedKFold
import string
import warnings
warnings.filterwarnings('ignore')
SEED = 42
def concat_df(train_data, test_data):
# Returns a concatenated df of training and test set
return pd.concat([train_data, test_data], sort=True).reset_index(drop=True)
def divide_df(all_data):
# Returns divided dfs of training and test set
return all_data.loc[:890], all_data.loc[891:].drop(['Survived'], axis=1)
df_train = pd.read_csv('../input/train.csv')
df_test = pd.read_csv('../input/test.csv')
df_all = concat_df(df_train, df_test)
df_train.name = 'Training Set'
df_test.name = 'Test Set'
df_all.name = 'All Set'
dfs = [df_train, df_test]
数据的处理
- 数据概览
查看训练集和测试集的数据集规模和属性值(表头、是否有缺失值、属性类型、通过随机采样来查看具体的数据)
表头: .columns()
是否有属性缺失值、属性类型: .info()
随机采样: .sample() - 数据缺失值处理
display_missing函数显示训练集和测试集中每个列中缺失值的计数。
训练集在年龄、船舱和船上有缺失值
测试集在年龄、客舱和票价中有缺失值
在处理缺失值的时候,用训练集和测试集的并集作为产生缺失值的依据比较好,否则填充的数据可能会过度适合训练集或测试集样本。与总样本相比,年龄、上船和票价的缺失值数量较少,但大约80%的机舱都是缺失值。所以年龄、上船和票价中缺失的值可以用描述性的统计方法来填补,但这对机舱来说是行不通的。 - 我们先来处理年龄的缺失值
年龄中缺少的值用中位数填充,但使用整个数据集的中位数并不是一个好的选择。Pclass(即社会阶层)与年龄(0.408106)的相关性较高,是填充年龄缺失值的最佳选择。
Pclass和性别分组后有不同的年龄中位数。当乘客等级增加时,男性和女性的平均年龄也会增加。然而,女性的中位年龄往往略低于男性。下面的中位年龄用于填充年龄特征中缺少的值。
#用分组的均值来填充
df_all['Age'] = df_all.groupby(['Sex', 'Pclass'])['Age'].apply(lambda x: x.fillna(x.median()))
- boarded是一个分类特性,整个数据集中只有2个缺失值。在大佬的分享中,是通过逻辑分析和查找资料得到的。这种方法固然准确,但是耗费时间,有的时候也难以查阅到合适的资料。大家用类似人员的boarded来预测即可。
- 以下为大佬的分析: 两位乘客都是女性,是上等舱的,而且她们的车票号码是一样的。这意味着他们互相认识,一起从同一个港口出发。上流社会女性乘客的乘船模式值是C(瑟堡),但这并不一定意味着她们从那个港口出发。当我在谷歌上搜索斯通乔治·纳尔逊夫人(玛莎·伊芙琳饰)时,我发现她和她的女仆艾米莉·伊卡尔德从南安普敦出发,在这个页面上玛莎·伊夫林·斯通:泰坦尼克号幸存者。
1912年4月10日,斯通夫人在南安普敦登上了泰坦尼克号,当时她正和她的女仆艾米莉·伊卡尔乘坐头等舱旅行。她住在B-28号客舱。
boarded中缺少的值将用这些信息填充。 - 票价的缺失值处理:只有一位乘客的车票价值缺失。我们可以假设票价与家庭规模(Parch和SibSp)和Pclass特性有关。一个拥有三等舱机票而没有家人的男性票价中值是填补缺失值的合理选择。
med_fare = df_all.groupby(['Pclass', 'Parch', 'SibSp']).Fare.median()[3][0][0]
df_all['Fare'] = df_all['Fare'].fillna(med_fare)
- 船舱的缺失值处理:大部分舱室的值缺失,但不能完全忽略该功能本身,因为有些舱室可能具有更高的存活率。其中,舱室值的第一个字母是舱室所在的甲板。而这些甲板主要用于一个乘客舱,但有些甲板是由多个乘客使用的类。所以我们通过绘制不同船舱中逃生的不同阶层的占比,来得到不同加甲板的特点和逃生率。
- 不同甲板的阶级:
A、B、C三层100%为头等舱乘客
D层有87%的头等舱乘客和13%的二等舱乘客
E等舱乘客占83%,二等乘客占10%,三等乘客占7%
F层有62%的二等舱和38%的三等舱乘客
G甲板100%为三等舱乘客
在T舱的甲板上有一个人,他是头等舱的乘客。T舱乘客与A甲板乘客最为相似,因此他被分为A组。
标记为M的乘客是客舱特征中缺失的值。我觉得找不到那些乘客真正的甲板,所以我决定用M型甲板
idx = df_all[df_all['Deck'] == 'T'].index
df_all.loc[idx, 'Deck'] = 'A'
- 甲板的生存率:
果然,每一个甲板都有不同的存活率,而且这些信息不能被丢弃。由图可见,头等舱乘客使用的客舱比二等舱和三等舱乘客使用的客舱具有更高的存活率。在我看来,M(失踪舱室值)的存活率最低,因为他们无法检索到受害者的船舱数据。这就是为什么我认为将该组标记为M是处理丢失数据的合理方法。它是一个具有共同特征的独特群体。Deck特性现在具有很高的基数,因此一些值根据它们的相似性进行分组。 - 类别太多,这里我们使用属性规约,来简化属性。
A、 B层和C层都被标为ABC,因为它们都只有一等舱的乘客
D和E甲板被标记为DE,因为它们具有相似的乘客等级分布和相同的存活率
F和G甲板被标记为FG,原因与上述相同
M甲板不需要与其他甲板组合,因为它与其他甲板非常不同,并且存活率最低。
df_all['Deck'] = df_all['Deck'].replace(['A', 'B', 'C'], 'ABC')
df_all['Deck'] = df_all['Deck'].replace(['D', 'E'], 'DE')
df_all['Deck'] = df_all['Deck'].replace(['F', 'G'], 'FG')
当然要记得删除原来的Cabin属性:
#axis 默认为0,指删除行,因此删除columns时要指定axis=1;
#inplace=True,则会直接在原数据上进行删除操作,删除后无法返回。
df_all.drop(['Cabin'], inplace=True, axis=1)
关于这两张图是怎么画出来的,代码如下:
第一张图:
#下面代码的知识背景的补充:关于df.columns.level的解释,见如下图片:
#取船舱属性的首字母作为甲板属性(M代表缺失的属性)
df_all['Deck'] = df_all['Cabin'].apply(lambda s: s[0] if pd.notnull(s) else 'M')
df_all_decks = df_all.groupby(['Deck', 'Pclass']).count().drop(columns=['Survived', 'Sex', 'Age', 'SibSp', 'Parch',
'Fare', 'Embarked', 'Cabin', 'PassengerId', 'Ticket']).rename(columns={'Name': 'Count'}).transpose()
def get_pclass_dist(df):
# Creating a dictionary for every passenger class count in every deck
deck_counts = {'A': {}, 'B': {}, 'C': {}, 'D': {}, 'E': {}, 'F': {}, 'G': {}, 'M': {}, 'T': {}}
decks = df.columns.levels[0]
#当pclass不存在时,直接使用deck_counts[deck][pclass],会报名为kayerror的错误。所以添加了报错处理。当报错时,先赋值为0。
for deck in decks:
for pclass in range(1, 4):
try:
count = df[deck][pclass][0]
deck_counts[deck][pclass] = count
except KeyError:
deck_counts[deck][pclass] = 0
df_decks = pd.DataFrame(deck_counts)
deck_percentages = {}
#计算每个甲板上,不同阶级人数的占比
for col in df_decks.columns:
deck_percentages[col] = [(count / df_decks[col].sum()) * 100 for count in df_decks[col]]
return deck_counts, deck_percentages
def display_pclass_dist(percentages):#画图函数
df_percentages = pd.DataFrame(percentages).transpose()
deck_names = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'M', 'T')
bar_count = np.arange(len(deck_names))
bar_width = 0.85
pclass1 = df_percentages[0]
pclass2 = df_percentages[1]
pclass3 = df_percentages[2]
plt.figure(figsize=(20, 10))
plt.bar(bar_count, pclass1, color='#b5ffb9', edgecolor='white', width=bar_width, label='Passenger Class 1')
plt.bar(bar_count, pclass2, bottom=pclass1, color='#f9bc86', edgecolor='white', width=bar_width, label='Passenger Class 2')
plt.bar(bar_count, pclass3, bottom=pclass1 + pclass2, color='#a3acff', edgecolor='white', width=bar_width, label='Passenger Class 3')
plt.xlabel('Deck', size=15, labelpad=20)
plt.ylabel('Passenger Class Percentage', size=15, labelpad=20)
plt.xticks(bar_count, deck_names)
plt.tick_params(axis='x', labelsize=15)
plt.tick_params(axis='y', labelsize=15)
plt.legend(loc='upper left', bbox_to_anchor=(1, 1), prop={'size': 15})
plt.title('Passenger Class Distribution in Decks', size=18, y=1.05)
plt.show()
all_deck_count, all_deck_per = get_pclass_dist(df_all_decks)
display_pclass_dist(all_deck_per)
第二张图:
df_all_decks_survived = df_all.groupby(['Deck', 'Survived']).count().drop(columns=['Sex', 'Age', 'SibSp', 'Parch', 'Fare',
'Embarked', 'Pclass', 'Cabin', 'PassengerId', 'Ticket']).rename(columns={'Name':'Count'}).transpose()
def get_survived_dist(df):
surv_counts = {'A':{}, 'B':{}, 'C':{}, 'D':{}, 'E':{}, 'F':{}, 'G':{}, 'M':{}}
decks = df.columns.levels[0]
for deck in decks:
for survive in range(0, 2):
surv_counts[deck][survive] = df[deck][survive][0]
df_surv = pd.DataFrame(surv_counts)
surv_percentages = {}
for col in df_surv.columns:
surv_percentages[col] = [(count / df_surv[col].sum()) * 100 for count in df_surv[col]]
return surv_counts, surv_percentages
def display_surv_dist(percentages):
df_survived_percentages = pd.DataFrame(percentages).transpose()
deck_names = ('A', 'B', 'C', 'D', 'E', 'F', 'G', 'M')
bar_count = np.arange(len(deck_names))
bar_width = 0.85
not_survived = df_survived_percentages[0]
survived = df_survived_percentages[1]
plt.figure(figsize=(20, 10))
plt.bar(bar_count, not_survived, color='#b5ffb9', edgecolor='white', width=bar_width, label="Not Survived")
plt.bar(bar_count, survived, bottom=not_survived, color='#f9bc86', edgecolor='white', width=bar_width, label="Survived")
plt.xlabel('Deck', size=15, labelpad=20)
plt.ylabel('Survival Percentage', size=15, labelpad=20)
plt.xticks(bar_count, deck_names)
plt.tick_params(axis='x', labelsize=15)
plt.tick_params(axis='y', labelsize=15)
plt.legend(loc='upper left', bbox_to_anchor=(1, 1), prop={'size': 15})
plt.title('Survival Percentage in Decks', size=18, y=1.05)
plt.show()
all_surv_count, all_surv_per = get_survived_dist(df_all_decks_survived)
display_surv_dist(all_surv_per)
特征工程
- 对于票价属性的处理:分箱
票价特征正偏,右端生存率极高。13个分位数的箱子用于票价功能。虽然箱子很多,但它们也能提供可观的信息增益。图中左侧的组存活率最低,右侧的组存活率最高。这种高存活率在分布图中不可见。中间还有一个不寻常的群体(15.742,23.25),存活率高。
df_all['Fare'] = pd.qcut(df_all['Fare'], 13)
- 对年龄属性的处理吗:分箱
年龄特征是一个正态分布,有一些尖峰和颠簸和10分位数为基础的仓位年龄。第一仓成活率最高,第四仓成活率最低。这些是分布中最大的峰值。还有一个不寻常的群体(34.0,40.0%)存活率高,在这个过程中被捕捉到。
- 对SibSp、Parch的处理:
通过添加SibSp、Parch和1来创建属性,描述家族的大小。SibSp是兄弟姐妹和配偶的计数,Parch是父母和子女的计数。添加这些列是为了得到家族的大小。最后加1(即为当前乘客)。图表清楚地表明,家庭规模是生存率的一个预测因素,因为不同的数值有不同的生存率。
带有1的族大小标记为“单独” 带有2、3和4的族大小标记为“小” 带有5和6的家庭尺寸标记为“中等”
带有7、8和11的家庭大小标记为“大” - 对票价属性的处理: 有太多独特的票价特征值需要分析,所以按频率对它们进行分组。
这项特征与家庭大小特征有何不同?许多旅客是结伴旅行的。这些团体包括朋友、保姆、女佣等。他们不算是家人,但他们用的是同一张票。
为什么不按车票的前缀分组呢?如果票证特性中的前缀有任何含义,那么它们已经被捕获到Pclass(阶层)或boarded(上船)特性中,因为这可能是从Ticket特性中派生的唯一逻辑信息。
根据下图,第2、3和4成员组的存活率较高。独自旅行的乘客存活率最低。第4组成员后,存活率急剧下降。这一模式与家族大小特征非常相似,但也有细微差别。综上,在这里不适用票价频率值,因为这样会创建具有高度相关性的相同特性。这种特征不会提供任何额外的信息增益。
df_all['Ticket_Frequency'] = df_all.groupby('Ticket')['Ticket'].transform('count')
- 对头衔的处理和修改:
通过提取名称前前缀功能创建标题。根据下面的图表,有很多标题出现的次数很少。有些标题似乎不正确,需要更换。小姐、夫人、女士、女士、女士、伯爵夫人、女人的头衔都被小姐/夫人/女士取代,因为她们都是女性。像Mlle、Mme和Dona这样的值实际上是乘客的名字,但它们被归类为标题,因为名称特征是用逗号分隔的。博士,上校,少校,乔科尔,上尉,长官,堂和牧师头衔被替换为博士/军事/贵族/神职人员,因为这些乘客具有相似的特征。大师是一个独一无二的头衔。它是给26岁以下的男性乘客的,是所有男性中存活率最高的。
已婚是一个基于Mrs标题的二元特征。在其他女性头衔中,拥有Mrs头衔(即已婚妇女)的存活率最高。所以将它作为一个特征。
#关于string.punctuation,见这篇博客:https://blog.csdn.net/kongsuhongbaby/article/details/83181768
#关于.split():结果返回一个划分的列表
#函数用来提取家族属性和头衔属性
def extract_surname(data):
families = []
for i in range(len(data)):
name = data.iloc[i]
if '(' in name:
name_no_bracket = name.split('(')[0]
else:
name_no_bracket = name
family = name_no_bracket.split(',')[0]
title = name_no_bracket.split(',')[1].strip().split(' ')[0]
for c in string.punctuation:
family = family.replace(c, '').strip()
families.append(family)
return families
df_all['Family'] = extract_surname(df_all['Name'])
df_train = df_all.loc[:890]
df_test = df_all.loc[891:]
dfs = [df_train, df_test]
#关于split(),看这篇博客:https://blog.csdn.net/liuweiyuxiang/article/details/90936521
df_all['Title'] = df_all['Name'].str.split(', ', expand=True)[1].str.split('.', expand=True)[0]
df_all['Is_Married'] = 0
df_all['Is_Married'].loc[df_all['Title'] == 'Mrs'] = 1
目标编码
- 提取姓氏功能用于从姓名特征中提取乘客的姓氏。使用提取的姓氏创建家族特征。
def extract_surname(data):
families = []
for i in range(len(data)):
name = data.iloc[i]
if '(' in name:
name_no_bracket = name.split('(')[0]
else:
name_no_bracket = name
family = name_no_bracket.split(',')[0]
title = name_no_bracket.split(',')[1].strip().split(' ')[0]
for c in string.punctuation:
family = family.replace(c, '').strip()
families.append(family)
return families
df_all['Family'] = extract_surname(df_all['Name'])
df_train = df_all.loc[:890]
df_test = df_all.loc[891:]
dfs = [df_train, df_test]
- 由于测试集中没有生存特征,所以从训练集中的家庭计算家庭存活率。将创建一个同时出现在训练集和测试集中的家族名列表。对于该列表中有1个以上成员的家庭计算生存率,并将其存储在“家庭生存率”功能中。
为测试集唯一(即仅出现在测试集)的家族创建一个额外的二进制特征族“生存率”。这一特征也是必要的,因为无法计算这些家庭的存活率。这一特征意味着家庭存活率不适用于这些乘客,因为无法检索他们的存活率。
票价_生存率和票价_生存率_缺失特征也是用相同的方法创建的。票价_生存率和家庭_生存率取平均值成为生存率,票价_生存率_缺失和家庭_生存率_缺失也取平均值,成为生存率_缺失。
#创建一个在训练和测试集中都存在的家庭和票价属性列表
non_unique_families = [x for x in df_train['Family'].unique() if x in df_test['Family'].unique()]
non_unique_tickets = [x for x in df_train['Ticket'].unique() if x in df_test['Ticket'].unique()]
df_family_survival_rate = df_train.groupby('Family')['Survived', 'Family','Family_Size'].median()
df_ticket_survival_rate = df_train.groupby('Ticket')['Survived', 'Ticket','Ticket_Frequency'].median()
family_rates = {}
ticket_rates = {}
#df_family_survival_rate.index是指所有的家族名称
#以下两段函数,分别生成了family_rates和ticket_rates两个字典。family_rates键代表家族名,值代表生存率的中位数。ticket_rates同理。
for i in range(len(df_family_survival_rate)):
# Checking a family exists in both training and test set, and has members more than 1
#检查家族这个属性是否在训练集和验证集中都出现过,并且成员数量>1
if df_family_survival_rate.index[i] in non_unique_families and df_family_survival_rate.iloc[i, 1] > 1:
family_rates[df_family_survival_rate.index[i]] = df_family_survival_rate.iloc[i, 0]
for i in range(len(df_ticket_survival_rate)):
# Checking a ticket exists in both training and test set, and has members more than 1
#检查票价这个属性是否在训练集和验证集中都出现过,并且成员数量>1
if df_ticket_survival_rate.index[i] in non_unique_tickets and df_ticket_survival_rate.iloc[i, 1] > 1:
ticket_rates[df_ticket_survival_rate.index[i]] = df_ticket_survival_rate.iloc[i, 0]
mean_survival_rate = np.mean(df_train['Survived'])
train_family_survival_rate = []
train_family_survival_rate_NA = []
test_family_survival_rate = []
test_family_survival_rate_NA = []
#分为在训练集和测试集中都出现和只出现在df_train中这样两种。
#利用上一段代码得到的family_rates和ticket_rates来创建属性。
for i in range(len(df_train)):
if df_train['Family'][i] in family_rates:
train_family_survival_rate.append(family_rates[df_train['Family'][i]])
train_family_survival_rate_NA.append(1)
else:
train_family_survival_rate.append(mean_survival_rate)
train_family_survival_rate_NA.append(0)
df_train['Family_Survival_Rate'] = train_family_survival_rate
df_train['Family_Survival_Rate_NA'] = train_family_survival_rate_NA
df_test['Family_Survival_Rate'] = test_family_survival_rate
df_test['Family_Survival_Rate_NA'] = test_family_survival_rate_NA
train_ticket_survival_rate = []
train_ticket_survival_rate_NA = []
test_ticket_survival_rate = []
test_ticket_survival_rate_NA = []
for i in range(len(df_train)):
if df_train['Ticket'][i] in ticket_rates:
train_ticket_survival_rate.append(ticket_rates[df_train['Ticket'][i]])
train_ticket_survival_rate_NA.append(1)
else:
train_ticket_survival_rate.append(mean_survival_rate)
train_ticket_survival_rate_NA.append(0)
for i in range(len(df_test)):
if df_test['Ticket'].iloc[i] in ticket_rates:
test_ticket_survival_rate.append(ticket_rates[df_test['Ticket'].iloc[i]])
test_ticket_survival_rate_NA.append(1)
else:
test_ticket_survival_rate.append(mean_survival_rate)
test_ticket_survival_rate_NA.append(0)
df_train['Ticket_Survival_Rate'] = train_ticket_survival_rate
df_train['Ticket_Survival_Rate_NA'] = train_ticket_survival_rate_NA
df_test['Ticket_Survival_Rate'] = test_ticket_survival_rate
df_test['Ticket_Survival_Rate_NA'] = test_ticket_survival_rate_NA
特征变换
- 对部分类别属性进行序列化编码:
上船、性别、舱位、头衔和家庭大小为对象类型,年龄和票价特征为类别类型。对上述类型使用LabelEncoder转换为数字类型。LabelEncoder基本上从0到n标记类别。这个过程对于模型学习这些特性是必要的。
non_numeric_features = ['Embarked', 'Sex', 'Deck', 'Title', 'Family_Size_Grouped', 'Age', 'Fare']
for df in dfs: #这里dfs是训练集和测试集的并集
for feature in non_numeric_features:
df[feature] = LabelEncoder().fit_transform(df[feature])
- 对部分类别属性进行热编码:
分类特征(Pclass、Sex、Deck、boarded、Title)通过Onehotcoder转换为一个热编码特性。年龄和票价特征不会转换,因为它们与前面的属性不同,它们是有序的。
cat_features = ['Pclass', 'Sex', 'Deck', 'Embarked', 'Title', 'Family_Size_Grouped']
encoded_features = []
#建立了一个dataframe,值为每个属性生成的OneHotEncoder,列名是通过列表来实现
for df in dfs:
for feature in cat_features:
encoded_feat = OneHotEncoder().fit_transform(df[feature].values.reshape(-1, 1)).toarray()
n = df[feature].nunique()
cols = ['{}_{}'.format(feature, n) for n in range(1, n + 1)]
encoded_df = pd.DataFrame(encoded_feat, columns=cols)
encoded_df.index = df.index
encoded_features.append(encoded_df)
df_train = pd.concat([df_train, *encoded_features[:6]], axis=1)
df_test = pd.concat([df_test, *encoded_features[6:]], axis=1)
结论
- 年龄和票价特征被结合在一起。装箱有助于处理异常值并且揭示了这些特征中的一些相同性质的组。通过添加Parch和SibSp特征和1来创建家族大小。票价_生存率是通过计算票价的出现次数来创建的。
- 名字特征非常有用。首先,标题和已婚特征是由名字的前缀创建的。其次,通过目标编码乘客姓氏,生成Family_Survival_Rate和Family_Survival_Rate_NA特征。
- 非数值型特征采用标签编码,分类特征采用热编码。创建了5个新的特征(家庭大小,头衔,已婚,生存率和生存率_缺失),并删除了编码后无用的特征。
df_all = concat_df(df_train, df_test)
drop_cols = ['Deck', 'Embarked', 'Family', 'Family_Size', 'Family_Size_Grouped', 'Survived',
'Name', 'Parch', 'PassengerId', 'Pclass', 'Sex', 'SibSp', 'Ticket', 'Title',
'Ticket_Survival_Rate', 'Family_Survival_Rate', 'Ticket_Survival_Rate_NA', 'Family_Survival_Rate_NA']
df_all.drop(columns=drop_cols, inplace=True)
随机森林
- 训练集和测试集的生成:
X_train = StandardScaler().fit_transform(df_train.drop(columns=drop_cols))
y_train = df_train['Survived'].values
X_test = StandardScaler().fit_transform(df_test.drop(columns=drop_cols))
- 创建了两个随机森林分类器,其中一个是单个模型,另一个用于k-折叠交叉验证。
single_best_model = RandomForestClassifier(criterion='gini',
n_estimators=1100,
max_depth=5,
min_samples_split=4,
min_samples_leaf=5,
max_features='auto',
oob_score=True,
random_state=SEED,
n_jobs=-1,
verbose=1)
leaderboard_model = RandomForestClassifier(criterion='gini',
n_estimators=1750,
max_depth=7,
min_samples_split=6,
min_samples_leaf=6,
max_features='auto',
oob_score=True,
random_state=SEED,
n_jobs=-1,
verbose=1)
- 在公共排行榜中,single_best_model
的最高准确度为0.82775。然而,它在k-fold交叉验证中的性能并不好。所以,这是一个很好的用来实验和调参的模型。
在5折交叉验证的公共排行榜中,leaderboard_model的最高准确度为0.83732。此模型是为排行榜分数创建的,它被调整为稍微过拟合。之所以它被设计为过拟合,是因为每一次X_检验的估计概率要除以N(N为折数)。如果将此模型作为单一模型使用,则很难正确预测大量样本。 - 我应该使用哪种模型?
leaderboard_model过拟合测试集,所以不建议在实际项目中使用这样的模型。
single_best_model是个很好的开始实验和学习决策树的模型
StratifiedKFold用于分层目标变量。每一折是通过在目标变量(是否存活)中保留每个类的样本百分比来实现的。
#np.zeros(shape, dtype=float, order='C'),若shape是一个维度为mxn的矩阵形式,则:zeros((m,n),...)
#关于roc_auc的知识,大家看这篇博客:https://blog.csdn.net/w1301100424/article/details/84546194
N = 5
oob = 0
probs = pd.DataFrame(np.zeros((len(X_test), N * 2)), columns=['Fold_{}_Prob_{}'.format(i, j) for i in range(1, N + 1) for j in range(2)])
importances = pd.DataFrame(np.zeros((X_train.shape[1], N)), columns=['Fold_{}'.format(i) for i in range(1, N + 1)], index=df_all.columns)
fprs, tprs, scores = [], [], []
skf = StratifiedKFold(n_splits=N, random_state=N, shuffle=True)
#trn_idx是指训练集,val_idx是指本地验证集,fold是交叉验证的折数
for fold, (trn_idx, val_idx) in enumerate(skf.split(X_train, y_train), 1):
print('Fold {}\n'.format(fold))
# 拟合模型
leaderboard_model.fit(X_train[trn_idx], y_train[trn_idx])
# 计算训练集上AUC得分
trn_fpr, trn_tpr, trn_thresholds = roc_curve(y_train[trn_idx], leaderboard_model.predict_proba(X_train[trn_idx])[:, 1])
trn_auc_score = auc(trn_fpr, trn_tpr)
# 计算验证AUC得分
val_fpr, val_tpr, val_thresholds = roc_curve(y_train[val_idx], leaderboard_model.predict_proba(X_train[val_idx])[:, 1])
val_auc_score = auc(val_fpr, val_tpr)
scores.append((trn_auc_score, val_auc_score))
fprs.append(val_fpr)
tprs.append(val_tpr)
# X_test发生的可能性
#predict是训练后返回预测结果,是标签值。
#而predict_proba返回的是一个 n 行 k 列的数组, 第 i 行 第 j 列上的数值是模型预测 第 i 个预测样本为某个标签的概率,并且每一行的概率和为1。
probs.loc[:, 'Fold_{}_Prob_0'.format(fold)] = leaderboard_model.predict_proba(X_test)[:, 0] #预测为0的
probs.loc[:, 'Fold_{}_Prob_1'.format(fold)] = leaderboard_model.predict_proba(X_test)[:, 1] #预测为1的
importances.iloc[:, fold - 1] = leaderboard_model.feature_importances_
#RandomForestClassifier(n_estimators=200,oob_score=True),其中obb_score:用于选择最佳特征组合。
#这里的obb_score_:返回模型的精度
oob += leaderboard_model.oob_score_ / N
print('Fold {} OOB Score: {}\n'.format(fold, leaderboard_model.oob_score_))
特征重要性图
importances['Mean_Importance'] = importances.mean(axis=1)
importances.sort_values(by='Mean_Importance', inplace=True, ascending=False)
plt.figure(figsize=(15, 20))
sns.barplot(x='Mean_Importance', y=importances.index, data=importances)
plt.xlabel('')
plt.tick_params(axis='x', labelsize=15)
plt.tick_params(axis='y', labelsize=15)
plt.title('Random Forest Classifier Mean Feature Importance Between Folds', size=15)
plt.show()
ROC曲线
def plot_roc_curve(fprs, tprs):
tprs_interp = []
aucs = []
mean_fpr = np.linspace(0, 1, 100)
f, ax = plt.subplots(figsize=(15, 15))
#为每一折交叉验证绘制ROC图并且计算AUC值
#enumerate(sequence, [start=0]),其中start表示下标开始的数值。其中,循环里的i代表start下标值
#numpy.interp(x,xp,fp)主要使用场景为一维线性插值。就是通过xp.fp序列的拟合,来得到x对应的值
for i, (fpr, tpr) in enumerate(zip(fprs, tprs), 1):
tprs_interp.append(np.interp(mean_fpr, fpr, tpr))
tprs_interp[-1][0] = 0.0
roc_auc = auc(fpr, tpr)
aucs.append(roc_auc)
ax.plot(fpr, tpr, lw=1, alpha=0.3, label='ROC Fold {} (AUC = {:.3f})'.format(i, roc_auc))
# 绘制ROC图
plt.plot([0, 1], [0, 1], linestyle='--', lw=2, color='r', alpha=0.8, label='Random Guessing')
mean_tpr = np.mean(tprs_interp, axis=0)
mean_tpr[-1] = 1.0
mean_auc = auc(mean_fpr, mean_tpr)
std_auc = np.std(aucs)
# 绘制ROC均值
ax.plot(mean_fpr, mean_tpr, color='b', label='Mean ROC (AUC = {:.3f} $\pm$ {:.3f})'.format(mean_auc, std_auc), lw=2, alpha=0.8)
# 绘制平均ROC曲线的标准差
std_tpr = np.std(tprs_interp, axis=0)
tprs_upper = np.minimum(mean_tpr + std_tpr, 1)
tprs_lower = np.maximum(mean_tpr - std_tpr, 0)
ax.fill_between(mean_fpr, tprs_lower, tprs_upper, color='grey', alpha=.2, label='$\pm$ 1 std. dev.')
ax.set_xlabel('False Positive Rate', size=15, labelpad=20)
ax.set_ylabel('True Positive Rate', size=15, labelpad=20)
ax.tick_params(axis='x', labelsize=15)
ax.tick_params(axis='y', labelsize=15)
ax.set_xlim([-0.05, 1.05])
ax.set_ylim([-0.05, 1.05])
ax.set_title('ROC Curves of Folds', size=20, y=1.02)
ax.legend(loc='lower right', prop={'size': 13})
plt.show()
plot_roc_curve(fprs, tprs)
最终提交
#endwith()判断字符串是否以指定后缀结尾,如果以指定后缀结尾返回 True,否则返回 False。
#将属于类别1的概率的列放入class_survived中
class_survived = [col for col in probs.columns if col.endswith('Prob_1')]
probs['1'] = probs[class_survived].sum(axis=1) / N #属于类别1(即是幸存者)的概率
probs['0'] = probs.drop(columns=class_survived).sum(axis=1) / N #属于类别0的概率
probs['pred'] = 0 #先对所有的类别值赋为0
#将属于类别1的概率大于0.5的筛选出来,类别标为1
pos = probs[probs['1'] >= 0.5].index
probs.loc[pos, 'pred'] = 1
y_pred = probs['pred'].astype(int)
submission_df = pd.DataFrame(columns=['PassengerId', 'Survived'])
submission_df['PassengerId'] = df_test['PassengerId']
submission_df['Survived'] = y_pred.values
submission_df.to_csv('submissions.csv', header=True, index=False)
submission_df.head(10)