锐单电子商城 , 一站式电子元器件采购平台!
  • 电话:400-990-0325

机器学习完整项目实战附代码(一):探索型数据分析+特征工程+建模+报告

时间:2022-10-07 08:30:00 a1型变送器

1. 项目背景

??泰坦尼克号沉没是历史上最臭名昭著的沉船之一。1912年4月15日,在她的处女航空,被广泛认为不沉的泰坦尼克号在与冰山相撞后沉没。不幸的是,船上的每个人都没有足够的救生艇,导致224名乘客和船员中有1502人死亡。虽然生存中有一些运气因素,但似乎有些群体比其他群体更有可能生存。在这里,建立一个预测模型来回答这个问题:什么样的人更有可能生存?使用乘客数据(即姓名、年龄、性别、社会经济阶层等)。

1.1 项目目标:

??这是一个监督分类机器学习任务:给定一组包含目标(在本例中幸存)survived)的数据,我们希望训练一个可以学习将特征(也称为解释变量)映射到目标的模型。

  • 监督问题: 我们可以知道数据的特征和目标,我们的目标是训练可以学习两者之间映射关系的模型。
  • 分类问题: survived是离散变量。

??在训练中,我们希望模型能够学习特征和分数之间的关系,所以我们给出了特征和答案。然后,为了测试模型的学习效果,我们评估了一个从未见过答案的测试集

  1. 使用提供的泰坦尼克号人员数据开发一个模型,可以预测哪些人员更有可能生存
  2. 然后解释结果,找到最可预测的变量。

数据集
链接:https://pan.baidu.com/s/1tk9Qmni1rPghyNFKBaOO5w
提取码:9pgb

1.2 工作流程

  1. 数据清理和格式化
  2. 探索性数据分析
  3. 特征工程:数据预处理、特征选择、[特征减少]
  4. 比较几种基于性能指标的机器学习模型
  5. 最佳模型超参数调整
  6. 在测试集中评估最佳模型
  7. 解释模型结果
  8. 得出结论和报告

1.3 导入库

项目所需的工具

  • 使用标准数据科学和机器学习库:numpy,pandas和sckit-learn
  • 使用matplotlib和seaborn进行可视化
  • 输入缺失值和缩放值:sklearn.impute,sklearn.preprocessing
  • 机器学习模型:
  • 将数据分为训练集和测试集:from sklearn.model_selection import train_test_split
  • 超参数调整:from sklearn.model_selection import RandomizedSearchCV, GridSearchCV
  • 复制对象:copy
  • 解释模型:lime
#用于数据操作pandas和numpy import numpy as np import pandas as pd #设置DataFram显示数量 pd.set_option('display.max_column',60)#最多显示60列 #可视化工具包 import matplotlib.pyplot as plt import seaborn as sea # 如遇中文显示问题,可添加以下代码 from pylab import mpl mpl.rcParams['font.sans-serif'] = ['SimHei']  # 指定默认字体 mpl.rcParams['axes.unicode_minus'] = False  # 解决保存图像为负号的问题 # 复制对象 import copy 

2. 数据清理和格式化

2.1 加载和检查数据

# 把数据读入 pandas DataFrame data_raw=pd.read_excel(r'./Data/Titanic Dataset/titanic3.xls') data_clean= copy.deepcopy(data_raw) #查看数据 data_clean.head() 

在这里插入图片描述

#查看数据大小 data_clean.shape 

