Python {Article061}

ようこそ「Python」へ...

タイタニック号で機械学習のパイプライン(Pipeline)を学ぶには【sklearn Pipeline】

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

この記事では機械学習(ML: Machine Learning)でクロスバリデーション(Cross-Validation)を行うときにパイプライン(Pipeline)を使う方法を解説します。 「記事(Article048)」ではデータをラングリングするとき、Pandasのget_dummies()メソッドを使用しました。 そして、「記事(Article059)」ではデータをラングリングするとき、 Pandasのapply(), map(), transform()メソッド等を使用しました。

しかし、データのラングリングをPandasで行うと新規の検証データを取得したとき、その都度検証データのラングリングが必要になります。 なので、Pandasでのラングリングは可能な限り回避してSklearnの「make_column_transformer, OneHotEncoder」を使用します。 make_column_transformer()を使用するとPandasのget_dummies()と同等の処理が可能になります。 この場合、新規の検証データを取得したときデータのラングリングを行わないでそのデータを評価することができるといったメリットがあります。

ここではCross-Validationを行うときに、 Pipelineを使用してPandasの列「Sex, Embarked」をmake_column_transformer()でstr型からbit型に変換します。 行5-7ではPandasのDataFrameの列「Sex(性別)」「Embarked(乗船港)」の値をstr型からbit型に変換します。 行20ではmake_pipeline()でmake_column_transformerとClassifierを連結しています。 つまり、モデルの評価を行う前にPandasのDataFrameの特定の列をstr型からbit型に変換してラングリングさせています。 行21ではcross_val_score()にPipelineを指定しています。

from sklearn.compose import make_column_transformer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import OneHotEncoder

col_trans = make_column_transformer(
    (OneHotEncoder(), ['Sex', 'Embarked']), 
    remainder='passthrough')

clf = [ KNeighborsClassifier(n_neighbors=13),     
        DecisionTreeClassifier(),                   
        RandomForestClassifier(n_estimators=13),    
        GaussianNB(),                               
        SVC(),                                          
        ExtraTreeClassifier(),                       
        GradientBoostingClassifier(n_estimators=10, learning_rate=1, max_features=3, max_depth=3, random_state=10),  
        AdaBoostClassifier(),                       
        ExtraTreesClassifier()]     

for i in range(len(clf)):
    pipe = make_pipeline(col_trans, clf[i])
    score = cross_val_score(pipe, X, y, cv=5, n_jobs=1, scoring='accuracy')


最近よく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!
図F: Cross-Validation
click image to zoom!
Pipeline[1]
click image to zoom!
Pipeline[2]
click image to zoom!
Pipeline[3]


