Python {Article059}

ようこそ「Python」へ...

タイタニック号で機械学習のデータラングリングを学ぶには【Data Wrangling】

タイタニックのサバイバルデータで機械学習(Machine Learning)シリーズは次の8つの記事から構成されています。 機械学習に興味のある方は以下に掲載されている記事を順番に読むことをおすすめします。

この記事では機械学習(ML: Machine Learning)で使用するデータをラングリング(Wrangling)する方法を解説します。 データのラングリングとは、機械学習の入力情報として使用するために「生データ」をさまざまな形式に変換・マッピングすることです。 もっとも一般的なのは、文字型(str型)のデータを数値(int型, float型)にマッピングすることです。 たとえば、今回するタイタニックの生データには列「Sex」に性別として「male, female」が格納されています。 MLでは文字型のデータは処理できないのでここでは「male▶0」、「female▶1」にマッピングしています。

文字型のデータだけではなく数値のデータもマッピングすることがあります。 タイタニックの生データの列「Age」には年齢が格納されていますが、 ここでは年齢を表1のように数値(0-4)にマッピングしています。

表1
年齢
16歳以下 17歳~26歳 27歳~36歳 37歳~62歳 63歳以上
Child(0) Young(1) Adult(2) Mid-Age(3) Senior(4)

データの特定の値が空(Null)のときは、そのデータを中央値で置換したりします。 たとえば、タイタニックの列「Age」には年齢が格納されていますが、年齢が「空」のケースがあります。 この場合、その人の名前から中央値を計算して置換します。 タイタニックのデータは英語圏のデータなので名前に敬称「Mr, Miss, Mrs,...」がつけられています。 この敬称から年齢の中央値を計算して、中央値の年齢で置換します。 Pyhtonで記述すると次のようなコードになります。

train['Age'].fillna(train.groupby('Title_map')['Age'].transform('median'))

データのラングリングを行うにはPandasの各種メソッドを使って行います。 少なくともPandasのloc(), iloc(), unique(), value_counts()メソッド、そしてapply(), applymap(), map(), transform()などのメソッドは使いこなしておく必要があります。 さらに、不正な値を修正・削除するためにdrop(), groupby(), fillna(), isnull()メソッドなども使うことがあります。 本番ではstr型のデータをint型、float型に変換するとき、PandasのDataFrameで行うのではなく、 「記事(Article061)」で解説しているSklearnのmake_column_transformer()を使用します。

最近よくAI(Artificial Intelligence)、ML(Machine Learning)、DL(Deep Learning)という言葉を聞きますが、 AIはMLとDLを含んだ総称です。 そしてMLはDLを含んでいます。 そしてDLはNeural Networks(ニューラルネットワーク)を使用しています。 MLとDLはAIのサブセットということになります。 DL(Deep Learning)については「記事(Article028)」で詳しく解説しています。

ML(Machine Learning)は、Supervised Learning, UnSupervised Learning, Reinforcement Learningの3つに分類されています。 それぞれのタイプの概念は図(B, C, D)を参照してください。 そしてSupervised LearningはアルゴリズムによりReguression, Classfication, Clusteringの3つに分類されています。 それぞれのアルゴリズムの種類は図(E)を参照してください。

今回予測するのは、 タイタニック号の乗船客が「生存するか」「死亡するか」の2択ですから、 MLのClassficationのアルゴリズムを利用することになります。 ここではClassficationの8種類のアルゴリズム (KNeighborsClassifier, DecisionTreeClassifier, RandomForestClassifier, GaussianNB, SVC, ExtraTreeClassifier, GradientBoostingClassifier, AdaBoostClassifier) を使用して乗船客の生死を予測します。

ML(Macine Learning)を使用して予測するとき、予測するデータの属性によりアルゴリズム(モデル)の評価方法が異なります。 「記事(Article056)」で解説した売上データからお客さんが商品を「買う、買わない」といった予測では、 accuracy_score(正解率)で予測値を評価してもとくに問題はありません。 ところが、今回のタイタニックのような人間の「生死」を予測するケースでは、accuracy_score(正解率)だけで予測を評価することはできません。 たとえば、モデルが「死亡する」と予測して、その予測が外れてもとくに問題にはなりません。 ところが、モデルが「生存する」と予測して、その予測が外れると「死亡」するということになるので問題になります。 このような場合は、モデルの予測値を4パターンに分けて評価する必要があります。 MLはこれら4パターンの評価情報を取得する方法としてclassification_report()とconfusion_matrix()メソッドを用意しています。