加载数据后,我们需要解决的问题:
1)解数据strong。
  我们通常会看到每一列的第一行是各种名词,就是所谓的表头,理解这些名词的含义对于处理数据非常重要,但是我们面对的数据来自各个领域,数据科学家不是精通各个领域专业知识的杂家,这时候就需要通过各种手段去理解数据:

  1. 是否幸存:1-是,0-否,为目标列。数值型
  2. 船舱等级:1/2/3:头等舱/二等舱/三等舱,头等舱更靠近救生艇。数值型
  3. 姓名:英文全名+头衔。字符串类型
  4. 性别:male/female。字符串类型
  5. 年龄:存在小于1的数值(如0.9167),存在缺失值,字符串类型
  6. 船上兄弟姐妹/配偶人数:数值型
  7. 乘客在船上的父母/孩子数量:数值型
  8. 船票号:字符串型
  9. 船票价:与船舱等级相关,也可能乘客年龄也可能相关
  10. 客舱号码:描述用户所住的船舱编号。由两部分组成,仓位号和房间编号。字符串类型。
  11. 登船的港口:描述乘客登船的港口。字符串类型
  12. 救生艇:数值型
  13. body:记录遇难者编号,存在缺失值,数值型。该特征包含目标信息(幸存),容易造成数据泄露现象,建议删除该列

数据泄露

  1. 特征包含目标信息:如body特征包含了目标"幸存"的信息
  2. 训练集和测试集存在交叉污染:
    • 划分数据集(训练集+测试集)前填充缺失值
    • 划分数据集(训练集+测试集)前做特征缩放
    • 其它操作导致测试集"包含"训练集信息

  数据泄露会导致模型在训练、评估时的正确率很高(训练集、测试集均表现很好),而在实际使用过程中正确率很低。

:仅做初步分析、探索规律(特征和目标、特征和特征关系)时,可以不考虑训练集和测试集交叉污染情况,而在正式做特征工程、建模、评估模型时一定要注意测试集和训练集的独立性以防数据泄露。

  舱位等级越低,所居住的位置就越靠近船舱的底部。泰坦尼克号共有10层甲板(不包含高级船员住舱的屋顶),其中8层供乘客使用。救生艇甲板:该甲板位于最顶层,因两侧安放了救生艇而得名

2)识别特征类型
特征主要类型有:时间型、数值型、类别型、文本型

  • 类别特征:类别型特征将相似属性归为一类,但是大多数模型都不能直接处理文本型数据,必须要转换为数值型才能使用。类别型特征可以分为定类变量、定序变量。
  1. 定类变量:如性别(男、女、其他),三种取值之间是相互独立的,彼此之间完全没有关系,这种变量称之为名义变量。
  2. 定序变量:如学历(小学、初中、高中),三种取值不是完全独立的,我们可以明显看出,在性质上可以有高中>初中>小学这样 的联系,学历有高低,但是学历的取值之间却不是可以计算的,我们不能说小学 + 某个取值 = 初中。这是有序变 量。
  • 数值特征:数值型随样本的不同而进行变化,一般分为连续型,离散型,经常使用归一化,离散化等方法进行处理。数值型特征可以分为定距变量、定定比变量。
  1. 定距变量:如温度(>25摄氏度、>30摄氏度 、>35摄氏度),各个取值之间有联系,且是可以互相计算的,而且两者的差值有 意义。比如35摄氏度 - 25摄氏度 = 10摄氏度,分类之间可以通过数学计算互相转换。这是有距变量。
  2. 定比变量:如质量(>10kg、>50kg、>100kg),各个取值之间有联系,不仅可以计算差值,还可以计算其商值。

本例中:

  • 定类变量:性别、登船的港口、是否幸存
  • 定序变量:船舱等级:分类变量,1、2、3
  • 混合型:船票号、客舱号、救生艇

2.2 数据类型和缺失值

  'dataframe.info’方法是一种通过显示每列的数据类型非缺失值的数量来评估数据的快速方法。注意若某列即存在字符串又存在数字,则意味着带有数字的列将不会表示为数字,因为pandas会将具有任何字符串值的列转换为所有字符串的列

data_clean.info()

  • 年龄字段应为数值型,将年龄字段转换为数值型特征
  • 性别字段为分类字段-定类,将性别字段编码为0,1,即转换为数值型特征,以观察其性质,后续要one_hot编码

2.2.1 将数据转换为正确类型

data_clean['年龄']=data_clean['年龄'].astype(float)
data_clean['性别']=data_clean['性别'].replace({ 
         'male':1,'female':0})
