問いと設定
平均効果が正でも、同じ効果が全員にあるとは限りません。DoWhyで因果構造を指定し、EconMLのCausalForestDMLでmarker・ageに条件づけた平均効果を推定します。
真の効果を1.5+marker−0.7×age_z+0.4×I(marker>0)とした合成データを使います。marker・ageは効果修飾因子でも共通原因でもあります。severity・baselineも割付と結果の共通原因です。
5,000行のうち70%を学習用、30%を評価用に分けます。true_tauは検証だけに使い、DoWhyへ渡す学習データから削除します。真の効果を入力特徴量に混ぜる情報漏洩を防ぐためです。
実行できるコード
Python 3.12.14で実行しました。解析はすべて合成データを使う研究・学習用の例です。
専用の仮想環境で、次の版をインストールして実行します。数値は2026-10-02の実行結果です。
python -m pip install numpy==2.4.6 pandas==3.0.6 scipy==1.15.3 scikit-learn==1.6.1 statsmodels==0.15.0 dowhy==0.14 econml==0.16.0
次のコードは上から一括実行できます。seedを固定していますが、依存ライブラリや実装の変更で細かな数値は変わり得ます。
import numpy as np
import pandas as pd
from scipy.special import expit
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier
from dowhy import CausalModel
rng = np.random.default_rng(20261002)
n = 5000
age = rng.normal(55,10,n)
severity, baseline, marker = rng.normal(size=(3,n))
age_z = (age-55)/10
treatment = rng.binomial(1,expit(-.2+.7*severity+.4*baseline-.15*age_z+.35*marker))
tau = 1.5+marker-.7*age_z+.4*(marker>0)
outcome = tau*treatment-1.6*severity+.6*baseline-.2*age_z+.5*marker+rng.normal(size=n)
df = pd.DataFrame(dict(age=age,severity=severity,baseline=baseline,marker=marker,treatment=treatment,outcome=outcome,true_tau=tau))
train,test = train_test_split(df,test_size=.3,random_state=20261002)
covariates = ['age','severity','baseline','marker']
graph = 'digraph {' + ''.join(f'{x} -> treatment; {x} -> outcome;' for x in covariates) + 'treatment -> outcome;}'
model = CausalModel(data=train.drop(columns='true_tau'),treatment='treatment',outcome='outcome',
graph=graph,effect_modifiers=['marker','age'])
estimand = model.identify_effect()
params = {'init_params':{
'model_y':RandomForestRegressor(n_estimators=100,min_samples_leaf=10,random_state=20261002,n_jobs=1),
'model_t':RandomForestClassifier(n_estimators=100,min_samples_leaf=10,random_state=20261002,n_jobs=1),
'discrete_treatment':True,'n_estimators':200,'min_samples_leaf':10,'cv':3,
'random_state':20261002,'n_jobs':1},'fit_params':{}}
estimate = model.estimate_effect(estimand,method_name='backdoor.econml.dml.CausalForestDML',
effect_modifiers=['marker','age'],method_params=params)
# Column order must match effect_modifiers.
pred = estimate.estimator.effect(test[['marker','age']]).reshape(-1)
print('Held-out true ATE, estimated mean CATE, RMSE:',round(test.true_tau.mean(),4),round(pred.mean(),4),round(np.sqrt(np.mean((pred-test.true_tau)**2)),4))
for name,mask in [('marker>0, age<60',(test.marker>0)&(test.age<60)),('marker<=0, age>=60',(test.marker<=0)&(test.age>=60))]:
print(name,'n:',mask.sum(),'truth:',round(test.loc[mask,'true_tau'].mean(),4),'estimate:',round(pred[mask].mean(),4))
実行結果
| held-out評価 | 真値の平均 | 推定CATEの平均 | n |
|---|---|---|---|
| 全体 | 1.7218 | 1.7626 | 1500 |
| marker>0、age<60 | 3.0992 | 3.0492 | 515 |
| marker≤0、age≥60 | −0.0291 | 0.0994 | 235 |
評価用1,500行のCATEのRMSEは0.3081でした。表の推定CATE平均はheld-out特徴分布上の平均であり、EconMLが学習時に計算するdoubly robust ATEと同じ指標ではありません。
CATEは個人の効果の観測値ではない
CATEは、同じXを持つ人についての条件付き平均効果です。ある患者のY(1)−Y(0)を両方観測した値ではありません。EconMLのeffectはモデルのCATE予測であり、個人の因果効果を確認したものとして扱いません。
今回の低効果群では真値平均が約0でも、予測は0.0994でした。平均が近い群があっても、境界や裾での平滑化、少数標本、推定誤差は残ります。RMSEを計算できたのは合成データでtrue_tauが既知だからです。実データの結果だけから同じRMSEを算出することはできません。
Cross-fittingとheld-out評価を分ける
CausalForestDMLは、結果と介入のnuisanceモデルをcross-fittingで学習し、残差を用いて効果を推定します。それとは別に、本記事は学習に使わないtestを設けました。パラメータの選択をこのtestで繰り返せば、独立した最終評価ではなくなるので追加の評価設計が必要です。
effect_modifiersはmarker、ageの順です。予測時にも同じ列順を使います。共通原因と効果修飾因子は役割が異なり、必要な調整変数を修飾因子だけに限定してはいけません。
未観測交絡の不在、positivity、consistencyが因果解釈の前提です。機械学習の予測精度はそれらの代わりになりません。異質性を見てから都合の良い閾値を探索する場合には、探索の多重性、群別の不確実性、別標本での検証を扱います。
この1回のシミュレーションは治療対象の選別や用量決定の根拠ではありません。次に必要なのは、反復生成・外部評価・群別誤差・信頼区間・意思決定の損失を含む検証です。