Mímisbrunnr知恵の泉

← マーケティングサイエンス 一覧

🎓 レベル:発展 | 重要度:A(必須)

📎 関連:マーケティング予算最適化 | 前提:アップリフトモデリングA/Bテストの設計と分析

要点(BLUF)

1. 予測(誰が買うか)と因果(介入の効果)は別物

アップリフトモデリング は、A/B でランダム割付した4セグメントから、施策で態度が変わる量=アップリフト(CATE)を測りました。そこには2つの理想的な条件がありました——ランダム化されている(処置群と対照群が比較可能=交絡なし)ことと、特徴がセグメントという少数のカテゴリだったことです。現実のマーケティングは、しばしばどちらも満たしません。A/B が回せない(過去ログしかない、全顧客に配信済み、倫理・コスト上できない)ことは多く、特徴は年齢・購買履歴・閲覧行動など何十・何百次元にもなります。この観察データ+多特徴で CATE を取り出すのが、**因果機械学習(causal machine learning)**です。

ここでまず潰すべき誤解が、「機械学習でよく当たるモデルを作れば、施策の効果もわかる」という思い込みです。違います。需要予測やコンバージョン予測の ML が学ぶのは、観測された結果の条件つき期待 E[YX]E[Y\mid X]=「誰が買うか」です。一方、施策の価値が知りたいのは「介入したら、買う確率がどれだけ変わるか」=τ(x)\tau(x)。前者は予測(prediction)、後者は**因果(causation)**で、目的関数からして別物です。どれだけ予測精度(当てはまり)を上げても、介入の効果は副産物として出てきません

なぜ素朴な方法が破綻するか。観察データでは、処置を受けた人と受けない人がもともと違うからです。本ノートの設定では、特徴 XX(顧客のエンゲージメント度とでも思ってください)が高い人ほど施策を打たれやすい(傾向 P(T=1X)=0.2+0.6XP(T{=}1\mid X)=0.2+0.6X)。これが交絡です。すると「処置群の平均反応 − 対照群の平均反応」という素朴な差は、施策の効果に加えて「処置群はもともと反応が高い層が多い」という素性の違いを丸ごと拾ってしまいます。広告・販促の効果測定 の前後比較が時間方向でベースラインの違いを増分に混ぜたのと同じ病が、ここでは群間で起きます。

アップリフトモデリング4つの離散カテゴリで考えた効果の異質性は、ここでは連続関数 τ(x)\tau(x) に一般化されます。「説得可能・鉄板・無関心・天邪鬼」という名前のセグメントの代わりに、特徴 xx ごとに効果の大きさが連続的に変わる——その曲線を、交絡を除きながら推定するのが目標です。

flowchart TB
  OBS["観察データ(ランダム化なし)<br/>処置Tは特徴Xに依存=交絡"] --> Q{"何を知りたい?"}
  Q -->|"誰が買うか(予測)"| PRED["反応を当てるML E[Y given X]<br/>当てはまりは良くなる"]
  Q -->|"介入の効果(因果)"| CAUSAL["CATE τ(x)=処置効果<br/>=処置時の反応 − 非処置時の反応"]
  PRED --> WARN["素朴な差も予測モデルも<br/>交絡で効果を誤る(過大/過小)"]
  CAUSAL --> DML["直交化(Double ML)<br/>YとTをXで残差化して回帰"]
  DML --> OK["交絡を除いた因果効果<br/>→ CATEで誰に介入するか(ポリシー)"]

2. CATE・素朴な差のバイアス・直交化(Double ML)の数式

知りたい量は アップリフトモデリング と同じ CATE(条件つき平均処置効果)です。特徴 xx の人を処置したときと、しなかったときの反応の差。

τ(x)  =  E[YT=1,x]処置したときの反応    E[YT=0,x]処置しなかったときの反応\tau(x) \;=\; \underbrace{E[Y\mid T{=}1,\, x]}_{\text{処置したときの反応}} \;-\; \underbrace{E[Y\mid T{=}0,\, x]}_{\text{処置しなかったときの反応}}

観察データでこれが推定できるための前提が、条件無交絡(unconfoundedness)です。「XX を固定すれば、処置はランダムに割り振られたのと同じ」=観測した交絡要因 XX で条件づければ、処置の有無と潜在結果が独立になる、という仮定です。

