欠損値処理シリーズ 第10回:
多変量補完② — MICE・MissForest と欠損インジケータ、選び方ガイド

欠損値処理シリーズ 第10回:多変量補完② — MICE・MissForest と欠損インジケータ、選び方ガイド

ついにシリーズ最終回です。

第1回で欠損のメカニズム(MCAR / MAR / MNAR)を紹介し、削除戦略(第3〜4回)、単変量補完(第5〜7回)、時系列補完(第8回)、KNN補完(第9回)と進んできました。

最終回となる今回は、さらに高度な多変量補完である MICEMissForest、そして補完と併用すると効果的な 欠損インジケータ を扱います。

そして最後に、これまでの全手法を実際の予測タスクで総合比較 し、「結局どの手法を使えばいいのか」という問いに指針を示して締めくくります。

MICE(Multivariate Imputation by Chained Equations)

MICE は「連鎖方程式による多変量補完」と訳されます。

少し難しそうな名前ですが、考え方は理解しやすいものです。

 

 MICE の基本的な考え方

MICE は、「欠損のある各列を、他の列を使って順番に予測する」 ことを繰り返す手法です。

流れを言葉で説明すると次のようになります。

1. まず、すべての欠損を仮の値(平均値など)で埋める
2. 1つの列を選び、その列の 本来の欠損箇所 を一旦「未知」に戻す
3. 他の列を説明変数として、その列を 回帰モデルで予測 し、欠損箇所を予測値で埋め直す
4. 次の列について同じことを行う
5. すべての列を1巡したら、また最初の列に戻り、これを 何度も繰り返す(反復)
6. 補完値が安定したら終了

 

補足:なぜ「連鎖方程式」と呼ぶのか

各列を「他の列で予測する回帰式」が、列から列へと 連鎖的に 適用され、それを反復するためです。1回予測して終わりではなく、何度も予測を更新することで、変数間の関係を反映した精度の高い補完値に収束していきます。

 

 scikit-learn での MICE 実装:IterativeImputer

scikit-learn では IterativeImputer クラスが MICE の考え方を実装しています。

これは現在も「実験的機能」という位置づけのため、使う前に専用のインポート文が必要です。

以下、コードです。

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings('ignore')
from sklearn.model_selection import train_test_split

# 実験的機能の有効化(IterativeImputer を使う前に必須)
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer

# Titanicデータセットの読み込み
df = sns.load_dataset('titanic')
numeric_cols = ['age', 'fare', 'pclass', 'sibsp', 'parch']
df_num = df[numeric_cols].copy()

# 学習用とテスト用に分割
df_train, df_test = train_test_split(
    df_num, 
    test_size=0.2, 
    random_state=42
)

# MICE(IterativeImputer)で補完
mice_imputer = IterativeImputer(max_iter=10, random_state=42)

train_imputed = mice_imputer.fit_transform(df_train)
test_imputed = mice_imputer.transform(df_test)

# MICE の出力を DataFrame に戻す
df_train_mice = pd.DataFrame(
    train_imputed,
    columns=numeric_cols,                         
    index=df_train.index
)
df_test_mice = pd.DataFrame(
    test_imputed, 
    columns=numeric_cols,                        
    index=df_test.index
)

print("-"*50)
print(f"補完前の欠損(学習): {df_train['age'].isnull().sum()}")
print(f"補完後の欠損(学習): {df_train_mice['age'].isnull().sum()}")
print(f"補完後の age の統計:")
print(df_train_mice['age'].describe().round(2))
print("-"*50)
print(f"補完後の欠損(テスト): {df_test_mice['age'].isnull().sum()}")
print(f"補完後の age の統計:")
print(df_test_mice['age'].describe().round(2))
  • from sklearn.experimental import enable_iterative_imputer:この行を実行しないと IterativeImputer がインポートできません。# noqa は「この行の文法警告を無視する」というコメント記法です
  • max_iter=10:反復回数の上限(連鎖を10巡する)
  • random_state=42:再現性の確保(第7回で学んだ通り重要)
  • fit_transform/transform:これまで同様、データ漏洩を防ぐ流れ

 

以下、実行結果です。

--------------------------------------------------
補完前の欠損(学習): 140
補完後の欠損(学習): 0
補完後の age の統計:
count    712.00
mean      29.05
std       13.68
min       -6.43
25%       22.00
50%       27.82
75%       36.00
max       80.00
Name: age, dtype: float64
--------------------------------------------------
補完後の欠損(テスト): 0
補完後の age の統計:
count    179.00
mean      30.34
std       13.29
min        0.83
25%       22.00
50%       27.83
75%       37.00
max       71.00
Name: age, dtype: float64

 

