scikit-learnのSimpleImputerで欠損値の補完

とある講演を聞いていて、SimpleImputerという機能の存在を知ったのでその紹介です。(その講演自体は別のテーマで話されていたのですが、その中でSimpleImputerは常識みたいに扱われていたのに、僕は使ったことがなかったので試そうと思いました。)

これは何かというと、pandasのDataFrameやNumPyのArray中にある欠損値を補完してくれるものです。目的はpandasのDataFrameの機能でいうところのfillna()に近いですね。
fillna()で十分だという意見もあると思いますし、実際僕もfillna()で済ませてきたのでこれの存在を知らなかったのですが、ちゃんとSimpleImputerのメリットもあるのでその辺説明していきたいと思います。

ドキュメントはこちらです。
sklearn.impute.SimpleImputer — scikit-learn 1.0.1 documentation
6.4. Imputation of missing values — scikit-learn 1.0.1 documentation

version 0.20 から登場したモデルで、その前まで存在した、 sklearn.preprocessing.Imputer の置き換えとして実装されたようですね。

とりあえず補完対象となる欠損値を含むデータがないと始まらないので、適当に準備します。

import pandas as pd


# 欠損値を含むDataFrameを生成
df = pd.DataFrame(
    {
        "col1": [8, None, 6, 3, None],
        "col2": [None, 8, 2, 2, 10],
        "col3": [3, 10, None, 0, 3],
    }
)
print(df)
"""
   col1  col2  col3
0   8.0   NaN   3.0
1   NaN   8.0  10.0
2   6.0   2.0   NaN
3   3.0   2.0   0.0
4   NaN  10.0   3.0
"""

これで一部のデータが欠損しているDataFrameができましたね。それでは、SimpleImputer を使ってきましょう。SimpleImputer を使うときには、まず欠損値を埋める方法を決める必要があります。 その列の欠損してない値の 平均値、中央値、最頻値を用いて欠損値を埋めるか、もしくは定数を使って埋めることになります。

埋め方を決めたらそれは strategy 引数で指定します。対応は以下の通りです。
– mean ・・・ 平均値
– median ・・・ 中央値
– most_frequent ・・・ 最頻値
– constant ・・・ 定数 (別途、fill_value 引数で定数を指定する)

今回はお試しなので、 mean (平均値) でやってみます。

# インスタンスを生成
imp_mean = SimpleImputer(strategy='mean')
# 学習
imp_mean.fit(df)
# 学習したパラメーター(補完に使う平均値を表示)
print(imp_mean.statistics_)
# [5.66666667 5.5        4.        ]

# 欠損値を補完
imp_ary = imp_mean.transform(df)
# 結果を表示
print(imp_ary)
"""
[5.66666667 5.5        4.        ]
[[ 8.          5.5         3.        ]
 [ 5.66666667  8.         10.        ]
 [ 6.          2.          4.        ]
 [ 3.          2.          0.        ]
 [ 5.66666667 10.          3.        ]]
"""

# 補完後の型はNumpyのArraryになる
print(type(imp_ary))
# <class 'numpy.ndarray'>

fit で各列の平均値を学習し、その値を使ってNaNだったところを埋めてくれていますね。
注意しないといけないのは transform して戻ってくるデータはNumPyのArrayになっていることです。(上のサンプルコードで型を見ている通り。)

欠損値補完後のデータもDataFrameで欲しいんだという場合は再度DataFrameに変換する必要があるようです。モデルの引数でそういうオプションがあるといいのですが、今の時点のバージョン(1.0.1)ではなさそうなので自分でやりましょう。

imp_df = pd.DataFrame(imp_mean.transform(df), columns=df.columns)
print(imp_df)
"""
       col1  col2  col3
0  8.000000   5.5   3.0
1  5.666667   8.0  10.0
2  6.000000   2.0   4.0
3  3.000000   2.0   0.0
4  5.666667  10.0   3.0
"""

正直、このように単一のDataFrameにたいしてそのDataFrameの統計量を使って補完するのであれば、fillnaの方が使いやすい気がします。次のようにして同じ結果が得られるので。