data_clean.info()


data_clean.describe(percentiles=None, include=None, exclude=None)作用是生成数值型数据的描述性统计数据,总结数据集分布的集中趋势,分散和形状,不包括 NaN值。参数含义:

  • percentiles:包括在输出中的百分位数。全部应该介于0和1之间。默认值为 ,返回第25,第50和第75百分位数。[.25, .5, .75]
  • include:默认是None 结果将包括所有数字列
  • exclude:默认是None,结果将不包含任何内容。

对于数值数据,则结果将包括count, mean,std,min,max以及第25,第50和第75百分位数,其中第50百分位数等价于中位数。

data_clean.describe()

2.3 处理缺失值

  数据存在缺失值是不可避免的现象,* 如果列中缺失值的比例很高,那么它对我们的模型可能不会有用,可以选择删除。删除列的阈值取决于实际问题。

现在已经有了正确的列数据类型,可以通过查看每列中缺失值的百分比来进行分析

  • 可视化缺失值
import missingno as msno
msno.matrix(data_clean)
# msno.bar(data_clean)

2.3.1 删除缺失值占比大于阈值的列

  • 计算数据特列特征缺失值数量以及比例
def missing_values_table(df):#输入:dataframe数据,输出:缺失总量即比例,降序输出
    #计算总的缺失值数量并降序处理
    mis_val = df.isnull().sum().sort_values(ascending=False)
    mis_val = mis_val[mis_val>0]#提取有缺失值的列
    #计算缺失值比例
    percent = round(mis_val* 100 /len(df),2)
    mis_val_table_ren_columns=pd.concat([mis_val,percent], axis=1, keys=['Missing Values','Percent'])
    #打印总结信息:总的列数,有数据缺失的列数
    print ("数据集共有 " + str(df.shape[1]) + " 列.\n"+"其中 " + str(mis_val_table_ren_columns.shape[0]) +
              " 列有缺失值")
    # 返回带有缺失值信息的dataframe
    return mis_val_table_ren_columns
#查看缺失值
missing_values_table(data_clean)


  在处理真实世界数据时的一个常见问题是缺失值。缺失值可能是由各种原因引起的,在我们训练机器学习模型之前必须填充或删除这些缺失值。首先,让我们了解每列中有多少缺失值。如果列中缺失值的比例很高,那么它对我们的模型可能不会有用。

缺失值应该达到多少比例时才允许删除,这取决于实际问题,对于此项目:
我们将删除缺失值超过50%的列:此外,具体特征具体分析,若特征很有必要,则即使缺失值比例很高也选择保留

  • body特征列包含目标信息且缺失值占比达90.8%。选择直接删除处理×
  • 救生艇对乘客能否有机会存活有很大影响,所以虽然其缺失值占比达62.87%,这里暂时先保留该列**…**
  • 由于客舱号码可以够描述乘客居住位置,不同船舱距离甲板的距离不同,那么不同船舱的乘客能够登上甲板的顺序肯定不同,不考虑其他因素,这也会对他们能否有机会存活起到一定影响,所以虽然其缺失值占比达77.46%,这里暂时先保留该列,然后在训练模型的时候检测有无该列时对模型的影响,然后决定是否保留该各种**…**
  • 其他字段缺失值占比低于50%,这里暂时均保留
#删除body列
data_clean = data_clean.drop(['body','救生艇'],axis=1)

2.3.2填充缺失值

缺失项

  • 年龄
  • 船票价
  • 登船的港口
  • 客舱号码

填充策略

  1. 直接填充以(均值、中位数、众数)填充缺失列的值
  2. 计算相关系数,根据最相关的几个字段筛选后取值(均值、中位数、众数)来填充
  3. 计算相关系数,以最相关的几个字段为基准,选择距离最近即最相似的样本对应的值来填充(KNN)
  4. 通过模型预测填充
  • 策略1:填充方式比较粗暴,不够合理,适用于缺失值较少
  • 策略2:填充方式较为合理,适用于缺失值较少
  • 策略3:KNN填充,适用于没有连片缺失的数据(比如最相似的k个样本都缺失,KNN则不在起作用)
  • 策略4:更准确
  1. 分析年龄