age の欠損が、他の列(fare, pclass など)との関係を使って補完されます。

 

MICE の特徴
  • 強み:変数間の関係を反映し、MAR に対して非常に効果的。統計的に洗練された手法
  • 弱み:計算コストが高い。反復回数や内部の回帰モデルの選択に結果が左右される

IterativeImputer は内部で使う予測モデルを変更でき、デフォルトはベイズ線形回帰です。次に紹介する MissForest は、この内部モデルを ランダムフォレスト にしたものと考えると理解しやすいです。

 

MissForest(ランダムフォレストによる補完)

MissForest は、MICE の「各列を他の列で予測する」という枠組みの中で、予測モデルに ランダムフォレスト(Random Forest) を使う手法です。

 

 MissForest の利点

ランダムフォレストやを使うことで、線形回帰では捉えられない 非線形な関係特徴量同士の複雑な相互作用 も学習できます。

また、数値変数とカテゴリ変数の両方を扱える実装が多いのも利点です。

処理の流れは MICE とほぼ同じです。

1. 連続変数は平均、カテゴリ変数は最頻値で初期補完
2. データを「観測部分」と「欠損部分」に分け、観測部分でランダムフォレストを学習
3. 学習したモデルで欠損部分を予測し、埋め直す
4. これを、補完値の変化が小さくなるまで反復(通常5〜6回程度で収束)

 

 実装方法その1:専用ライブラリ MissForest

専用のライブラリをインストールして使う方法があります。MissForestです。

以下でインストールできます。

pip install MissForest

 

では、実際に実施してみましょう。

以下、コードです。

from missforest import MissForest

# MissForestで補完
imputer = MissForest()

train_mf = imputer.fit_transform(df_train)
test_mf = imputer.transform(df_test)

# MissForest の出力を DataFrame に戻す
df_train_mf = pd.DataFrame(
    train_mf,
    columns=numeric_cols,
    index=df_train.index
)
df_test_mf = pd.DataFrame(
    test_mf,
    columns=numeric_cols,
    index=df_test.index
)

print()
print("-"*50)
print(f"補完前の欠損(学習): {df_train['age'].isnull().sum()}")
print(f"補完後の欠損(学習): {df_train_mf['age'].isnull().sum()}")
print(f"補完後の age の統計:")
print(df_train_mf['age'].describe().round(2))
print("-"*50)
print(f"補完後の欠損(テスト): {df_test_mf['age'].isnull().sum()}")
print(f"補完後の age の統計:")
print(df_test_mf['age'].describe().round(2))

 

以下、実行結果です。

100%|██████████| 5/5 [00:01<00:00,  3.56it/s]
100%|██████████| 5/5 [00:00<00:00, 391.44it/s]
100%|██████████| 5/5 [00:00<00:00, 502.52it/s]
--------------------------------------------------
補完前の欠損(学習): 140
補完後の欠損(学習): 0
補完後の age の統計:
count    712.00
mean      29.65
std       13.32
min        0.42
25%       22.00
50%       28.90
75%       36.00
max       80.00
Name: age, dtype: float64
--------------------------------------------------
補完後の欠損(テスト): 0
補完後の age の統計:
count    179.00
mean      30.66
std       13.41
min        0.83
25%       22.00
50%       29.66
75%       37.00
max       71.00
Name: age, dtype: float64

 

実行すると、ランダムフォレストの予測力を使った高精度な補完が実現できます。

 

注意:ライブラリのバージョンに注意

MissForest 系のライブラリは、過去に missingpy というパッケージが使われていましたが、メンテナンス状況が変わっています。インストール時は最新のパッケージ名と使い方を公式ドキュメントで確認してください。本記事では、より安定して使える scikit-learn での代替実装 を主に紹介します。

 

 実装方法その2:scikit-learn で MissForest 相当を再現する

実は、第1節で使った IterativeImputer内部モデルをランダムフォレストに差し替える だけで、MissForest と同等の補完が実現できます。

以下、コードです。

from sklearn.ensemble import RandomForestRegressor

# 内部モデルにランダムフォレストを指定 = MissForest相当
missforest_like = IterativeImputer(
    estimator=RandomForestRegressor(n_estimators=100, random_state=42),
    max_iter=10,
    random_state=42
)

train_mf2 = missforest_like.fit_transform(df_train)
test_mf2 = missforest_like.transform(df_test)

