欠損値の補完方法を6種類比較してみた

目次

はじめに

目的と背景

データ分析やコンペに取り組んでいると、欠損値の処理は避けて通れません。「とりあえず平均で埋める」という選択をすることも多いですが、実際にどのくらい精度に影響するのか、体感でしか語れていないことに気がつきました。

そこで今回は、完全なデータセットにあえて欠損値を人工的に作り、正解がわかっている状態でいくつかの保管方法を比較する、という実験をしてみました。

試したことの概要

・データセット:California Housing(scikit-learn組み込み、欠損なしの完全データ)
・欠損のさせ方:MCAR(完全ランダムな欠損)を10%、30%、50%の3パターンで発生させる
・比較した補完方法(6種類)
 1. 平均値
 2. 中央値
 3. KNN(K近傍法)
4. MICE(InteractiveImputer)
5. MissForest(InteractiveImputer + RamdomForest)
 6. Autoencoder(ニューラルネットワークによる復元)
・もう一つの軸:「他の特徴量と相関が強い列」と「ほぼ無相関な列」、どちらかを欠損させるかによって結果がどう変わるのかも検証
・評価方法:(1)真の値との誤差(補完精度そのもの)、(2)補完後のデータで学習したモデルの予測精度の2軸で評価

設定環境

使用したツールとバージョン

ツール用途
Google Colab実行環境
Python3.12
scikit-learnデータの読み込み、各種Imputer・RandomForest
TensorFlow/KerasAutoencoderの実装
pandas/numpyデータ操作
matplotlib結果の可視化
japanize-matplotlibグラフの日本語表示

初期設定と準備

!pip install japanize-matplotlib -q  # 日本語のグラフ表示のためライブラリを追加

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import japanize_matplotlib  # matplotlibの日本語文字化け対策

from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error
from sklearn.impute import SimpleImputer, KNNImputer
from sklearn.experimental import enable_iterative_imputer  # IterativeImputer有効化に必須
from sklearn.impute import IterativeImputer

import tensorflow as tf
from tensorflow.keras import layers, models

RANDOM_STATE = 42
np.random.seed(RANDOM_STATE)
tf.random.set_seed(RANDOM_STATE)

※再現性のために乱数シードを固定しています。

実装手順

ステップ1:データの読み込みとベースラインの計測
まずは欠損が一切ない状態でのモデルの精度を「ベースライン」として測っておきます。

data = fetch_california_housing(as_frame=True)
df = data.frame
X = df.drop(columns=['MedHouseVal'])
y = df['MedHouseVal']

X_train_full, X_test, y_train_full, y_test = train_test_split(
    X, y, test_size=0.2, random_state=RANDOM_STATE
)

baseline_model = RandomForestRegressor(n_estimators=200, random_state=RANDOM_STATE, n_jobs=-1)
baseline_model.fit(X_train_full, y_train_full)
baseline_rmse = np.sqrt(mean_squared_error(y_test, baseline_model.predict(X_test)))

ステップ2:欠損させる列を「相関の強さで選ぶ」
KNNやMICEのような補完手法は「他の列の値から予測する」という考え方が根底にあります。つまり、他の列と相関が強い列を欠損させた場合とほぼ無相関な列を欠損させた場合とでは、補完方法の効き方が変わるはずです。
この仮説を検証するため、決め打ちではなく相関行列から動的に列を選びました。

corr_matrix = X.corr()
avg_abs_corr = (corr_matrix.abs().sum() - 1) / (corr_matrix.shape[1] - 1)
avg_abs_corr_sorted = avg_abs_corr.sort_values(ascending=False)

STRONG_COL = avg_abs_corr_sorted.index[0]   # 最も他列と相関が強い列
WEAK_COL = avg_abs_corr_sorted.index[-1]    # 最も他列と無相関に近い列

ステップ3:欠損を人工的に発生させる
完全なデータからランダムに値を取り除き、真の値を別途補完しておきます。これにより、後で「補完値vs真の値」の誤差を直接計算できます。

def introduce_missing_mcar(X, columns, missing_rate, seed=0):
    X_missing = X.copy()
    rng = np.random.RandomState(seed)
    true_values = {}
    masks = {}
    for col in columns:
        n = len(X_missing)
        n_missing = int(n * missing_rate)
        idx = rng.choice(n, size=n_missing, replace=False)
        mask = np.zeros(n, dtype=bool)
        mask[idx] = True
        true_values[col] = X_missing.loc[mask, col].copy()
        X_missing.loc[mask, col] = np.nan
        masks[col] = mask
    return X_missing, true_values, masks

ステップ4:6種類の保管方法を実装する
全て「欠損ありのDataFrameを受け取り、保管ずみDataFrameを返す」という同じインターフェースに揃えました。特にユニークなのは以下の2つです。

MissForest(RandomForestベースの補完)

def impute_missforest(X_missing):
    imputer = IterativeImputer(
        estimator=RandomForestRegressor(n_estimators=50, random_state=RANDOM_STATE, n_jobs=-1),
        max_iter=5,
        random_state=RANDOM_STATE,
    )
    return pd.DataFrame(imputer.fit_transform(X_missing), columns=X_missing.columns, index=X_missing.index)

