Causal Forest(因果推論)ってどういうことか限界まで噛み砕いてみた
こんにちは、せーのです。
今日はCausal Forest、因果推論のお話です。
因果推論、というのは「この施策をしたからこうなった」という結果に対する原因について正確に検証するための方法論です。
因果関係と似た概念で「相関関係」というのがあります。例えば気温が高いとアイスが売れる、これは因果関係です。同じく気温が高いと水難事故が増える、これも因果関係です。
ですがデータだけ見るとアイスが売れている時は水難事故が増える、とも読めます。でも水難事故が増える原因はアイスが売れたから、ではないですよね。これを相関関係といいます。因果推論、というのはあるアクションが結果にどれくらい影響を及ぼしているのか、というのを把握するために行うもの、と考えてもらえればわかりやすいと思います。
機械学習に詳しい方だと、「XGBoost に『支援を受けたかどうか』の列を入れて、SHAP でその寄与を見れば、誰に施策を打つべきか分かるだろう」と思うかもしれません。以前の私も、そう思い込んでいました。ですが、その方法で分かるのは「支援を受けたかどうかという列が、結果を当てるのにどれくらい役に立ったか」です。私が知りたかったのは「この人に支援したら、支援しなかった場合と比べてどれだけ良くなるのか」でしたから、まったく別のものを見ていたことになります。
このふたつの違いは、学校のテストにたとえると分かりやすいと思います。予測モデルがやっているのは、「この生徒は次のテストで何点を取りそうか」を当てることです。一方 Causal Forest がやっているのは、「補習を受けたら何点になるか」と「補習を受けなかったら何点になるか」を並べて、その差が何点あるかを求めることです。前者が出すのは点数そのもの、後者が出すのは補習で伸びる分だけ、というわけですね。
そこで今回は、Causal Forest を一度きちんと整理して、手元で動かしてみることにしました。因果推論の話は略語が多くて身構えてしまうので、この記事では出てきた用語をその場で日本語に直しながら進めます。扱う範囲は次の3つです。
- Causal Forest が何を推定する道具なのか
- Random Forest やメタラーナーと呼ばれる手法群と、どこが違うのか
- 架空のデータを使って、学習・評価・データクレンジングまで実際に回した結果
DevelopersIO には T-Learner や X-Learner、TMLE、Uplift RF の記事がすでにありますが、Causal Forest そのものを試した記事は見当たりませんでした。今回はそこを埋める内容になります。
題材は、ある自治体の住民健診を想定した架空のデータにしました。健診の結果を見て、塩分を摂りすぎている住民の方に個別の減塩指導を行う、という保健事業をイメージしていただければと思います。対象は架空の住民 1,000 人です。ここでは「減塩指導を受けたかどうか」が効果を知りたいアクションで、「尿中のナトリウムとカリウムの比がどれだけ下がったか」が測りたい変化になります。以降、前者を 支援、後者を 結果 と呼んでいきます。
実在のデータではなく架空のデータを使うのは、自分で作ったデータであれば「本当はどれだけ効いたのか」という正解を知ったうえで、推定が当たっているかどうかを採点できるからです。この理由については、あとの章であらためて説明します。
結論から言うと、 この 1,000 人分のデータでは、次のような結果になりました。
- 支援する相手をランダムに決めたデータであれば、全体平均の効果はほぼ正解と一致します
- 状態が悪い人ほど支援を受けやすい、という現場に近いデータでは、平均は少しずれます。ただし「誰に効きそうか」という順位は拾えました
- 欠けている数値を中央値で埋めるだけでは、因果の推定は改善しませんでした
- 支援したあとに測った値を特徴量に入れると、推定そのものが壊れました
学習にかかった時間は CPU で数秒、SageMaker Training(ml.m5.xlarge)の課金対象は 89 秒でした。GPU は使っていません。
実装の細かい仕様は EconML の CausalForestDML と、本家である R の grf を正として確認してください。
この記事で使う言葉
本題に入る前に、言葉を揃えておきます。因果推論は略語が多く、同じものを複数の呼び方で書いてしまうと読みにくいためです。
| この記事での呼び方 | よく見る用語 | 意味 |
|---|---|---|
| 支援 | 処置、treatment、T |
その人が受けた介入のことです。今回は減塩支援を受けたかどうか(0 か 1)を指します |
| 結果 | アウトカム、outcome、Y |
効果を測りたい対象です。今回は尿中 Na/K 比の改善量です |
| 属性 | 共変量、特徴量、X |
年齢や血圧など、支援を受ける前に分かっている情報です |
| 交絡 | confounding | 「支援の受けやすさ」と「結果の出やすさ」の両方に影響する要因です。血圧が高い人ほど支援を受けやすく、かつ数値も動きやすい、といった関係が該当します |
| 傾向スコア | propensity score、e(x) |
その属性の人が支援を受ける確率のことです |
| 全体平均の効果 | ATE(Average Treatment Effect) | 全員に同じ支援を届けたときの、差の平均です |
| 属性ごとの効果 | CATE(Conditional ATE) | 「62歳で Na/K が高い人」のような層に絞ったときの、差の平均です |
| 個人の効果 | ITE(Individual TE) | 一人ひとりの本当の差です。現実には観測できません |
| ランダム割付 | RCT(無作為化比較試験) | 誰が支援を受けるかを、属性と無関係にくじ引きで決める方法です |
| 観察データ | 観察研究 | 状態が悪い人ほど支援を受けやすい、といった偏りを含む現実のデータです |
| 重なり | overlap、正値性 | 似た属性の人の中に、支援を受けた人と受けていない人の両方がいる状態を指します |
| リーケージ | leakage | 支援を受けたあとに判明した値を、うっかり特徴量に入れてしまうことです |
なお、尿中 Na/K 比は、ナトリウム(塩分)とカリウムの比率です。この値が高いほど、塩分が多くカリウムが不足している目安になります。日本高血圧学会の資料では、最適な目標が 2 未満、実行可能な目標が 4 未満、一般集団の平均はおおよそ 4 前後とされています。この記事で「改善」と書いているのは、この比率が下がることを指します。
Causal Forestとは(おさらい)
ざっくり言うと、Random Forest や XGBoost は「この人の結果はいくつになりそうか」を当てるモデルです。それに対して Causal Forest が当てるのは、属性ごとの効果(CATE) になります。
ある一人の人について、理論上はふたつの結果が存在します。
Y(1): 支援を受けた世界での結果Y(0): 支援を受けなかった世界での結果
このふたつを潜在アウトカムと呼びます。たとえば 62歳で支援前の Na/K が 5.2 だった方について、支援を受けた場合の改善量が Y(1)、受けなかった場合が Y(0) です。この差こそが、その人にとっての効果になります。
ところが、現実に観測できるのは片方だけです。支援を受けた方の「もし受けなかったら」は、タイムマシンでもない限り分かりません。これが因果推論の根本問題と呼ばれるもので、個人の本当の効果は観測できない、という制約になります。
では何を推定するのかというと、似た属性の人を集めて、「支援を受けた人たちの平均」と「受けていない人たちの平均」の差を取ります。これが属性ごとの効果(CATE)です。式にすると、次のようになります。
τ(x) = 属性が x の人たちについて、Y(1) と Y(0) の差を平均したもの
全員をまとめた平均が ATE、属性で絞った平均が CATE という関係です。個人の本当の効果は見えないため、実務では CATE をその代わりとして扱います。マーケティングの領域では、同じものを Uplift(底上げ量)と呼ぶことが多いですね。
ここでよく出てくる疑問が、「全体平均がほぼゼロなら、その施策は無駄なのでは」というものです。これはそうとも限りません。全体平均が 0 でも、ある層では +0.5、別の層では −0.5 になっている、ということが普通に起こります。一律に配れば損をして、効く人に絞れば得をする、というわけです。Causal Forest を使う動機は、だいたいこの「誰に届けるか」にあります。
なぜ予測モデルでは足りないのか
いちばん引っかかりやすいのが、この部分だと思います。支援を受けたかどうかを特徴量に入れて結果を予測すれば、「支援が効いた人」が見えそうな気がしますよね。ですが、モデルが捉えているのはあくまで関連であって、差ではありません。
たとえば、次のふたつが同時に成り立っているとします。
- もともと状態が悪い人ほど、支援を受けやすい
- もともと状態が悪い人ほど、あとから測った数値も動きやすい
このとき予測モデルは、「支援を受けた人の結果はこうなりやすい」という関係を学びます。事実の要約としては正しいのですが、「その人が支援を受けなかった場合と比べてどれだけ変わったか」までは切り分けられていません。
SHAP は、予測モデルがどの特徴をどれだけ使ったかを説明する道具です。したがって SHAP で支援フラグの寄与が最も大きいと出たとしても、それは「支援の有無が予測に効いている」という意味であって、「誰に効くか」を示したものではない、というわけです。
実際、このあとの実験では、同じデータを XGBoost に学習させたときの重要度の首位は 支援フラグ で 0.66、Causal Forest の首位は 支援前の Na/K で 0.95 となりました。見ているものが違う、とはっきり分かる結果です。
違いを整理すると、予測モデルは「結果がどうなりそうか」を答えるもの、Causal Forest は「やらなかった場合と比べてどれだけ違うか」を答えるもの、というイメージです。
アルゴリズムの骨格
実装としては、学術の世界では R の grf が定番で、Python では EconML の CausalForestDML がよく使われます。今回は Python で進めます。理論的な土台は Generalized Random Forests(Athey, Tibshirani, Wager 2019)という枠組みです。
その前に、名前にある Forest(森)について触れておきます。これは Random Forest と同じで、決定木を何百本も育てて束ねたもの、という意味です。決定木というのは、「支援前の Na/K が 4.5 以上か」「年齢が 55 歳未満か」といった条件で人を次々に枝分かれさせて、最後に似た人だけが集まった小さなグループを作る仕組みですね。この行き着いた先のグループを葉と呼びます。以降、「木」と書いているのはこの決定木1本のことだと思ってください。
そのうえで、押さえておきたい点は3つあります。
1. 属性から予想できる分を、先に差し引く
いきなり効果を測りにいくのではなく、まず「その人の属性を見ただけで予想がついてしまう分」を計算して、実際の値から引いておきます。この工程を担当するのが、あとで出てくるコードの model_y と model_t です。
model_y: 属性だけを使って結果を予測します。「この人なら、これくらいは改善しそうだ」という見込みですねmodel_t: 属性だけを使って支援の受けやすさを予測します。これが傾向スコアにあたります
このふたつの予想ができたら、実際の値との差を取ります。
- 結果のずれ = 実際の結果 − 属性から予想した結果
- 割付のずれ = 実際に支援を受けたか − 属性から予想した受けやすさ
それぞれの木が見ているのは、この「ずれ」同士の関係です。血圧が高い人ほど支援を受けやすく、かつ改善量も動きやすい、といった交絡は、属性から予想できる部分にすでに含まれています。ですから差を取った時点で、いったん取り除かれることになります。あとに残るのは「属性から見ると意外なほど支援を受けた人」と「意外なほど改善した人」で、このふたつがどれくらい重なるかを効果と見なす、という流れです。
Double Machine Learning(DML)や R-learner と呼ばれる考え方です。名前はいかめしいのですが、やっていることは「予想できる分は先に引いて、残った分だけを見る」ということですね。
2. 枝の分け方が違う
通常の決定木は、「結果が似ている人」が同じ葉に集まるように枝を分けます。これに対して Causal Forest は、効果の差が大きくなる境目で枝を分けます。人によって効果が違うことを異質性と呼びますが、その異質性を探しにいく木、と考えると分かりやすいです。
3. Honesty(正直な推定)
木を1本作るとき、データをふたつのグループに分けます。片方で「どこで枝を分けるか」を決め、もう片方で「分けたあとの効果はいくつか」を計算します。
同じデータで両方を決めてしまうと、たまたま効いて見えた枝をそのまま信じることになり、信頼区間が実態より狭く出てしまいます。学習データと評価データを分ける、という当たり前の作法を木の内部でも徹底する仕組み、と考えてください。EconML では honest=True がデフォルトです。ここは変えずに使うのがおすすめです。
このほかに、対象の人と同じ葉に入った人を重みとして効果を求める仕組みや、木を小分けにしてばらつきを見る信頼区間(Bootstrap-of-little-bags)も備わっています。細かくチューニングしなくても、規模の小さい表形式データであればそれなりに動く、というのが原著や実務レビュー(Rehill 2024)での評価です。
学習の前に確認しておく前提
モデルを回す前に、そのデータで効果を語ってよいかを確認します。論文での作法は「データを確認する → 変数の関係図を描く → 前提を確認する → 推定する → 評価する」という順序です。
ここで出てくる関係図は DAG(有向非巡回グラフ)と呼ばれるもので、「何が何に影響するのか」を矢印だけで描いた図です。言葉で説明するより見ていただいたほうが早いので、今回のデータを描いてみます。
矢印の向きが、そのまま時間の流れになっている点に注目してください。支援前の属性から支援へ矢印が伸びているのは、状態が悪い人ほど支援を受けやすい、という偏りを表しています。同じ属性から結果にも矢印が伸びていますので、この属性がまさに交絡にあたります。そして真ん中の「支援 → 結果」が、今回推定したい効果です。
問題になるのは、右側の「支援後の Na/K」です。ここには結果から矢印が入っています。支援の効果を受けたあとの値なのですから、当然ですね。これを特徴量として使うのは、答えを見ながら答えを推定するようなものです。このように図を描いておくと、「矢印が結果から出ているのか、結果に入っているのか」で入れる列と外す列を判断できますので、迷いにくくなります。
確認すべき前提は4つです。名前はいかめしいのですが、中身は現場の感覚に近いものばかりです。
| 前提 | 意味 | 満たされないと |
|---|---|---|
| SUTVA | ある人への支援が、他の人の結果に影響しないこと | 家族をまとめて指導する、地域全体に呼びかけが広がる、といった場合に崩れます |
| 無視可能性 | 観測している属性を揃えれば、誰が支援を受けるかはその人の潜在的な結果と無関係とみなせること | 観測できていない交絡が残ります。しかもデータだけでは検証できません |
| 正値性(重なり) | どの属性の人にも、支援を受ける可能性と受けない可能性の両方があること | ほぼ全員が支援を受けている層では、比較する相手がいなくなります |
| 時点の定義 | 特徴量は支援を受ける前に分かっている情報だけにすること | あとから測った値を入れると、リーケージが起きます |
このうち無視可能性が、いちばん重要でありながら、いちばん検証しにくい前提です。「血圧と支援前の Na/K を揃えれば、あとはくじ引きと同じとみなせる」と言い切れるかどうかは、データを眺めていても分かりません。
ここで、ランダム割付と観察データの差が効いてきます。ランダム割付というのは、誰に支援するかを乱数で決めてしまう方法です。本人の状態とはまったく無関係に決まりますので、記録に残っていない要因まで含めて、支援を受けたグループと受けていないグループが平均的に同じ顔ぶれになります。つまり無視可能性が、設計の時点ですでに満たされているわけですね。
これに対して観察データは、現場の判断の結果として残ったデータです。今回でいえば、保健師さんが「この方は放っておくと危ないな」と考えて声をかけた記録、ということになります。その判断には、血圧や Na/K のような記録に残る数値だけでなく、面談での様子や生活の事情といった、データに残らない情報も混ざります。たとえば「家族に食事を作ってくれる人がいるかどうか」で声のかけやすさが変わり、しかもそれが改善のしやすさにも効いていたとしましょう。この要因は交絡として残り続けますが、手元の列をどう揃えても取り除けません。記録されていないものは、揃えようがないからです。
そのため観察データでできるのは、「割付に使った変数を落とさない」「支援後の値を入れない」という、列の選び方を間違えないところまでになります。残りは現場の方に「どういう基準で声をかけていたのですか」と伺って、その基準がデータに入っているかどうかを確認していく作業です。
評価を「正解判定」にしない理由も、ここにあります。モデルの点数が良いことと、前提が成り立っていることは、まったく別の話だからです。
他の手法との違い
「誰に効くか」を推定する手法は、Causal Forest だけではありません。大きく3つの系統があります。
- 平均を正しく求める系統: 傾向スコアで重みを付ける IPW と、それをより頑健にした TMLE です。「全体として効いたのか」という問いに向いています
- 好きな予測器を組み合わせる系統: S-Learner、T-Learner、X-Learner などで、メタラーナーと総称されます。予測モデルの組み立て方の工夫、と考えると分かりやすいです
- 木の分け方そのものを効果向きにする系統: Causal Forest と、名前の似ている Uplift RF です
DevelopersIO の既存記事との対応も含めて、表に整理します。
| 手法 | やっていること | 向いている問い | Causal Forest との違い |
|---|---|---|---|
| Random Forest / XGBoost | 結果を予測します | 高リスク者の特徴を知りたい、精度を上げたい | 支援フラグを入れても因果の差にはなりません。SHAP はあくまで関連の説明です |
| 傾向スコア IPW | 支援を受けにくい人を重く、受けやすい人を軽く重み付けして平均を求めます | 支援全体として効いたのか | 平均は求められますが、誰に効くかまでは分かりません |
| TMLE | IPW の結果を、予測モデルでもう一段補正します | IPW の結果が妥当かを確かめたい | こちらも平均寄りの手法です。TMLE の記事があります |
| S-Learner | 支援フラグ込みの予測モデルを1つ作り、フラグを 0 と 1 に切り替えた差を取ります | 支援の影響が弱いとき | 支援の効果が他の特徴に埋もれやすくなります |
| T-Learner | 支援あり用と支援なし用の予測モデルを2つ作り、その差を取ります | 両方のグループが十分にあるとき | 片方の人数が少ないと不安定になります。T-Learner の記事があります |
| X-Learner | グループ別に求めた効果を、傾向スコアで混ぜ合わせます | 支援を受けた人が少ないとき | 土台にする予測器を自分で選ぶ必要があります。X-Learner の記事があります |
| Uplift RF | 枝の分け方を、マーケティング向けの指標に置き換えます | Qini の最適化 | 理論の組み立て方や信頼区間の求め方が異なります。Uplift RF の記事があります |
| Causal Forest | 異質性を探す木に、予想できる分の差し引きと Honesty を組み合わせます | 誰に効くかを、信頼区間つきで知りたい | 上の3要素がセットになっている点が特徴です |
違いを整理すると、IPW や TMLE は「平均を正確に求める」もの、メタラーナーは「手持ちの予測器で差を組み立てる」もの、Causal Forest は「差が大きい境目を探すように枝を分ける木」、というイメージです。名前の似ている Uplift RF は、中身は別物と考えておいたほうがよさそうです。
なお、Causal Forest が常に勝つ必要もありません。このあとの実験では、同じ観察データで T-Learner のほうが個別効果の誤差がわずかに小さい、という結果になりました。1,000 人規模だと差は小さく、常勝という書き方はできません。
やってみましょう: 減塩支援の合成データ
実データは使いません。公開されている疫学の目安に寄せて作った、架空の 1,000 人分のデータを使います。このように、ルールを決めてプログラムで作ったデータを合成データと呼びます。そのルール全体のことは DGP(データ生成過程)と呼ばれます。
シナリオは次のとおりです。ある自治体の住民健診を模した架空のデータで、支援にあたるのは個別の減塩指導を受けたかどうか、結果にあたるのは尿中 Na/K 比の改善量です。
なぜ実データではなく合成データを使うのかというと、先ほど書いたとおり、個人の本当の効果は現実には観測できないからです。「支援が効いた人」と「支援がなくても良くなった人」は、その人の履歴を眺めるだけでは区別がつきません。合成データであれば、作った側が正解の効果を知っているため、推定が当たっているかどうかを採点できます。評価の練習台としては、これがいちばん確実です。
今回の本筋は観察データのほうです。血圧と支援前の Na/K が高い人ほど支援を受けやすい、という偏りをあらかじめ入れてあります。全員が 50% の確率で支援を受けるランダム割付のデータは、比較用として用意しました。
真の効果
結果の列は、改善量として benefit という名前にしました。Na/K が下がるほど値が大きくなる向きです。元の比率のままだと「小さいほど良い」となってグラフが読みにくいので、ここで符号を反転させています。
プログラムの中で設定した本当の効果は、次のとおりです。τ(タウ)は効果を表す記号です。
τ =
0.20 # 誰にでも少しは効く
+ 0.28 * (nak0 - 4) # 支援前の Na/K が高いほど効く
+ 0.10 * (bmi - 23) / 4 # BMI が高いほど効く
+ 0.12 * (sbp - 130) / 20 # 収縮期血圧が高いほど効く
+ 0.06 * (6 - veg_week) / 6 # 野菜の摂取が少ないほど効く
- 0.18 * (age < 55) * (health_c <= 2) # 若くて健康意識が低いと効きにくい
読み方としては、塩分の摂取が多い人、BMI や血圧が高い人、野菜が少ない人ほど支援が効く、という設定です。逆に、若くて健康意識が低い層では効きにくくしてあります。指導内容を守りにくい状況を再現したものです。
観察データ側の「支援の受けやすさ」は、次のように偏らせました。logit は確率を扱いやすい数値に変換する関数だと考えてください。血圧、Na/K、年齢が高いほど、支援を受ける確率が上がります。
logit(e) = -0.8 + 0.025*(sbp-130) + 0.35*(nak0-4) + 0.08*(age-60)
乱数の種を 42 に固定して生成したところ、次のような数値になりました。
- 観察データで支援を受けた人の割合は 30.2%
- 本当の全体平均の効果(ATE)は 0.12
- 個人ごとの本当の効果は、下位10%が −0.36、中央値が 0.12、上位10%が 0.58
単位は尿中 Na/K 比の改善量です。一般集団の平均が 4 前後で目標が 2 未満ですから、平均 0.12 というのは「全員に配ってもわずかしか動かない」という規模になります。ところが上位層では 0.5 を超えていますので、効く人に絞れば話が変わってきます。しかも下位層はマイナスですから、全員に配ると損をする人が混ざっている状態でもあります。
属性は9つ用意しました。年齢、性別、BMI、収縮期血圧、支援前の Na/K、睡眠時間、ストレス、週あたりの野菜摂取、健康意識です。このうち睡眠時間とストレスは効果の主な要因ではなく、現場でよくある「欠損しやすい列」「交絡になりやすい列」の役割を担当しています。
学習: EconMLの最小コード
使うのは EconML の CausalForestDML です。引数の役割は次のように分かれています。
| 引数 | 渡すもの | 役割 |
|---|---|---|
Y |
結果(今回は benefit) |
効果を測る対象です |
T |
支援(今回は treated) |
支援を受けたかどうかです |
X |
人によって効果が変わりそうな属性 | 「誰に効くか」の境目を探すために使います |
W |
交絡を取り除くための属性 | 支援の受けやすさの偏りを調整するために使います |
同じ列を X と W の両方に渡しても問題なく動きます。ただし、支援の割付に関係した変数(今回であれば血圧や支援前の Na/K)は、少なくとも W には入れておいてください。ここが抜けると、先ほどの「属性から予想できる分を差し引く」工程が不十分になります。
model_y が結果の予測、model_t が支援の受けやすさの予測を担当します。どちらも勾配ブースティングを指定していますが、ロジスティック回帰や線形回帰でも動きます。まずは素直な学習器で回してみて、あとから差し替えるので十分です。
from econml.dml import CausalForestDML
from sklearn.ensemble import GradientBoostingRegressor, GradientBoostingClassifier
features = ["age", "sex", "bmi", "sbp", "nak0", "sleep_h", "stress", "veg_week", "health_c"]
est = CausalForestDML(
model_y=GradientBoostingRegressor(random_state=42),
model_t=GradientBoostingClassifier(random_state=42),
n_estimators=400, # 木の本数
min_samples_leaf=20, # 葉ひとつに最低何人入れるか
honest=True, # 枝を分ける人と、効果を測る人を分ける
discrete_treatment=True, # 支援が 0/1 のとき
random_state=42,
)
est.fit(Y=df["benefit"], T=df["treated"], X=df[features], W=df[features])
cate = est.effect(df[features]) # 一人ずつの効果
ate = est.ate(df[features]) # その平均
木の本数は 200〜800 本あたりが目安で、今回は 400 本にしました。min_samples_leaf は 1,000 人規模なら 15〜30 程度がよさそうです。葉を小さくしすぎると、その葉の中に支援を受けた人か受けていない人のどちらかしかいなくなり、差が計算できなくなってしまいます。
また、1,000 人に対して属性が9つという規模であれば、各木が使う特徴はほぼ全部になります。列を増やしすぎると葉の中の人数が足りなくなりますので、最初は絞っておくのがおすすめです。
手元での事前確認では、1条件あたり 0.7〜0.8 秒で学習が終わりました。環境は Python 3.12.8、econml 0.17.0、scikit-learn 1.9.0 です。なお macOS で XGBoost を入れると、OpenMP のライブラリ(libomp)が足りずに読み込みで落ちることがあります。私の環境では brew install libomp で解決しました。
SageMaker Trainingで回す
1,000 行程度であれば、手元のノート PC で十分足ります。では、なぜわざわざ SageMaker に載せるのでしょうか。
正直なところ、学習そのものは数秒で終わるのに、ジョブ全体では約2分かかっています。その大半は、学習用コンテナを取得する時間と、その中で econml を導入する時間です。それでも載せておく意味は、次の3点だと考えています。
- データと成果物を S3 に残せます
- データが増えたときに、インスタンスを大きくするだけで対応できます
- SageMaker には組み込みの Causal Forest アルゴリズムがありません。scikit-learn 用のコンテナに自分でライブラリを追加する型を、一度書いておけます
健診データが数千件から数万件に増えたり、他のデータと結合して列が増えたりしても、同じ入口を使い回せる、というのが利点ですね。GPU は使っていません。「1,000 人規模なら CPU で足りる」という一般的な見積もりを、実測で確認した形になります。
全体の流れは次のとおりです。
[手元] 合成データの CSV を作成
↓ アップロード
[S3] .../causal-forest/20260909/input/
↓ SageMaker Training
[ml.m5.xlarge] scikit-learn 用コンテナ
pip install econml
python train.py
↓
[S3] 学習済みモデル(model.tar.gz)
コンテナの中では、次の4条件を順番に学習させています。
- 綺麗な観察データ
- 汚したデータに、欠損の中央値補完だけを行ったもの
- 定義・範囲・重複・重なりまでクレンジングしたもの
- 支援後の Na/K を特徴量に入れてしまった失敗例
起動には SageMaker Python SDK 3.x の ModelTrainer を使いました。検証用アカウントの東京リージョンで、既存の実行ロールを利用し、インスタンスは ml.m5.xlarge を1台、ディスクは 30GB です。コンテナイメージは sagemaker-scikit-learn:1.4-2-py312-cpu-py3 を指定しています。
実験結果(2026年9月9日実施)
| 項目 | 記録 |
|---|---|
| ジョブ名 | causal-forest-nak-20260909010320 |
| 状態 | Completed(正常終了) |
| インスタンス | ml.m5.xlarge × 1 |
| コンテナ内の Python | 3.12.3 |
| 課金対象の秒数 | 89 |
| 学習部分の合計(4条件) | 7.45 秒 |
| 1条件あたりの学習時間 | 1.7〜1.9 秒 |
内訳としては、インスタンスの準備に 47 秒、イメージの取得に 36 秒、学習に 40 秒、成果物のアップロードに 13 秒かかり、ジョブ作成から完了までは 136 秒でした。このうち課金対象は 89 秒です。東京リージョンの ml.m5.xlarge は1時間あたり 0.27〜0.30 USD 程度ですので、89 秒であれば約 0.007 USD になります。
ひとつ注意点として、コンテナに最初から入っている scikit-learn は 1.4 系ですが、econml を導入する過程で 1.9.0 まで上がっていました。イメージ名のバージョンと実行時のバージョンはずれることがある、と思っておいたほうがよさそうです。
乱数の種が同じであれば、クラウドと手元の結果はほぼ一致します。以下の表の PEHE は、一人ひとりの効果をどれだけ正確に当てられたかを表す指標です。詳しくは次の章で説明しますが、小さいほど良い、と考えてください。
| 条件 | 平均効果(SageMaker) | 平均効果(手元) | PEHE(SageMaker) | PEHE(手元) |
|---|---|---|---|---|
| 綺麗な観察データ | 0.175 | 0.174 | 0.186 | 0.185 |
| 中央値補完のみ | 0.153 | 0.153 | 0.184 | 0.184 |
| クレンジング後 | 0.185 | 0.185 | 0.160 | 0.161 |
| 支援後の値を入れた失敗例 | 0.024 | 0.024 | 0.326 | 0.326 |
差は小数第3位の範囲に収まりました。きちんと実行できています。
評価は「正解判定」ではない
個人の本当の効果は、現実には観測できません。そのため評価は何段階かに分かれます。ここでは、合成データだからこそできる採点と、実データでも使える確認作業を分けて見ていきます。
合成データだからできること
| 指標 | 意味するもの | 内容 |
|---|---|---|
| ATE bias | 平均のずれ | 推定した全体平均と、本当の全体平均との差です。0 に近いほど平均が当たっています |
| PEHE | 個別効果の誤差 | 推定した効果と本当の効果の差を二乗して平均し、平方根を取った値です。予測モデルでいう RMSE にあたります |
| Coverage | 信頼区間の的中率 | 一人ずつ「効果はこの範囲でしょう」と幅を示したとき、その内側に本当の効果があった人の割合です |
| 重要度の再現 | 変数を拾えているか | プログラムで効かせた列を、モデルも重要だと判断しているかどうかです |
PEHE は Precision in Estimation of Heterogeneous Effects の略で、「人によって違う効果を、どれだけ精密に当てられたか」を表します。本当の効果が分からないと計算できませんので、実データでは使えません。この記事での主力指標ではありますが、本番の採点表にはならない、という点は押さえておいてください。
数値の大きさにも注目してみてください。観察データの本当の平均効果が 0.12 なのに対して、PEHE は 0.19 前後です。つまり平均そのものよりも、一人ひとりの誤差のほうが大きいという状態です。「平均としてはだいたい合っているけれど、この人個人の効果となるとまだ粗い」というのが、1,000 人規模での実感になります。このあと層に分けて評価する理由も、ここにあります。
手元での事前確認の結果です。木は 400 本、葉の最小人数は 20、Honesty は有効にしてあります。
| データ | 推定した平均 | 本当の平均 | 平均のずれ | PEHE | 信頼区間の的中率 |
|---|---|---|---|---|---|
| ランダム割付 | 0.120 | 0.124 | −0.004 | 0.169 | 0.601 |
| 観察データ | 0.174 | 0.124 | +0.050 | 0.185 | 0.545 |
| 中央値補完のみ | 0.153 | 0.125 | +0.029 | 0.184 | 0.568 |
| クレンジング後 | 0.185 | 0.142 | +0.043 | 0.161 | 0.628 |
| 支援後の値を追加 | 0.024 | 0.125 | −0.100 | 0.326 | 0.060 |
ランダム割付であれば、平均はほぼ正解と一致しました。一方で観察データでは +0.05 のずれが出ています。支援の受けやすさが血圧と Na/K に依存しているため、取り切れなかった交絡が残っているのだと読めます。
ここで気になるのが、信頼区間の的中率が、ランダム割付でも 0.601 にとどまっている点ではないでしょうか。この数字が何を数えたものなのか、順番に説明します。
Causal Forest は一人ひとりについて、「この人の効果は 0.30」という一点の見積もりだけでなく、その上下の幅も出してくれます。それが effect_interval です。
lb, ub = est.effect_interval(df[features], alpha=0.05)
alpha=0.05 は「外れてもよいのは 5% までにしてください」という指定で、つまり 95% 信頼区間を出してくれ、とこちらから頼んでいることになります。返ってくる lb と ub が、一人ずつの下限と上限です。ある人について「効果の見積もりは 0.30、幅は 0.05 から 0.55」といった形で返ってきます。
そして今回は合成データですから、その人の本当の効果(たとえば 0.42)も分かっています。そこで一人ずつ、本当の効果がこの幅の内側にあったかどうかを判定していき、1,000 人のうち何割が内側だったかを数えたものが、この的中率になります。
こちらが 95% と指定して出してもらった幅ですから、1,000 人であれば 950 人くらいは内側に入っていてほしいところです。ところが実際に内側だったのは 601 人でした。これは幅の申告が狭すぎる、言い換えるとモデルが自分の見積もりを過信している状態を意味します。
ですから 1,000 人規模では、個人単位の幅を「この人には効く、この人には効かない」という判断材料に使わないほうがよさそうです。次に説明する層ごとの評価まで落として使うのが、実務的だと思います。
なお「中央値補完のみ」の条件については補足があります。欠損値をそのままでは学習器に渡せないため、列の中央値で埋めています。完全に生のデータを投入したわけではありません。
予測モデルとの比較
同じ属性を使って、XGBoost に改善量を予測させ、どの列を重視したかを並べてみます。左が XGBoost、右が Causal Forest です。横軸が重要度で、棒が長いほどそのモデルがその列を重視した、という意味になります。