由于年龄是连续字段,可以查看与其相关度最高的字段,并根据相关性最高的字段填充(均值、中位数、众数),而不是直接粗暴的填充(均值、中位数、众数)。

  • 计算相关系数
#计算相关系数
data_clean.corr().abs()['年龄'].sort_values(ascending=False)#ascending=False,降序,默认:True,升序


  可以看到船舱等级字段和年龄字段关联度最高,其次是船上兄弟姐妹/配偶人数,那么据此将他们进行筛选,得到每个船舱等级、船上兄弟姐妹/配偶人数对应船舱等级的平均年龄,据此来填充年龄字段的缺失值。

groups = data_clean[['船舱等级','船上兄弟姐妹/配偶人数','年龄']].groupby(['船舱等级','船上兄弟姐妹/配偶人数']).mean()
groups

  • 填充年龄字段缺失值
for i in data_clean['年龄'].index:
    if pd.isnull(data_clean['年龄'][i]):
        #value_age = groups.loc['female',:].loc[1,'年龄']
        value_age = groups.loc[data_clean['船舱等级'][i],:].loc[data_clean['船上兄弟姐妹/配偶人数'][i],'年龄']
        data_clean.loc[i,'年龄']= value_age
missing_values_table(data_clean)


2. 填充登船的港口的缺失值

登船的港口字段为定类数据且缺失值较少,选择用众数进行填充

#使用港口的众数填充缺失值
value_mode = data_clean.登船的港口.dropna().mode()[0]
data_clean.登船的港口=data_clean.登船的港口.fillna(value=value_mode)
missing_values_table(data_clean)


3. 填充船票价缺失值

  船票价是连续字段,可以查看与其相关度最高的字段,并根据相关性最高的字段填充(均值、中位数、众数),而不是直接粗暴的填充(均值、中位数、众数)。

#计算相关系数
data_clean.corr().abs()['船票价'].sort_values(ascending=False)#ascending=False,降序,默认:True,升序

  • 船票价字段与船舱等级关联度最高,其次是幸存、乘客在船上的父母/孩子数量。
  • 由于只缺失一个数值,即没有连片缺失,所以这里采用KNN填充,根据船舱等级、幸存字段判断距离最近,寻找即最相似的的乘客对应的票价

注意关联字段不能选择幸存,因为幸存字段是目标字段,根据幸存字段来填充缺失值,会使特征空间包含目标信息,从而导致数据泄露

def KNNImputerNum(inx,k=1):
    #inx = data_clean[['Latitude','Longitude','Borough']]
    result = copy.deepcopy(inx)
    base = copy.deepcopy(inx.iloc[:,:-1])
    col = list(result.columns)[-1]
    for row in result.index:
        if pd.isnull(result.loc[row,col]):
            #计算距离
            dis = np.linalg.norm((base-base.loc[row,:]),axis=1)
            index = dis.argsort()[k]
            while pd.isnull(inx.iloc[index,-1]) and k<dis.shape[0]:
                k+=1
                index = dis.argsort()[k]
            if k<dis.shape[0]:
                #填充缺失值
                result.loc[row,col]=inx.iloc[index,-1]
            else:
                print('无法填充,该'+col+'整列缺失!')
                return
    return result
#填充信息 
tmp =copy.deepcopy(data_clean[['船舱等级','乘客在船上的父母/孩子数量','船票价']])
data_clean[['船舱等级','乘客在船上的父母/孩子数量','船票价']]=KNNImputerNum(tmp,k=1)

missing_values_table(data_clean)


