前回、クーポンの効果は 平均422円 と推定できました。
ここで一つ考えてみてください。
「クーポンは全員に422円ずつ効く」のと「効く人は800円、効かない人は0円で、ならすと422円」のとでは、施策の打ち方がまったく変わります。
今回は、この 平均の裏側 にある、個々人の効果を取り出します。
結論を先に3行で書きます。
est.effect(X)と書くだけで、5,000人それぞれの効果が出ます。分布は −820円から +1,437円まで広がり、6.8%の人にはマイナスです。- 効くのは「たまにしか買わず、しばらく来ていない人」。効かないのは「ヘビーユーザー」です。
- そして実際の配布は、この真逆でした。最も効く層には8%しか配られておらず、最も効かない層には81%配られていました。
この記事を読み終えると、次のことができるようになります。
- ATE・CATE・ITE の3つの「効果」を区別して説明できる
est.effect(X)で一人ひとりの効果を取り出し、分布として眺められる- 「誰に効くか」を属性の組み合わせで表にできる
- CATEを個人の予測値のように扱ってはいけない理由を、信頼区間で示せる
準備:前回と同じ推定器を作る
データも推定器も前回とまったく同じです。
データはec_coupon.csv(5,000人分)を使います。以下からダウンロードできます。
前回のノートブックを開いたままの方は、この部分は飛ばしてください。
以下、コードです。
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)
まず、動かしてみる:一人ひとりの効果を取り出す
前回は est.ate(X) で平均を出しました。
今回は ate を effect に変えるだけです。これだけで、一人ひとりの効果のようなものを取り出せます。
以下、コードです。
tau = est.effect(X) # 5,000人ぶんの効果が配列で返る
df["cate"] = tau
print(
f"平均: {tau.mean():.0f}円 "
f"最小: {tau.min():.0f}円 最大: {tau.max():.0f}円")
print(f"効果がマイナスの人: {(tau < 0).mean():.1%}")
print(f"原価100円を下回る人: {(tau < 100).mean():.1%}")
以下、実行結果です。
平均: 422円 最小: -820円 最大: 1437円 効果がマイナスの人: 6.8% 原価100円を下回る人: 12.8%
平均は前回と同じ422円ですが、最小と最大の差は2,000円以上あります。分布を描いてみます。
山の中心は400〜500円あたりにありますが、左には裾が長く伸びています。原価100円を下回る人が12.8%、8人に1人です。この人たちに配った分は、そのまま損失になります。
ATE・CATE・ITE:3つの「効果」
ここで用語を整理しておきます。健康診断で考えると分かりやすいです。
| 用語 | 意味 | 健康診断で言うと | EconMLでの出し方 |
|---|---|---|---|
| ATE 平均処置効果 |
全員をならした効果 | 受診者全体の平均値 | est.ate(X) |
| CATE 条件付き平均処置効果 |
属性が同じ人たちの平均的な効果 | 「40代男性」の平均値 | est.effect(X) |
| ITE 個別処置効果 |
その人一人だけの効果 | あなた自身の数値 | 原理的に推定できない |
大事なのは3行目です。第1回で見たとおり、一人の人について「配った場合」と「配らなかった場合」を両方観測することはできません。だから ITE(その人一人の効果)は、どんな手法を使っても直接は測れないのです。
その代わりに測れるのが CATE です。「購入頻度3回・休眠45日の人たち」のように属性で条件を絞り、その中での平均的な効果を出す。est.effect(X) が返しているのは、一人ひとりの X に対応した CATE です。
つまり
effect(X)の出力は「その人の効果」ではなく、「その人と同じ属性を持つ人たちの、平均的な効果」です。この違いは記事の後半でもう一度効いてきます。
誰に効くのか
X に入れた2つの変数、購入頻度と休眠日数で切って、効果の平均を並べてみます。
以下、コードです。
freq = pd.cut(
df["past_freq"], [0, 2, 4, 6, 9, 100],
labels=["1-2回", "3-4回", "5-6回", "7-9回", "10回以上"]
)
days = pd.cut(
df["days_since_last"], [0, 14, 30, 60, 120, 400],
labels=["〜14日", "15-30日", "31-60日", "61-120日", "121日〜"]
)
print(
df.pivot_table(
index=freq,
columns=days,
values="cate",
aggfunc="mean"
).round(0)
)
以下、実行結果です。
days_since_last 〜14日 15-30日 31-60日 61-120日 121日〜 past_freq 1-2回 525.0 567.0 632.0 754.0 971.0 3-4回 379.0 419.0 482.0 593.0 808.0 5-6回 217.0 251.0 316.0 422.0 622.0 7-9回 20.0 59.0 120.0 235.0 NaN 10回以上 -270.0 -218.0 -150.0 -30.0 NaN
きれいな階段になっています。右上ほど効き、左下ほど効かない。
右上は「年に1〜2回しか買わず、4か月以上来ていない人」で、効果は971円。左下は「年10回以上買っていて、2週間以内に来た人」で、効果は −270円です。
マイナスの意味を考えてみてください。ヘビーユーザーはクーポンがなくても買います。そこにクーポンを配ると、買う量は変わらず、値引きのぶんだけ購入金額が減る。つまり配らなければ得られていた売上を、自分で削っていることになります。
配り方が真逆だった
効果の上位10%と下位10%について、どんな人で、実際にどれくらい配られていたのかを見てみます。
以下、コードです。
top = df[df["cate"] >= df["cate"].quantile(0.9)] # 効果の上位10%
bot = df[df["cate"] <= df["cate"].quantile(0.1)] # 効果の下位10%
for name, g in [("上位10%", top), ("下位10%", bot)]:
print(
f"{name}: 効果 {g['cate'].mean():.0f}円 / "
f"購入 {g['past_freq'].mean():.1f}回 / "
f"休眠 {g['days_since_last'].mean():.0f}日 / "
f"配布率 {g['coupon'].mean():.0%}"
)
以下、実行結果です。
上位10%: 効果 846円 / 購入 1.5回 / 休眠 115日 / 配布率 8% 下位10%: 効果 -101円 / 購入 9.7回 / 休眠 21日 / 配布率 81%
最も効く層には8%しか配られておらず、最も効かない層には81%配られていました。
これは現場の判断としては自然です。「優良顧客を大事にしよう」と考えれば、ヘビーユーザーにクーポンを配ります。
しかし効果で見ると、配るべき相手と配っていた相手が、ほぼ裏返しになっていました。第1回で単純比較が2,268円と過大に出た原因も、まさにこの配り方にあります。
「では誰に配れば利益が最大になるか」は、原価と信頼区間を織り込んで別の記事で扱います。今回は「平均だけ見ていたら、この裏返しに気づけなかった」ことを押さえてください。
答え合わせ
このデータには正解の列 _true_cate があります。推定した5,000個の効果と、正解を突き合わせます。
以下、コードです。
print(f"推定と正解の相関: {df['cate'].corr(df['_true_cate']):.3f}")
以下、実行結果です。
推定と正解の相関: 0.994
相関0.994。「誰に効いて、誰に効かないか」の順序を、ほぼ正確に言い当てています。第1回の単純比較や層別では、そもそも一人ひとりの効果という発想すらありませんでした。
ただし、この図は正解を仕込んだ学習用データだから描けるものです。実務データに _true_cate はありません。実務での信頼性の確かめ方は、別の記事で扱います。
ありがちな失敗:CATEを個人の予測値として扱う
ここまで来ると、こう言いたくなります。「会員IDが1234のAさんは、効果が521円です」。
これは2つの意味で言い過ぎです。信頼区間を見てみます。
以下、コードです。
lo, hi = est.effect_interval(X, alpha=0.05)
df["lo"], df["hi"] = lo, hi
one = df[(df["past_freq"] == 3) & (df["days_since_last"] == 45)].iloc[0]
print(
f"購入3回・休眠45日の人: 推定 {one['cate']:.0f}円 "
f"95%区間 [{one['lo']:.0f}, {one['hi']:.0f}]"
)
以下、実行結果です。
購入3回・休眠45日の人: 推定 521円 95%区間 [435, 608]
1つ目。区間の幅は173円あります。5,000人分の区間幅の中央値は235円です。「521円」と言い切れるほどの精度はありません。
2つ目。先ほど整理したとおり、この521円は「購入3回・休眠45日という属性の人たちの平均的な効果」であって、Aさん個人の効果ではありません。同じ属性でも、Aさんには800円効き、Bさんには200円しか効かないかもしれない。それは原理的に分からないのです。
報告書にはこう書いてください。
購入3回・休眠45日前後の会員層では、クーポン1枚あたり平均500円程度の効果が見込める(95%信頼区間 435〜608円)。
主語を「Aさん」から「会員層」に変える。それだけで、言えることと言えないことの線が正しく引けます。
まとめ
今回のポイントです。
est.effect(X)で一人ひとりの効果(CATE)が出る。分布は −820〜1,437円、6.8%はマイナス、12.8%は原価割れ- ATE は全体の平均、CATE は属性が同じ人たちの平均、ITE はその人一人の効果。ITE は原理的に測れない
- 効くのは「たまにしか買わず、しばらく来ていない人」、効かないのはヘビーユーザー。実際の配布は真逆だった
- CATE は個人の予測値ではない。主語は「Aさん」ではなく「Aさんのような会員層」

