EconMLで実践する ビジネス因果推論
(第4回)
その変数、入れていいのか
(XとWの役割分担で答えが変わる)

EconMLで実践する ビジネス因果推論(第4回)その変数、入れていいのか(XとWの役割分担で答えが変わる)

ここまで3回、購入頻度と休眠日数を X に、会員ランクや流入経路を W に入れてきました。「なぜその振り分けなのか」は後回しにしてきましたが、今回がその答え合わせです。

(第1回)「クーポンを使った人ほど買っている」は本当に効果か
 https://www.salesanalytics.co.jp/datascience/datascience349/

(第2回)コード7行で「本当の効果」に迫る(LinearDMLでATEを推定)
 https://www.salesanalytics.co.jp/datascience/datascience351/

(第3回)平均の裏に隠れた「誰に効くか」を取り出す(ATEからCATEへ)
 https://www.salesanalytics.co.jp/datascience/datascience353/

機械学習では「使える列はとりあえず全部入れる」がだいたい正解です。因果推論では、それが最大の事故のもとになります。

結論を先に3行で書きます。

  1. 同じデータ・同じモデルでも、変数の指定を変えるだけで推定値は 139円から932円まで動きます。正解は413円です。
  2. 壊すのは2つ。交絡を入れ忘れること(過大に出る)と、施策の「後」に動く変数を入れること(効果が消える)です。
  3. XW の入れ替えは、平均の効果をほとんど壊しません。変わるのは「誰に効くか」の切り口です。

この記事を読み終えると、次のことができるようになります。

  • 手元の列を「入れる・入れない・XかWか」の3段階で仕分けできる
  • 施策の後に記録された変数を入れると、なぜ効果が消えるのかを説明できる
  • 矢印の向きで、交絡変数と「入れてはいけない変数」を見分けられる
  • 「とりあえず全部入れる」がなぜ危険かを、数字で示せる

準備:施策の「後」に動く変数を1列足す

今回は、これまでのデータに変数site_visits_30d30日間のサイト訪問回数)が追加されたデータセットを使います。

以下からダウンロードできます。

ec_coupon.csv

クーポンを受け取った人はサイトを見に来る回数が増え、たくさん買った人ほどサイトに来ています。つまり、施策の後に、施策と成果の両方の影響を受けて動く変数です。

以下、コードです。

import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.ensemble import GradientBoostingClassifier
from econml.dml import LinearDML

df = pd.read_csv("ec_coupon.csv")
print(df.groupby("coupon")["site_visits_30d"].mean().round(1))

 

以下、実行結果です。

coupon
0    3.4
1    7.0
Name: site_visits_30d, dtype: float64

 

クーポンを配った人は、平均7.0回。配らなかった人は3.4回。いかにも「効果に関係ありそう」な列です。予測モデルなら真っ先に入れたくなります。

 

次に、推定を1行で呼べる関数を用意しておきます。中身は前回までと同じ LinearDML です。

以下、コードです。

Y = df["spend_30d"]
T = df["coupon"]
X = df[["past_freq", "days_since_last"]]
W = pd.get_dummies(df[["member_rank", "channel"]], drop_first=True)
W["tenure_months"] = df["tenure_months"]

def estimate(label, **kwargs):
    est = LinearDML(model_y=GradientBoostingRegressor(random_state=0),
                    model_t=GradientBoostingClassifier(random_state=0),
                    discrete_treatment=True, cv=5, random_state=0)
    est.fit(Y, T, **kwargs)
    Xa = kwargs.get("X")
    lo, hi = est.ate_interval(Xa, alpha=0.05)
    print(f"{label}: {est.ate(Xa):.0f}円  95%区間 [{lo:.0f}, {hi:.0f}]")

 

3通りの指定で、推定値を並べる

同じデータ、同じモデルで、変数の指定だけを変えます。

以下、コードです。

estimate("① 交絡を入れない\t\t", X=X)

W_bad = W.copy()
W_bad["site_visits_30d"] = df["site_visits_30d"]
estimate("② 施策の後の変数を入れる\t", X=X, W=W_bad)

estimate("③ 正しい指定\t\t", X=X, W=W)

 

以下、実行結果です。

