第3回で「誰に効くか」を見るとき、X には購入頻度と休眠日数を入れました。なぜその2つだったのか。正直に言えば、「効きそうだから」という勘です。
実務では、その勘が外れます。今回は勘に頼らず、施策より前に決まっている変数を全部入れて、どれが効いているかを森に選ばせます。
結論を先に3行で書きます。
CausalForestDMLに5変数を渡すと、feature_importances_が 購入頻度と休眠日数で全体の87% を占めると返してきました。第3回で人が選んだ2つと一致します。- 森(
CausalForestDML)は効果の形を仮定しません。直線(LinearDML)の答えと重なれば「直線で十分だった」と分かり、ずれれば直線では捉えられない形があると分かります。 - ただし森(
CausalForestDML)は万能ではなく、信頼区間が広く、データの端では平均のほうへ縮みます。形が単純と分かったら、直線(LinearDML)に戻すのが実務の順番です。
この記事を読み終えると、次のことができるようになります。
CausalForestDMLで、X を絞らずに効果を推定できるfeature_importances_で「効果の差を生んでいる変数」を見つけられる- 通常のランダムフォレストと因果フォレストの違いを、人に説明できる
- 森(
CausalForestDML)と直線(LinearDML)を、どういう順番で使い分けるかが分かる
データは前回までと同じ ec_coupon.csv です。以下からダウンロードできます。
まず、動かしてみる:X を絞らずに全部入れる
第4回のフローチャートで「入れる」側に残った5列を、X と W に仕分けせず、まとめて X に渡します。
以下、コードです。
import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.ensemble import GradientBoostingClassifier
from econml.dml import CausalForestDML
df = pd.read_csv("ec_coupon.csv")
Y = df["spend_30d"]
T = df["coupon"]
# 施策より前に決まっている5列を、仕分けせずに全部 X に入れる
X = pd.DataFrame({
"past_freq":
df["past_freq"],
"days_since_last":
df["days_since_last"],
"member_rank":
df["member_rank"].map({"ブロンズ": 0, "シルバー": 1, "ゴールド": 2}),
"channel":
df["channel"].map({"自然検索": 0, "広告": 1, "メルマガ": 2}),
"tenure_months":
df["tenure_months"],
})
est = CausalForestDML(
model_y=GradientBoostingRegressor(random_state=0),
model_t=GradientBoostingClassifier(random_state=0),
discrete_treatment=True, n_estimators=1000, min_samples_leaf=100,
cv=5, random_state=0
)
est.fit(Y, T, X=X)
lo, hi = est.ate_interval(X, alpha=0.05)
print(f"推定ATE: {est.ate(X):.0f}円 95%区間 [{lo:.0f}, {hi:.0f}]")
以下、実行結果です。
推定ATE: 381円 95%区間 [217, 545]
正解413円を区間に含んでいます。ただし区間の幅は328円。第2回の LinearDML は幅143円でしたから、2倍以上広い。理由は後半で扱います。
読みどころは2つ。n_estimators=1000 は木の本数。min_samples_leaf=100 は「葉(枝の末端)に最低100人は残す」指定で、小さくすると細かい違いを拾える代わりに区間が広がります。
効果の差を生んでいる変数はどれか
森(CausalForestDML)が学習の中で「どの変数で枝を分けると効果の差が大きくなったか」を数えたものが feature_importances_ です。
以下、コードです。
imp = pd.Series(
est.feature_importances_,
index=X.columns
).sort_values(ascending=False)
print(imp.round(3))
以下、実行結果です。
past_freq 0.484 days_since_last 0.382 tenure_months 0.086 member_rank 0.029 channel 0.019 dtype: float64
購入頻度が0.484、休眠日数が0.382。この2つで全体の87%です。在籍月数がわずかに続き、会員ランクと流入経路はほぼゼロ。
第3回で「効きそう」と勘で選んだ2変数を、森はデータだけから選び直しました。逆に言えば、会員ランクは交絡変数としては重要でも(第4回)、「ランクによって効き方が変わる」わけではない、ということも分かります。
この図が本当に役に立つのは、勘が外れたときです。ノーマークだった列が上位に来たら、そこから施策の設計が変わります。
裏で何が起きているのか:同じ「木」でも分け方が違う
「ランダムフォレストなら知っている」という方ほど混乱するところです。名前は似ていますが、枝を分ける基準が違います。
通常のランダムフォレストは、購入金額 Y が近い人どうしを同じ枝にまとめます。
「たくさん買う人」と「あまり買わない人」を分けていく。クーポンの有無は、数ある特徴量の1つにすぎません。
因果フォレストは、クーポンの効果が近い人どうしを同じ枝にまとめます。
枝の中で「配布あり」と「配布なし」の差を計算し、その差が左右で最も大きく違うところで分ける。だから葉に残るのは「効き方が似た人たち」で、葉の中の配布あり/なしの差が、そのままその人たちの効果になります。
たとえるなら、通常の森は「テストの点が近い子を集める」、因果の森は「同じ薬の効き方が近い子を集める」。集め方が違うので、できあがる枝もまったく違います。
因果フォレストは、データを2つに分け、片方で枝の分け方を決め、もう片方で葉の中の効果を計算します。
同じデータで両方やると「効果が大きく見える分け方」を探した上でそれを測ることになり、過大に出る。第2回の交差適合と同じ発想で、CausalForestDML では既定(honest=True)です。
森(CausalForestDML)の答えと直線(LinearDML)の答えを、正解と並べる
森は効果の形を仮定しません。第2回・第3回の LinearDML は「効果は X に対して直線的に変わる」と仮定していました。この2つを、正解と一緒に並べてみます。
以下、コードです。
df["cate_forest"] = est.effect(X)
freq = pd.cut(df["past_freq"], [0, 2, 4, 6, 9, 100],
labels=["1-2回", "3-4回", "5-6回", "7-9回", "10回以上"])
print(df.groupby(freq)["cate_forest"].mean().round(0))
以下、実行結果です。
past_freq 1-2回 508.0 3-4回 425.0 5-6回 355.0 7-9回 124.0 10回以上 78.0 Name: cate_forest, dtype: float64
真ん中の3〜9回では、森・直線・正解の3本がほぼ重なります。ここで重なるということは、「効果が購入頻度に対して直線的に変わる」という第3回の仮定が、このデータでは正しかったということです。森は仮定なしでその形にたどり着いたので、直線の答えを裏付ける証拠になります。
いっぽう両端では、森だけがずれています。1-2回では正解676円に対して森は508円。10回以上では正解−258円に対して森は+78円で、マイナスの効果を捉えきれていません。
森は「近くの人の平均」で効果を出すので、データの端では近くの人がすべて内側にいて、平均が内側へ引き寄せられます。直線は端まで伸ばせますが、森は伸ばせません。
では、森(CausalForestDML)と直線(LinearDML)、どちらを使うか?
| CausalForestDML(森) | LinearDML(直線) | |
|---|---|---|
| 効果の形 | 仮定しない | X に対して直線と仮定 |
| X の選び方 | 全部入れて、森に選ばせる | 人が選ぶ |
| 信頼区間の幅(このデータ) | 広い(ATE で328円) | 狭い(143円) |
| データの端 | 平均のほうへ縮む | 端まで伸ばせる(仮定が正しければ) |
| 向いている場面 | 効果の形も、効く変数も分からない最初 | 形が単純と分かったあと |
実務の順番はこうなります。最初に森(CausalForestDML)で当たりをつけ、効く変数と効果の形を見る。形が単純なら直線(LinearDML)に戻して区間を狭める。直線(LinearDML)で始めて仮定のずれに気づかないより、森(CausalForestDML)で始めて確かめるほうが安全です。
ありがちな失敗:重要度を「効果の大きさ」と読む
feature_importances_ は、予測モデルの特徴量重要度と同じ顔をしているので、同じように読みたくなります。ここに罠があります。
因果フォレストの重要度は、「その変数で分けると、効果の“違い”がどれだけ説明できるか」です。効果の大きさではありません。
会員ランクの重要度は0.029でした。これは「ランクによって効き方が変わらない」という意味であって、「ランクは効果に無関係」ではありません。第4回で見たとおり、ランクは交絡変数として必須です。重要度が低いからと W からも外すと、第4回の①(932円)に戻ります。
もう1つ。施策後の変数を入れてもエラーにはなりません。サイト訪問回数を X に足すと推定ATEは103円まで落ちます。森(CausalForestDML)であっても、変数の仕分けは第4回のルールのままです。
まとめ
今回のポイントです。
CausalForestDMLは X を絞らずに全部渡せる。feature_importances_が効果の差を生む変数を教えてくれる- このデータでは購入頻度と休眠日数で87%。第3回の勘と一致した
- 通常の森(
CausalForestDML)は「Y が近い人」を、因果の森は「効果が近い人」を集める。honest splittingで区間が信用できる - 森(
CausalForestDML)は形を仮定しないが、区間が広く、端では平均に縮む。森(CausalForestDML)で当たりをつけ、形が単純なら直線(LinearDML)に戻す - 重要度は「効果の違いを説明する力」。低くても交絡なら W に入れる

