ここまで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行で書きます。
- 同じデータ・同じモデルでも、変数の指定を変えるだけで推定値は 139円から932円まで動きます。正解は413円です。
- 壊すのは2つ。交絡を入れ忘れること(過大に出る)と、施策の「後」に動く変数を入れること(効果が消える)です。
XとWの入れ替えは、平均の効果をほとんど壊しません。変わるのは「誰に効くか」の切り口です。
この記事を読み終えると、次のことができるようになります。
- 手元の列を「入れる・入れない・XかWか」の3段階で仕分けできる
- 施策の後に記録された変数を入れると、なぜ効果が消えるのかを説明できる
- 矢印の向きで、交絡変数と「入れてはいけない変数」を見分けられる
- 「とりあえず全部入れる」がなぜ危険かを、数字で示せる
準備:施策の「後」に動く変数を1列足す
今回は、これまでのデータに変数site_visits_30d(30日間のサイト訪問回数)が追加されたデータセットを使います。
以下からダウンロードできます。
クーポンを受け取った人はサイトを見に来る回数が増え、たくさん買った人ほどサイトに来ています。つまり、施策の後に、施策と成果の両方の影響を受けて動く変数です。
以下、コードです。
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。どちらも信頼区間が正解を含んでいません。しかも区間の幅は③と変わらないので、数字だけ見ても間違いに気づけません。
①は第2回で見た「W の渡し忘れ」と同じです。会員ランクという交絡を引き算していないので、「ゴールド会員はもともと買う」分が効果に上乗せされます。
今回の主役は②です。訪問回数という、施策の後に動く変数を W に足しただけで、効果が3分の1に縮みました。何が起きたのか。
なぜ「後の変数」を入れると効果が消えるのか
W に入れるということは、「その変数の値が同じ人同士で比べる」ということです。会員ランクを W に入れるのは「同じランクの人同士で比べたい」からでした。
では、訪問回数を W に入れると、どうなるか。「訪問回数が同じ人同士で比べる」ことになります。
- クーポンを配られた人は訪問が増えます(平均7.0回)
- 配られなかった人は少ない(3.4回)
訪問回数を揃えるということは、「クーポンをもらったのに訪問が少なかった人」と「クーポンなしなのに訪問が多かった人」を比べることになります。前者はクーポンが響かなかった人、後者はもともと熱心な人。この比較では、クーポンの効果は打ち消されて当然です。
矢印で描くと、はっきりします。
会員ランクは、クーポン配布にも購入金額にも矢印が出ていきます。施策より前に決まっていて、両方の原因になっている。これが交絡変数で、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)の切り口になる、という一点です。
だから実務の順番はこうなります。
| 優先度 | やること | 間違えると |
|---|---|---|
| 施策の後に動く変数を外す | 効果が消える(②) | |
| 交絡になりうる変数を入れる | 効果が過大に出る(①) | |
| 「誰に効くか」を見たい変数を X に、残りを W に | 平均は変わらない。CATEの切り口が変わる |
1と2を間違えると数字そのものが嘘になります。3は、嘘にはなりません。迷ったら W に入れておけば、平均の効果は守られます。
手元の列を仕分ける
ここまでを、列を1つずつ見ていくときの手順に落とします。
最初の問いが最も重要です。「この列の値は、施策を打つ前に決まっていたか」。
会員ランク、流入経路、過去の購入頻度は、クーポンを配る前に決まっています。サイト訪問回数、クーポンの開封、購入後のレビュー投稿は、配った後に動きます。
実務データでは、この区別が曖昧なまま列が並んでいます。
「訪問回数」という列名だけでは、施策前の30日なのか施策後の30日なのか分かりません。列の定義を確かめる作業が、因果推論では分析の半分を占めます。
ありがちな失敗:施策の後に記録された変数を、うっかり入れる
今回いちばん避けてほしい失敗は、②そのものです。
怖いのは、この失敗がエラーにならず、精度が上がったように見えることです。
訪問回数は購入金額をよく予測します。予測モデルなら「重要な特徴量が見つかった」と喜ぶ場面です。因果推論では、その瞬間に効果が消えます。
よく見かける「後の変数」を挙げておきます。
- メールの開封・クリック(施策の結果)
- 施策期間中のサイト訪問・アプリ起動回数
- 営業訪問の後に出した提案書の枚数
- キャンペーン後のアンケート回答
- 購入後に付いたレビューやポイント残高
どれも「効果に関係ありそう」に見えます。だから入れたくなる。関係があるからこそ、入れてはいけないのです。
まとめ
今回のポイントです。
- 変数の指定だけで、推定値は139円から932円まで動く。正解は413円
- 壊すのは「交絡の入れ忘れ」(過大)と「施策の後の変数を入れる」(消える)の2つ
- 矢印が出ていく変数は W に、矢印が入ってくる変数は入れない
- X と W の入れ替えは平均を壊さない。X は「誰に効くか」の切り口を決めるだけ
- 列を仕分ける最初の問いは「施策の前に決まっていたか」

