🥝

欠測値補完の方法で因果探索はどう変わる?多重代入×LiNGAMで実験してみた

に公開

はじめに🧭

因果探索手法の一つであるLiNGAM(Linear Non-Gaussian Acyclic Model)は、変数間の因果関係を推定する強力な手法ですが、標準的なLiNGAMは欠測値に対応していません。 そのため、欠測値を含むデータに対してLiNGAMを適用するには、事前に適切な欠測値処理が必要です。

一般的な欠測値の対応方法には、欠測値を含むデータを削除する方法と、欠測値を補完する方法 があります。しかし、これらの方法はサンプルサイズの減少やバイアス[1]のリスクを伴います。

そこで本稿では、欠測値のバラつきを考慮した補完手法として、多重代入(Multiple Imputation) を用いたアプローチを紹介します。

想定読者と環境🎯

想定読者🧐

  • Pythonでlingamパッケージを使って因果探索をしている人
  • LiNGAMアルゴリズムで欠測値の扱いに困っている人

動作環境💻

  • OS: Windows11
  • Python: 3.12.10
    • lingam: 1.11.0
    • numpy: 2.3.3
    • pandas: 2.3.2
    • scikit-learn: 1.7.2
    • scipy: 1.16.2

動作確認した日📅

  • 2025年9月22日

欠測値とは?その種類と影響🧩

欠測値の適切な対処には、まず「なぜ欠測が発生したのか」という欠測のメカニズムを理解することが重要です。

欠測値の分類🧪

欠測値は欠測のメカニズムに応じて、以下の3つに分類されます。

  • MCAR(Missing Completely at Random):
    欠測が完全にランダムに発生しており、他の変数や自身の値とは無関係で、分析結果への影響が最も少ないです。
    そのため、欠測値を含む行を削除しても推定に偏りが生じないため、単純な削除が許容される場合が多いです。
  • MAR(Missing at Random):
    欠測の有無が他の観測済みの変数に依存しており、多重代入などの適切な補完手法を用いることで、推定の偏りを回避できる可能性が高いです。
    なお、MARの仮定にはMCARも含まれるため、実務上はまず多重代入を検討するのが一般的です。
  • MNAR(Missing Not at Random):
    欠測の有無が欠測している変数自身の値に依存しており、欠測メカニズムを明示的にモデル化する必要があります。
    そのため、一般的な統計ソフトウェアでの対応は困難です。実務上はまず多重代入を試みることもありますが、バイアスが残る可能性が高いため、結果の解釈や限界について十分に検討する必要があります。

欠測のメカニズムの判定は、統計的検定だけでは困難であり、ドメイン知識やデータ収集過程の理解が重要となります。

欠測値の対応方法の比較📊

欠測値の対応方法として、欠測値の削除単純な削除多重代入のそれぞれの特徴を比較しました。

  • 欠測値の削除
    実装が簡単で、分析がシンプルになり、欠測値の扱いによるバイアスを考慮しなくて済みます。
    ただし、欠測が多い場合はサンプルサイズが大幅に減少します。もし、欠測がMCARでない場合にバイアスが生じます。
  • 単純な補完(平均値補完や中央値補完など):
    サンプルサイズを維持でき、分析の実施が簡単です。
    ただし、補完値のバラつきを無視するため、推定値の分散が過小評価されやすく、統計的検定で有意性が過大評価されるなど、過信した分析に繋がる可能性があります。
  • 多重代入
    欠測値のバラつきを反映した推定が可能で、バイアスや分散の過小評価を抑えられます。また、欠測がMARの場合、より信頼性の高い分析結果が得られる可能性があります。
    ただし、計算コストが高くなります。

実験準備:多重代入×LiNGAMで因果探索してみた🔍

アプローチ

本稿では、導入が容易なMCARの欠測を人工的に発生させ、それぞれの欠測値対応方法を適用した後に、LiNGAMによる因果探索を行い、推定された因果グラフの違いを比較します。

多重代入の実装方法

多重代入には sklearn の IterativeImputer を使います。
https://scikit-learn.org/stable/modules/generated/sklearn.impute.IterativeImputer.html

from sklearn.experimental import enable_iterative_imputer # 
from sklearn.impute import IterativeImputer
_imp = IterativeImputer(sample_posterior=True)
X_i = _imp.fit_transform(X)