- XGBoost(改善量の予測): 支援フラグが 0.661、支援前の Na/K が 0.156。残りは 0.02 前後に分散しました
- Causal Forest: 支援前の Na/K が 0.952。BMI が 0.015、血圧が 0.008 でした
プログラムで最も強く効かせた列である支援前の Na/K は、Causal Forest がきちんと拾ってくれました。ただし「若くて健康意識が低いと効きにくい」という組み合わせ(交互作用)は、重要度にはほとんど現れていません。
重要度は「枝を分けるときにその列を何回使ったか」に近い数値です。因果関係の証明ではありませんし、組み合わせの効果まで必ず示してくれるわけでもない、という限界があります。
一方で、XGBoost 側で支援フラグが首位になること自体は、まったく正しい挙動です。ここは大事なところなので、順番に掘り下げます。
XGBoost にお願いした仕事は、改善量を当てることでした。今回のデータでは、支援を受けた人には、受けなかった場合と比べて平均 0.12 だけ改善量が上乗せされています。ですから当てる側からすると、「この人は支援を受けたのか」さえ分かれば、狙うべき値の水準が決まって誤差が大きく減ります。重要度 0.661 は、その事情がそのまま出た数字です。この図が語っているのは、「支援を受けた人たちは、受けていない人たちより改善量が大きかった」というグループ単位の傾向になります。
では、なぜそれが「誰に効くか」の答えにならないのでしょうか。支援フラグは、支援を受けた 302 人にとっては全員が同じ 1 だからです。全員が同じ値しか持たない列からは、「支援を受けた人の中で、誰がよく効いて、誰が効かなかったのか」を区別しようがありません。SHAP でこの列の寄与をどれだけ細かく眺めても、返ってくるのは「支援には効果がある」という全体でたった1本の答えであって、名簿に順位をつけるための材料ではない、というわけですね。
これに対して Causal Forest には、そもそも支援フラグを特徴量として渡していません。支援は T という別枠で渡していて、X に入っているのは支援前の属性だけです。図の右側にも treated という行が並んでいますが、棒がまったく伸びていないのはそのためですね。そのうえで「効果の差が大きくなる境目」を探した結果、支援前の Na/K が 0.952 で首位になりました。こちらが意味しているのは、「支援前の Na/K が高い人と低い人とでは、支援したときに得られる改善量そのものが違う」ということです。
この差は、そのまま打ち手の差になります。XGBoost の答えから言えるのは「支援には効果があるらしい」までで、では誰に案内を出すのかまでは決められません。Causal Forest の答えがあれば、支援前の Na/K が高い方から順に声をかける、という優先順位を作れます。もっとも、重要度が教えてくれるのは「この列で効果が変わる」というところまでで、「Na/K が高いほど効く」という向きや、その大きさまでは読み取れません。そこも含めて、このあとの GATE と散布図で確かめていきます。
なお、予測モデルでは絶対に効果を語れない、という話でもありません。いまの XGBoost で支援フラグを 0 と 1 に切り替えて予測の差を取れば、それは前の表に出てきた S-Learner そのものです。差を取る操作を自分で書けば、予測モデルからでも効果の推定には持っていけます。逆に言えば、重要度や SHAP を眺めているだけでは、その差は出てこないということですね。
重なりの確認
傾向スコアは、その属性の人が支援を受ける確率でした。この推定値を、支援を受けた人と受けていない人で重ねてヒストグラムにします。片方に偏った塊があれば、その層について効果は語りません。ほぼ全員が支援を受けている層には、比較する相手がいないためです。
たとえば傾向スコアが 0.03 の人であれば、似た属性の 100 人のうち3人しか支援を受けていない計算になります。その3人の改善を「この層の効果」と呼ぶのは、偶然に振り回されやすくなります。逆に 0.99 であれば、支援を受けていない人がほとんどいませんので、「受けなかった場合」を推し量ることができません。そのため両端は、除外するか語らないかのどちらかになります。この除外操作を trim と呼びます。
観察データで描いたものが、次の図です。横軸が傾向スコア、縦軸が人数で、オレンジが支援を受けた人(treated)、緑が受けていない人(control)を表しています。ふたつの色が重なっている範囲が、支援あり・なしの両方が揃っていて比較できる範囲、という読み方になります。