# 出力を DataFrame に戻す
df_train_mf2 = pd.DataFrame(
    train_mf2,
    columns=numeric_cols,
    index=df_train.index
)
df_test_mf2 = pd.DataFrame(
    test_mf2,
    columns=numeric_cols,
    index=df_test.index
)

print("-"*50)
print(f"補完前の欠損(学習): {df_train['age'].isnull().sum()}")
print(f"補完後の欠損(学習): {df_train_mf2['age'].isnull().sum()}")
print(f"補完後の age の統計:")
print(df_train_mf2['age'].describe().round(2))
print("-"*50)
print(f"補完後の欠損(テスト): {df_test_mf2['age'].isnull().sum()}")
print(f"補完後の age の統計:")
print(df_test_mf2['age'].describe().round(2))
  • estimator=RandomForestRegressor(...)IterativeImputer の内部予測モデルをランダムフォレストに変更
  • n_estimators=100:ランダムフォレストの決定木の本数
  • これにより MICE の枠組みでランダムフォレストを使う = MissForest 相当の補完になる

 

以下、実行結果です。

--------------------------------------------------
補完前の欠損(学習): 140
補完後の欠損(学習): 0
補完後の age の統計:
count    712.00
mean      29.46
std       13.56
min        0.42
25%       21.00
50%       28.91
75%       37.13
max       80.00
Name: age, dtype: float64
--------------------------------------------------
補完後の欠損(テスト): 0
補完後の age の統計:
count    179.00
mean      30.67
std       13.59
min        0.83
25%       21.00
50%       29.55
75%       38.00
max       71.00
Name: age, dtype: float64

 

IterativeImputerestimator を差し替えるだけで、さまざまなアルゴリズムベースの補完を試せるのが scikit-learn の柔軟なところです。

 

欠損インジケータ(Missing Indicator)

ここまでは「欠損値をいかに正確な値で埋めるか」を考えてきました。

しかし、発想を変えて 「欠損していたという事実そのもの」を特徴量として残す というテクニックがあります。

これが 欠損インジケータ(missing indicator) です。

 

 欠損インジケータとは

欠損のある列について、「その値が欠損していたかどうか」を表す新しい列(0 / 1のフラグ) を追加します。

たとえば age 列に対して age_missing 列を作り、age が欠損していた行は1、そうでない行は0とします。

なぜこれが有効なのでしょうか?

それは、「欠損していたこと自体が予測に役立つ情報を持つ」場合がある からです。

第1回で触れた MNAR を思い出してください。「高所得者ほど年収を答えない」なら、「年収が欠損していた」という事実は「高所得者である可能性」を示唆します。

この情報を、補完値とは別に 明示的にモデルへ伝えられる のが欠損インジケータの強みです。

 

 pandas での簡単な実装

以下、コードです。

df_ind = df_num.copy()

# age が欠損していた行を 1、そうでない行を 0 とするフラグ列を追加
df_ind['age_missing'] = np.where(df_ind['age'].isnull(), 1, 0)

print("age_missing 列の内訳:")
print(df_ind['age_missing'].value_counts())
print(df_ind[['age', 'age_missing']].head(10))
  • np.where(条件, 真のときの値, 偽のときの値):条件に応じて0/1を割り当てる
  • このフラグ列を作ったあと、age 列自体は中央値や KNN などで補完する

 

以下、実行結果です。

age_missing 列の内訳:
age_missing
0    714
1    177
Name: count, dtype: int64
    age  age_missing
0  22.0            0
1  38.0            0
2  26.0            0
3  35.0            0
4  35.0            0
5   NaN            1
6  54.0            0
7   2.0            0
8  27.0            0
9  14.0            0

 

age_missing 列が追加され、欠損だった行が1になっていることがわかります。

 

 scikit-learn での実装

scikit-learn には専用の MissingIndicator クラスもあります。

以下、コードです。

from sklearn.impute import MissingIndicator

indicator = MissingIndicator()
mask_train = indicator.fit_transform(df_train)

# どの列にインジケータが作られたか確認
print(f"インジケータ対象の列インデックス: {indicator.features_}")
print(f"インジケータ行列の形状: {mask_train.shape}")
  • MissingIndicator():欠損のある列に対して真偽値のフラグ行列を生成
  • indicator.features_:どの列にインジケータが作られたかを示す

 

以下、実行結果です。

インジケータ対象の列インデックス: [0]
インジケータ行列の形状: (712, 1)

 