IterativeImputer は、他の特徴量を使って欠測値を予測する多変量補間手法です。具体的には、以下のようなステップで補完を行います:

  1. 欠測値のある列を1つ選ぶ
  2. 他の列を使ってその列の欠測値を予測(回帰モデルなど)
  3. 補完された列を使って、次の欠測列を予測
  4. これを繰り返して、全ての欠測値を補完

この手法は、MICE(Multivariate Imputation by Chained Equations) とも呼ばれ、統計解析でもよく使われます。
通常の補完では、予測値をそのまま使いますが、sample_posterior=True を指定すると、予測値のバラつきを考慮してランダムにサンプリングします。これは、ベイズ的なアプローチで、複数回補完して統計的なバラつきを評価したい場合に有効です。

真の因果グラフ

今回は、欠測値の処理方法の違いによる因果探索結果の比較を行うために、真の因果グラフを定義します。

m = np.array([
    [ 0.000,  0.000,  0.000,  0.895,  0.000,  0.000],
    [ 0.565,  0.000,  0.377,  0.000,  0.000,  0.000],
    [ 0.000,  0.000,  0.000,  0.895,  0.000,  0.000],
    [ 0.000,  0.000,  0.000,  0.000,  0.000,  0.000],
    [ 0.991,  0.000, -0.124,  0.000,  0.000,  0.000],
    [ 0.895,  0.000,  0.000,  0.000,  0.000,  0.000]
])
make_dot(m)

真の因果グラフ

こちらの因果グラフを基にデータを1000行生成します。

import pandas as pd
import numpy as np
np.random.seed(0)
sample_size = 1000
error_vars = [0.2, 0.2, 0.2, 0.2, 0.2, 0.2]
params = [0.5 * np.sqrt(12 * v) for v in error_vars]
generate_error = lambda p: np.random.uniform(-p, p, size=sample_size)
e = np.array([generate_error(p) for p in params])
X = np.linalg.pinv(np.eye(len(m)) - m) @ e
X = pd.DataFrame(X.T ,columns=['x0', 'x1', 'x2', 'x3', 'x4', 'x5'])
x0 x1 x2 x3 x4 x5
-0.04367 0.25618 0.36331 -0.13329 -0.40957 -0.24547
0.51309 -0.41532 0.14267 0.20080 0.59383 0.14570
0.54547 0.42988 0.42214 0.43158 -0.07350 -0.18013
... ... ... ... ... ...

欠測値の導入(MCAR)

生成したデータには欠測値がないため、完全にランダムな欠測値(MCAR)を導入します。
今回はすべての変数に対して約20%の欠測値を導入します。

random.seed(0)
prop_missing = [0.20]*X.shape[1]
missing_pos = []
for i, prop in enumerate(prop_missing):
    mask = np.random.uniform(0, 1, size=len(X))
    missing_pos.append(mask < prop)
missing_pos = np.array(missing_pos).T
X_org = X.copy()
X[missing_pos] = np.nan
変数 欠測値割合(%)
x0 20.5
x1 22.5
x2 20.9
x3 20.3
x4 20.6
x5 20.3

ここから欠測値の処理方法の違いによって、因果探索結果にどのような影響が見られるか実験していきます。

実験と結果比較:補完方法ごとの因果グラフの違い🔍

多重代入

  • 多重代入では、代入回数分のデータセットが生成されるため、すべてのデータセットに共通の因果構造を仮定して因果探索できるMultiGroupDirectLiNGAMを用いて、因果グラフを推定します。
  • 多重代入では、少なくとも20回程度の代入を行うことで、統計的に信頼性の高い結果が得られるとされています。

これらの方針に基づき、まずは20回の代入を実施し、20個のデータセットを作成します。

from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
imp = IterativeImputer(sample_posterior=True, random_state=0)
n_repeats = 20 # 多重代入回数
X_list = []
for i in range(n_repeats):
    X_ = imp.fit_transform(X)
    X_list.append(X_)

次に、これら20個のデータセットを用いてMultiGroupDirectLiNGAMによる因果探索を実行します。
この手法では、各データセットに対して隣接行列が推定され、合計で20個の隣接行列が得られます。
これらの隣接行列から、各要素の出現割合が60%以上のものを抽出し、中央値を採用して統合した隣接行列を構築します。

model = lingam.MultiGroupDirectLiNGAM(random_state=0)
result = model.fit(X_list)

