🎉

SoftImputeによる欠損値補完とデータリークの影響分析

2024/04/16に公開

LightGBMを使用して数値予測を行った際、訓練データでは高いパフォーマンスが出たものの、本番データのパフォーマンスが著しく低下しました。原因を調査したところ、SoftImputeを用いた欠損値補完がデータリークを引き起こしている可能性がありました。

SoftImputeによる欠損値補完の手順とその影響の考察を以下に記します。

データリークの定義とその影響

データリークとは、訓練プロセスでモデルが本来利用できない情報(答えや未来の情報など)を学習してしまい、実際には一般化していないにも関わらず、誤って高い性能を示してしまう現象です。今回は、答え(目的変数)が間接的に、意図せずモデルに漏れてしまっていたようです。

ちなみに、データリークと似た用語にデータリーケージがあります。データリーケージはより広い概念で、訓練プロセスに限らず、データ収集、訓練データとテストデータの分割、モデルの設計などにおける情報漏洩を指すようです。

SoftImpute: 低ランク近似による欠損値補完法

SoftImputeは、不完全なデータセット内の欠損値を推定するために低ランク行列近似を利用するアルゴリズムです。この方法は特に、ユーザが映画に付けた評価のような、スパース(多くの要素が欠損している)データセットに有効です。

動作の概要

行列の因子分解:
SoftImputeは、データ行列を部分的に観測された行列として扱い、これを低ランクの行列の積として近似します。具体的には、特異値分解(SVD)の一種を使用して行列を因子化し、有意な特異値のみを保持します。

反復的な補完プロセス:
アルゴリズムはまず、観測されたデータ点を使用して行列の初期近似を行い、次に欠損データを推定します。この過程は、観測されたエントリーと予測されたエントリーの間の一貫性を高めるように設計されています。反復ごとに行列の補完が改善され、収束するまで続けられます。

動作イメージとして、映画評価のシナリオを考えます。

あるユーザが特定の映画に評価を付けていない場合、SoftImputeはそのユーザの他の映画への評価や、類似ユーザの評価パターンを考慮して、欠損している評価を推測します。このアルゴリズムは、全ユーザーの映画評価行列が、比較的少数の潜在的要因によって説明できる、という仮定に基づいています。

データリークのリスク

SoftImputeによる補完では、欠損列の補完に他の列の情報が利用されるため、データリークのリスクがあります。そのため、データリークを防ぐためにSoftImputeの使用前にデータセットから目的変数を除外する必要があります。

欠損値補完がパフォーマンスに与える影響の検証

以下のような回帰式で表すことができる特徴量X1~4と従属変数yを定義しました。

y = 2.5 * {X1の二乗} - 1.5 * {X2とX3の積} + 3.0 * {X4の三乗} + 10

さらに、各列のランダムな行に欠損値を作り、それをSoftImputeで補完した場合、補完しなかった場合で評価指標を比較しました。

データセットの生成

以下の手順で10,000行のデータセットを作りました。

import numpy as np
import pandas as pd

num_samples = 10000
feature_names = ['X1', 'X2', 'X3', 'X4']

# ランダムな特徴量を生成
data = np.random.randn(num_samples, len(feature_names))
df = pd.DataFrame(data, columns=feature_names)

# 目的変数を生成
random_error = np.random.randn(num_samples)
df['y'] = 2.5 * (df['X1'] ** 2) - 1.5 * (df['X2'] * df['X3']) + 3.0 * (df['X4'] ** 3) + random_error + 10

# 一部を欠損値に置換
for col in feature_names:
  missing_indices = np.random.choice(df.index, size=int(0.1 * len(df)), replace=False)
  df.loc[missing_indices, col] = np.nan

# id列の追加
df['id'] = df.index + 1

print(df.head())

欠損のある行は列ごとにランダムで、全体の10%の行が欠損しています。

また、シーケンシャルなid列も追加しました。このid列は本来は不要ですが、今回は他の変数と無関係な列が混ざっている場合の影響を確認したかったので、追加しました。

index X1 X2 X3 X4 y id
0 0.8385910387289963 1.7785894873568542 0.2399594504790437 1.8631970092441676 28.569714417253316 1
1 -0.24305536535595398 1.805018418654492 2.254205100222001 0.38845914238608714 3.743578780214703 2
2 0.47621991898155497 -0.24856316403473153 0.6122596458687415 0.7668071862692792 11.803071830683308 3
3 -1.4672962963089733 0.8874455182924537 0.7515620210029368 0.12155042617919491 14.604336824751536 4
4 -1.6594890997660756 -1.0064923894410656 -0.45834572496567616 NaN 11.20381609754238 5

SoftImputeによる欠損値の補完

まずはfancyimputeというライブラリをインストールします。

!pip install fancyimpute

次に、SoftImputeで欠損値を補完します。

今回は検証のため、以下の4パターンのデータセットを試しました。

  • 欠損値の補完を行わない
  • SoftImputeによる欠損値補完を行う
    • yとidを除外して補完(本来の使い方)
    • yを残して補完(データリークのリスクあり)
    • yとidを残して補完

yを残して補完は、本番データセットでは利用できない目的変数の列yの情報を利用して、欠損値を補完します。このパターンでは、欠損値補完の過程でデータリークが起き、過学習につながるリスクがあります。