(Y(0),Y(1))TXτ(x)=E[YT=1,x]E[YT=0,x],δATE=Ex[τ(x)]\big(Y(0),\,Y(1)\big)\perp T \mid X \quad\Longrightarrow\quad \tau(x)=E[Y\mid T{=}1,x]-E[Y\mid T{=}0,x],\qquad \delta_{\text{ATE}}=E_x\big[\tau(x)\big]

この仮定のもとでなら、各 xx の中で処置群と対照群が比較可能になり、CATE が識別され、ATE はその母集団平均になります。A/Bテストの設計と分析ランダム化は、XX で条件づけるまでもなく無条件で無交絡を保証する特別な場合です。観察データはそれが効かないので、「効果に関係する交絡を全部 XX に入れた」と仮定して、XX で調整します。

では、なぜ素朴な差が外れるのか。アウトカムを「ベースライン μ(x)=E[Y(0)x]\mu(x)=E[Y(0)\mid x] + 効果 τ(x)\tau(x) が処置時に乗る」と書くと、素朴な差の極限はこう分解できます。

E[YT=1]E[YT=0]素朴な差=E[τ(X)T=1]処置群での効果+(E[μ(X)T=1]E[μ(X)T=0])交絡バイアス\underbrace{E[Y\mid T{=}1]-E[Y\mid T{=}0]}_{\text{素朴な差}} =\underbrace{E[\tau(X)\mid T{=}1]}_{\text{処置群での効果}} +\underbrace{\big(E[\mu(X)\mid T{=}1]-E[\mu(X)\mid T{=}0]\big)}_{\text{交絡バイアス}}

第2項が交絡バイアスです。処置が XX に依存し(処置群は高 XX が多い)、ベースライン μ(x)\mu(x)XX に依存する(高 XX ほど元から反応が高い)と、両群のベースライン水準が食い違い、その差が効果に化けます。本ノートでは高 XX ほど処置されやすく μ(x)=1+2x\mu(x)=1+2x も高いので、素朴な差は上に膨らみます(§3で +0.792+0.792)。

これを外すのが Double Machine Learning(直交化)です。発想は 広告・販促の効果測定 の回帰と同じ「交絡を XX の関数で説明して引き算する」ですが、YY だけでなく処置 TT XX で説明して引くのが肝です。アウトカムと処置をそれぞれ XX で予測し、残差をとります。

Y~=YE^[YX],T~=TE^[TX]\tilde Y = Y-\hat E[Y\mid X],\qquad \tilde T = T-\hat E[T\mid X]

ここで E^[TX]\hat E[T\mid X] は「特徴 XX の人が処置される確率」=傾向スコア e(x)e(x) の推定です。Y~\tilde Y は「XX から予測できる分を除いたアウトカムの動き」、T~\tilde T は「XX から予測できる分を除いた処置の動き(=偶然そうなった処置のばらつき)」。この残差同士を回帰した係数が、交絡を除いた因果効果です。

θ^=iT~iY~iiT~i2\hat\theta=\frac{\sum_i \tilde T_i\,\tilde Y_i}{\sum_i \tilde T_i^{2}}

これは回帰分析の FWL(Frisch–Waugh–Lovell)定理——「ある変数の係数は、他の説明変数で互いを残差化してから単回帰しても同じ」——の応用です。XX で残差化することで、YYTT の**XX 由来の連動(交絡)が両方から消え**、残った T~\tilde TY~\tilde Y の関係だけが効果を映します。「Double」は、E[YX]E[Y\mid X]E[TX]E[T\mid X] という2つの予測モデル(ここは勾配ブースティング等の任意の ML でよい)を使うこと。残差をとる直交化のおかげで、どちらの予測が多少ずれても効果の推定が大きくは狂わない(Neyman 直交性)という頑健さが生まれます。

⚠️ 異質効果のときの中身τ(x)\tau(x)xx で変わる(異質)とき、θ^\hat\theta が拾うのは厳密には処置のばらつきで重みづけた平均効果 E[Var(TX)τ(X)]/E[Var(TX)]E[\mathrm{Var}(T\mid X)\,\tau(X)]/E[\mathrm{Var}(T\mid X)] です。本ノートの数値設定ではこれがちょうど ATE 0.350.35 に一致します(だから §3 で 0.350.35 を回復します)が、一般には重みづき平均である点に注意。xx ごとの効果が知りたいなら、次節のように CATE を直接モデル化します。直交化・傾向スコア・二重頑健の理論は因果推論テキストの領域です。