| データ | 傾向スコアの最小値 | 最大値 | 0.05 未満 | 0.95 超 |
|---|---|---|---|---|
| 観察データ | 0.025 | 0.856 | 29 人 | 0 人 |
| 中央値補完のみ | 0.021 | 0.854 | 36 人 | 0 人 |
| クレンジング + trim 後 | 0.051 | 0.854 | 0 人 | 0 人 |
図を見ると、緑(支援なし)の山が左側の 0.1〜0.2 あたりに集中しているのが分かります。つまり支援を受けていない人が「受けにくそう」な側に偏っている状態ですね。逆に右端の 0.8 付近になると、今度はオレンジ(支援あり)しかいなくなります。0.95 を超える人は、今回ほとんど出ていません。問題になるのは下側と、「高齢かつ血圧が高い層で、支援を受けていない人がいなくなる」ケースです。データを汚す際にこの層へ支援を寄せたのですが、該当したのは 5人だけで、操作後の支援率は 100% になりました。事例としては少々弱いので、件数は正直に書いておきます。trim を行ったあとは、傾向スコアが 0.05〜0.85 の範囲に収まりました。

GATE(効果が高そうな順に層を分ける)
GATE は Group Average Treatment Effect の略で、グループごとの平均効果を意味します。手順は次のとおりです。
- データを学習用 70%、検証用 30% に分けます
- 学習用のデータだけでモデルを作ります
- 検証用の一人ひとりについて、効果の見積もりを出します
- 見積もりの小さい順に並べて5等分します(Q1 が効きにくそうな層、Q5 が効きそうな層)
上の層ほど本当の改善も大きくなっていれば成功、という見方をします。ここで学習とグループ分けを同じデータで行うと、評価が甘くなりますので、必ず分けてください。検証用の 300 人を5等分すると、1グループ 60 人になります。
なお表の「グループ内の単純差」は、そのグループの中で「支援を受けた人の平均 − 受けていない人の平均」を、そのまま引き算した値です。Causal Forest を使わない、いちばん素朴な比較になります。
結果が次の図です。左から順に Q1、Q2 と並んでいて、青が本当の効果、赤がモデルの見積もりです。青と赤の高さが揃っていれば見積もりが正確、左から右へ階段状に上がっていれば順位づけが機能している、という2点を見ていきます。

