LiNGAMによる因果探索
LiNGAM(Linear Non-Gaussian Acyclic Model)は代表的な因果探索手法の一つで,近年は市販のソフトウェア等にも実装されるなど実務での活用が進んでいるようです.LiNGAMは,その名の通り,関数形が線形かつ誤差項がガウス分布(正規分布)以外に従う場合に,データから因果関係を明らかにすることができる手法です.
LiNGAMは,PythonのlingamパッケージやRのpcalgパッケージで利用することができ,これらのパッケージで提供される関数にデータフレームを入力すれば,簡単に因果グラフを推定することができます.因果グラフとは,観測変数をノード,因果関係を矢印に見立てて構築されるネットワーク構造のことです.特に,因果探索では,因果グラフとしてDAG(Directed Acyclic Graph)と呼ばれる有向非巡回グラフを推定することが一般的です.
因果グラフの例として,以下にレストラン経営に関する因果グラフを作成してみました.実務においては,曜日・天気・出店地域などの様々な要因によって利益が 変動すると考えられ,さらに複雑な因果関係が分析対象となることが想定されます.
関数形がLiNGAMの推定に与える影響について
LiNGAMは,関数形が線形かつ誤差項がガウス分布以外に従うという前提のもとで因果関係を推定する手法です.そのため,関数形が線形でない(線形近似が難しい)場合には,誤った因果関係が推定される可能性があります.パッケージや市販のソフトウェアを使えばLiNGAMを簡単に実行することができてしまいますが,利用者はこうした前提があることを理解してLiNGAMを使う必要があると思われます.
ここでは,上に挙げた因果グラフよりも簡単な以下のような因果関係をもつデータに対してLiNGAMを適用してみましょう.具体的には, 以下のフローチャートのfun
に,非線形な関数が入ったときにLiNGAMの結果がどのように変わるかを確認します.
ただし,この記事では最も一般的なLiNGAMであるDirect LiNGAM,誤差項として非ガウス分布の一様分布を用いています. また,係数の絶対値が0.001未満の因果関係は無視するものとしました.
因果グラフの推定に先立ってx11
・x12
・x22
列をもつ1,000行のデータフレームおよび因果探索用の関数を用意しておきましょう.
コード
import numpy as np
import os
import pandas as pd
import lingam
= np.random.default_rng(1234)
rng
= 1000
n = pd.DataFrame({
data 'x11': 1 + rng.random(n),
'x12': 2 + rng.random(n),
'x22': 3 + rng.random(n)
})
# DirectLiNGAMの結果をデータフレームとして返す関数
def discover_causality(data):
= lingam.DirectLiNGAM()
model
model.fit(data)
return pd.DataFrame(
model.adjacency_matrix_,=data.columns,
columns=data.columns
index\
)= 'node_to')\
.reset_index(names
.melt(='node_to',
id_vars='node_from'
var_name\
)lambda df: df[np.logical_not(np.isclose(df.value, 0, rtol=0, atol=1e-3))])\
.pipe(=['node_from', 'node_to', 'value'])
.reindex(columns
# mermaidファイルを出力する関数
def write_mermaid(df, file):
with open(file, 'w') as f:
'flowchart LR\n')
f.write(for row in df.itertuples():
' {}-->|{:.3f}|{}\n'.format(row.node_from, row.value, row.node_to))
f.write(
# 出力先のフォルダ
dir = 'be-careful-with-function-forms-in-lingam'
if not os.path.exists(dir):
dir) os.makedirs(
因果グラフに掛け算が含まれるケース(fun
が*
)
fun
が掛け算(*
)のときにDirect LiNGAMの推定結果がどのようになるかを見てみましょう. 推定された因果グラフは真の因果グラフと同等の構造をもっており,今回のケースでは,因果グラフが正しく推定されていることがわかります. このように,足し算(線形)でなく掛け算のケースでも因果関係の推定がうまくいくケースがあるようです. ただし,観測変数の数やデータ数によっては状況が大きく異なるかもしれません.
= data\
data_nonlinear_prod
.assign(=lambda df: df.x11 + df.x12 + rng.random(n),
x21=lambda df: df.x21 * df.x22 + rng.random(n),
x31
)
= discover_causality(data_nonlinear_prod)
causality_nonlinear_prod print(causality_nonlinear_prod)
node_from node_to value
3 x11 x21 0.975641
8 x12 x21 0.999905
14 x22 x31 4.524066
19 x21 x31 3.477884
コード
dir, 'dag_lingam_nonlinear_prod.mmd')) write_mermaid(causality_nonlinear_prod, os.path.join(
因果グラフにべき乗が含まれるケース(fun
が**
)
次に,fun
がべき乗(**
)のときにDirect LiNGAMの推定結果がどのようになるかを見てみましょう. 推定された因果グラフは真の因果グラフと異なる構造をもっており,もともと因果関係が存在しない上流の観測変数間にも誤った因果関係が推定されていることがわかります.このように,Direct LiNGAMに非線形な関数が含まれる場合には,その関数と直接関係しない観測変数間においても,誤った因果関係が推定されてしまうリスクがあることがわかります.
= data\
data_nonlinear_power
.assign(=lambda df: df.x11 + df.x12 + rng.random(n),
x21=lambda df: df.x21 ** df.x22 + rng.random(n),
x31
)
= discover_causality(data_nonlinear_power)
causality_nonlinear_power print(causality_nonlinear_power)
node_from node_to value
1 x11 x12 -0.147973
3 x11 x21 0.832946
8 x12 x21 0.798157
17 x21 x22 -0.400541
22 x31 x22 0.002306
コード
dir, 'dag_lingam_nonlinear_power.mmd')) write_mermaid(causality_nonlinear_power, os.path.join(
まとめ
この記事では,非線形な関数をもつデータに対してLiNGAMを適用すると,誤った因果関係が推定される可能性があることを示しました. そのため,LiNGAMの適用にあたっては,因果関係が線形で表せることを確認することが重要です.
一方で,因果関係がわからない状況なのに,因果関係が線形で表せることがわかるという状況はまれであると思われます. そのため,事後的な線形性の確認や実験などを通じて,慎重に因果関係を特定することが求められます.