3. 素朴な差は交絡で過大、Double MLが真値を回復(コード)

まず素朴な差が交絡でどれだけ外れるかを見ます。N=5000N{=}5000、特徴 XUniform(0,1)X\sim\text{Uniform}(0,1)傾向 P(T=1X)=0.2+0.6XP(T{=}1\mid X)=0.2+0.6X(高 XX ほど処置されやすい=交絡)、アウトカム Y=1.0+2.0X+τ(X)T+N(0,0.5)Y=1.0+2.0X+\tau(X)\,T+N(0,0.5)異質効果 τ(X)=0.2+0.3X\tau(X)=0.2+0.3X。真の ATE は E[τ(X)]=0.2+0.30.5=0.35E[\tau(X)]=0.2+0.3\cdot0.5=0.35。素朴な差(処置群平均 YY − 対照群平均 YY)がこの 0.350.35 をどれだけ過大評価するか確かめます。

import numpy as np

rng = np.random.default_rng(0)
N = 5000
X = rng.uniform(0.0, 1.0, N)            # 顧客特徴(1次元、例:エンゲージメント度)
e = 0.2 + 0.6 * X                        # 傾向 P(T=1|X):高Xほど介入されやすい(交絡)
T = rng.binomial(1, e)                   # 観察データの処置割付(ランダムでない)
tau = 0.2 + 0.3 * X                      # 真のCATE τ(x)=異質な処置効果
Y = 1.0 + 2.0 * X + tau * T + rng.normal(0.0, 0.5, N)   # アウトカム

ate_true = 0.2 + 0.3 * 0.5               # E[τ(X)] = 0.35
naive = Y[T == 1].mean() - Y[T == 0].mean()

print(f"処置された割合 P(T=1) = {T.mean():.3f}")
print(f"処置群の平均X = {X[T==1].mean():.3f} / 対照群の平均X = {X[T==0].mean():.3f}  ← 交絡(高Xほど処置)")
print(f"素朴な差(処置群平均Y − 対照群平均Y) = {naive:+.3f}")
print(f"真のATE  E[τ(X)] = 0.2 + 0.3*0.5      = {ate_true:+.3f}")
print(f"素朴差のバイアス = {naive - ate_true:+.3f}(交絡で過大評価)")

出力:

処置された割合 P(T=1) = 0.502
処置群の平均X = 0.603 / 対照群の平均X = 0.394  ← 交絡(高Xほど処置)
素朴な差(処置群平均Y − 対照群平均Y) = +0.792
真のATE  E[τ(X)] = 0.2 + 0.3*0.5      = +0.350
素朴差のバイアス = +0.442(交絡で過大評価)

出力の意味:処置されたのは全体の 50.2%50.2\% ですが、その内訳が偏っています——処置群の平均 XX0.6030.603、対照群は 0.3940.394。傾向 0.2+0.6X0.2+0.6X のとおり、高 XX の人ほど処置に回り、群が素性から違ってしまいました。これが交絡です。結果、素朴な差は +0.792+0.792で、真の ATE 0.350.35+0.442+0.442 も過大評価します。誤差の出どころは§2の分解どおり——処置群は μ(x)=1+2x\mu(x)=1+2x で測るベースラインがもともと高い(高 XX だから)ので、その差 2×(0.6030.394)0.422\times(0.603-0.394)\approx0.42 が効果に化けています。ここで強調したいのは、E[YX]E[Y\mid X] をどれだけ精密に予測する ML を作っても、この素朴な差の病は治らないこと。予測は群の違いを「説明」はしても、介入の効果を分離してはくれません。必要なのは、次の直交化です。

同じデータに **Double ML(直交化)**をかけます。E[YX]E[Y\mid X]E[TX]E[T\mid X]XX の多項式で回帰(numpy.linalg.lstsq)して残差 Y~,T~\tilde Y,\tilde T を作り、残差同士を回帰した係数 θ^\hat\theta が因果効果です。素朴な差と並べて、真の ATE 0.350.35 を回復するか見ます。

