美文网首页
kaggle入门之战——Titanic

kaggle入门之战——Titanic

作者: 单调不减 | 来源:发表于2019-06-07 23:48 被阅读0次

    机器学习的理论部分大致过了一遍了,下一步要理论联系实践了。Kaggle是一个很好的练手场,这个数据挖掘比赛平台最宝贵的资源有两个:

    • 各种真实场景产生的数据集。这些数据集并不像著名的MNIST、CIFAR-10等数据集一样整齐和完整,它们当中有缺失值,有难以处理的特征,也有离群值和噪声,这更接近实际应用中的情况,可以锻炼我们数据预处理、数据清洗、特征工程的能力。
    • 各路大牛在线分享经验,讨论从数据中发现的模式。这其实才是Kaggle最吸引人的地方,因为这些数据比赛并没有官方的最好答案,在这里一切以最后的score说话,很多高分team处理数据的方法是很值得学习的。

    和多数小白一样,在面对Kaggle众多比赛无从下手的时候,我选择从最简单的Titanic预测生还者比赛入门。

    1、数据清洗和预处理

    数据的质量决定模型能达到的上界。这话说的无比正确。机器学习的算法模型是用来挖掘数据中潜在模式的,但若是数据太过杂乱,潜在的模式就很难找到,更糟的情况是,我们所收集的数据的特征和我们想预测的标签之间并没有太大关联,这时候这个特征就像噪音一样只会干扰我们的模型做出准确的预测。

    综上所述,我们要对拿到手的数据集进行分析,并看看各个特征到底会不会显著影响到我们要预测的标签值。

    首先我们导入机器学习和数据挖掘中最常用也最强大的几个库——numpy、pandas、matplotlib。

    import numpy as np
    import pandas as pd
    import matplotlib.pyplot as plt
    

    第一步是使用pd.read_csv()方法导入数据:

    train = pd.read_csv(r'C:\Users\Sidney Nash\Desktop\data\train.csv')
    test = pd.read_csv(r'C:\Users\Sidney Nash\Desktop\data\test.csv')
    

    第二步是展示数据和观察数据(由于数据量大,一般用head()展示前五行):

    train.head()
    

    可以看到,每个乘客有12个属性,其中PassengerId在这里只起到索引作用,而Survived是我们要预测的目标,因此实际上我们需要处理的特征一共有10个,因此我们可以先去掉PassengerId和Survived,并对余下特征进行观察:

    drop_features=['PassengerId','Survived']
    train_drop=train.drop(drop_features,axis=1)
    train_drop.head()
    

    我们看到,这些特征的类型各不相同,有整数型的、浮点数型的,也有字符型的,我们先审视一下各特征的类型:

    train_drop.dtypes.sort_values()
    
    out:
    Pclass        int64
    SibSp         int64
    Parch         int64
    Age         float64
    Fare        float64
    Name         object
    Sex          object
    Ticket       object
    Cabin        object
    Embarked     object
    dtype: object
    

    然后按把同类型的放在一起:

    train_drop.select_dtypes(include='int64').head()
    
    train_drop.select_dtypes(include='float64').head()
    
    train_drop.select_dtypes(include='object').head()
    

    知道了各类型的特征都有哪些,下一步我们要看看特征中是否有缺失值:

    train.isnull().sum()[lambda x: x>0]
    
    out:
    Age         177
    Cabin       687
    Embarked      2
    dtype: int64
    

    可以看到,Age和Cabin两个特征的缺失值数量较多,于是我们接下来重点关注这两个特征的缺失值补全就行了,是这样吗?NoNoNo,这只是训练集train,我们不能想当然地认为test中的情形和train相同,我们看一下test中的情况:

    test.isnull().sum()[lambda x: x>0]
    
    out:
    Age       86
    Fare       1
    Cabin    327
    dtype: int64
    

    果然还是有些差别的,虽然差别很微小,但依然可能对模型结果产生影响。

    实际上,pandas中有更为方便的方法来列出缺失值以及各特征的一些统计信息,那就是info()和describe():

    train.info()
    
    out:
    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 891 entries, 0 to 890
    Data columns (total 12 columns):
    PassengerId    891 non-null int64
    Survived       891 non-null int64
    Pclass         891 non-null int64
    Name           891 non-null object
    Sex            891 non-null object
    Age            714 non-null float64
    SibSp          891 non-null int64
    Parch          891 non-null int64
    Ticket         891 non-null object
    Fare           891 non-null float64
    Cabin          204 non-null object
    Embarked       889 non-null object
    dtypes: float64(2), int64(5), object(5)
    memory usage: 83.6+ KB
    
    train.describe()
    

    当然这里我们只列出了train的统计信息,对test也是同理。进行到这里,我们发现每次都对train和test进行同样的操作很麻烦,后续我们添加新特征或删改原特征的时候更是如此,为什么不把train和test合并起来统一操作呢?下面我们把两者合并(注意,合并的时候不能让各个记录重新排序,否则我们难以再将train和test拆分开来):

    titanic=pd.concat([train, test], sort=False)
    #这里记录train中的记录个数是为了后面将train和test拆分开来
    len_train=train.shape[0]
    

    现在我们查看合并后的titanic的信息:

    titanic.info()
    
    out:
    <class 'pandas.core.frame.DataFrame'>
    Int64Index: 1309 entries, 0 to 417
    Data columns (total 12 columns):
    PassengerId    1309 non-null int64
    Survived       891 non-null float64
    Pclass         1309 non-null int64
    Name           1309 non-null object
    Sex            1309 non-null object
    Age            1046 non-null float64
    SibSp          1309 non-null int64
    Parch          1309 non-null int64
    Ticket         1309 non-null object
    Fare           1308 non-null float64
    Cabin          295 non-null object
    Embarked       1307 non-null object
    dtypes: float64(3), int64(4), object(5)
    memory usage: 132.9+ KB
    
    len_train==891
    
    out:
    True
    

    我们可以看到Age、Fare、Cabin和Embarked四个特征均有缺失值(注意这里的Survived并没有缺失值,因为只有train有标签Survived,test的Survived是我们的训练目标)。

    处理缺失值一般有两种方式,第一种就是舍弃包含较多缺失值的特征,这个做法对我们这个小规模的数据集来说不太适用,因为我们本来掌握的信息就不多,若再舍弃一些信息,可能会造成模型效果不佳。第二种是进行缺失值补全,一般根据实际情况将缺失值补全为0、特征列均值或中位数,或者将缺失值划入一个新的类别特殊处理。

    在进行特征值补全之前,我们先处理一个虽然没有缺失值但很难利用的特征——Name。

    每个人的名字都是独一无二的,名字和幸存与否看起来并没有直接关联,那怎么利用这个特征呢?有用的信息其实隐藏在称呼当中,比如我们猜测上救生艇的时候是女士优先,那么称呼为Mrs和Miss的就比称呼为Mr的更可能幸存。于是我们从Name特征中其称呼并建立新特征列Title。

    titanic['Title'] = titanic.Name.apply(lambda name: name.split(',')[1].split('.')[0].strip())
    

    看一下效果:

    titanic.head()
    

    ok,我们已经把称呼提取出来了,下面我们来看看称呼的种类和数量:

    titanic.Title.value_counts()
    
    out:
    Mr              757
    Miss            260
    Mrs             197
    Master           61
    Rev               8
    Dr                8
    Col               4
    Mlle              2
    Ms                2
    Major             2
    Capt              1
    Lady              1
    Jonkheer          1
    Don               1
    Dona              1
    the Countess      1
    Mme               1
    Sir               1
    Name: Title, dtype: int64
    

    emmm,大类似乎只有四个:Mr、Miss、Mrs、Master,其余称呼非常少,因此我们将其余称呼都归为一类——Rare。

    List=titanic.Title.value_counts().index[4:].tolist()
    mapping={}
    for s in List:
        mapping[s]='Rare'
    titanic['Title']=titanic['Title'].map(lambda x: mapping[x] if x in mapping else x)
    
    titanic.Title.value_counts()
    
    out:
    Mr        757
    Miss      260
    Mrs       197
    Master     61
    Rare       34
    Name: Title, dtype: int64
    

    这样我们就把称呼划分成了5大类,下一步我们可以根据称呼来对Age的缺失值进行补全了,这是因为Miss用于未婚女子,通常其年龄比较小,Mrs则表示太太,夫人,一般年龄较大,因此利用称呼中隐含的信息去推测其年龄是合理的。下面我们根据Title进行分组并对Age进行补全。

    grouped=titanic.groupby(['Title'])
    median=grouped.Age.median()
    median
    
    out:
    Title
    Master     4.0
    Miss      22.0
    Mr        29.0
    Mrs       35.5
    Rare      44.5
    Name: Age, dtype: float64
    

    可以看到,不同称呼的乘客其年龄的中位数有显著差异,因此我们只需要按称呼对缺失值进行补全即可,这里使用中位数(平均数也是可以的,在这个问题当中两者差异不大,而中位数看起来更整洁一些)。

    def newage (cols):
        age=cols[0]
        title=cols[1]
        if pd.isnull(age):
            return median[title]
        return age
    
    titanic.Age=titanic[['Age','Title']].apply(newage,axis=1)
    
    titanic.info()
    
    out:
    <class 'pandas.core.frame.DataFrame'>
    Int64Index: 1309 entries, 0 to 417
    Data columns (total 13 columns):
    PassengerId    1309 non-null int64
    Survived       891 non-null float64
    Pclass         1309 non-null int64
    Name           1309 non-null object
    Sex            1309 non-null object
    Age            1309 non-null float64
    SibSp          1309 non-null int64
    Parch          1309 non-null int64
    Ticket         1309 non-null object
    Fare           1308 non-null float64
    Cabin          295 non-null object
    Embarked       1307 non-null object
    Title          1309 non-null object
    dtypes: float64(3), int64(4), object(6)
    memory usage: 183.2+ KB
    

    可以看到,Age的缺失值已经被我们补全了。下面我们对Cabin、Embarked以及Fare进行补全。

    首先Cabin代表的是舱位号,这个信息似乎并不能由其它的特征推出来(我本来想着ticket这个特征会有些帮助,但是我发现这个船票号似乎并没有什么规律),那么我们可以想一想为什么舱位号的缺失值这么多呢?会不会是因为只有幸存者才能准确地登记自己的舱位号呢?也就是说,会不会舱位号的有无才是关键所在呢?为了验证这个想法,我们画出幸存与舱位的关系图(注意这里只能画出train中has_Cabin和Survived的关系):

    titanic['has_Cabin'].loc[~titanic.Cabin.isnull()]=1
    titanic['has_Cabin'].loc[titanic.Cabin.isnull()]=0
    pd.crosstab(titanic.has_Cabin[:len_train],train.Survived).plot.bar(stacked=True)
    

    不难看出,舱位号缺失的乘客的幸存率远远低于有舱位号的乘客,这就验证了我之前的猜想。于是我们把Cabin的缺失值划为单独的一类。

    titanic.Cabin = titanic.Cabin.fillna('U')
    titanic[:10]
    

    然后我们看Embarked特征,这个特征表示在那个港口登船,只有两个缺失值,对结果影响不大,所以直接把缺失值补全为登船港口人数最多的港口(这其实应用的是先验概率最大原则)。

    most_embarked = titanic.Embarked.value_counts().index[0]
    titanic.Embarked=titanic.Embarked.fillna(most_embarked)
    

    最后我们补全Fare,这个只有一个缺失值,对结果影响不大,所以直接用票价Fare的中位数补全。

    titanic.Fare = titanic.Fare.fillna(titanic.Fare.median())
    

    至此我们的缺失值补全就完成了,check一下:

    titanic.info()
    
    out:
    <class 'pandas.core.frame.DataFrame'>
    Int64Index: 1309 entries, 0 to 417
    Data columns (total 13 columns):
    PassengerId    1309 non-null int64
    Survived       891 non-null float64
    Pclass         1309 non-null int64
    Name           1309 non-null object
    Sex            1309 non-null object
    Age            1309 non-null float64
    SibSp          1309 non-null int64
    Parch          1309 non-null int64
    Ticket         1309 non-null object
    Fare           1309 non-null float64
    Cabin          1309 non-null object
    Embarked       1309 non-null object
    Title          1309 non-null object
    dtypes: float64(3), int64(4), object(6)
    memory usage: 183.2+ KB
    

    ok,下一步我们进行特征工程对补全后的特征做进一步处理。

    2、特征工程

    首先我们处理Cabin特征,原因很简单,因为它的形式比较复杂(字母加数字),不方便直接使用。

    我们猜测Cabin特征中的开头字母应该是舱位等级,于是我们把Cabin特征中的数字去掉以方便分析:

    titanic['Cabin'] = titanic.Cabin.apply(lambda cabin: cabin[0])
    
    titanic.Cabin.value_counts()
    
    out:
    U    1014
    C      94
    B      65
    D      46
    E      41
    A      22
    F      21
    G       5
    T       1
    

    舱位等级A~G没有问题,但是T是什么舱呢……我个人感觉这个应该是登记错误吧,看了眼票价并不是很贵,于是手动把它标记成G好了……

    titanic['Cabin'].loc[titanic.Cabin=='T']='G'
    
    titanic.Cabin.value_counts()
    
    out:
    U    1014
    C      94
    B      65
    D      46
    E      41
    A      22
    F      21
    G       6
    Name: Cabin, dtype: int64
    

    我们现在看看这样划分是否对预测有用。

    pd.crosstab(titanic.Cabin[:len_train],train.Survived).plot.bar(stacked=True)
    

    看起来似乎A~F的幸存率都不低,不过各舱位之间还是有差别的。

    接下来我们对其它特征进行筛选或组合,标准就是和幸存率相关性大。

    我们先看Parch(直系亲人)和SibSp(旁系亲人)两个特征。

    pd.crosstab(titanic.Parch[:len_train],train.Survived).plot.bar(stacked=True)
    
    pd.crosstab(titanic.SibSp[:len_train],train.Survived).plot.bar(stacked=True)
    

    看起来这两个特征对幸存率的影响差不多啊……那我们把这两个特征整合成一个特征好了,Parch+SibSp+1就是家庭大小,我们创建新特征FamilySize。

    titanic['FamilySize'] = titanic.Parch + titanic.SibSp + 1
    pd.crosstab(titanic.FamilySize[:len_train],train.Survived).plot.bar(stacked=True)
    

    可以看到,孤身一人的死亡率极高……两口之家三口之家和四口之家的存活概率都比较高,家庭规模再大一些的死亡率又有所回升,这个结果也在情理之中。

    于是我们添加FamilySize这个特征,并去掉SibSp和Parch两个特征。

    titanic=titanic.drop(['SibSp','Parch'],axis=1)
    

    接下来我们看Sex特征。

    pd.crosstab(titanic.Sex[:len_train],train.Survived).plot.bar(stacked=True)
    

    女性的幸存率比男性要大很多,看来lady first的绅士风度并不只是说说而已。

    接下来我们看Pclass特征。

    pd.crosstab(titanic.Pclass[:len_train],train.Survived).plot.bar(stacked=True)
    

    不出所料,阶级高的人存活率要远大于阶级低的人。

    接下来是Title。

    pd.crosstab(titanic.Title[:len_train],train.Survived).plot.bar(stacked=True)
    

    直观上看,这个特征其实和Sex包含的信息差不多啊,都是男性幸存率低女性幸存率高。那么它还包含更多有用信息吗,我们把Sex和Title组合起来看看。

    pd.crosstab([titanic.Title[:len_train],titanic.Sex[:len_train]],train.Survived).plot.bar(stacked=True)
    

    可以看到,相比Sex,Title确实提供了更多信息,比如虽然男性的幸存率低,但是男孩(Master)的幸存率却达到了将近50%。而Title在Sex的补充下也呈现出新的信息,比如虽然Rare的幸存率虽然并不太高,但Rare中的女性幸存率却很高。

    综上,我们要保留Title特征。

    那么Age特征呢?首先将其当作连续特征是不好处理的,所以我们应划分年龄段来观察:

    pd.crosstab(pd.cut(titanic.Age,8)[:len_train],train.Survived).plot.bar(stacked=True)
    

    看起来只有年龄很小的孩子受到了优待,年轻人死亡率最高,而年纪稍长者存活率较高,老者存活率最低。

    我们按照年龄段将Age转化为类别特征:

    titanic.Age=pd.cut(titanic.Age,8,labels=False)
    

    接下来看Embarked特征。

    pd.crosstab(titanic.Embarked[:len_train],train.Survived).plot.bar(stacked=True)
    

    可以看到C港口上船的乘客幸存率最高,其它两个港口幸存率较低。原因不详。

    目前为止,我们得到的特征长这样:

    我们看到,形式比较复杂的特征还有Name、Ticket和Fare三个,Name的信息我们认为已经被Title涵盖了,因此直接舍弃。

    titanic=titanic.drop('Name',axis=1)
    

    Ticket我不知道其中含义,而且我认为其中的重要信息应该已经包含在Cabin当中了,于是我选择舍弃Ticket这个特征。

    titanic=titanic.drop('Ticket',axis=1)
    

    最后,我们看Fare特征,我个人认为这个船票价格并不太重要,因为其信息应该已经包含在Pclass、Cabin以及Embarked当中了,但我猜想这个票价可能和位置有关(靠窗啊卧铺啊之类),蕴含了我们舍弃的Ticket的部分信息,因此我决定保留这个特征,并对其进行和Age类似的离散化操作。

    pd.crosstab(pd.qcut(titanic.Fare,4)[:len_train],train.Survived).plot.bar(stacked=True)
    

    果然票价越高幸存率越高。

    我们按照Fare段将Fare转化为类别特征:

    titanic.Fare=pd.cut(titanic.Fare,4,labels=False)
    

    我们看一下现在的特征形式:

    titanic.head()
    

    基本上完成了,最后我们只需把Sex、Cabin、Embarked和Title特征转化成int类型即可:

    titanic.Sex=titanic.Sex.map({'male':1,'female':0})
    titanic.Cabin=titanic.Cabin.map({'A':0,'B':1,'C':2,'D':3,'E':4,'F':5,'G':6,'U':7})
    titanic.Embarked=titanic.Embarked.map({'C':0,'Q':1,'S':2})
    titanic.Title=titanic.Title.map({'Mr':0,'Miss':1,'Mrs':2,'Master':3,'Rare':4})
    

    至此特征工程就完成了:

    3、训练模型

    训练模型阶段我们只需要用sklearn库即可完成大部分操作,因为不知道何种模型是最适合我们的数据的,因此我们可以尝试多种模型,最终选出表现较好的模型进行集成,得到最终的分类器。

    首先我们还原train和test数据。

    train=titanic[:len_train]
    test=titanic[len_train:]
    

    然后我们可以给由这两个数据集产生相应的X和y了:

    X_train=train.loc[:, 'Pclass':]
    y_train=train['Survived']
    
    X_test=test.loc[:, 'Pclass':]
    

    然后我们尝试一下不同的模型,看看哪个效果比较好,这里考虑LR、SVM和DecisionTree。

    from sklearn.linear_model import LogisticRegression
    from sklearn.svm import SVC
    from sklearn.tree import DecisionTreeClassifier
    
    log_reg=LogisticRegression()
    log_reg.fit(X_train, y_train)
    svm_clf = SVC()
    svm_clf.fit(X_train, y_train)
    tree_clf=DecisionTreeClassifier()
    tree_clf.fit(X_train, y_train)
    
    print(log_reg.score(X_train,y_train))
    print(svm_clf.score(X_train,y_train))
    print(tree_clf.score(X_train,y_train))
    
    out:
    0.8181818181818182
    0.8462401795735129
    0.8933782267115601
    

    可以看到决策树模型表现更好一些,而决策树的集成是随机森林,因此我们直接使用随机森林来拟合数据。

    from sklearn.model_selection import cross_val_score
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.model_selection import GridSearchCV
    
    RF=RandomForestClassifier(random_state=1)
    PRF=[{'n_estimators':[10,100],'max_depth':[3,6],'criterion':['gini','entropy']}]
    GSRF=GridSearchCV(estimator=RF, param_grid=PRF, scoring='accuracy',cv=2)
    scores_rf=cross_val_score(GSRF,X_train,y_train,scoring='accuracy',cv=5)
    
    model=GSRF.fit(X_train, y_train)
    pred=model.predict(X_test)
    output=pd.DataFrame({'PassengerId':test['PassengerId'],'Survived':pred})
    output.to_csv('C:/logfile/submission.csv', index=False)
    

    最终提交得分为0.7945,top 21%。对于入门之战来说结果已经不错了。后来又参考了网上大牛的做法,相比上面的模型,将Age的缺失值补全分得更为细致了一些(结合Title和Sex的信息进行补全而不仅仅是Title),并且采用了超参数调优的SVM模型,最终达到了0.8133的得分,排名瞬间变成top 7%。看来有很多人都卡在了0.8左右的精度上。

    相关文章

      网友评论

          本文标题:kaggle入门之战——Titanic

          本文链接:https://www.haomeiwen.com/subject/xhhmxctx.html