ColumnTransformerで特徴ごとに異なる変換を行うパイプラインを構築する

2021.08.23

データアナリティクス事業本部の鈴木です。

前回はscikit-learnのpipelineモジュールで簡単なパイプラインを構築しました。

scikit-learnのpipelineモジュールで機械学習パイプラインを作る | DevelopersIO

今回はカラムごとに異なった前処理を適用するパイプラインの構築方法をご紹介します。

準備

検証した環境

  • コンテナ:jupyter/datascience-notebook
  • scikit-learn:0.24.2

データの作成

今回はseaborn-dataのpenguinsを利用します。

penguinsは、以下のpalmerpenguinsをもとにしたデータです。

allisonhorst/palmerpenguins: A great intro dataset for data exploration & visualization (alternative to iris).

以下のようにしてデータを作成しておきます。

import seaborn as sns
from sklearn.model_selection import train_test_split

# データのロード
df = sns.load_dataset('penguins')

# カラムの型と欠損値の有無を確認
df.info()
## <class 'pandas.core.frame.DataFrame'>
## RangeIndex: 344 entries, 0 to 343
## Data columns (total 7 columns):
##  #   Column             Non-Null Count  Dtype  
## ---  ------             --------------  -----  
##  0   species            344 non-null    object 
##  1   island             344 non-null    object 
##  2   bill_length_mm     342 non-null    float64
##  3   bill_depth_mm      342 non-null    float64
##  4   flipper_length_mm  342 non-null    float64
##  5   body_mass_g        342 non-null    float64
##  6   sex                333 non-null    object 
## dtypes: float64(4), object(3)
## memory usage: 18.9+ KB

df.isnull().sum()
## species               0
## island                0
## bill_length_mm        2
## bill_depth_mm         2
## flipper_length_mm     2
## body_mass_g           2
## sex                  11
## dtype: int64

# 特徴と推定対象に分離
X = df.drop("species", axis=1)
y = df["species"]

# 訓練データとテストデータに分離
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

今回は、ペンギンの種類(speciesカラムの値)を推定したいです。

特徴に2つカテゴリー変数があることが分かります。欠損値は、量的変数のbill_length_mmなどに加え、カテゴリー変数である性別のカラムにもあります。

2つのカテゴリー変数のうち、片方は欠損値があるため、もう片方のカテゴリー変数とは異なった処理をする必要があるのが今回のポイントです。

パイプラインの作成

前処理の方針

データセットに含まれる値から、以下の3通りに分けて前処理をすることとしましょう。

  • 量的変数:欠損値を補完し、標準化する。

  • カテゴリー変数

    1. 欠損値があるもの:欠損値を補完し、One-Hotエンコーディングする。
    2. 欠損値がないもの:One-Hotエンコーディングする。

これらを満たすパイプラインを作成してみましょう。

カラムごとに異なる処理を行うパイプラインの作成

カラムごとに異なるtransformersを適用するには、ColumnTransformerを使います。

ColumnTransformerのコンストラクタのtransformers引数に、名前、対象となるカラムの指定および各カラムに適用するtransformerのタプルのリストを渡します。

対象となるカラムの指定は、ColumnTransformerに渡されるオブジェクトがDataFrameであれば、列名のリストを使えるほか、make_column_selectorを使ってより柔軟な選択を行うことも可能です。

transformersには、OneHotEncoderのような単一のtransformerを渡すこともできます。また、今回のように、カラムに対して順番の決まった前処理が行いたい場合(例えば、欠損値を埋めた後に標準化したい)には、パイプラインのインスタンスを渡すことができます。

from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder
from sklearn.preprocessing import StandardScaler
from sklearn.experimental import enable_hist_gradient_boosting
from sklearn.ensemble import HistGradientBoostingClassifier

# 量的変数の前処理
# 量的変数に対して行うパイプラインを構築する
numeric_features = ["bill_length_mm", "bill_depth_mm", 
                    "flipper_length_mm", "body_mass_g"]

numeric_transformer = Pipeline(steps=[
    ('num_imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler())])

# カテゴリー変数の前処理1
# islandカラムに対して行うパイプラインを構築する
categorical_transformer1 = Pipeline(steps=[
    ('onehot1', OneHotEncoder(handle_unknown='ignore'))])

# カテゴリー変数の前処理2
# sexカラムに対して行うパイプラインを構築する
categorical_transformer2 = Pipeline(steps=[
    ('cat_imputer', SimpleImputer(strategy='constant',
                                  fill_value="unknown")),
    ('onehot2', OneHotEncoder(handle_unknown='ignore'))])

# ColumnTransformerの作成
preprocessor = ColumnTransformer(
    transformers=[
        ('num', numeric_transformer, numeric_features),
        ('cat1', categorical_transformer1, ["island"]),
        ('cat2', categorical_transformer2, ["sex"])])

# 全体のパイプラインの作成
pipe = Pipeline([("preprocessor", preprocessor),  
                 ("Classifier", HistGradientBoostingClassifier())])

上記コードでは以下の4つのパイプラインを作成しました。

  1. 量的変数を処理するnumeric_transformer
  2. 欠損値のないカテゴリー変数を処理するcategorical_transformer1
  3. 欠損値のあるカテゴリー変数を処理するcategorical_transformer2
  4. 処理全体のパイプラインとなるpipe

特に1から3のパイプラインは、ColumnTransformerのコンストラクタ内で、それぞれどのカラムに適用するか指定しています。

作成したパイプラインを確認します。

from sklearn import set_config
set_config(display='diagram')   
pipe

作成したパイプライン

最後に参考までにscoreを実行し、推論ができることを確認しておきます。

# 学習
pipe.fit(X_train, y_train)

# 推論結果のaccuracyを計算する。
pipe.score(X_test, y_test)
## 0.9767441860465116

最後に

ColumnTransformerを使って、カラムごとに異なるtransformersを適用するパイプラインを構築できました。

また、その前処理の順序を制御することができました。

実際の前処理では、このようにカラムごとに異なる前処理をすることがほとんどなので、ColumnTransformerを使いこなすことで、実戦でもさらにパイプラインが活躍しそうですね。

参考