import numpy as np

rng = np.random.default_rng(0)
N = 5000
X = rng.uniform(0.0, 1.0, N)
e = 0.2 + 0.6 * X
T = rng.binomial(1, e)
tau = 0.2 + 0.3 * X
Y = 1.0 + 2.0 * X + tau * T + rng.normal(0.0, 0.5, N)

# Xの多項式基底(柔軟な回帰で E[Y|X]・E[T|X] を当てる。実務はここを勾配ブースティング等のMLに)
def poly(x, deg=5):
    return np.column_stack([x**k for k in range(deg + 1)])   # 切片+x+…+x^deg
Xb = poly(X, 5)

# (1) E[Y|X] を回帰して残差化:Y_tilde = Y −(Xで予測したY)
by, *_ = np.linalg.lstsq(Xb, Y, rcond=None)
Y_res = Y - Xb @ by
# (2) E[T|X](傾向スコア)を回帰して残差化:T_tilde = T −(Xで予測したT)
bt, *_ = np.linalg.lstsq(Xb, T, rcond=None)
T_res = T - Xb @ bt
# (3) 残差同士を回帰:T_tilde への Y_tilde の係数が因果効果(直交化=FWL)
theta = (T_res @ Y_res) / (T_res @ T_res)

naive = Y[T == 1].mean() - Y[T == 0].mean()
print(f"素朴な差            = {naive:+.3f}(交絡で過大)")
print(f"Double ML(直交化) = {theta:+.3f}")
print(f"真のATE             = {0.35:+.3f}")

出力:

素朴な差            = +0.792(交絡で過大)
Double ML(直交化) = +0.340
真のATE             = +0.350

出力の意味Double ML は +0.340+0.340 と、真の ATE 0.350.35 をほぼ回復しました(残り 0.010.01 は有限標本のノイズ)。素朴な差 +0.792+0.792 から 0.450.45 も下がり、交絡バイアスが消えています。やったことは「YYTTそれぞれ XX で予測して残差をとり、残差同士を回帰しただけ」——式 θ^=T~Y~/T~2\hat\theta=\sum\tilde T\tilde Y/\sum\tilde T^2TTXX で残差化する一手間(傾向スコアの除去)が、YY だけ調整する素朴な回帰との違いで、これが交絡を断ち切ります。ここでは E[YX],E[TX]E[Y\mid X],E[T\mid X] を多項式回帰で当てましたが、この2つを勾配ブースティングやランダムフォレスト等の機械学習に差し替えても、直交化の枠組みはそのままです(だから「機械学習」が名前に付きます)。柔軟な ML を使うほど、多数の交絡 XX を非線形に調整でき、しかも直交化が推定を頑健にします。

4. CATEを推定して「誰に介入するか」を決める(コード)

ATE(平均効果)が回復できたら、次は xx ごとの CATE、そして「誰に介入すべきか」というポリシーです。CATE を直接モデル化する最も素直な方法が、交互作用つきの結果回帰(S-learner)です。条件無交絡のもとで E[YX,T]E[Y\mid X,T] を正しく β0+βXX+βTT+βXT(X ⁣ ⁣T)\beta_0+\beta_X X+\beta_T T+\beta_{XT}(X\!\cdot\!T) と書ければ、

τ(x)=E[YT=1,x]E[YT=0,x]=βT+βXTx\tau(x)=E[Y\mid T{=}1,x]-E[Y\mid T{=}0,x]=\beta_T+\beta_{XT}\,x

となり、τ^(x)=β^T+β^XTx\hat\tau(x)=\hat\beta_T+\hat\beta_{XT}x で CATE が出ます。真は τ(x)=0.2+0.3x\tau(x)=0.2+0.3x なので、βT0.2\beta_T\approx0.2βXT0.3\beta_{XT}\approx0.3 が回復するはずです。numpy.linalg.lstsq で当てます。

import numpy as np

rng = np.random.default_rng(0)
N = 5000
X = rng.uniform(0.0, 1.0, N)
e = 0.2 + 0.6 * X
T = rng.binomial(1, e)
tau = 0.2 + 0.3 * X
Y = 1.0 + 2.0 * X + tau * T + rng.normal(0.0, 0.5, N)