Autoencoderによる補完(仮埋め→ニューラルネットで復元→置き換え、を数回繰り返す)

def impute_autoencoder(X_missing, encoding_dim=4, epochs=60, n_iterations=2):
    col_mean = X_missing.mean()
    col_std = X_missing.std()
    mask = X_missing.isnull().values
    X_filled = X_missing.fillna(col_mean)
    X_scaled_values = ((X_filled - col_mean) / col_std).values.astype(np.float32)
    input_dim = X_scaled_values.shape[1]

    for _ in range(n_iterations):
        autoencoder = models.Sequential([
            layers.Input(shape=(input_dim,)),
            layers.Dense(encoding_dim, activation='relu'),
            layers.Dense(input_dim, activation='linear'),
        ])
        autoencoder.compile(optimizer='adam', loss='mse')
        autoencoder.fit(X_scaled_values, X_scaled_values, epochs=epochs, batch_size=64, verbose=0)
        X_reconstructed = autoencoder.predict(X_scaled_values, verbose=0)
        X_scaled_values[mask] = X_reconstructed[mask]

    X_scaled_final = pd.DataFrame(X_scaled_values, columns=X_missing.columns, index=X_missing.index)
    return X_scaled_final * col_std + col_mean

ステップ5:実験ループを回す
「相関が強い列」「相関が弱い列」の2パターン×欠損率(10%・30%・50%)×6手法、で全組み合わせを実行し、(1)補完精度 (2)下流モデルの精度を記録しました。

SCENARIOS = {'strong_corr': STRONG_COL, 'weak_corr': WEAK_COL}
MISSING_RATES = [0.1, 0.3, 0.5]
results = []

for scenario_name, col in SCENARIOS.items():
    for rate in MISSING_RATES:
        X_missing, true_values, masks = introduce_missing_mcar(X_train_full, [col], rate, seed=RANDOM_STATE)
        mask = masks[col]
        true_v = true_values[col].values

        for name, impute_fn in IMPUTERS.items():
            X_imputed = impute_fn(X_missing)
            imputed_v = X_imputed.loc[mask, col].values
            imputation_rmse = np.sqrt(mean_squared_error(true_v, imputed_v))

            model = RandomForestRegressor(n_estimators=200, random_state=RANDOM_STATE, n_jobs=-1)
            model.fit(X_imputed, y_train_full)
            downstream_rmse = np.sqrt(mean_squared_error(y_test, model.predict(X_test)))

            results.append({
                'scenario': scenario_name, 'column': col, 'missing_rate': rate,
                'method': name, 'imputation_rmse': imputation_rmse, 'downstream_rmse': downstream_rmse,
            })

結果

実行結果
storing_corr(他の特徴量と相関が強い列 = AveRooms AveBedrmsと0.85、MedIncと0.33の相関)と、weak_corr(ほぼ無相関な列 = Ave0ccup どの列とも相関係数0.1未満)について、実際に全欠損率(10%/30%/50%)×6手法を実行した結果をグラフにまとめました。

グラフ
まずは今回の実験の前提となる、特徴量間の相関行列です。ここから相関が強い列としてAveRooms(AveBedrmsと0.85、MedIncと0.33)、「ほぼ無相関な列」としてAve0ccup(全ての列との相関係数が0.1未満)を自動選定しました。

相関が強い列(AveRooms)を欠損させた場合

相関が弱い列(Ave0ccup)を欠損させた場合

成功例

相関が強い列ではMICE・MissForestが圧勝AveRoomsの欠損率10%時点で、Mean/Median/KNNの補完RMSEが2.17〜2.44に対し、MICEは0.82、MissForestは0.64。他の相関の強い列(AveBedrms)から予測する仕組みが機能しました。

相関が強い列では手法感の差がほぼ消えるAveRooms30%・50%とも全手法が16〜18の範囲に収まり、大差なし。相関がないと高度な手法も「当てずっぽう」の域を出ないことがわかる。

失敗例・想定外だった点

・補完精度がいい≠下流モデルが良い:AveRooms50%欠損で、MissForestは補完RMSEが最良(1.30)なのに下流RMSEは最悪(0.527、他は0.506〜0.508)。真の値に近づけることとモデルが学習しやすくなることは別物でした。
・KNN/Autoencoderが相関列で平均値に完敗:Ave0ccup10%欠損で、KNN(3.29)・Autoencoder(3.45)がMean(0.86)より大幅に悪化。Populationなど大きなスケールの列が距離・損失計算を支配した可能性があり、標準化を挟むべきだった。
・特徴量間の相関の弱さ≠目的変数への影響の小ささ:Ave0ccup欠損時の下流RMSE悪化幅は、むしろAveRooms(相関が強い列)より大きく出ました。

まとめ

得られた知見

・相関が強い列ならMICE・MissForestが有効、相関が弱い列なら平均値で十分な場合が多い
・補完精度と下流モデルの精度は必ずしも一致しない(MissForestが好例)
・KNN/Autoencoderはスケールさに弱く、標準化なしでは本来の性能を発揮できない

CTA
  • URLをコピーしました!
  • URLをコピーしました!
この記事を書いた人
目次