① 交絡を入れない		: 932円  95%区間 [846, 1019]
② 施策の後の変数を入れる	: 139円  95%区間 [62, 215]
③ 正しい指定		: 422円  95%区間 [350, 493]

 

正解は413円です。①は2倍以上、②は3分の1。どちらも信頼区間が正解を含んでいません。しかも区間の幅は③と変わらないので、数字だけ見ても間違いに気づけません。

変数の指定ごとの推定ATEと信頼区間交絡を入れないと932円、施策の後の変数を入れると139円、正しい指定で422円。全部Xに入れると395円、XとWを入れ替えると385円で、いずれも正解413円を区間に含む。同じデータ、同じモデル。変数の指定だけで推定値はこう動く推定ATEと95%信頼区間。縦の線が仕込んでおいた正解(413円)0円250円500円750円1,000円正解 413円① 交絡を入れないW を渡さない932円② 施策の後の変数を入れる訪問回数を W に追加139円③ 正しい指定X=頻度・休眠、W=ランク・経路・在籍422円参考:迷いやすい指定④ 全部 X に入れるW を使わず X に全部395円⑤ X と W を入れ替えるX=ランク・経路・在籍、W=頻度・休眠385円壊すのは「入れ忘れ」と「入れてはいけない変数」。X と W の入れ替えは、平均には効かない

①は第2回で見た「W の渡し忘れ」と同じです。会員ランクという交絡を引き算していないので、「ゴールド会員はもともと買う」分が効果に上乗せされます。

今回の主役は②です。訪問回数という、施策の後に動く変数を W に足しただけで、効果が3分の1に縮みました。何が起きたのか。

 

なぜ「後の変数」を入れると効果が消えるのか

W に入れるということは、「その変数の値が同じ人同士で比べる」ということです。会員ランクを W に入れるのは「同じランクの人同士で比べたい」からでした。

では、訪問回数を W に入れると、どうなるか。「訪問回数が同じ人同士で比べる」ことになります。

  • クーポンを配られた人は訪問が増えます(平均7.0回)
  • 配られなかった人は少ない(3.4回)

訪問回数を揃えるということは、「クーポンをもらったのに訪問が少なかった人」と「クーポンなしなのに訪問が多かった人」を比べることになります。前者はクーポンが響かなかった人、後者はもともと熱心な人。この比較では、クーポンの効果は打ち消されて当然です。

矢印で描くと、はっきりします。

変数の4つの役割と矢印の向き施策T・成果Y・交絡変数W・効果修飾変数Xに加え、施策の後に動く変数(サイト訪問回数)を図示。矢印が出ていく変数はWに入れ、矢印が入ってくる変数は入れない。変数には4つの役割がある:矢印の向きで見分ける会員ランク・流入経路交絡変数 Wクーポン配布施策 T30日購入金額成果 Y購入頻度・休眠日数効果修飾変数 X30日サイト訪問回数施策の後に動く変数知りたい効果効き方を変える配られると増える買った人ほど多い矢印が「出ていく」変数 → W に入れる施策の前に決まり、施策と成果の両方に影響する矢印が「入ってくる」変数 → 入れない施策や成果の結果として、後から動く

会員ランクは、クーポン配布にも購入金額にも矢印が出ていきます。施策より前に決まっていて、両方の原因になっている。これが交絡変数で、W に入れなければなりません。

訪問回数は逆です。クーポン配布からも購入金額からも矢印が入ってきます。施策や成果の「結果」として後から動く変数です。これを入れると、効果の通り道をふさいだり、ないはずの関係を作り出したりします。

矢印が出ていく変数は W に入れる。矢印が入ってくる変数は入れない。因果ダイアグラム(DAG)の細かい理論を知らなくても、この一行で実務の大半は判断できます。

 

X と W を間違えたら、どうなるか

ここで多くの方が気にする点です。「効果を左右する変数は X、交絡は W」と言われても、実際には両方を兼ねる変数がほとんどです。会員ランクは交絡ですが、ランクによって効き方も違うかもしれない。

試してみます。

以下、コードです。

estimate("④ 全部 X に入れる\t\t", X=pd.concat([X, W], axis=1))
estimate("⑤ X と W を入れ替える\t", X=W, W=X)

 