欠損インジケータの使いどころと注意点
  • 使いどころ:欠損自体が情報を持つ(MNARが疑われる)場合に特に有効。線形モデルと相性が良い
  • 注意点:欠損のある列が多いと、フラグ列が増えて特徴量が膨張する。複数の列が同じ行で欠損していると、フラグ列同士が高い相関を持つことがある
  • 基本戦略:補完(値を埋める)と欠損インジケータ(事実を残す)を 併用 すると、両方の利点を得られる

 

全手法の総合比較実験

これまで学んだ主要な手法を使って Titanic の 生存予測 を行い、どの補完手法が最も良い精度を出すかを実際に比較してみましょう。

以下、コードです。

from sklearn.impute import SimpleImputer, KNNImputer
from sklearn.pipeline import Pipeline
from sklearn.metrics import accuracy_score, f1_score
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier

# データ準備:数値特徴量で生存を予測
df_all = sns.load_dataset('titanic')
features = ['age', 'fare', 'pclass', 'sibsp', 'parch']
X = df_all[features].copy()
y = df_all['survived'].copy()

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 比較する補完手法を定義
imputers = {
    'Median': SimpleImputer(strategy='median'),
    'KNN (k=5)': KNNImputer(n_neighbors=5),
    'MICE': IterativeImputer(
        max_iter=1000, 
        random_state=42
    ),
    'MissForest-like': IterativeImputer(
        estimator=RandomForestRegressor(
            n_estimators=1000,
            random_state=42
        ),
        max_iter=10,
        random_state=42
    ),
}

results = []

for name, imputer in imputers.items():
    # 補完 → ランダムフォレスト分類器のパイプライン
    pipe = Pipeline([
        ('impute', imputer),
        ('model', RandomForestClassifier(
            n_estimators=1000,
            random_state=42,
            class_weight='balanced'
        )),
    ])
    pipe.fit(X_train, y_train)
    pred = pipe.predict(X_test)
    acc = accuracy_score(y_test, pred)
    f1 = f1_score(y_test, pred)

    results.append({
        'Method': name,
        'Accuracy': round(acc, 4),
        'F1': round(f1, 4)
    })

results_df = pd.DataFrame(results)
print(results_df)
  • stratify=y:生存/非生存の比率を学習・テストで揃える(層化分割)
  • Pipeline:補完 → モデル学習を1つにまとめる
  • CCA だけは行削除のため別処理にしている

 

以下、実行です。

Method Accuracy F1
0 Median 0.6257 0.5315
1 KNN (k=5) 0.6089 0.5000
2 MICE 0.6089 0.5000
3 MissForest-like 0.6369 0.5517

 

各補完手法での Accuracy(正解率)と F1 スコアが一覧で表示されます。

 

欠損インジケータ(missing indicator) 込みで予測モデルを作って比較してみます。

scikit-learn の補完手法であれば、add_indicator=Trueとすることで MissingIndicator をその中に簡単に組み込むことができます。

以下、コードです。

from sklearn.impute import SimpleImputer, KNNImputer, MissingIndicator
from sklearn.pipeline import Pipeline, FeatureUnion
from sklearn.metrics import accuracy_score, f1_score
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier

# データ準備:数値特徴量で生存を予測
df_all = sns.load_dataset('titanic')
features = ['age', 'fare', 'pclass', 'sibsp', 'parch']
X = df_all[features].copy()
y = df_all['survived'].copy()

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 比較する補完手法を定義
# add_indicator=True を使うことで、補完後の特徴量に「欠損していたかどうか」の
# 欠損インジケータ列を自動で追加して予測に利用する
imputers = {
    'Median + Indicator': SimpleImputer(
        strategy='median',
        add_indicator=True
    ),
    'KNN (k=5) + Indicator': KNNImputer(
        n_neighbors=5,
        add_indicator=True
    ),
    'MICE + Indicator': IterativeImputer(
        max_iter=1000,
        random_state=42,
        add_indicator=True
    ),
    'MissForest-like + Indicator': IterativeImputer(
        estimator=RandomForestRegressor(
            n_estimators=1000,
            random_state=42
        ),
        max_iter=10,
        random_state=42,
        add_indicator=True
    ),
}

results = []

for name, imputer in imputers.items():
    # 補完 + 欠損インジケータ → ランダムフォレスト分類器のパイプライン
    pipe = Pipeline([
        ('impute', imputer),
        ('model', RandomForestClassifier(
            n_estimators=10000,
            random_state=42,
            class_weight='balanced'
        )),
    ])
    pipe.fit(X_train, y_train)
    pred = pipe.predict(X_test)
    acc = accuracy_score(y_test, pred)
    f1 = f1_score(y_test, pred)

    results.append({
        'Method': name,
        'Accuracy': round(acc, 4),
        'F1': round(f1, 4)
    })