Cross-ValidationにPipelineを使用してモデルを評価する前にmake_column_transformer()を行う

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

    Visual Studio Code(VSC)を起動したら新規ファイルを作成して行1-34をコピペします。 行2-32ではPythonのライブラリを取り込んでいます。 行34ではPythonの警告を抑止しています。 ライブラリをまだインストールしていないときは「pip install」で事前にインストールしておいてください。
    ### Import the libraries
    from functools import reduce
    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from pandas.core.reshape.reshape import stack 
    import seaborn as sns
    
    # Importing Classifier Modules
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.tree import DecisionTreeClassifier, ExtraTreeClassifier
    from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier, BaggingClassifier, AdaBoostClassifier, GradientBoostingClassifier
    from sklearn.naive_bayes import GaussianNB
    from sklearn.svm import SVC    
    
    # Cross Validation(k-fold)
    from sklearn.model_selection import KFold
    from sklearn.model_selection import cross_val_score
    
    # Pipeline and GridSearchCV
    from sklearn.preprocessing import StandardScaler
    from sklearn.model_selection import train_test_split
    from sklearn.pipeline import Pipeline
    from sklearn.model_selection import GridSearchCV
    import sklearn.metrics as skm 
    
    # Make 
    from sklearn.compose import make_column_transformer
    from sklearn.pipeline import make_pipeline
    from sklearn.preprocessing import OneHotEncoder
    
    import warnings
    
    warnings.simplefilter('ignore')
    click image to zoom!
    図1
    図1はVisual Studio Code(VSC)の画面です。
  2. PandasのDataFrameにタイタニックのサバイバルデータを取り込む

    行2ではCSVファイルのパスを定義しています。 当サイトからダウンロードするときは行3のコメント(#)を外してください。 行4ではPandasのread_csv()メソッドでCSVファイルをDataFrameに取り込んでいます。
    ### Load the data
    csv_file = 'data/csv/titanic/train.csv'
    #csv_file = 'https://money-or-ikigai.com/menu/python/article/data/titanic/train.csv'
    raw = pd.read_csv(csv_file)
    click image to zoom!
    図2
    図2は実行画面です。 VSCのインタラクティブ・ウィンドウにDataFrame(raw)の構造と内容が表示されています。 CSVファイルには891人の乗客データが格納されています。
  3. データをクリーンナップする

    行19-20ではDataFrameの列「Embarked」の値がNullの行を削除しています。 行24-31ではDataFrameの列「Age」の値がNullのとき年齢の中央値(median)で置換しています。 ここでは段順に年齢の中央値を計算するのはなく敬称(Mr, Miss, Mrs,...)毎の中央値を計算しています。 行57-58ではDataFrameから不要の列「PassengerId, Name, Ticket, Cabin, Title」を削除しています。
    ### Data Cleanup
    raw.isnull().sum()
    # raw
    # PassengerId      0
    # Survived         0
    # Pclass           0
    # Name             0
    # Sex              0
    # Age            177 => Fill with median
    # SibSp            0
    # Parch            0
    # Ticket           0
    # Fare             0
    # Cabin          687 
    # Embarked         2 => Remove rows
    # dtype: int64
    
    # Remove rows if 'Embarked' is null
    filter_mask = raw['Embarked'].notna()
    raw = raw[filter_mask]  # drop rows
    #raw.isnull().sum()
    
    # Fill 'Age with median if 'Age' is null
    raw['Title'] = raw['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)
    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 }
    
    raw['Title_map'] = raw['Title'].map(title_mapping)
    raw['Title_map'] = raw['Title_map'].fillna(0)
    raw['Age'].fillna(raw.groupby('Title_map')['Age'].transform('median'), inplace=True)
    raw.isnull().sum()
    # raw.shape
    
    train = raw.copy()
    #df.info()
    # Int64Index: 889 entries, 0 to 890
    # Data columns (total 14 columns):
    #  #   Column       Non-Null Count  Dtype  
    # ---  ------       --------------  -----  
    #  0   PassengerId  889 non-null    int64  => Drop
    #  1   Survived     889 non-null    int64  
    #  2   Pclass       889 non-null    int64  
    #  3   Name         889 non-null    object => Drop
    #  4   Sex          889 non-null    object => Transform
    #  5   Age          889 non-null    float64
    #  6   SibSp        889 non-null    int64  
    #  7   Parch        889 non-null    int64  
    #  8   Ticket       889 non-null    object => Drop
    #  9   Fare         889 non-null    float64
    #  10  Cabin        889 non-null    object => Drop
    #  11  Embarked     889 non-null    object => Transform
    #  12  Title        889 non-null    object => Drop
    #  13  Title_map    889 non-null    int64  
    # dtypes: float64(2), int64(6), object(6)
    
    features_drop = ['PassengerId','Name','Ticket','Cabin','Title']
    temp = train.drop(features_drop, axis=1)
    
    X = temp.drop('Survived', axis=1)    # Exclude Survived column
    y = temp['Survived']                 # Survived column only
    click image to zoom!
    図3-1
    図3-1にはDataFrame(raw)のNullの件数を列毎に表示しています。 列「Age, Cabin, Embarked」にNullの値が存在します。
    click image to zoom!
    図3-2
    図3-2にはDataFrame(raw)の列「Embarked」の値がNullの行を削除した後の状態を表示しています。 列「Embarked」のNullの件数が0件になっています。
    click image to zoom!
    図3-3
    図3-3には列「Age」のNullを中央値(median)で置換後の状態を表示しています。 列「Age」のNullの件数が0件になっています。
    click image to zoom!
    図3-4
    図3-4にはDataFrame(X)とSeries(y)の構造と内容を表示しています。 DataFrame(X)には889件の乗客データが格納されています。 Series(y)には889件の生死(0:Dead, 1:Survived)のデータが格納されています。
  4. OneHotEncoder(), make_column_transformer()の処理を事前に確認する

    行2-3ではOneHotEncoder()でDataFrame(X)の列「Sex(female, male)」がどのように変換されるか確認しています。 行6-7ではOneHotEncoder()でDataFrame(X)の列「Embarked(C, Q, S)」がどのように変換されるか確認しています。
    ### Test transform columns
    ohe = OneHotEncoder(sparse=False)
    ohe.fit_transform(X[['Sex']])
    #ohe.categories_
    
    ohe = OneHotEncoder(sparse=False)
    ohe.fit_transform(X[['Embarked']])
    #ohe.categories_
    click image to zoom!
    図4-1
    図4-1にはDataFrame(X)の列「Sex」をstr型からbit型に変換した結果が表示されています。 それぞれの配列の要素が「1」のとき、性別「female, male」に対応します。
    click image to zoom!
    図4-2
    図4-2にはDataFrame(X)の列「Embarked」をstr型からbit型に変換した結果が表示されています。 それぞれの配列の要素が「1」のとき、乗船港「C, Q, S」に対応します。 C = Cherbourg, Q = Queenstown, S = Southampton
  5. Cross-ValidationにPipelineを使用して複数のモデルを評価する

    行4ではCross-ValidationのK-Fold(分割回数)を設定しています。 ここでは学習データ(X)を10回分割させます。

    行8-10ではDataFrame(X)の列「Sex, Embarked」をstr型からbit型に変換させています。 列「Sex(female, male)」は2要素の配列に分割されます。配列の要素0が「1」のときは「female」、 配列の要素1が「1」のときは「male」を意味します。 列「Embarked(C, Q, S)」は3要素の配列に分割されます。配列の要素0が「1」のときは「C」、 配列の要素1が「1」のときは「Q」、配列の要素2が「1」のときは「S」を意味します。

    行18-26ではモデルを定義しています。 行29-39のforループではCross-Validation()を実行してそれぞれのモデルを評価します。 Cross-Validion()の引数にはPipelineを指定してDataFrame(X)の列「Sex, Embarked」をstr型からbit型に変換させています。
    ### Modeling
    
    ## Cross Validation(k-fold)
    k_fold = KFold(n_splits=10, shuffle=True, random_state=0)
    # KFold(n_splits=5, *, shuffle=False, random_state=None)
    
    ## Transform columns
    col_trans = make_column_transformer(
        (OneHotEncoder(), ['Sex', 'Embarked']), 
        remainder='passthrough')
    
    #  4   Sex          889 non-null    object => Transform
    #  11  Embarked     889 non-null    object => Transform
    
    ## Cross Validation Score()
    # cross_val_score(clf[i], X_train, y_train, cv=k_fold, n_jobs=1, scoring=scoring)
    
    clf = [ KNeighborsClassifier(n_neighbors=13),     
            DecisionTreeClassifier(),                   
            RandomForestClassifier(n_estimators=13),    
            GaussianNB(),                               
            SVC(),                                          
            ExtraTreeClassifier(),                       
            GradientBoostingClassifier(n_estimators=10, learning_rate=1, max_features=3, max_depth=3, random_state=10),  
            AdaBoostClassifier(),                       
            ExtraTreesClassifier()]                     
    
    
    for i in range(len(clf)):
        pipe = make_pipeline(col_trans, clf[i])
        #print('Pipeline:', pipe)
    
        score = cross_val_score(pipe, X, y, cv=k_fold, n_jobs=1, scoring='accuracy')
    
        print(f'Score of Model({i}):', 
            '{0:.2f}'.format(round(np.mean(score)*100, 2)), clf[i].__class__.__name__)
            
        #print('Cross-validation scores:', score)                    
        #print('-'*80)
    click image to zoom!
    図5
    図5には実行結果が表示されています。 ここでは、RandomForestClassifier()がもっとも高いスコアになっています。
  6. ここで解説したコードをまとめて掲載

    最後にここで解説したすべてのコードをまとめて掲載しましたので参考にしてください。
    
    ### Import the libraries
    
    from functools import reduce
    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from pandas.core.reshape.reshape import stack 
    import seaborn as sns
    
    # Importing Classifier Modules
    from sklearn.neighbors import KNeighborsClassifier
    from sklearn.tree import DecisionTreeClassifier, ExtraTreeClassifier
    from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier, BaggingClassifier, AdaBoostClassifier, GradientBoostingClassifier
    from sklearn.naive_bayes import GaussianNB
    from sklearn.svm import SVC    
    
    # Cross Validation(k-fold)
    from sklearn.model_selection import KFold
    from sklearn.model_selection import cross_val_score
    
    # Pipeline and GridSearchCV
    from sklearn.preprocessing import StandardScaler
    from sklearn.model_selection import train_test_split
    from sklearn.pipeline import Pipeline
    from sklearn.model_selection import GridSearchCV
    import sklearn.metrics as skm 
    
    # Make 
    from sklearn.compose import make_column_transformer
    from sklearn.pipeline import make_pipeline
    from sklearn.preprocessing import OneHotEncoder
    
    import warnings
    
    warnings.simplefilter('ignore')
    
    
    # %%
    
    ### Load the data
    csv_file = 'data/csv/titanic/train.csv'
    #csv_file = 'https://money-or-ikigai.com/menu/python/article/data/titanic/train.csv'
    raw = pd.read_csv(csv_file)
    
    
    # %%
    
    ### Data Cleanup
    raw.isnull().sum()
    # raw
    # PassengerId      0
    # Survived         0
    # Pclass           0
    # Name             0
    # Sex              0
    # Age            177 => Fill with median
    # SibSp            0
    # Parch            0
    # Ticket           0
    # Fare             0
    # Cabin          687 
    # Embarked         2 => Remove rows
    # dtype: int64
    
    # %%
    
    # Remove rows if 'Embarked' is null
    filter_mask = raw['Embarked'].notna()
    raw = raw[filter_mask]  # drop rows
    raw.isnull().sum()
    
    # %%
    
    # Fill 'Age with median if 'Age' is null
    raw['Title'] = raw['Name'].str.extract(' ([A-Za-z]+)\.', expand=False)
    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 }
    
    raw['Title_map'] = raw['Title'].map(title_mapping)
    raw['Title_map'] = raw['Title_map'].fillna(0)
    raw['Age'].fillna(raw.groupby('Title_map')['Age'].transform('median'), inplace=True)
    raw.isnull().sum()
    # raw.shape
    # (889, 14)
    
    
    # %%
    
    train = raw.copy()
    #df.info()
    # Int64Index: 889 entries, 0 to 890
    # Data columns (total 14 columns):
    #  #   Column       Non-Null Count  Dtype  
    # ---  ------       --------------  -----  
    #  0   PassengerId  889 non-null    int64  => Drop
    #  1   Survived     889 non-null    int64  
    #  2   Pclass       889 non-null    int64  
    #  3   Name         889 non-null    object => Drop
    #  4   Sex          889 non-null    object => Transform
    #  5   Age          889 non-null    float64
    #  6   SibSp        889 non-null    int64  
    #  7   Parch        889 non-null    int64  
    #  8   Ticket       889 non-null    object => Drop
    #  9   Fare         889 non-null    float64
    #  10  Cabin        889 non-null    object => Drop
    #  11  Embarked     889 non-null    object => Transform
    #  12  Title        889 non-null    object => Drop
    #  13  Title_map    889 non-null    int64  
    # dtypes: float64(2), int64(6), object(6)
    
    features_drop = ['PassengerId','Name','Ticket','Cabin','Title']
    temp = train.drop(features_drop, axis=1)
    
    X = temp.drop('Survived', axis=1)    # Exclude Survived column
    y = temp['Survived']                 # Survived column only
    
    
    # %%
    
    ## Test transform columns
    ohe = OneHotEncoder(sparse=False)
    ohe.fit_transform(X[['Sex']])
    #ohe.categories_
    
    # %%
    
    ## Test transform columns
    ohe = OneHotEncoder(sparse=False)
    ohe.fit_transform(X[['Embarked']])
    #ohe.categories_
    
    # array([[0., 0., 1.], => S
    #        [1., 0., 0.], => C
    #        [0., 0., 1.], => S
    #        ...,
    #        [0., 0., 1.],
    #        [1., 0., 0.],
    #        [0., 1., 0.]])
    
    # %%
    
    ### Modeling
    
    ## Cross Validation(k-fold)
    k_fold = KFold(n_splits=10, shuffle=True, random_state=0)
    # KFold(n_splits=5, *, shuffle=False, random_state=None)
    
    ## Transform columns
    col_trans = make_column_transformer(
        (OneHotEncoder(), ['Sex', 'Embarked']), 
        remainder='passthrough')
    
    #  4   Sex          889 non-null    object => Transform
    #  11  Embarked     889 non-null    object => Transform
    
    ## Cross Validation Score()
    # cross_val_score(clf[i], X_train, y_train, cv=k_fold, n_jobs=1, scoring=scoring)
    
    clf = [ KNeighborsClassifier(n_neighbors = 13),     
            DecisionTreeClassifier(),                   
            RandomForestClassifier(n_estimators=13),    
            GaussianNB(),                               
            SVC(),                                          
            ExtraTreeClassifier(),                       
            GradientBoostingClassifier(n_estimators=10, learning_rate=1, max_features=3, max_depth =3, random_state = 10),  
            AdaBoostClassifier(),                       
            ExtraTreesClassifier()]                     
    
    def model_fit():
        for i in range(len(clf)):
            pipe = make_pipeline(col_trans, clf[i])
            #print('Pipeline:', pipe)
    
            score = cross_val_score(pipe, X, y, cv=k_fold, n_jobs=1, scoring='accuracy')
    
            print(f'Score of Model({i}):', 
                '{0:.2f}'.format(round(np.mean(score)*100, 2)), clf[i].__class__.__name__)
            
            #print('Cross-validation scores:', score)                    
            #print('-'*80)
    
    model_fit()
    
    
    # %%