# 交互作用つき回帰 Y ~ 1, X, T, X*T を最小二乗で当てる(S-learner=結果回帰)
Xi = np.column_stack([np.ones(N), X, T, X * T])
b, *_ = np.linalg.lstsq(Xi, Y, rcond=None)
b0, bX, bT, bXT = b

# 推定CATE  tau(x) = beta_T + beta_XT * x
print(f"推定係数:  beta0={b0:+.3f}  beta_X={bX:+.3f}  beta_T={bT:+.3f}  beta_XT={bXT:+.3f}")
print(f"推定CATE  tau(x) = {bT:+.3f} + {bXT:+.3f}*x   (真は 0.200 + 0.300*x)")
for x0 in [0.1, 0.5, 0.9]:
    print(f"   x={x0}:  推定tau={bT + bXT*x0:+.3f}   真のtau={0.2 + 0.3*x0:+.3f}")
print(f"推定ATE = mean(tau(X)) = {bT + bXT*X.mean():+.3f}   (真 0.350)")

出力:

推定係数:  beta0=+1.003  beta_X=+2.013  beta_T=+0.197  beta_XT=+0.290
推定CATE  tau(x) = +0.197 + +0.290*x   (真は 0.200 + 0.300*x)
   x=0.1:  推定tau=+0.226   真のtau=+0.230
   x=0.5:  推定tau=+0.342   真のtau=+0.350
   x=0.9:  推定tau=+0.458   真のtau=+0.470
推定ATE = mean(tau(X)) = +0.342   (真 0.350)

出力の意味:交互作用回帰は β^T=+0.197\hat\beta_T=+0.197(真 0.20.2)・β^XT=+0.290\hat\beta_{XT}=+0.290(真 0.30.3)を回復し、推定 CATE τ^(x)=0.197+0.290x\hat\tau(x)=0.197+0.290x が真の 0.2+0.3x0.2+0.3x にほぼ重なります(x=0.1,0.5,0.9x=0.1,0.5,0.9 で誤差は 0.010.01 前後)。注目すべきは、交絡があるのに CATE が正しく出たこと。素朴な差(§3)は交絡で +0.792+0.792 も外したのに、なぜこちらは当たるのか——XX を回帰に入れて条件づけ、結果モデルを正しく特定したからです(無交絡 + 正しい関数形)。βX=2.013\beta_X=2.013 がベースラインの 2X2X を、交絡をまたいで正しく拾い、効果 T,X ⁣ ⁣TT,X\!\cdot\!T と分離しています。なお推定 ATE は β^T+β^XTXˉ=0.342\hat\beta_T+\hat\beta_{XT}\cdot\bar X=0.342 で、§3 の Double ML(0.3400.340)とも整合します。両者は別ルート(直交化 vs 結果回帰)で同じ効果に達しました。

最後が本ノートの締め、ポリシー学習です。CATE がわかれば「誰に介入するか」を最適化できます。1コンバージョンの価値を VV、1人あたり介入コストを cc とすると、集合 SS に介入したときの純利益iS(Vτ(xi)c)\sum_{i\in S}\big(V\,\tau(x_i)-c\big)。これを最大化する答えは明快で、Vτ(xi)c>0V\,\tau(x_i)-c>0 すなわち τ(xi)>c/V\tau(x_i)>c/V(損益分岐 CATE)を超える顧客だけに介入することです。V=2000V{=}2000 円・c=600c{=}600 円(損益分岐 0.300.30)で、(1) 全員介入(2) 推定 CATE で狙う(3) 無作為に同人数を、真の τ\tau で採点して比べます。

import numpy as np

rng = np.random.default_rng(0)
N = 5000
X = rng.uniform(0.0, 1.0, N)
e = 0.2 + 0.6 * X
T = rng.binomial(1, e)
tau = 0.2 + 0.3 * X
Y = 1.0 + 2.0 * X + tau * T + rng.normal(0.0, 0.5, N)

# 交互作用回帰で推定したCATE(§4と同じ)
Xi = np.column_stack([np.ones(N), X, T, X * T])
b, *_ = np.linalg.lstsq(Xi, Y, rcond=None)
tau_hat = b[2] + b[3] * X                 # 推定CATE

