静かなる名辞

pythonとプログラミングのこと

2019/03/22:TechAcademyがteratailの質問・回答を盗用していた件
2019/03/26:TechAcademy盗用事件 公式発表と深まる疑念


【python】sklearnのSimpleImputerで欠損値補完をしてみる

はじめに

 欠損値補完(nanの処理)はだいたいpandasでやる人が多いですが、最近のscikit-learnはこの辺りの前処理に対するサポートも充実してきているので、平均値で補完する程度であればかえってscikit-learnでやった方が楽かもしれません。

 ということで、sklearn.impute.SimpleImputerを使ってみようと思います。

sklearn.impute.SimpleImputer — scikit-learn 0.21.3 documentation

使い方

 とても単純です。とりあえず、これを見てください。

import numpy as np
from sklearn.datasets import load_iris
from sklearn.impute import SimpleImputer

# 再現性確保
np.random.seed(0)

# irisでやることにする
iris = load_iris()
X = iris.data.copy()

# 全体の約1割のデータをnanに置き換える
mask = ~np.random.randint(10, size=X.shape).astype(np.bool)
X[mask] = np.nan
print(X[:10])  # 表示

# モデルの定義
imp = SimpleImputer()

# トランスフォーム
X_imputed = imp.fit_transform(X)
print(X_imputed[:10])  # 表示

結果

[[5.1 nan 1.4 0.2]
 [4.9 3.  1.4 0.2]
 [4.7 3.2 1.3 0.2]
 [4.6 3.1 1.5 0.2]
 [5.  3.6 1.4 0.2]
 [5.4 3.9 1.7 0.4]
 [4.6 3.4 nan 0.3]
 [5.  nan 1.5 0.2]
 [4.4 2.9 1.4 0.2]
 [4.9 3.1 nan 0.1]]
[[5.1        3.06296296 1.4        0.2       ]
 [4.9        3.         1.4        0.2       ]
 [4.7        3.2        1.3        0.2       ]
 [4.6        3.1        1.5        0.2       ]
 [5.         3.6        1.4        0.2       ]
 [5.4        3.9        1.7        0.4       ]
 [4.6        3.4        3.8162963  0.3       ]
 [5.         3.06296296 1.5        0.2       ]
 [4.4        2.9        1.4        0.2       ]
 [4.9        3.1        3.8162963  0.1       ]]

 ご覧の通り、埋まります。桁数が違うので場所がわかりやすいですね。

 欠損値部分は列の平均値で埋められます。これはSimpleImputerのstrategyパラメータで変更できます(たとえばstrategy="median"とすれば中央値で補完してくれます)。

 なお、sklearnの他のモデルと同様、補完に使われる値は「学習データから取得される」ことを覚えておきましょう。

>>> import numpy as np
>>> from sklearn.impute import SimpleImputer
>>> imp = SimpleImputer()
>>> imp.fit([[100], [101],[99]])
SimpleImputer(add_indicator=False, copy=True, fill_value=None,
              missing_values=nan, strategy='mean', verbose=0)
>>> imp.transform([[np.nan], [1], [2], [0]])
array([[100.],  # ここが1になったりはしない
       [  1.],
       [  2.],
       [  0.]])

 こういう望ましい性質があるので、pandasでやるより機械学習向きです。カテゴリ変数のときにも書いたことですが。

【python】機械学習でpandas.get_dummiesを使ってはいけない - 静かなる名辞

効果のほどを見る

 欠損値のあるデータを単にdropするのと、平均値補完とどちらが良いのでしょうか?

 せっかくなのでirisの分類で試してみます。なお、面倒臭いので欠損値を作るのは学習データだけです。また、分類器は議論の余地が少ないKNNとします。

import numpy as np
from sklearn.datasets import load_iris
from sklearn.neighbors import KNeighborsClassifier    
from sklearn.model_selection import train_test_split
from sklearn.impute import SimpleImputer
from sklearn.pipeline import Pipeline
from sklearn.metrics import classification_report

# 再現性確保
np.random.seed(0)

# データ生成
dataset = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
    dataset.data, dataset.target, stratify=dataset.target, random_state=0)
mask = ~np.random.randint(10, size=X_train.shape).astype(np.bool)
X_train[mask] = np.nan

# モデル定義
knn = KNeighborsClassifier()

# 処理
def drop():
    print("# drop")
    idx = ~(np.isnan(X_train).any(axis=1))
    X_train_, y_train_ = X_train[idx], y_train[idx]
    knn.fit(X_train_, y_train_)
    y_pred = knn.predict(X_test)
    print(classification_report(
        y_test, y_pred, digits=3, 
        target_names=dataset.target_names))
                    
def impute():
    print("# impute")
    imp = SimpleImputer()
    pl = Pipeline([("imputer", imp), ("KNN", knn)])
    pl.fit(X_train, y_train)
    y_pred = pl.predict(X_test)
    print(classification_report(
        y_test, y_pred, digits=3, 
        target_names=dataset.target_names))
    
drop()
impute()

結果

# drop
              precision    recall  f1-score   support

      setosa      1.000     1.000     1.000        13
  versicolor      0.929     1.000     0.963        13
   virginica      1.000     0.917     0.957        12

    accuracy                          0.974        38
   macro avg      0.976     0.972     0.973        38
weighted avg      0.976     0.974     0.974        38

# impute
              precision    recall  f1-score   support

      setosa      1.000     1.000     1.000        13
  versicolor      1.000     1.000     1.000        13
   virginica      1.000     1.000     1.000        12

    accuracy                          1.000        38
   macro avg      1.000     1.000     1.000        38
weighted avg      1.000     1.000     1.000        38

 補完の方が良いのは確かですが、どちらも良すぎて議論しづらいので、データセットを難易度が少し高いwineにします。

# 中略
from sklearn.datasets import load_wine

# 中略
dataset = load_wine()

結果

# drop
              precision    recall  f1-score   support

     class_0      0.812     0.867     0.839        15
     class_1      0.750     0.667     0.706        18
     class_2      0.538     0.583     0.560        12

    accuracy                          0.711        45
   macro avg      0.700     0.706     0.702        45
weighted avg      0.714     0.711     0.711        45

# impute
              precision    recall  f1-score   support

     class_0      0.737     0.933     0.824        15
     class_1      0.769     0.556     0.645        18
     class_2      0.538     0.583     0.560        12

    accuracy                          0.689        45
   macro avg      0.682     0.691     0.676        45
weighted avg      0.697     0.689     0.682        45

 今度はdropした方が良いです。このように、どう処理するのが良いのかは割とケースバイケースです。たとえば欠損値の出現率を変えると勝ち負けが変わります。

 どんなときでも躊躇なく平均値で補完すれば良いとは言えないのが、ちょっと嫌な所です。

まとめ

 なにはともあれ、気楽に使えるものがある、というのは素晴らしいことです。

余談

 実はもう少し高級な補完をしてくれるIterativeImputerなるモデルも存在しますが、

This estimator is still experimental for now: the predictions and the API might change without any deprecation cycle.
sklearn.impute.IterativeImputer — scikit-learn 0.21.3 documentation

 という扱いなのと、軽く触ってみた感じそこそこ使いづらいので、今後紹介するかどうかは未定です。気が向いたら書こうと思います。