4.分析客舱号码

  客舱号码缺失值比例很大,达到近78%,通常缺失项很大的特征一般会选择直接删除,但由于客舱号码特征比较重要,客舱号码特征可以反映乘客所在的位置,即可以透露出乘客距离救生艇的远近,从而影响乘客生还率这一目标,所以这里暂时先选择保留以分析其性质,并在训练模型中观察这一特征有无对模型的影响,从而决定是否保留这一特征。

  • 客舱号码由舱位号和编号(如B5,B代表舱位,可能与船舱等级有关,5可能表示床铺号或者房间号,不同等级的船舱的床铺数也不同)
  • 使用同一张的船票的乘客对应舱位号应相同
  • 提取客舱号码舱位号,对客舱号统一码格式化为舱位号格式

对客舱号码字段作如下处理:

  1. 提取客舱号码字段的舱位号
  2. 根据船票号填充缺失项的舱位号
  3. 查看此时客舱号码缺失比例,若缺失比例依然很大,则缺失值替换为字母’U’(unknown)
  • 提取客舱号码舱位号
#提取字符
import re
# grade_num = data_clean.dropna(subset=['客舱号码']).loc[:,['船舱等级','客舱号码']]
grade_num = data_clean.loc[:,['船舱等级','客舱号码']]

for i in grade_num.dropna(subset=['客舱号码']).index:
    strs = list(set(re.findall('[a-zA-Z]+',grade_num.loc[i,'客舱号码'])))
    if len(set(strs))>1:
        strs = ''.join(strs)
    else:
        strs = strs[0]
    data_clean.loc[i,'客舱号码'] = strs

#查看客舱号码字段类别
data_clean.客舱号码.unique()


根据船票号填充客舱号码缺失项

null_lists = data_clean[pd.isnull(data_clean.客舱号码)].index
# tmp = data_clean[['船票号','客舱号码']]
for i in  null_lists:
    ticket_num = data_clean.loc[i,'船票号']
    cabin_lists = set(list(data_clean[data_clean.船票号==ticket_num].客舱号码.dropna()))
    if len(cabin_lists)>0:
# print(cabin_lists)
        data_clean.loc[i,'客舱号码']=cabin_lists

missing_values_table(data_clean) 


客舱号码字段仍有大量缺失值,对缺失项统一填充为Unknown的首字母’U’。

data_clean.客舱号码=data_clean.客舱号码.fillna(value='U')
missing_values_table(data_clean)

data_clean['客舱号码'].unique()


此时可查看客舱号码与其他特征的关系图

  • 客舱号码与船舱等级的关系
# barch(data_clean,'船舱等级','幸存')
def barch(inx,tab1,tab2):#tab1:,tab2:lenged类别
    #生成临时列
    l = len(inx[tab1].unique())
    df = pd.DataFrame(inx[tab1].unique(),columns=['tmp'])
    df.index=df['tmp'].values
    #统计每种类别(tab1)在不同类别tab2下的数量
    lists = inx[tab2].unique().tolist()
    for li in lists:
        counts = inx[tab1][inx[tab2]==li].value_counts()
        
        tmp = pd.DataFrame({ 
         str(li):counts})
        df = pd.concat([df,tmp],axis=1)
    df=df.drop(['tmp'],axis=1)#删除临时列
    
    # 添加计数列
    df_count=pd.DataFrame(df.apply(lambda x:x.sum(),axis=1),columns=['counts'])
    #生成百分比数据
    df_percentage = pd.concat([df,df_count],axis=1)
    #遍历列,计算百分比
    for col in df_percentage.columns:
        df_percentage[col]=round(df_percentage[col]/df_percentage['counts']*100,2)
    #删除原有计数列
    df_percentage = df_percentage.drop(['counts'],axis=1)
    
    #分别绘制堆积条形图和百分比堆积条形图
    fig,ax_arr = plt.subplots(1,2,figsize=(10, 5))
    
    df.plot(kind='barh', stacked=True,ax=ax_arr[0]).invert_yaxis()
    ax_arr[0].set_title(tab1+' vs '+tab2)
    ax_arr[0].set_xlabel('数量')
    ax_arr[0].set_ylabel(tab1)
    
    df_percentage.plot(kind='barh', stacked=True,ax=ax_arr[1]).invert_yaxis()
    ax_arr[1].set_title(tab1+' vs '+tab2)
    ax_arr[1].set_xlabel('百分比')
    ax_arr[1].set_ylabel(tab1)    
    
    plt.show()
    #防止文字遮挡
    plt.tight_layout()
