EconMLで実践する ビジネス因果推論
(第2回)
コード7行で「本当の効果」に迫る
(LinearDMLでATEを推定)

EconMLで実践する ビジネス因果推論(第2回)コード7行で「本当の効果」に迫る(LinearDMLでATEを推定)

前回は、クーポン施策の「本当の効果」が 413円 だったことを、答えを仕込んだサンプルデータで確かめました。単純比較の2,268円も、会員ランクで層別した594円も、正解には届きませんでした。

では、正解を知らない実務のデータで、どうやってその413円に迫るのか。今回いよいよEconMLを動かします。

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

  1. LinearDML を使うと、実質7行のコードで 422円(95%信頼区間 350〜493円) という推定値が出ます。正解の413円を区間の中に捉えています。
  2. 仕組みは単純です。YからもTからも「交絡で説明できる分」を先に引き、残った“素のゆらぎ”同士を比べるだけです。
  3. ただし引き算のやり方に一手間あります。交差適合(cross-fitting)を省くと、推定値は決まった向きに偏ります。

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

  • LinearDML で平均的な効果(ATE)を推定し、信頼区間まで報告できる
  • 推定値を「1件あたり何円の粗利か」という意思決定の言葉に翻訳できる
  • 二重機械学習が裏で何をしているかを、人に説明できる
  • 交差適合を省いてはいけない理由を、数字で示せる

データは前回と同じ ec_coupon.csv(5,000人分)を使います。以下からダウンロードできます。

ec_coupon.csv

列名 意味 役割
member_rank 会員ランク(ブロンズ/シルバー/ゴールド) 交絡しそうな変数
channel 流入経路(自然検索/広告/メルマガ) 交絡しそうな変数
tenure_months 登録からの経過月数 交絡しそうな変数
past_freq 過去1年の購入回数 効果を左右しそうな変数
days_since_last 最終購入からの休眠日数 効果を左右しそうな変数
coupon クーポンを配ったか(0 / 1) 施策
spend_30d その後30日間の購入金額 成果
_true_cate その人に対する本当の効果 答え合わせ用

まず、動かしてみる

説明は後回しにして、先に結果を出します。

以下、コードです。

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")

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"]           # 交絡しそうな変数

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, X=X, W=W)

print(f"推定ATE: {est.ate(X):.0f}円")
print(
    "95%信頼区間:", 
    [round(v) for v in est.ate_interval(X, alpha=0.05)]
)

 

以下、実行結果です。

推定ATE: 422円
95%信頼区間: [350, 493]

 

422円。前回明かした正解は413円でしたから、9円のずれで捉えられました。信頼区間 350〜493円も、正解をしっかり含んでいます。

単純比較の2,268円、層別の594円と比べてください。同じデータから、桁違いに正しい数字が出ています。

コードの読みどころは4つだけです。

書き方 意味
model_y Yを予測するモデル。ここでは購入金額を予測する
model_t Tを予測するモデル。「この人にクーポンが配られやすかったか」を予測する
discrete_treatment=True 施策が0/1であることを伝える。忘れやすい
cv=5 交差適合の分割数。この記事の後半のテーマ

XW の使い分けは、この連載で最も重要なところです。第4回で1本まるごと使って扱うので、今回は「効果を左右しそうな変数は X、交絡しそうな変数は W」とだけ覚えてください。

 

この数字を、報告書の言葉に直す

422円のままでは、まだ意思決定に使えません。クーポン1枚の原価が100円だとしましょう。

以下、コードです。

cost = 100
lo, hi = est.ate_interval(X, alpha=0.05)
print(f"1枚あたりの粗利: {est.ate(X) - cost:.0f}円")
print(
    f"10万枚配ったときの利益: {(est.ate(X) - cost) * 100000 / 10000:,.0f}万円 "
    f"(悪めに見て {(lo - cost) * 100000 / 10000:,.0f}万円)")

 

以下、実行結果です。

1枚あたりの粗利: 322円
10万枚配ったときの利益: 3,216万円 (悪めに見て 2,500万円)

 

ここまで来ると、会議で使える形になります。

クーポン1枚あたり322円の粗利。10万枚配れば3,200万円ほど、悪めに見積もっても2,500万円は残る。

信頼区間の下限を使った「悪めに見て」の数字を必ず添えてください。点推定だけを報告するのは、天気予報で降水確率を言わずに「晴れです」と言い切るようなものです。

 

裏で何が起きているのか

ここからが今回の本題です。LinearDML の DML は Double Machine Learning、日本語で二重機械学習といいます。「二重」は、機械学習モデルを2つ使うことから来ています。

二重機械学習の3ステップ YからWで説明できる分を引き、TからもWで説明できる分を引き、残った残差同士を回帰する。その直線の傾きが交絡を取り除いたあとの効果になる。二重機械学習がやっている3つのこと1Yから、Wで説明できる分を引く会員ランクなどで購入金額を予測し、実際との差だけを残す 2Tからも、Wで説明できる分を引く同じ人にクーポンが配られやすかったかを予測し、その差だけを残す 3残った“素のゆらぎ”同士を回帰するこの直線の傾きが、交絡を取り除いたあとの効果になる身長と体重を比べるとき、両方から「年齢で説明できる分」を先に取り除くのと同じ

やっていることは、拍子抜けするほど単純です。

まず、購入金額 Y のうち「会員ランクや流入経路で説明できる分」を機械学習で予測し、実際の値から引きます。残ったものが Yの残差 です。次に、クーポンが配られたかどうか T についても同じことをします。「この人は配られやすい人だったか」を予測して引くと、Tの残差 が残ります。

