はじめに
欠損値補完(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でやるより機械学習向きです。カテゴリ変数のときにも書いたことですが。
効果のほどを見る
欠損値のあるデータを単に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
という扱いなのと、軽く触ってみた感じそこそこ使いづらいので、今後紹介するかどうかは未定です。気が向いたら書こうと思います。
追記:けっきょく書きました。
www.haya-programming.com