# return df,df_percentage

barch(data_clean,'客舱号码','船舱等级')


如图,可以看出客舱号码很大程度上受制于船舱等级,头等舱乘客普遍居住在ABCDET这几个舱位,二等舱乘客则分散在DEF舱位

  • 查看各舱位等级与是否生还的关系
barch(data_clean,'客舱号码','幸存')


综上,虽然客舱号字段缺失值比例较大,但其与生存率有很大关联,所以暂时不做删除处理。

2.4 处理重复样本

  • 重复样本相当于对某部分样本集合的过采集,从而很可能会提高了这部分样本在全局Loss中所占的比重,模型求解的最终结果会偏向于降低这部分样本的训练误差,而牺牲其他样本的训练误差。
  • 重复未必代表冗余,重复的样本也反映了某种信息,某些样本出现的频率比较高,若直接删除可能导致某些模型不准确存在偏差

如何处理重复样本?删除or保留?

  1. 假设数据采集没有问题:
    • 重复数据本身代表了一种真实分布,也就是你的测试集也服从这种分布,那么不该删除,因为这种重复数据表明了某种类型的数据非常重要,出现频率非常高,你的模型该以此类为优先级
    • 由于样本各类别重复比例不一定相同,删除重复样本很可能会改变原数据集的分布的,从而影响模型。
  2. 结合实际业务分析:
    • 结合实际业务分析,比如泰坦尼克号数据,有没有特征完全相同的样本(乘客)?姓名、年龄、性别…,本项目选择直接删除重复处理(若存在重复样本)
    • 若样本id也一样,则明显是错误采集,那么应该删掉。
  3. 若重复样本比例比较大,可以选择新增特征列,表示样本出现的次数或者样本是否重复,然后删除重复的样本。
  • 检查是否有重复样本
data_clean.duplicated().value_counts()


没有重复样本,不做处理。
#data_clean=data_clean.drop_duplicates()

3. 探索性数据分析

  探索性数据分析(EDA)是一个开始式流程,我们制作绘图并计算统计数据,以便探索我们的数据。

  • 目的是找到异常,模式,趋势、分布或关系。 例如,找到两个变量之间的相关性、使用哪些特征可用于建模决策。
  • 简而言之,EDA的目标是确定我们的数据可以告诉我们什么! EDA通常以高级概述(high-level overview)开始,然后在我们找到要检查的感兴趣的区域时缩小到数据集的特定部分。

要开始EDA,我们将专注于幸存变量,它是我们的机器学习模型的目标。

通过 describe 和 matplotlib 可视化查看数据各个特征的相关统计量(柱状图)
'data.describe(percentiles=None,include=None,exclude=None)'作用是生成数值特征的描述性统计数据,总结数据集分布的集中趋势,,不包括NaN值。参数含义:

  • percentiles:包括在输出中的百分位数。全部应该介于0和1之间。默认值为第25,第50和第75百分位数
  • include:默认是None,结果将包括所有数字列
  • exclude:默认是None,结果将不包括任何内容。 对于数字数据,则结果将包括count,mean,std,min,max以及第25,第50和第75百分位数,其中第50百分位数等价于中位数
#统计每列信息
data_clean.describe() 

  • 可视化查看数据的相关统计量(柱状图)
data_desc = data_clean.describe()# 查看数据描述
data_desc=data_desc.drop(['count'],axis=0)# 去除count行
plt.figure(figsize=(15,5))# 控制画布大小

# _,color_lists=generate_colors(7,'Paired')
i = 0
for col in data_desc.columns:
    i+=1
    ax = plt.subplot(4,4,i)
    ax.set_title(col)
	#plt.bar(data_desc.index,data_desc[col],color=color_lists)
 

相关文章