そして、この2つの残差だけを回帰します。数式にすると1行です。

Y − E[Y|W] = θ × ( T − E[T|W] ) + 誤差

日本語に直すと「Yの残差 = θ × Tの残差」。この θ が求めたい効果です。

身長と体重の関係を調べるとき、両方から「年齢で説明できる分」を先に取り除いてから比べますよね。あれとまったく同じ発想です。

 

残差を実際に見てみる

言葉だけだと掴みにくいので、実際のデータで描いてみます。

交絡を取り除く前と後の比較 左は平均を比べただけの元のデータで差は2,268円。右はWで説明できる分をYとTの両方から引いた残差の散布図で、その回帰直線の傾きが交絡を取り除いたあとの効果にあたる。交絡を取り除く前と、取り除いたあと元のデータ平均を比べただけ配布あり5,151円配布なし2,883円差 +2,268円交絡が混ざったまま残差にしたあとWで説明できる分を、YからもTからも引いた +2,0000−2,000Yの残差Tの残差 →この傾き=効果の推定値 配布なし配布あり縦にばらついたままでよい。傾きさえ正しく測れれば、それが効果になる

右のグラフで、横軸がTの残差、縦軸がYの残差です。この点群に引いた直線の傾きが、効果の推定値になります。

2つのかたまりに分かれているのはTが0か1しかないためです。左の灰色が配らなかった人、右のオレンジが配った人。ただし横位置はきれいに0と1ではなく、「配られやすさの予測」を引いたあとの位置にばらけています。もともと配られやすかった優良顧客は、配られていても中央寄りに来る。これが交絡を消している正体です。

縦方向のばらつきは大きいままですが、それで構いません。個人の購入金額を当てたいわけではなく、傾きだけが知りたいからです。

この最も単純な形(Xを使わず、全部Wに入れる)で傾きを計算すると357円になります。EconMLはここに X を加えて「属性ごとに効き方が違う」ことも織り込むので、422円まで正解に近づきます。仕組みの骨格はこの散布図がすべてで、X はその精密化です。

 

なぜデータを分けるのか:交差適合

ひとつだけ、素直にやると失敗する箇所があります。

「Wで説明できる分を予測して引く」とき、予測モデルの学習に使ったデータで、そのまま予測してはいけません。自分が学習に使った人の値は、モデルが覚えてしまっているぶん当たりすぎます。すると残差が本来より小さくなり、傾きが歪みます。

そこでデータを5つに分け、4つで学習して残り1つを予測する、を5回繰り返します。これが交差適合(cross-fitting)で、cv=5 がその指定です。

どれくらい違うのか。同じ条件のデータセットを20個つくって、交差適合ありとなしで比べてみました。

交差適合の有無による推定値のズレ 20個のデータセットで推定値が正解から何円ずれたかを比べると、交差適合ありは平均+3円でズレの向きもばらばら、交差適合なしは平均-57円で20回中19回が過小になった。交差適合をやめると、推定値は片側に偏る同じ条件で作った20個のデータセットで、推定値が正解から何円ずれたか -100円 -50円 正解ぴったり +50円交差適合あり 平均 +3円9/20交差適合なし 平均 -57円19/20正解より小さかった回数交差適合なしは20回中19回で過小。ばらつきではなく、決まった向きのズレ

交差適合ありは平均で +3円 のずれ。20回のうち9回が正解より小さく、11回が大きい。つまりずれる向きがばらばらで、これは単なる誤差です。

いっぽう交差適合なしは平均 −57円。しかも20回中19回で正解より小さく出ました。これはばらつきではなく、決まった向きの偏りです。データを増やしても消えません。

cv の既定値は2なので、指定しなくても交差適合自体は効いています。ただ2分割では学習に使えるデータが半分になるため、実務では5前後にしておくのが無難です。

 

ありがちな失敗:Wを渡し忘れる

fit() の引数は多く、W は省略してもエラーになりません。試しに外してみます。

以下、コードです。

est_bad = LinearDML(
    model_y=GradientBoostingRegressor(random_state=0),
    model_t=GradientBoostingClassifier(random_state=0),
    discrete_treatment=True, cv=5, random_state=0,
)
est_bad.fit(Y, T, X=X)          # W を渡していない

print(f"推定ATE: {est_bad.ate(X):.0f}円")
print("95%信頼区間:", [round(v) for v in est_bad.ate_interval(X, alpha=0.05)])

 

以下、実行結果です。

推定ATE: 932円
95%信頼区間: [846, 1019]

 

932円。正解413円の2倍以上です。しかも信頼区間は846〜1019円と狭く、正解を含んでいません。

ここが怖いところです。間違った推定値ほど、狭い区間で自信ありげに出てくることがあります。区間が狭いことは正しさの証明になりません。コードが動いたかどうかではなく、交絡になりうる変数を W に入れ切れたかを毎回確認してください。

 

まとめ

今回のポイントです。

  • LinearDML で推定した結果は422円、95%信頼区間 350〜493円。正解413円を捉えた
  • 仕組みは「YからもTからもWで説明できる分を引き、残差同士を回帰する」だけ
  • 交差適合を省くと、ばらつきではなく決まった向きの偏りが出る(20回中19回で過小)
  • 点推定だけでなく信頼区間を必ず報告する。1枚322円の粗利、悪めに見て2,500万円、という形まで翻訳する
  • W の入れ忘れは、狭い区間の間違った数字を生む

EconMLで実践する ビジネス因果推論(第1回)「クーポンを使った人ほど買っている」は本当に効果か