# 介入の経済性:1コンバージョンの価値 V、1人あたり介入コスト c。
# 純利益 = V*τ − c。介入する価値があるのは τ > c/V の顧客だけ。
V, c = 2000.0, 600.0
breakeven = c / V                         # = 0.30(このCATEを超えたら黒字)

def net_value(mask):
    return float((V * tau[mask] - c).sum())   # 真のτで採点(合成データ)

# (1) 全員に介入
net_all = net_value(np.ones(N, dtype=bool))
# (2) CATEで狙う:推定CATEが損益分岐を超える顧客だけ介入
cate_mask = tau_hat > breakeven
n_t = int(cate_mask.sum())
net_cate = net_value(cate_mask)
# (3) 無作為に同人数だけ介入
rand_idx = rng.permutation(N)[:n_t]
rand_mask = np.zeros(N, dtype=bool); rand_mask[rand_idx] = True
net_rand = net_value(rand_mask)

print(f"損益分岐CATE = c/V = {breakeven:.2f}(τ がこれを超える顧客だけ介入する価値)")
print(f"(1) 全員に介入({N}人)                 : 純利益 {net_all:+,.0f} 円")
print(f"(2) CATEで狙う({n_t}人, tau_hat>{breakeven:.2f}): 純利益 {net_cate:+,.0f} 円")
print(f"(3) 無作為に同人数({n_t}人)           : 純利益 {net_rand:+,.0f} 円")
print(f"CATE狙い − 全員   = {net_cate - net_all:+,.0f} 円(少人数なのに全員超え)")
print(f"CATE狙い − 無作為 = {net_cate - net_rand:+,.0f} 円(同人数での差)")

出力:

損益分岐CATE = c/V = 0.30(τ がこれを超える顧客だけ介入する価値)
(1) 全員に介入(5000人)                 : 純利益 +495,906 円
(2) CATEで狙う(3219人, tau_hat>0.30): 純利益 +664,562 円
(3) 無作為に同人数(3219人)           : 純利益 +324,820 円
CATE狙い − 全員   = +168,656 円(少人数なのに全員超え)
CATE狙い − 無作為 = +339,743 円(同人数での差)

出力の意味:損益分岐 CATE は c/V=0.30c/V=0.30。推定 CATE が 0.300.30 を超えるのは XX が概ね 1/31/3 以上の 3,2193{,}219で、そこだけに介入するポリシーの純利益は +664,562+664{,}562。これが全員介入(+495,906+495{,}906 円)を上回るのがポイントです——CATE 狙いは全員より**1,7811{,}781 人も少なく介入しているのに、利益は +168,656+168{,}656 円多い。理由は、τ(x)\tau(x) が小さい低 XX 客(Vτ<cV\,\tau<c=介入コストが効果の価値を下回る赤字客)を外した**から。全員介入はこの赤字客を抱え込んで利益を削ります。さらに、同じ 3,2193{,}219 人を無作為に選ぶと +324,820+324{,}820しか出ず、CATE 狙いはその 2 倍以上(差 +339,743+339{,}743 円)。同じ人数・同じ予算でも、「誰に介入するか」を CATE で決めるだけで利益が倍増します。これは アップリフトモデリング で「効果が正のセグメントだけ狙えばブランケットに勝つ」と見たのと同じ構造を、連続的な CATE と損益分岐へ一般化したものです。「効果の大きい人だけを狙う」が、観察データ+多特徴の世界でも最適なターゲティングです。

§3・§4 の結果を1枚にまとめます。左に素朴な差 vs Double ML vs 真の ATE、右に推定 CATE 曲線と真の CATE、介入の損益分岐ラインと介入領域を描きます。

import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib

rng = np.random.default_rng(0)
N = 5000
X = rng.uniform(0.0, 1.0, N)
e = 0.2 + 0.6 * X
T = rng.binomial(1, e)
tau = 0.2 + 0.3 * X
Y = 1.0 + 2.0 * X + tau * T + rng.normal(0.0, 0.5, N)

naive = Y[T == 1].mean() - Y[T == 0].mean()

# Double ML(直交化)の点推定
def poly(x, deg=5):
    return np.column_stack([x**k for k in range(deg + 1)])