| 分位 | 人数 | 本当の効果の平均 | モデルの見積もり | グループ内の単純差 |
|---|---|---|---|---|
| Q1 | 60 | −0.345 | −0.203 | −0.283 |
| Q2 | 60 | −0.074 | −0.055 | +0.143 |
| Q3 | 60 | +0.154 | +0.130 | +0.358 |
| Q4 | 60 | +0.343 | +0.343 | +0.270 |
| Q5 | 60 | +0.540 | +0.458 | +0.622 |
順位は下から上まできれいに並びました。Q1 はマイナス、Q5 はプラスです。全体平均 0.12 の内側に、効かない層と効く層が同居していることが分かります。ただし両端の Q1 と Q5 では、見積もりが本当の値より内側に寄っています。
一方で単純差のほうは、Q2 以降でかなり振れています。Q2 は本当の効果が −0.07 なのに、単純差は +0.14 と符号まで逆転しました。同じ分位の中でも支援を受けた人と受けていない人の構成が違うため、単純な引き算では交絡が残ってしまうわけです。グループ内で引き算しただけの値を効果と呼ぶのは危うい、という良い例になりました。
実データで GATE を確認するときは、単純差ではなく、モデルが出したグループ平均のほうを見るのが無難です。今回は正解が分かっていますので、本当の効果とモデルの見積もりを中心に見ています。
一人ひとりを点で描いた散布図でも、同じ傾向が確認できます。横軸がその人の本当の効果、縦軸がモデルの見積もりで、点線は「見積もりが本当の値とぴったり一致する線」です。すべての点が点線の上に乗っていれば完璧、という図ですね。