print(df.fillna(value=df.mean()))
"""
       col1  col2  col3
0  8.000000   5.5   3.0
1  5.666667   8.0  10.0
2  6.000000   2.0   4.0
3  3.000000   2.0   0.0
4  5.666667  10.0   3.0
"""

これは、fillnaが列ごとに個別の値を設定できることを利用し、valueにそれぞれの列の平均値(df.mean())を渡すことによって実現しているものです。

ちなみに、SimpleImputer で strategy=’constant’ を指定する場合、fillnaのように列ごとに違う値を指定することはできません。次のように定数を一つだけ指定してそれで補完します。
この点はfillnaと比較したときに明確なデメリットですね。

imp_cons = SimpleImputer(strategy='constant', fill_value=-1)
imp_cons.fit(df)
print(imp_cons.transform(df))
"""
[[ 8. -1.  3.]
 [-1.  8. 10.]
 [ 6.  2. -1.]
 [ 3.  2.  0.]
 [-1. 10.  3.]]
"""

strategy=’constant’ の場合、補完する数値を計算する必要がないので、fit()したときに何を学習しているのか不明だったのですが、どうやらここで渡した列数を記憶しているようです。(fit に3列のDataFrameを渡すと、transformメソッドも3列のDataFrameしか受け付けなくなる。)

さて、これまでの説明だと、どうもfillna()の方が便利に思えてきますね。(Numpyにはfillnaがないので、元のデータがDataFrameではなく2次元のArrayの場合は使えるかもしれませんが。)

ここから SimpleImputer のメリットの紹介になります。

一つの目のメリットは、一度学習させた補完値を他のデータにも適用することができるということです。例えば機械学習の前処理でデータを補完する場合、訓練データの欠損値をある値で補完したのであれば、検証データの欠損値もその値で補完したいですし、本番環境にリリースして実運用が始まった後も欠損値があるデータに対して補完をかけるのであれば訓練時と同じデータで補完したいです。

SimpleImputer であれば、 fitした時点で補完する値を学習しているので、別のデータに対してtransformしてもその値を使って補完してくれます。
fillna でも、補完に使うデータをどこかに退避しておいてそれを使えばいいじゃないか、という声も聞こえてきそうですし、実際そうなのですが、fitしたモデルを保存しておいてそれを使う方が、補完に使うデータ(辞書型か配列か)をどこかに退避しておくより気楽に感じます。(個人の感想です。なぜそう感じるのかは自分でもよくわかりません。)

# 元と別のデータ
df2 = pd.DataFrame({
    "col1": [7, None, 2],
    "col2": [2, 2, None],
    "col3": [6, 8, None],
})

# 学習済みの値を使って補完される
print(imp_mean.transform(df2))
"""
[[7.         2.         6.        ]
 [5.66666667 2.         8.        ]
 [2.         5.5        4.        ]]
"""

もう一つの利点は、これがscikit-learnのモデルなので、Pipelineに組み込めるということです。完全に適当な例なのですが、
平均値で欠損値の補完 -> データの標準化 -> ロジスティック回帰
と処理するパイプラインを構築してみます。
ロジスティック回帰をやるので正解ラベルも適当に作ります。

from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression


# ロジスティック回帰のサンプルを動かすために架空の正解ラベルを作る
y = [0, 0, 0, 1, 1]

# 欠損値を補完した後、StandardScalerで標準化してロジスティック回帰で予測するパイプライン
clf = Pipeline([
    ("si", SimpleImputer(strategy="mean")),
    ("ss", StandardScaler()),
    ("lr", LogisticRegression()),
])
# 学習
clf.fit(df, y)

このようにPipelineに組み込めるというのは fillna の方にはないメリットだと思います。

繰り返しますが、上の例は完全に適当に作ったパイプラインです。
一般的な話として、前処理次に欠損値を補完した後に標準化するという前処理を推奨しているわけではないのでご注意ください。あくまでも、SimpleImputerを組み込んだパイプラインは動きますということだけを示した例です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です