「初級」編では、データの取り込み、データの分析(可視化)、学習、予測、予測評価の順番に説明します。 「予測評価」では、予測を評価するための情報を取得する3種類の方法(メソッド)を解説します。 さらに、なぜclassification_report(), confusion_matrix(), accuracy_score()の3種類のメソッドが用意されているのか、 そして、これらのメソッドで取得した評価情報をどのように活用するのかについても説明しています。

「中級」編では、Pandas、Matplotlibを使用したデータ分析を詳しく解説しています。 データを分析するには可視化することが重要ですが、Pandasのplot()メソッドを使用する簡単にデータを可視化することができます。 さらにMatplotlibを使用するとグラフを見栄えよくする、見やすくする、グラフにさまざまな補足情報を表示するといったことが可能になります。

「上級」編では、複数のアルゴリズム(モデル)を使用して実際に予測して、予測を評価する方法について解説しています。 予測値を調整するには、モデルにさまざまなパラメータを追加して、さらにパラメータの値(範囲)も同時に調整する必要があります。 これらを効率的に行う方法としてPipelineを使用したGridSearchCV()、RandomizedSearchCV()メソッドについて解説しています。 RandomizedSearchCV()を使用すると、モデルにどのようなパラメータを追加すると予測が改善するかを効率的に行うことができます。 さらに、GridSerachCV()を使用すると、モデルのパラメータの値(範囲)の調整を効率的に行うことができます。

ここではVisula Studio Code(VSC)の「Python Interactive window」 を使用してJupter Notebookのような環境で説明します。 VSCを通常の環境からインタラクティブな環境に切り換えるにはコードを記述するときコメント「# %%」を入力します。 詳しい、操作手順については「ここ」 を参照してください。 インタラクティブな環境では、Pythonの「print(), plt.show()」などを使う必要がないので掲載しているコードでは省略しています。 VSCで通常の環境で使用するときは、必要に応じて「print(), plt.show()」等を追加してください。

この記事では、Pandas、Matplotlibのライブラリを使用しますので 「記事(Article001) | 記事(Article002) | 記事(Article003) | 記事(Article004)」 を参照して事前にインストールしておいてください。 Pythonのコードを入力するときにMicrosoftのVisula Studio Codeを使用します。 まだ、インストールしていないときは「記事(Article001)」を参照してインストールしておいてください。

説明文の左側に図の画像が表示されていますが縮小されています。 画像を拡大するにはマウスを画像上に移動してクリックします。 画像が拡大表示されます。拡大された画像を閉じるには右上の[X]をクリックします。 画像の任意の場所をクリックして閉じることもできます。

click image to zoom!
図A: AI/ML/DL
click image to zoom!
図B: Supervised
click image to zoom!
図C: UnSupervised
click image to zoom!
図D: Reinforcement
click image to zoom!
図E: Algorithms
click image to zoom!
Data Mapping[1]
click image to zoom!
Data Mapping[2]
click image to zoom!
Data Mapping[3]
click image to zoom!
Data Mapping[4]
click image to zoom!
Data Mapping[5]