実際の点の雲は、緩やかな S 字を描いています。真ん中あたりは点線に沿っているのですが、両端に行くほど点線から離れていきます。本当の効果が 1.0 を超える人でも見積もりは 0.5 あたりで止まり、逆に −1.0 の人でも −0.2 あたりまでしか下がっていません。効果が極端な人ほど、見積もりが平均側へ引き寄せられているわけです。GATE で Q1 と Q5 の見積もりが内側に寄っていたのも、これと同じ現象を層ごとに見たものになります。
キャリブレーションと Qini(参考)
キャリブレーションは、見積もりの大きさと本当の大きさが比例しているかを確認する作業です。体温計がいつも 0.5 度高めに出る、といったずれを調べるイメージですね。R の grf には専用の関数が用意されていますが、今回は本当の効果に回帰させて簡易的に再現しました。
本当の効果 ≒ a × 全体の平均見積もり + b × (その人の見積もり − 全体平均)
a が 1 に近ければ平均が合っており、b が 1 に近ければ効果の大小の向きと幅が合っている、という読み方をします。実測では a が 0.71、b が 1.37 でした。平均はやや大きめに出ており、大小の向きは合っているものの幅が広がっている、ということになります。GATE で見た「順位は合っているが両端は縮む」という傾向とも矛盾しません。
Qini は、マーケティングでよく使われる考え方です。効果が高い順に並べて、上位何%に施策を配ったときにどれだけ得をするかを、ランダムに配った場合と比較します。今回は参考として単純差で見たところ、上位 10% が 0.79、上位 50% が 0.57、全体では 0.34 でした。順位付けどおり、上位ほど効いています。ただし単純差での計算ですので、参考程度の扱いにとどめます。
本格的な指標としては RATE や AUTOC、QINI があり、効果が一部の人に集中している場合は AUTOC、広く薄く効いている場合は QINI が向いているとされています。詳細は Yadlowsky et al.(2025)と grf の解説を参照してください。
メタラーナーとの比較
同じ観察データを使って、T-Learner(支援あり用と支援なし用の Random Forest を作って差を取る方法)と並べてみました。
| 手法 | 推定した平均 | 平均のずれ | PEHE |
|---|---|---|---|
| Causal Forest | 0.174 | +0.050 | 0.185 |
| T-Learner(RF) | 0.178 | +0.055 | 0.170 |
個別効果の誤差については、T-Learner のほうがわずかに良い結果でした。平均はどちらもやや大きめに出ています。1,000 人規模で、効果の主な要因がほぼ足し算で表現できる設定であれば、モデル2つの引き算でも十分に戦えるということですね。Causal Forest を選ぶ理由は「常に精度が高いから」ではなく、Honesty と信頼区間、そして異質性向きの分割方法がセットになっているから、と考えるのが正確だと思います。
データが汚れているとどう壊れるか
ここからは、これまで使ってきた綺麗なデータをわざと汚してみて、推定の結果がどう変わるかを見ていきます。
予測モデルを作るときの前処理は、目的がはっきりしています。欠損を埋めて、外れ値を直して、重複を消す。そうすれば予測の精度が戻ってきます。ところが因果推論の場合は、少し事情が違います。同じように手を入れても精度が戻らないどころか、前処理のやり方しだいでは、そもそも効果を推定してよいという土台のほうが崩れてしまうことがあるからです。
崩れ方は、先ほど確認した前提に対応して3種類あります。
- 交絡を取り除けなくなる: 誰に支援するかを決める際に使われた情報が、欠損や補完によって歪んでしまうと、「属性から予想できる分」を正しく差し引けなくなります
- 時点が混ざる: 支援を受けたあとに測った値が特徴量に紛れ込むと、答えを見ながら答えを推定することになってしまいます
- 重なりが消える: ある層で支援を受けた人しか残らなくなると、比べる相手がいなくなり、その層の効果を語れなくなります
やっかいなのは、どれも予測の精度を眺めているだけでは気づけないことです。それどころか、精度のほうは良くなってしまう場合すらあります。そこで、現場で起こりそうな汚れを、これまでと同じ合成データに加えてみました。
| 汚れ | 内容 |
|---|---|
| 睡眠・ストレスの欠損 | 1,000 人中 159 人。健康意識が低い人ほど欠けやすくしました |
| BMI の外れ値 | 8件。桁がひとつずれたケースと、身長をメートルのまま計算したケースです |
| 同一人物の重複行 | 15 行追加し、合計 1,015 行になりました |
| 高齢かつ血圧が高い層 | 該当5人。操作後は支援率が 100% になり、比較相手がいなくなりました |
| 支援後に測った Na/K | 特徴量の候補に混ぜました。リーケージの再現です |
このうち欠損については、完全にランダムに欠けているわけではない点が重要です。健康意識が低い人ほど欠けやすくしてありますので、欠損行をすべて削除すると、意識が高い人だけが残ることになります。予測モデルの文脈で MCAR(完全にランダムな欠損)と呼ばれる状況とは異なる、というわけです。
クレンジングの順番
では、実際に手を入れていきます。ここでポイントになるのが、作業の順番です。予測モデルでやりがちな「とりあえず中央値で埋める」は、この流れの一部でしかありません。
- 同一人物 ID の重複を削除します(最初の行を残します)。このあと中央値や傾向スコアを計算していきますので、同じ人が二重に数えられたままだと、その人の値が実際より強く効いてしまいます
- BMI は 15〜40 の範囲外をいったん欠損にしてから、中央値で補完します。桁がひとつずれた値を含んだまま中央値を計算すると、埋めるための値そのものがずれてしまうためです。外れ値だったことはフラグとして残しておきます
- 睡眠時間とストレスも中央値で補完し、欠けていたことをフラグに残します。今回は健康意識が低い人ほど欠けやすくしてありますので、「欠けていた」という事実自体が、その人を表す情報になっているからです
- 支援後の Na/K は特徴量に使いません。 3種類の崩れ方のうち、時点の管理にあたる工程です
- 傾向スコアを仮に推定して、0.05 未満または 0.95 超の人を除外します(trim)。欠損を埋めたあとでないと傾向スコア自体が計算できませんので、この位置になります
- 支援と結果については、欠けている行を補完せずに削除します。この2つを埋めるという行為は、効果の計算材料を自分で作り出すことにほかならないので、避けてください
| 処理段階 | 人数 | 支援を受けた割合 | 備考 |
|---|---|---|---|
| 汚した直後 | 1,015 | 0.301 | 重複 15 行を含みます |
| 重複削除後 | 1,000 | 0.303 | 同一人物は最初の行を採用 |
| 範囲チェックと補完後 | 1,000 | 0.303 | BMI 外れ値フラグ 8、睡眠欠損フラグ 159 |
| trim 後 | 969 | 0.312 | 傾向スコア 0.05 未満の 31 人を除外 |
クレンジング前後の比較
ここまでの条件を、まとめて並べます。まずは個別効果の誤差(PEHE)を棒グラフにしたものです。棒が短いほど、一人ひとりの効果を正確に当てられている、という見方になります。