from fancyimpute import SoftImpute

soft_impute = SoftImpute()
# yとidを除外して補完(本来の使い方)
df_imputed = df.copy()
df_imputed[feature_names] = soft_impute.fit_transform(df_imputed[feature_names])

# yを残して補完(データリークが起こる使い方)
df_imputed_include_y = df.copy()
df_imputed_include_y[feature_names + ['y']] = soft_impute.fit_transform(df_imputed_include_y[feature_names + ['y']])

# yとidを残して補完
df_imputed_include_all = df.copy()
df_imputed_include_all[df_imputed_include_all.columns] = soft_impute.fit_transform(df_imputed_include_all[df_imputed_include_all.columns])

パフォーマンスの比較

検証結果は次のとおりです。

index RMSE MAE MAPE method
0 3.66 1.89 0.88 21.72 欠損値補完なし
1 3.75 1.91 0.87 21.12 yとidを除外して補完
2 3.35 1.82 0.9 20.57 yを残して補完
3 3.71 1.89 0.88 20.79 yとidを残して補完

決定係数R²が最も高かったのは、やはりyを残して補完のパターンでした。このパターンでは、欠損値補完に目的変数yの情報を利用しているので、データリークが起きていると考えられます。

決定係数が次に高かったのは欠損値補完なしyとidを残して補完で、この両者は同程度の性能でした。yとidを残して補完のパターンでもデータリークが起き、高い決定係数になると予想していたのですが、意外な結果でした。

yとidを除外して補完のパターンは最も性能が低かったのですが、これは全ての特徴量がランダムで生成されたので、他の特徴量を利用して欠損値を補完するSoftImputeと相性が悪かったのでは、と考えました。

特徴量間に相関関係を持たせて再検証

特徴量間に相関関係がある場合は、SoftImputeは有効なのか、という追加の検証をします。

詳細は割愛しますが、以下のように特徴量間に相関を持たせてデータセットを作り直しました。

means = np.zeros(len(feature_names))

# 共分散行列(各特徴量間の相関を示す)
covariance_matrix = [
    [1.0, 0.5, 0.3, 0.2],
    [0.5, 1.0, 0.4, 0.3],
    [0.3, 0.4, 1.0, 0.5],
    [0.2, 0.3, 0.5, 1.0]
]

# 多変量正規分布を使用
data = np.random.multivariate_normal(means, covariance_matrix, num_samples)
df = pd.DataFrame(data, columns=feature_names)

# オフセットで値をシフトして全て正の値にする
offsets = df.min() * -1 + 0.01
df[feature_names] = df[feature_names] + offsets

# 目的変数yの計算
random_error = np.random.randn(num_samples)
df['y'] = 2.5 * (df['X1'] ** 2) - 1.5 * (df['X2'] * df['X3']) + 3.0 * (df['X4'] ** 3) + random_error + 10

このデータセットを使い、SoftImputeによる欠損値補完、パフォーマンス評価を行なった結果が以下です。

index RMSE MAE MAPE method
0 43.9 14.85 0.9 10.71 欠損値補完なし
1 48.98 20.9 0.87 15.14 yとidを除外して補完
2 31.77 16.06 0.95 13.01 yを残して補完
3 49.38 20.79 0.87 17.71 yとidを残して補完

yを残して補完のパフォーマンスがさらに向上し、次に欠損値補完なしの評価指標も改善しました。

SoftImputeによる欠損値補完を正しく行なったyとidを除外して補完のパターンは、欠損値補完なしのパターンよりも悪い結果になってしまったのが意外でした。欠損値の扱いもLightGBMに委ねたほうが良い結果になる、ということかもしれません。

欠損値の割合を増やして再検証

ここまでは欠損値の割合を全体の10%に設定していましたが、80%に増やして再検証してみました。SoftImputeは本来、欠損値の多いデータセットに使用するらしいので、欠損値の割合で結果がどう変わるのかを確認するためです。

結果は以下のようになりました。

index RMSE MAE MAPE method
0 141.87 105.32 0.02 89.75 欠損値補完なし
1 141.94 105.49 0.02 89.43 yとidを除外して補完
2 9.86 2.03 1.0 2.07 yを残して補完
3 27.76 10.54 0.96 11.09 yとidを残して補完

yを残して補完yとidを残して補完の性能が著しく向上しました。

これはSoftImputeによって補完される値の割合が増大したため、それに比例してデータリークの度合いも増大したのだと考えられます。

まとめ

ダミーのデータセットを用いて、SoftImputeによる欠損値補完が機械学習モデルのパフォーマンスに与える影響を検証しました。また、欠損値の割合を10%から80%に変えることで、その影響の変化も確認しました。

検証の結果は以下の通りです。

  • データリークの問題:目的変数を含むデータで欠損値補完を行うと、データリークが発生し、実際の性能よりも過大評価される結果となった
  • 欠損率の影響:欠損率が高まるにつれて、データリークによる影響が増大した
  • 性能向上の不発:目的変数を除外した状態で欠損値補完を行っても、性能の向上は見られなかった

この結果から、SoftImputeによる欠損値補完の際にはデータリークに注意する必要があるとわかります。また、SoftImputeの有効性はデータセットの性質により大きく異なるため、使用する際は欠損値補完なしのパターンも試し、実際に性能を比較してみるのが良さそうです。

Discussion