以下、実行結果です。

④ 全部 X に入れる		: 395円  95%区間 [319, 472]
⑤ X と W を入れ替える		: 385円  95%区間 [308, 461]

 

どちらも正解413円を区間に含んでいます。X と W を間違えても、平均の効果は壊れません。

理由は単純で、LinearDML は X も W も両方とも「交絡を引き算する」段階で使うからです。X は W の役割を兼ねています。違いは、X に入れた変数だけが「誰に効くか」(第3回の CATE)の切り口になる、という一点です。

だから実務の順番はこうなります。

 優先度  やること 間違えると
1
施策の後に動く変数を外す 効果が消える(②)
2
交絡になりうる変数を入れる 効果が過大に出る(①)
3
「誰に効くか」を見たい変数を X に、残りを W 平均は変わらない。CATEの切り口が変わる

1と2を間違えると数字そのものが嘘になります。3は、嘘にはなりません。迷ったら W に入れておけば、平均の効果は守られます。

 

手元の列を仕分ける

ここまでを、列を1つずつ見ていくときの手順に落とします。

変数を入れる・入れない・XかWかを決めるフローチャート施策より前に決まっているか、施策と成果の両方に影響するか、効果の違いをその変数で見たいかの3つの問いを順にかけて、入れない・W・X・入れなくてよい、に仕分ける。この変数、入れる?入れない?X?W?候補の列を1つずつ、上から順に3つの問いにかける施策より前に決まっている変数か?はいいいえ入れない施策の後に動く変数。効果を消してしまう施策と成果の両方に影響するか?いいえはいW に入れる(交絡変数)入れ忘れると過大に。違いも見たいなら X 可効果の違いを、その変数で見たいか?いいえはいX に入れる(効果修飾変数)「誰に効くか」の切り口になる入れなくてよい迷ったら W に入れても、平均は壊れない順番が大事。1つ目の問いで「いいえ」なら、そこで終わり

最初の問いが最も重要です。「この列の値は、施策を打つに決まっていたか」。

会員ランク、流入経路、過去の購入頻度は、クーポンを配る前に決まっています。サイト訪問回数、クーポンの開封、購入後のレビュー投稿は、配った後に動きます。

実務データでは、この区別が曖昧なまま列が並んでいます。

「訪問回数」という列名だけでは、施策前の30日なのか施策後の30日なのか分かりません。列の定義を確かめる作業が、因果推論では分析の半分を占めます。

 

ありがちな失敗:施策の後に記録された変数を、うっかり入れる

今回いちばん避けてほしい失敗は、②そのものです。

怖いのは、この失敗がエラーにならず、精度が上がったように見えることです。

訪問回数は購入金額をよく予測します。予測モデルなら「重要な特徴量が見つかった」と喜ぶ場面です。因果推論では、その瞬間に効果が消えます。

よく見かける「後の変数」を挙げておきます。

  • メールの開封・クリック(施策の結果)
  • 施策期間中のサイト訪問・アプリ起動回数
  • 営業訪問の後に出した提案書の枚数
  • キャンペーン後のアンケート回答
  • 購入後に付いたレビューやポイント残高

どれも「効果に関係ありそう」に見えます。だから入れたくなる。関係があるからこそ、入れてはいけないのです。

 

まとめ

今回のポイントです。

  • 変数の指定だけで、推定値は139円から932円まで動く。正解は413円
  • 壊すのは「交絡の入れ忘れ」(過大)と「施策の後の変数を入れる」(消える)の2つ
  • 矢印が出ていく変数は W に、矢印が入ってくる変数は入れない
  • X と W の入れ替えは平均を壊さない。X は「誰に効くか」の切り口を決めるだけ
  • 列を仕分ける最初の問いは「施策の前に決まっていたか」
Screenshot

【月1 特定テーマ講座(10月)】
Python で学ぶ 明日からできる「欠損値処理」超入門

【開催日時】 全2回(土)2026/10/17,10/31(13:30〜18:00)
【受講形式】 当日Zoom( or 復習用に後日動画視聴)
【参加費用】 2万2千円(税込み)/人