| 条件 | 平均のずれ | PEHE | 重なり | モデルが重視した列 |
|---|---|---|---|---|
| ランダム割付 | −0.004 | 0.169 | 割付が半々で問題なし | 支援前の Na/K 0.97 |
| 観察データ | +0.050 | 0.185 | 最小 0.025、29 人が下端 | 支援前の Na/K 0.95 |
| 中央値補完のみ | +0.029 | 0.184 | 下端が 36 人 | 支援前の Na/K 0.93。BMI がやや上昇 |
| クレンジング + trim | +0.043 | 0.161 | 0.05〜0.85 に収束 | 支援前の Na/K 0.93 |
| 支援後の値を追加 | −0.100 | 0.326 | 重なりを見る以前に推定が壊れる | 支援後の Na/K 0.52 が首位 |
この表から読み取っていただきたいのは、次の4点です。
1. 欠損を埋めただけでは、因果の推定は良くなりません
中央値補完のみの条件の PEHE は 0.184 で、綺麗な観察データの 0.185 とほとんど変わりませんでした。
2. 支援後に測った値を入れると、当てはまりは良くなるのに推定は壊れます
支援後の Na/K は「支援前の値 − 改善量」から作った列です。モデルから見れば答えに近い列ですので、当然のように重要度の首位になります。その結果、PEHE は約2倍に悪化し、信頼区間の的中率は 6%、平均の見積もりは 0.02 まで潰れてしまいました。
なぜ平均まで壊れるのでしょうか。支援後の値には、支援の結果そのものが含まれているためです。先ほど説明したとおり、この手法は属性から予想できる分を差し引いて、残った「ずれ」同士の関係を効果と見なします。答えに近い列を属性側に入れてしまうと、そのずれの大部分を属性側が吸収してしまい、効果が小さく見積もられます。現場でいえば「支援後の血圧」「支援後の受診回数」「プログラムを完走したかどうか」などが、同じ役割を果たしてしまいます。
3. 重なりがない層について、個人の効果を出してはいけません
似た属性で支援を受けなかった人がいない場合、モデルには比較する相手がありません。それでも数字自体は出てきますが、その値は手元のデータから計算されたものではなく、他の層の傾向から線を伸ばして当てはめたものになります。数字が出ること自体は、その層について語ってよいという保証にはならない、というわけですね。
4. 評価は予測精度ではありません
予測が当たることと、効果の差が当たることは別物です。実際、支援後の値を入れた条件は、改善量の予測としてはむしろよく当たります。それでいて効果の推定としては、今回いちばん悪い結果でした。合成データであれば平均のずれと PEHE で採点できますが、実データではどちらも計算できません。そこで次の章では、その代わりに何を見るかを整理します。
trim すると平均のずれが悪化することがある
ここで数値を見て、「クレンジングは失敗だったのでは」と思われた方もいるかもしれません。クレンジング後の PEHE は 0.161 で最も良いのに、平均のずれは中央値補完のみの条件より悪化しているためです。
理由は、trim によって語る対象の集団そのものが入れ替わるためです。傾向スコアが 0.05 未満だった 31 人を除外すると、残った 969 人の本当の平均効果は、除外前の 0.124 から 0.142 に上がります。除外されたのは「ほとんど支援を受けない人」ですが、この層はもともと血圧や Na/K が低めで、効果も小さい人たちでした。効果の小さい人が抜けたぶん、残った集団の平均が持ち上がったわけですね。
そして平均のずれは、この新しい 969 人の平均 0.142 に対する差として計算されます。比べる基準そのものが動いているので、PEHE と足並みを揃えて良くなるとは限らない、ということになります。
つまり trim は、推定を綺麗にする魔法ではなく、語れる範囲をあえて狭める操作だと考えたほうが正確です。レポートを書くときは、除外した人数と、残った集団の定義を必ず添えておきましょう。
実データでは何を見るか
実データでは、PEHE も本当の平均効果も手に入りません。使えるのは次の確認作業です。
| 確認方法 | 分かること | 注意点 |
|---|---|---|
| 傾向スコアのヒストグラム | 重なりがあるかどうか | 0 や 1 に近い層は除外するか、効果を語らないようにします |
| 傾向スコアで重みを付けたあとの属性バランス | 支援あり・なしの2グループが、比べてよいくらい似た顔ぶれになったか | 年齢や血圧などを1列ずつ比べます。差をばらつきで割った標準化平均差(SMD)が 0.1 未満、というのがよく使われる目安です |
| 検証用データでの GATE | 上位と下位で効果に差があるか | 学習と評価のデータは必ず分けます |
| キャリブレーション | 平均と、効果の大小の向き | 今回の a と b は簡易的な再現です |
| Qini など | 効果が高い順に配るルールの質 | 学習データで図を描いて成果を主張しないようにします |
| 変数重要度と1変数ごとのグラフ | 説明のための材料 | 重要度は因果関係の証明にはなりません |
| 偽の支援・偽の結果での検証 | 見えていない交絡が残っていないか | 効果があるはずのない組み合わせ(支援と無関係な結果や、支援を始める前の期間)で推定してみて、効果が出てしまわないかを確認します |
| 分野の知識との照合 | 話として筋が通っているか | 最終的な判断は、その分野の専門家に委ねます |
逆に、よくある失敗は次の4つです。
- 予測精度だけを見て「良いモデルができた」と判断してしまう
- 学習に使ったデータで Qini を描き、成果として報告してしまう
- 傾向スコアを確認せずに、個人ごとの効果を語ってしまう
- 支援後の検査値を特徴量に入れて、「よく当たる」と評価してしまう
今回の失敗例は、まさに最後のパターンを再現したものです。
ここで、評価という作業そのものの捉え方について、今回感じたことを書いておきます。
予測モデルであれば、評価は分かりやすい作業でした。取り分けておいたテストデータで誤差を測れば点数が出ますので、その点数を見て良し悪しを判定できます。ところが因果推論では、本当の効果が手に入りません。つまり答え合わせができない、ということになります。
では何をしているのかというと、出てきた数字のうち、どれを意思決定に使ってよいのかを決める作業なのだと思います。Causal Forest は、頼めば全員分の効果を数字で返してくれます。比較相手がいない人についても、信頼区間の的中率が 6 割しかないモデルでも、数字そのものは出てきてしまいます。ですから確認項目をひととおり回して、たとえば次のように使ってよい範囲を確定させていきます。
- 傾向スコアが両端に寄っている層の数字は、出ていても採用しない
- 一人ひとりの数字をそのまま使うのではなく、5等分した層の平均として使う
- trim で 31 人を除外したのなら、結論は残った 969 人についてのものだと明記する
「このモデルは正しい」と証明するための作業ではなく、「ここまでは言えます、ここから先は言えません」という境目をはっきりさせるための作業、ということですね。地味な話ではあるのですが、この線引きを飛ばしたまま「この方には効きます」と配ってしまうのがいちばん怖いので、実装する側の仕事としてはここが本体だと踏んでいます。
お手元のデータで試される場合は、次の順序で進めるのがおすすめです。
- 支援、結果、支援前の属性を切り分けます
- 傾向スコアのヒストグラムで重なりを確認します
CausalForestDMLをhonest=Trueのまま実行します- 取り分けておいた検証データで GATE を描きます
- 支援後の値が特徴量に混ざっていないか、もう一度確認します
PEHE は諦めることになりますが、この5つを回すだけでも十分に始められます。
まとめ
Causal Forest を、架空の住民 1,000 人分の減塩支援データで、SageMaker Training まで通してみました。
- Causal Forest は結果を当てる木ではありません。 支援した場合としなかった場合の差を推定する木です。支援フラグを入れた XGBoost が答えてくれるのは「支援を受けた人たちのほうが改善量が大きかったか」までで、「この人に効くのか」ではありません
- ランダム割付であれば、全体平均はほぼ正解でした(ずれ −0.004)。 観察データでは平均が +0.05 ずれましたが、誰に効きそうかという順位は拾えています
- クレンジングの本体は、欠損補完よりも時点・重複・重なりの管理です。 支援後の値を入れると PEHE は約2倍に悪化し、信頼区間の的中率は 6% まで落ちました
- trim は対象集団を変えます。 PEHE が改善しても、平均のずれは悪化することがあります。除外した人数は必ず記録しておきましょう
- 1,000 人規模であれば GPU は不要です。
ml.m5.xlargeで課金 89 秒、学習部分は数秒でした。ジョブ時間の大半はコンテナの準備とライブラリの導入です - 個人ごとの信頼区間は過信できません。 ランダム割付でも的中率は 60% 程度でした。層別まで落として使いましょう
- T-Learner のほうが良い場合もあります。 Causal Forest が常に勝つ、という書き方はできません
最後に、手元で試すときのチェックリストを置いておきます。
- 支援・結果・支援前の属性について、時点の一覧表を先に作る
- 支援後に測った値を特徴量に入れない
- 傾向スコアのヒストグラムを確認し、両端の層では効果を語らない
- GATE や Qini は、取り分けておいた検証データで描く
- 予測精度だけで評価しない
- 1,000 行規模なら CPU で十分。SageMaker に載せるなら scikit-learn 用コンテナに
econmlを追加する
同じ合成データで T-Learner や X-Learner も並べてみると、それぞれの手法の癖が見えてきて面白いと思います。どうぞ参考にしてみてください。
参考資料
- Wager & Athey (2018) Estimation and Inference of Heterogeneous Treatment Effects using Random Forests. JASA. https://doi.org/10.1080/01621459.2017.1319839
- Athey, Tibshirani, Wager (2019) Generalized Random Forests. Ann. Statist. https://arxiv.org/pdf/1610.01271.pdf
- Künzel et al. (2019) Metalearners for estimating heterogeneous treatment effects. PNAS.
- Yadlowsky et al. (2025) Evaluating and optimizing treatment assignment with RATE. JASA.
- grf diagnostics
- EconML CausalForestDML
- Rehill, How do applied researchers use the Causal Forest? https://arxiv.org/html/2404.13356v2
- DALAB 因果フォレスト解説
- 日本高血圧学会 尿中 Na/K 比コンセンサス (2024) https://doi.org/10.1038/s41440-024-01861-x
- causalml で T-Learner
- X-Learner まとめ
- TMLE で CATE
- Uplift RF