results_df = pd.DataFrame(results)
print(results_df)

 

以下、実行結果です。

                        Method  Accuracy      F1
0           Median + Indicator    0.6369  0.5324
1        KNN (k=5) + Indicator    0.6313  0.5217
2             MICE + Indicator    0.6369  0.5324
3  MissForest-like + Indicator    0.6480  0.5532

 

この実験から学ぶべきこと

重要なのは「常に最強の手法」を覚えることではなく、「自分のデータで複数の手法を比較する」という姿勢 です。データの欠損メカニズム、欠損率、特徴量の性質によって最適な手法は変わります。今回作ったような比較パイプラインは、実務でそのまま再利用できます。

 

欠損値をネイティブに扱えるアルゴリズム

ここまで「欠損値を削除・補完してからモデルに渡す」前提で話してきました。

しかし実は、欠損値をそのまま受け取れるアルゴリズム も存在します。

アルゴリズム 欠損値の扱い
XGBoost 学習時に欠損を振り分ける最適な分岐方向を自動学習。missing パラメータで指定可
LightGBM 既定で NaN を欠損として扱う。zero_as_missing で挙動を変更可能
ナイーブベイズ 欠損のある特徴量を無視し、観測された特徴量だけで確率を計算

たとえば XGBoost は、欠損値を「どちらの枝に送ると最も予測に有利か」を学習時に自動で決めます。

 

「補完しない」という選択肢

勾配ブースティング系(XGBoost、LightGBM)を使う場合、無理に補完せず 欠損のまま渡したほうが精度が良い こともあります。補完は万能の前処理ではなく、「使うモデルが欠損を扱えるか」も含めて戦略を考えるのがプロの視点です。

 

欠損値処理の選び方

シリーズ全体の集大成として、「どの状況でどの手法を使うべきか」 を一覧にまとめます。

メカニズム(第1回) 推奨される処理 該当回
MCAR(完全にランダム) 欠損率が低ければ削除、または平均・中央値・ランダムサンプル補完 第3, 6, 7回
MAR(他変数に依存) KNN・MICE・MissForest などの多変量補完 第9, 10回
MNAR(値自体に依存) 欠損インジケータの併用、専用のモデリング手法 第10回

実務での意思決定は、おおよそ次の順序で考えます。

1. まず欠損率を確認(第2回)。極端に高い列(70〜80%超)は列削除を検討(第4回)
2. 欠損メカニズムを推定(第1〜2回)。グループ別の欠損率などから MAR の兆候を探る
3. 欠損率が5%未満で MCAR なら、対象列を絞った行削除(CCA)でも可(第3回)
4. 時系列データなら、LOCF/NOCB/線形補間を検討(第8回)
5. それ以外は補完。まずは中央値補完をベースラインにし(第6回)、多変量補完(KNN/MICE/MissForest相当)と精度を比較(第9, 10回)
6. 欠損自体が情報を持ちそうなら、欠損インジケータを併用(第10回)
7. XGBoost等を使うなら、補完せず欠損のまま渡す選択肢も検討(第10回)

最も重要なポイントを振り返ります。

  • 欠損メカニズムをまず見極める:機械的に「平均で埋める」「削除する」をしない
  • データ漏洩を防ぐ:補完値は必ず学習データだけから計算し、テストデータにも同じ値を適用する(fit_transform/transformPipelineの活用)
  • 再現性を確保する:ランダム性のある手法は random_state を固定する
  • 複数手法を比較する:唯一の正解はない。自分のデータで実際に試して決める

 

最後に

全10回にわたる「欠損値処理シリーズ」は、これで一旦終了です。

  • 第1〜2回:欠損のメカニズム(MCAR/MAR/MNAR)と、その検出・可視化
  • 第3〜4回:削除戦略(リストワイズ・特徴量削除・ペアワイズ)
  • 第5〜7回:単変量補完(定数・平均/中央値/最頻値・ランダムサンプル)
  • 第8回:時系列データの補完(LOCF/NOCB/線形補間)
  • 第9〜10回:多変量補完(KNN・MICE・MissForest)と総合比較

欠損値処理は地味な前処理に見えて、実はモデルの性能を大きく左右する、データサイエンスの腕の見せどころです。

まずメカニズムを見極め、データ漏洩を防ぎ、複数手法を比較する」という姿勢は、Titanic データだけでなく、皆さんが今後出会うあらゆる実データで役立つはずです。

ここまでお付き合いいただき、ありがとうございました。