# 非ゼロ要素の割合を計算
adj_matrix_list = np.stack(result.adjacency_matrices_)
non_zero_counts = np.count_nonzero(adj_matrix_list, axis=0)
probabilities = (non_zero_counts / adj_matrix_list.shape[0])

# 非ゼロ要素の中央値を計算
n_features = X.shape[1]
adjacency_matrix = np.zeros((n_features, n_features))
for to in range(n_features):
    for from_ in range(n_features):
        values = adj_matrix_list[:, to, from_]
        non_zero_values = values[np.abs(values) > 0]
        if non_zero_values.size > 0:
            median_value = np.median(non_zero_values)
            adjacency_matrix[to, from_] = median_value
# 非ゼロ要素の割合が60%以上のエッジを表示
adjacency_matrix_60 = np.where(probabilities>=0.6, adjacency_matrix, 0.0)
make_dot(adjacency_matrix_60, lower_limit=0.0, labels=X.columns.tolist())

この隣接行列をもとに、最終的な因果グラフを描画します。

多重代入 因果グラフ

この結果、真の因果グラフと一致する因果構造が正確に推定されました。

欠測値なし

比較のために、欠測値が存在しない元の完全なデータを用いて、DirectLiNGAMによる因果探索を行います。

model = lingam.DirectLiNGAM()
result = model.fit(X_org)
make_dot(result.adjacency_matrix_, lower_limit=0.0, labels=X.columns.tolist())

欠測値なし 因果グラフ

この結果、真の因果グラフと一致する因果構造が正確に推定されました。多重代入を用いた場合と同じ構造が得られており、欠測値があっても適切な補完を行えば、因果探索の精度を維持できることが示されました。

欠測値削除

次に、欠測値を含む行を削除する手法で検証します。
この処理により、データセットのサンプルサイズは、欠測値削除前の1000件から256件まで減少しています。
この欠測値削除後のデータを用いて、DirectLiNGAMによる因果探索を行います。

X_rem = X.copy()
X_rem = X_rem.dropna()
print(X_rem.shape[0]) # 256
model = lingam.DirectLiNGAM()
result = model.fit(X_rem)
make_dot(result.adjacency_matrix_, lower_limit=0.0, labels=X.columns.tolist())

欠測値削除 因果グラフ

この結果、一部の因果関係が正しく推定されない箇所が見られました。

平均値補完

最後に、欠測値を各変数の平均値で補完する手法で検証します。
欠測値補完後のデータを用いて、DirectLiNGAMによる因果探索を行います。

X_rem = X.copy()
X_imp = X_imp.fillna(X_imp.mean())
model = lingam.DirectLiNGAM()
result = model.fit(X_imp)
make_dot(result.adjacency_matrix_, lower_limit=0.0, labels=X.columns.tolist())

平均値補完 因果グラフ

この結果、いくつかの因果関係が正しく推定されていないことが確認できました。

考察✏️

今回の検証では、全ての変数に約20%の完全にランダムな欠測値(MCAR) を導入したデータに対して、複数の欠測値処理手法を用いて、因果探索した結果を比較しました。

その結果、多重代入を用いた因果探索が、欠測値導入前の構造の因果グラフを再現できることが確認されました。欠測値のバラつきを考慮することで、因果探索の精度が向上したと考えられます。

一方で、平均値補完では因果構造が大きく歪む傾向が見られました。これは、欠測値のバラつきを無視することで、推定にバイアスが生じたためと考えられます。

また、欠測値の削除では構造の変化は比較的小さかったものの、サンプルサイズの減少により推定の安定性が損なわれた可能性があります。MCARであればバイアスは抑えられますが、情報量の損失は避けられません。

おわりに🔚

欠測値を含むデータに対して因果探索を行う際、多重代入は非常に有力な選択肢となります。
欠測値のバラつきを統計的に考慮することで、妥当性と信頼性の高い因果推定が可能になります。
特に、LiNGAMモデルと組み合わせることで、欠測値のある現実的なデータにも柔軟に対応できるようになります。

脚注
  1. バイアス(Bias)
    バイアスとは、真の値からの系統的なズレを指します。
    たとえば、家庭の収入に関する調査で、高収入者ほど具体的な金額を回答をしない傾向がある場合、欠測値が特定の属性(高収入者)に偏って発生します。その結果、平均値などの統計量が実際よりも低く見積もられることがあります。 ↩︎

SCREEN AS 因果探索チーム

Discussion