Xb = poly(X, 5)
by, *_ = np.linalg.lstsq(Xb, Y, rcond=None)
bt, *_ = np.linalg.lstsq(Xb, T, rcond=None)
Y_res, T_res = Y - Xb @ by, T - Xb @ bt
theta = (T_res @ Y_res) / (T_res @ T_res)

# 交互作用回帰の推定CATE係数
Xi = np.column_stack([np.ones(N), X, T, X * T])
bb, *_ = np.linalg.lstsq(Xi, Y, rcond=None)
bT, bXT = bb[2], bb[3]

fig, (axL, axR) = plt.subplots(1, 2, figsize=(11, 4.4))

# 左:素朴な差 vs Double ML vs 真のATE
labels = ["素朴な差", "Double ML", "真のATE"]
vals = [naive, theta, 0.35]
bars = axL.bar(labels, vals, color=["C3", "C0", "gray"])
for bar, v in zip(bars, vals):
    axL.text(bar.get_x() + bar.get_width() / 2, v + 0.01, f"{v:.3f}", ha="center", fontsize=11)
axL.axhline(0.35, ls="--", color="gray", lw=1.2)
axL.set_ylabel("推定したATE")
axL.set_title("素朴な差は交絡で過大/Double MLが真値を回復")
axL.grid(alpha=0.3, axis="y")

# 右:推定CATE曲線 vs 真のCATE、損益分岐と介入領域
xs = np.linspace(0, 1, 200)
axR.plot(xs, 0.2 + 0.3 * xs, color="gray", lw=2.5, label="真のCATE τ(x)=0.2+0.3x")
axR.plot(xs, bT + bXT * xs, color="C0", lw=2, ls="--", label="推定CATE")
axR.axhline(0.30, color="C3", lw=1.3, ls=":", label="損益分岐 c/V=0.30")
x_star = (0.30 - bT) / bXT
axR.axvspan(x_star, 1, color="C2", alpha=0.12)
axR.annotate("介入する(τ>分岐)", (x_star + 0.5 * (1 - x_star), 0.46),
             ha="center", fontsize=9, color="C2")
axR.set_xlabel("顧客特徴 X"); axR.set_ylabel("CATE τ(x)")
axR.set_title("推定CATEとポリシー(介入の損益分岐)")
axR.legend(loc="upper left", fontsize=9)
axR.grid(alpha=0.3)

fig.suptitle("因果機械学習:交絡を直交化で除き、CATEで誰に介入するかを決める")
fig.tight_layout()
plt.show()

左図は3本の棒——素朴な差 0.7920.792 が真の ATE(破線 0.350.35)を大きく超えて突き出し、Double ML 0.3400.340 と真の ATE 0.3500.350 がほぼ同じ高さで並びます。交絡を入れたまま(素朴)か、直交化で抜いた(DML)かの差が一目瞭然です。右図は推定 CATE(青破線)が真の CATE(灰)にぴたり重なり、赤い点線が損益分岐 0.300.30。緑に塗った右側(X1/3X\gtrsim1/3)が介入すべき領域で、ここでは τ(x)>0.30\tau(x)>0.30 =効果の価値が介入コストを上回ります。左の赤い領域(低 XX)には触らない——この塗り分けがそのままポリシーです。

⚠️ よくある誤解

⚠️ 要最新確認:因果機械学習(Double ML・causal forest・メタ学習器)とアトリビューションは、手法もライブラリ(EconMLDoWhyCausalML 等)も活発に更新される領域です。本ノートは数理の核を numpy の最小実装で示しました。実務適用の際は、無交絡の妥当性検証・クロスフィッティング・二重頑健・信頼区間の出し方など、最新のベストプラクティスを確認してください。

関連ノート

これで全9章のテキストは完結です。マーケティングの意思決定は、「誰が買うか(予測)」から「介入したら何が変わるか(因果)」へ、そして「誰にどの介入を最適に当てるか(ポリシー)」へと進みます。第1章の指標とファネル、第2〜3章の顧客価値と価格、第4章の市場反応、第5〜6章の選択モデルとセグメンテーション、第7章の実験と因果、第8章のレコメンド——それらが最後にこの「予測 → 因果 → 個別最適な介入」という一本の軸に束ねられる、というのが本テキスト全体の到達点です。