タイタニックのサバイバルデータを機械学習で利用できるようにラングリングする

  1. まずはPythonのライブラリを取り込む

    Visual Studio Code(VSC)を起動したら新規ファイルを作成して行1-8をコピペします。 行2-6ではPythonのライブラリを取り込んでいます。 行8ではPythonの警告を抑止しています。 ライブラリをまだインストールしていないときは「pip install」で事前にインストールしておいてください。
    ### Import the libraries
    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    import seaborn as sns
    import warnings
    
    warnings.simplefilter('ignore')
    click image to zoom!
    図1
    図1はVisual Studio Code(VSC)の画面です。
  2. PandasのDataFrameにタイタニックのサバイバルデータを取り込む

    行2, 4ではCSVファイルのパスを定義しています。 当サイトからダウンロードするときは行3, 5のコメント(#)を外してください。 行6, 7ではPandasのread_csv()メソッドでCSVファイルをDataFrameに取り込んでいます。
    ### Load the data
    train_file = 'data/csv/titanic/train.csv'
    #train_file = 'https://money-or-ikigai.com/menu/python/article/data/titanic/train.csv'
    test_file = 'data/csv/titanic/test.csv'
    #test_file = 'https://money-or-ikigai.com/menu/python/article/data/titanic/test.csv'
    train = pd.read_csv(train_file)
    test = pd.read_csv(test_file)
    click image to zoom!
    図2-1
    図2-1にはDataFrame(train)の構造と内容が表示されています。 trainには891人の乗客データが格納されています。
    click image to zoom!
    図2-2
    図2-2にはDataFrame(test)の構造と内容が表示されています。 testには418人の乗客データが格納されています。
  3. DataFrameの列「Name(名前)」を数値(0-3)にマッピングする

    ここでは2段階でマッピングしています。 まずは列「Name(名前)」から敬称(Mr, Miss, Mrs,...)を抽出して新規列「Title」を追加します。 次に列「Title(敬称)」を数値(0-3)にマッピングします。
    ### Name / Title Mapping
    
    # Extract Title from the Name column
    train['Title'] = train['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)
    #print('Title:')
    #print(train['Title'].value_counts())
    
    title_mapping = {'Mr': 0, 'Miss': 1, 'Mrs': 2, 
                     'Master': 3, 'Dr': 3, 'Rev': 3, 'Col': 3, 'Major': 3, 'Mlle': 3,'Countess': 3,
                     'Ms': 3, 'Lady': 3, 'Jonkheer': 3, 'Don': 3, 'Dona' : 3, 'Mme': 3,'Capt': 3,'Sir': 3 }
    
    train['Title_map'] = train['Title'].map(title_mapping)
    #train[['Title', 'Title_map']]
    train['Title_map'] = train['Title_map'].fillna(0)
    train['Title_map'].isnull().sum()  # Title_map => 0
    
    # Extract Title from the Name column
    test['Title'] = test['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)
    #print('Title:')
    #print(train['Title'].value_counts())
    
    test['Title_map'] = test['Title'].map(title_mapping)
    #test[['Title', 'Title_map']]
    test['Title_map'] = test['Title_map'].fillna(0)
    test['Title_map'].isnull().sum()  # Title_map => 0
    click image to zoom!
    図4
    図4は実行画面です。 DataFrame(train, test)の列「Title_map」の不正データ(Null)の件数は0件と表示されています。
  4. DataFrameの列「Sex(性別)」を数値(0-1)にマッピングする

    ### Sex Mapping male => 0, femail => 1
    
    sex_mapping = {'male': 0, 'female': 1}
    train['Sex_map'] = train['Sex'].map(sex_mapping)
    #train[['Sex', 'Sex_map']].head(1)
    train['Sex_map'].isnull().sum()  # Sex_map => 0
    
    test['Sex_map'] = test['Sex'].map(sex_mapping)
    #test[['Sex', 'Sex_map']].head(1)
    test['Sex_map'].isnull().sum()  # Sex_map => 0
    click image to zoom!
    図5
    図5は実行画面です。 DataFrame(tran, test)の列「Sex」が数値(0-1)にマッピングされて新規列「Sex_map」に格納されています。 DataFrame(train, test)の列「Sex_map」の不正データ(Null)の件数は0件と表示されています。
  5. DataFrameの列「Age(年齢)」を数値(0-4)にマッピングする

    ここでは「年齢」を2段階でマッピングしています。 まずは、「年齢」がNaN(Not a Number)のとき、年齢の中央値で置換します。 次に「年齢」を数値(0-4)にマッピングします。
    ### Age Mapping
    
    # 1) Fix Age NaN => median    
    train['Age'].fillna(train.groupby('Title_map')['Age'].transform('median'), inplace=True)
    train['Age'].isnull().sum()   # Age => 0    
    
    test['Age'].fillna(test.groupby('Title_map')['Age'].transform('median'), inplace=True)
    test['Age'].isnull().sum()   # Age => 0 
    
    # 2) Age Mapping
    
    # Binning/Converting Numerical Age to Categorical Variable
    # feature vector map:
    # child:    0
    # young:    1
    # adult:    2
    # mid-age:  3
    # senior:   4
    
    def age_map(age):
        if age <= 16:
            return 0
        elif age > 16 and age <= 26:
            return 1
        elif age > 26 and age <= 36:
            return 2                        
        elif age > 36 and age <= 62:
            return 3             
        else:
            return 4
    
    train['Age_map'] = train.loc[:, 'Age'].apply(age_map)
    #train[['Age','Age_map']].head(1)
    train['Age_map'].isnull().sum()   # Age_map => 0  
    
    test['Age_map'] = test.loc[:, 'Age'].apply(age_map)
    #test[['Age','Age_map']].head(1)
    test['Age_map'].isnull().sum()   # Age_map => 0
    click image to zoom!
    図6
    図6は実行画面です。 DataFrame(tran, test)の列「Age」が数値(0-4)にマッピングされて新規列「Age_map」に格納されています。 DataFrame(train, test)の列「Age_map」の不正データ(Null)の件数は0件と表示されています。
  6. DataFrameの列「embarked(乗船港)」を数値(0-2)にマッピングする

    ここでは「乗船港」を2段階でマッピングしています。 まずは「乗船港」がNaN(Not a Number)のときは「S」で置換します。 次に「乗船港」を数値(0-2)でマッピングします。
    ### Map the value of Embarked
    
    # fill out missing embark with S embark
    train['Embarked'] = train['Embarked'].fillna('S')
    
    embarked_mapping = {'S':0,'C':1,'Q':2}
    train['Embarked_map'] = train['Embarked'].map(embarked_mapping)
    #train[['Embarked','Embarked_map']].head(1)
    train['Embarked_map'].isnull().sum()   # Embarked_map => 0  
    
    # fill out missing embark with S embark
    test['Embarked'] = test['Embarked'].fillna('S')
    
    test['Embarked_map'] = test['Embarked'].map(embarked_mapping)
    #test[['Embarked','Embarked_map']].head(1)
    test['Embarked_map'].isnull().sum()   # Embarked_map => 0
    click image to zoom!
    図7
    図7は実行画面です。 DataFrame(tran, test)の列「Embarked」が数値(0-4)にマッピングされて新規列「Embarked_map」に格納されています。 DataFrame(train, test)の列「Embarked_map」の不正データ(Null)の件数は0件と表示されています。
  7. DataFrameの列「Fare(乗船料)」を数値(0-3)にマッピングする

    ここでは「乗船料」を2段階でマッピングしています。 まずは「乗船料」がNaN(Not a Number)のとき、「乗船料」の中央値で置換します。 次に「乗船料」を数値(0-3)にマッピングします。
    ### Map Fare 
    
    # 1) Fill missing Fare with median fare for each Pclass
    train['Fare'].fillna(train.groupby('Pclass')['Fare'].transform('median'), inplace=True)
    #train['Fare'].isnull().sum()   # Fare => 0 
    
    test['Fare'].fillna(test.groupby('Pclass')['Fare'].transform('median'), inplace=True)
    #test['Fare'].isnull().sum()   # Fare => 0 
    
    # 2) Map Fare
    
    def fare_map(fare):
        if fare <= 17:
            return 0
        elif fare > 17 and fare <= 30:
            return 1
        elif fare > 30 and fare <= 100:
            return 2
        else:
            return 3
    
    train['Fare_map'] = train.loc[:, 'Fare'].apply(fare_map)
    #train[['Fare','Fare_map']].head(1)
    train['Fare_map'].isnull().sum()   # Fare_map => 0  
    
    test['Fare_map'] = test.loc[:, 'Fare'].apply(fare_map)
    #test[['Fare','Fare_map']].head(1)
    test['Fare_map'].isnull().sum()   # Fare_map => 0
    click image to zoom!
    図8
    図8は実行画面です。 DataFrame(tran, test)の列「Fare」が数値(0-3)にマッピングされて新規列「Fare_map」に格納されています。 DataFrame(train, test)の列「Fare_map」の不正データ(Null)の件数は0件と表示されています。
  8. DataFrameの列「Cabin」を数値(0-3)にマッピングする

    ここでは「Cabin」を3段階で数値にマッピングしています。 まずは、列「Cabin」から先頭1文字を抽出して新規列「Cabin_x」に格納します。 次に、列「Cabin_x」を数値(0-3)にマッピングして新規列「Capin_map」に格納します。 最後に、列「Cabin_map」の値がNaN(Not a Number)のとき中央値で置換します。
    ### Map the value of Cabin
    
    # 0) Extract first letter of the Cabin (X)
    train['Cabin_x'] = train['Cabin'].str[:1]    # X999 => X
    
    # 1) Fill missing Cabin_map
    cabin_mapping = {'A': 0, 'B': 0.4, 'C': 0.8, 'D': 1.2, 'E': 1.6, 'F': 2, 'G': 2.4, 'T': 2.8}
    train['Cabin_map'] = train['Cabin_x'].map(cabin_mapping)  # A => 0, B => 0.4
    #train[['Cabin_x','Cabin_map']].head(1)
    train['Cabin_x'].isnull().sum()   # Cabin_map => 687  
    
    test['Cabin_x'] = test['Cabin'].str[:1]    # X999 => X
    
    # Fill missing Cabin_map
    test['Cabin_map'] = test['Cabin_x'].map(cabin_mapping)  # A => 0, B => 0.4
    #test[['Cabin_x','Cabin_map']].head(1)
    test.isnull().sum()   # Cabin_map => 687  
    
    # 2) Fill missing Cabin_map with median Cabin_map for each Pclass
    train['Cabin_map'].fillna(train.groupby('Pclass')['Cabin_map'].transform('median'), inplace=True)
    train['Cabin_map'].isnull().sum()    # Cabin_map => 0
    
    test['Cabin_map'].fillna(test.groupby('Pclass')['Cabin_map'].transform('median'), inplace=True)
    test['Cabin_map'].isnull().sum()    # Cabin_map => 0
    click image to zoom!
    図9
    図9は実行画面です。 DataFrame(tran, test)の列「Cabin」が数値(0-3)にマッピングされて新規列「Cabin_map」に格納されています。 DataFrame(train, test)の列「Cabin_map」の不正データ(Null)の件数は0件と表示されています。
  9. DataFrameの列「SibSp, Parch」を数値(0-4)にマッピングする

    ここでは2段階で列「SibSp, Parch」を数値にマッピングしています。 まずは、列「SibSp(夫婦の人数)」と列「Parch(兄弟の人数)」を加算して新規列「FamilySize」に格納します。 次に、列「FamilySize」を数値(0-4)にマッピングして新規列「FamilySize_map」に格納します。
    ### Map the value of Family Size
    
    # 1) Add Spouse & Children
    train['FamilySize'] = train['SibSp'] + train['Parch'] + 1
    
    # 2) Map FamilySize
    family_mapping = {1: 0, 2: 0.4, 3: 0.8, 4: 1.2, 5: 1.6, 6: 2, 7: 2.4, 8: 2.8, 9: 3.2, 10: 3.6, 11: 4}
    train['FamilySize_map'] = train['FamilySize'].map(family_mapping)
    #train[['FamilySize','FamilySize_map']].head(1)
    train['FamilySize_map'].isnull().sum()   # FamilySize_map => 0 
    
    test['FamilySize'] = test['SibSp'] + test['Parch'] + 1
    test['FamilySize_map'] = test['FamilySize'].map(family_mapping)
    #test[['FamilySize','FamilySize_map']].head(1)
    test['FamilySize_map'].isnull().sum()   # FamilySize_map => 0
    click image to zoom!
    図10
    図10は実行画面です。 DataFrame(tran, test)の列「FamilySize」が数値(0-4)にマッピングされて新規列「FamilySize_map」に格納されています。 DataFrame(train, test)の列「FamilySize_map」の不正データ(Null)の件数は0件と表示されています。
  10. DataFrameから不要な列を削除する

    ### Drop columns
    
    features_drop = ['Name','Sex','Age','SibSp','Parch',
        'Ticket','Fare','Cabin','Embarked','Embarked_copy',
        'Title','Cabin_x','FamilySize','PassengerId'
    ]
    
    train = train.drop(features_drop, axis=1)
    test = test.drop(features_drop, axis=1)
    click image to zoom!
    図11
    図11は実行結果です。 不要な列を削除した結果、DataFrame(train, test)の全ての列が数値(int型、float型)になっています。
  11. DataFrameに不正なデータがないか検証する

    ### Verify train Dataframe
    
    ax = sns.heatmap(train.isnull(), yticklabels=False, cbar=False)
    ax.set_xlabel('Train DataFrame Columns')
    
    ax.set_title('Train Column Null Value Analysis\n(Titanic)')
    plt.show()
    
    train.isnull().sum()
    
    ### Verify test Dataframe
    
    ax = sns.heatmap(test.isnull(), yticklabels=False, cbar=False)
    ax.set_xlabel('Test DataFrame Columns')
    
    ax.set_title('Test Column Null Value Analysis\n(Titanic)')
    plt.show()
    
    test.isnull().sum()
    click image to zoom!
    図12-1
    図12-1は実行結果です。 DataFrame(train)の全ての列の不正データ(Null)の件数が0件になっています。
    click image to zoom!
    図12-2
    図12-2は実行結果です。 DataFrame(test)の全ての列の不正データ(Null)の件数が0件になっています。
  12. タイタニックのデータ(train/test)をCSVファイルに保存する

    ### Save train / test to csv file 
    
    train_file = 'data/csv/titanic/train_cleaned.csv'
    test_file = 'data/csv/titanic/test_cleaned.csv'
    
    train.to_csv(train_file, index=False)
    train = pd.read_csv(train_file)
    #train.info()
    
    test.to_csv(test_file, index=False)
    test = pd.read_csv(test_file)
    #test.info()
    click image to zoom!
    図13
    図13は実行結果です。 CSVファイル(train_cleaned.csv, test_cleaned.csv)にDataFrame(train, test)のデータが出力されています。
  13. ここで解説したコードをまとめて掲載

    最後にここで解説したすべてのコードをまとめて掲載しましたので参考にしてください。
    
    ### Import the libraries
    #import urllib.request
    #from PIL import Image
    #import os, os.path
    #import os
    #from functools import reduce
    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    #from pandas.core.reshape.reshape import stack # Plot the graphes
    import seaborn as sns
    import warnings
    
    warnings.simplefilter('ignore')
    sns.set() # setting seaborn default for plots
    
    # %%
    
    ### Load the data
    train_file = 'data/csv/titanic/train.csv'
    #train_file = 'https://money-or-ikigai.com/menu/python/article/data/titanic/train.csv'
    test_file = 'data/csv/titanic/test.csv'
    #test_file = 'https://money-or-ikigai.com/menu/python/article/data/titanic/test.csv'
    train = pd.read_csv(train_file)
    test = pd.read_csv(test_file)
    
    # --------------------------------- Fix train and test data
    
    #train.shape
    # (891, 12)     891 rows, 12 columns
    #test.shape
    # (418, 11)     418 rows, 11 columns    ★ NO Survived column
    
    #train.info()
    # RangeIndex: 891 entries, 0 to 890
    # Data columns (total 12 columns):
    #  #   Column       Non-Null Count  Dtype  
    # ---  ------       --------------  -----  
    #  0   PassengerId  891 non-null    int64  
    #  1   Survived     891 non-null    int64       0 = No, 1 = Yes 
    #  2   Pclass       891 non-null    int64       Ticket class 1 = 1st, 2 = 2nd, 3 = 3rd
    #  3   Name         891 non-null    object 
    #  4   Sex          891 non-null    object 
    #  5   Age          714 non-null    float64
    #  6   SibSp        891 non-null    int64       # of siblings / spouses aboard the Titanic
    #  7   Parch        891 non-null    int64       # of parents / children aboard the Titanic          
    #  8   Ticket       891 non-null    object 
    #  9   Fare         891 non-null    float64     Ticket number
    #  10  Cabin        204 non-null    object      Cabin number
    #  11  Embarked     889 non-null    object      Port of Embarkation C = Cherbourg, Q = Queenstown, S = Southampton
    # dtypes: float64(2), int64(5), object(5)
    
    #train[['Survived','Pclass','Age','SibSp','Fare']].describe()
    # index   Survived	Pclass	    Age	        SibSp	    Fare
    # ------------------------------------------------------------------
    # count	891.000000	891.000000	714.000000	891.000000	891.000000
    # mean	0.383838	2.308642	29.699118	0.523008	32.204208
    # std	0.486592	0.836071	14.526497	1.102743	49.693429
    # min	0.000000	1.000000	0.420000	0.000000	0.000000
    # 25%	0.000000	2.000000	20.125000	0.000000	7.910400
    # 50%	0.000000	3.000000	28.000000	0.000000	14.454200
    # 75%	1.000000	3.000000	38.000000	1.000000	31.000000
    # max	1.000000	3.000000	80.000000	8.000000	512.329200
    
    #train.isnull().sum()
    # PassengerId      0
    # Survived         0
    # Pclass           0
    # Name             0
    # Sex              0
    # Age            177    ★
    # SibSp            0
    # Parch            0
    # Ticket           0
    # Fare             0
    # Cabin          687    ★
    # Embarked         2    ★
    # dtype: int64
    
    #test.isnull().sum()    ★ NO Survived column
    # PassengerId      0
    # Pclass           0
    # Name             0
    # Sex              0
    # Age             86    ★
    # SibSp            0
    # Parch            0
    # Ticket           0
    # Fare             1    ★
    # Cabin          327    ★
    # Embarked         0
    # dtype: int64
    
    # %%
    
    ### Name / Title Mapping
    
    # Extract Title from the Name column
    train['Title'] = train['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)
    #print('Title:')
    #print(train['Title'].value_counts())
    
    title_mapping = {'Mr': 0, 'Miss': 1, 'Mrs': 2, 
                     'Master': 3, 'Dr': 3, 'Rev': 3, 'Col': 3, 'Major': 3, 'Mlle': 3,'Countess': 3,
                     'Ms': 3, 'Lady': 3, 'Jonkheer': 3, 'Don': 3, 'Dona' : 3, 'Mme': 3,'Capt': 3,'Sir': 3 }
    
    train['Title_map'] = train['Title'].map(title_mapping)
    train[['Title', 'Title_map']]
    train['Title_map'] = train['Title_map'].fillna(0)
    train['Title_map'].isnull().sum()  # Title_map => 0         
    
    # Extract Title from the Name column
    test['Title'] = test['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)
    #print('Title:')
    #print(train['Title'].value_counts())
    
    test['Title_map'] = test['Title'].map(title_mapping)
    test[['Title', 'Title_map']]
    test['Title_map'] = test['Title_map'].fillna(0)
    test['Title_map'].isnull().sum()  # Title_map => 0  
    
    
    # %%
    
    ### Sex Mapping male => 0, femail => 1
    
    sex_mapping = {'male': 0, 'female': 1}
    train['Sex_map'] = train['Sex'].map(sex_mapping)
    #train[['Sex', 'Sex_map']].head(1)
    train['Sex_map'].isnull().sum()  # Sex_map => 0   
    
    test['Sex_map'] = test['Sex'].map(sex_mapping)
    #test[['Sex', 'Sex_map']].head(1)
    test['Sex_map'].isnull().sum()  # Sex_map => 0   
    
    
    # %%
    
    ### Age Mapping
    
    # 1) Fix Age NaN => median 
    train['Age'].fillna(train.groupby('Title_map')['Age'].transform('median'), inplace=True)
    train['Age'].isnull().sum()   # Age => 0    
    
    test['Age'].fillna(test.groupby('Title_map')['Age'].transform('median'), inplace=True)
    test['Age'].isnull().sum()   # Age => 0    
    
    
    # %%
    
    # 2) Age Mapping
    
    # Binning/Converting Numerical Age to Categorical Variable
    # feature vector map:
    # child:    0
    # young:    1
    # adult:    2
    # mid-age:  3
    # senior:   4
    
    def age_map(age):
        if age <= 16:
            return 0
        elif age > 16 and age <= 26:
            return 1
        elif age > 26 and age <= 36:
            return 2                        
        elif age > 36 and age <= 62:
            return 3             
        else:
            return 4
    
    train['Age_map'] = train.loc[:, 'Age'].apply(age_map)
    #train[['Age','Age_map']].head(1)
    train['Age_map'].isnull().sum()   # Age_map => 0  
    
    test['Age_map'] = test.loc[:, 'Age'].apply(age_map)
    #test[['Age','Age_map']].head(1)
    test['Age_map'].isnull().sum()   # Age_map => 0  
    
    
    # %%
    
    ### Map the value of Embarked
    
    # fill out missing embark with S embark
    train['Embarked'] = train['Embarked'].fillna('S')
    
    embarked_mapping = {'S':0,'C':1,'Q':2}
    train['Embarked_map'] = train['Embarked'].map(embarked_mapping)
    #train[['Embarked','Embarked_map']].head(1)
    train['Embarked_map'].isnull().sum()   # Embarked => 0, Embarked_map => 0  
    
    # fill out missing embark with S embark
    test['Embarked'] = test['Embarked'].fillna('S')
    
    test['Embarked_map'] = test['Embarked'].map(embarked_mapping)
    #test[['Embarked','Embarked_map']].head(1)
    test['Embarked_map'].isnull().sum()   # Embarked => 0, Embarked_map => 0  
    
    
    # %%
    
    ### Map Fare 
    
    # 1) Fill missing Fare with median fare for each Pclass
    train['Fare'].fillna(train.groupby('Pclass')['Fare'].transform('median'), inplace=True)
    #train['Fare'].isnull().sum()   # Fare => 0 
    
    test['Fare'].fillna(test.groupby('Pclass')['Fare'].transform('median'), inplace=True)
    #test['Fare'].isnull().sum()   # Fare => 0 
    
    # 2) Map Fare
    
    def fare_map(fare):
        if fare <= 17:
            return 0
        elif fare > 17 and fare <= 30:
            return 1
        elif fare > 30 and fare <= 100:
            return 2
        else:
            return 3
    
    train['Fare_map'] = train.loc[:, 'Fare'].apply(fare_map)
    #train[['Fare','Fare_map']].head(1)
    train['Fare_map'].isnull().sum()   # Fare_map => 0  
    
    test['Fare_map'] = test.loc[:, 'Fare'].apply(fare_map)
    #test[['Fare','Fare_map']].head(1)
    test['Fare_map'].isnull().sum()   # Fare_map => 0  
    
    
    # %%
    
    ### Map the value of Cabin
    
    # 0) Extract first letter of the Cabin (X)
    train['Cabin_x'] = train['Cabin'].str[:1]    # X999 => X
    
    # 1) Fill missing Cabin_map
    cabin_mapping = {'A': 0, 'B': 0.4, 'C': 0.8, 'D': 1.2, 'E': 1.6, 'F': 2, 'G': 2.4, 'T': 2.8}
    train['Cabin_map'] = train['Cabin_x'].map(cabin_mapping)  # A => 0, B => 0.4
    #train[['Cabin_x','Cabin_map']].head(1)
    #train['Cabin_map'].isnull().sum()   # Cabin_map => 687  
    
    test['Cabin_x'] = test['Cabin'].str[:1]    # X999 => X
    
    # Fill missing Cabin_map
    test['Cabin_map'] = test['Cabin_x'].map(cabin_mapping)  # A => 0, B => 0.4
    #test[['Cabin_x','Cabin_map']].head(1)
    #test['Cabin_map'].isnull().sum()   # Cabin_map => 687  
    
    # 2) Fill missing Cabin_map with median Cabin_map for each Pclass
    train['Cabin_map'].fillna(train.groupby('Pclass')['Cabin_map'].transform('median'), inplace=True)
    train['Cabin_map'].isnull().sum()    # Cabin_map => 0
    
    test['Cabin_map'].fillna(test.groupby('Pclass')['Cabin_map'].transform('median'), inplace=True)
    test['Cabin_map'].isnull().sum()    # Cabin_map => 0
    
    
    # %%
    
    ### Map the value of Family Size
    
    # 1) Add Spouse & Children
    train['FamilySize'] = train['SibSp'] + train['Parch'] + 1
    
    # 2) Map FamilySize
    family_mapping = {1: 0, 2: 0.4, 3: 0.8, 4: 1.2, 5: 1.6, 6: 2, 7: 2.4, 8: 2.8, 9: 3.2, 10: 3.6, 11: 4}
    train['FamilySize_map'] = train['FamilySize'].map(family_mapping)
    #train[['FamilySize','FamilySize_map']].head(1)
    train['FamilySize_map'].isnull().sum()   # FamilySize_map => 0 
    
    test['FamilySize'] = test['SibSp'] + test['Parch'] + 1
    test['FamilySize_map'] = test['FamilySize'].map(family_mapping)
    #test[['FamilySize','FamilySize_map']].head(1)
    test['FamilySize_map'].isnull().sum()   # FamilySize_map => 0 
    
    
    # %%
    
    ### Drop columns
    
    #train.isnull().sum()
    # PassengerId         0 => Remove
    # Survived            0 => Remove
    # Name                0 => Remove 
    # Sex                 0 => Remove
    # Age                 0 => Remove
    # SibSp               0 => Remove
    # Parch               0 => Remove
    # Ticket              0 => Remove
    # Fare                0 => Remove
    # Cabin             687 ★  => Remove
    # Embarked            0 => Remove
    # Embarked_copy       0 => Remove
    # Title               0 => Remove
    # Cabin_x           687 ★ => Remove
    # FamilySize          0 => Remove
    
    features_drop = ['Name','Sex','Age','SibSp','Parch',
        'Ticket','Fare','Cabin','Embarked',
        'Title','Cabin_x','FamilySize','PassengerId'
    ]
    
    train = train.drop(features_drop, axis=1)
    test = test.drop(features_drop, axis=1)
    
    
    # %%
    
    ### Verify train Dataframe
    
    ax = sns.heatmap(train.isnull(), yticklabels=False, cbar=False)
    ax.set_xlabel('Train DataFrame Columns')
    #ax.set_ylabel('Count')
    ax.set_title('Train Column Null Value Analysis\n(Titanic)')
    plt.show()
    
    train.isnull().sum()
    
    
    # %%
    
    ### Verify test Dataframe
    
    ax = sns.heatmap(test.isnull(), yticklabels=False, cbar=False)
    ax.set_xlabel('Test DataFrame Columns')
    #ax.set_ylabel('Count')
    ax.set_title('Test Column Null Value Analysis\n(Titanic)')
    plt.show()
    
    test.isnull().sum()
    
    
    # %%
    
    
    ### Save train / test to csv file 
    
    train_file = 'data/csv/titanic/train_cleaned.csv'
    test_file = 'data/csv/titanic/test_cleaned.csv'
    
    train.to_csv(train_file, index=False)
    train = pd.read_csv(train_file)
    #train.info()
    
    test.to_csv(test_file, index=False)
    test = pd.read_csv(test_file)
    #test.info()
    
    # %%