EconMLで実践する ビジネス因果推論
(第7回)
誰に配るかを決める
(SingleTreePolicyInterpreterに原価を入れて期待利益を出す)

EconMLで実践する ビジネス因果推論(第7回)誰に配るかを決める(SingleTreePolicyInterpreterに原価を入れて期待利益を出す)

第6回の1枚を見た上司が、次にこう言います。「で、何枚配る? いくら儲かる?」

ここで問いが変わります。これまでは「効果はいくらか」でした。今回は原価を入れて、「誰に配ると、いくら残るか」を答えます。クーポン1枚の原価は、第2回と同じ100円とします。

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

  1. 原価100円を入れた木(ディシジョンツリー)は「購入6回以下の人に配る」の1行になりました。5,000人のうち82%に配って、期待利益は139万円。いまの配り方(30%に配布)は26万円です。
  2. 同じ1,500枚でも、効果の高い順に配り直すだけで26万円が73万円になります。枚数も決めてよいなら「効果>原価」の人まで配るのが最大です。
  3. 境目の判断は点推定ではなく区間の下限で行います。点推定で切ると、正解がマイナスの205人に配ってしまいました。

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

  • 効果の推定値と原価から、配布案ごとの期待利益を計算できる
  • SingleTreePolicyInterpreter で「配る/配らない」を直接決める(ディシジョンツリー)を作れる
  • 「何枚配るか」を累積利益カーブで決められる
  • 点推定と区間の下限、どちらで境目を切るべきかを説明できる

データは前回までと同じ ec_coupon.csv、因果フォレストも第5回・第6回と同じものです。

まず、動かしてみる:原価を入れて「配る/配らない」の木(ディシジョンツリー)を作る

データは前回までと同じ ec_coupon.csv です。因果フォレストも第5回と同じものを使います。

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

ec_coupon.csv

前半は第5回のコードそのまま。33行以降が今回の追加です。

以下、コードです。

import numpy as np
import pandas as pd
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.ensemble import GradientBoostingClassifier
from econml.dml import CausalForestDML
from econml.cate_interpreter import SingleTreePolicyInterpreter

df = pd.read_csv("ec_coupon.csv")
Y = df["spend_30d"]
T = df["coupon"]
X = pd.DataFrame({
    "past_freq":       
        df["past_freq"],
    "days_since_last": 
        df["days_since_last"],
    "member_rank":     
        df["member_rank"].map({"ブロンズ": 0, "シルバー": 1, "ゴールド": 2}),
    "channel":         
        df["channel"].map({"自然検索": 0, "広告": 1, "メルマガ": 2}),
    "tenure_months":   
        df["tenure_months"],
})

# 第5回・第6回と同じ因果フォレスト
est = CausalForestDML(
    model_y=GradientBoostingRegressor(random_state=0),
    model_t=GradientBoostingClassifier(random_state=0),
    discrete_treatment=True, n_estimators=1000, min_samples_leaf=100,
    cv=5, random_state=0
)
est.fit(Y, T, X=X)

# 原価100円を入れて、「配る/配らない」を決める木(ディシジョンツリー)を作る
COST = 100
pol = SingleTreePolicyInterpreter(
    risk_level=0.05, max_depth=2, min_samples_leaf=200,
    random_state=0
)
pol.interpret(est, X, sample_treatment_costs=COST)
print(f"配る人の割合: {pol.treat(X).mean():.1%}")

 

以下、実行結果です。

配る人の割合: 81.7%

 

第6回の SingleTreeCateInterpreter と引数はほぼ同じで、違いは2つ。sample_treatment_costs=COST で原価を渡すこと、risk_level=0.05 で「効果の95%区間の下限」を使うことです。後者の意味は後半で扱います。

木(ディシジョンツリー)がどう分けたかは、第6回と同じ要領で葉(リーフ)ごとに集計できます。

以下、コードです。

df["effect"] = est.effect(X) # 効果の推定値
df["lower"] = est.effect_interval(X, alpha=0.05)[0] # 効果の下限
df["leaf"] = pol.tree_model_.apply(X) # どの葉(リーフ)に属するか
df["treat"] = pol.treat(X) # 配るかどうか

# 葉(リーフ)ごとに集計
tbl = df.groupby("leaf").agg(
    人数=("leaf", "size"),
    購入回数=("past_freq", lambda s: f"{s.min()}-{s.max()}"),
    効果=("effect", "mean"),
    下限=("lower", "mean"),
    配る=("treat", "first"),
).round(0)

print(tbl)

 

以下、実行結果です。

        人数  購入回数     効果     下限  配る
leaf
1     4084   1-6  441.0  296.0   1
3      303   7-7  170.0  -28.0   0
4      613  8-18   84.0 -142.0   0

 

ルールは1行です。「過去1年の購入回数が6回以下なら配る。7回以上には配らない。」購入7回の303人は効果の点推定が170円で原価を超えていますが、下限が−28円なので「配らない」側に入りました。

  

配布案を期待利益で比べる

期待利益は「配った人の(効果−原価)の合計」です。5つの配布案を同じ物差しで並べます。_true_cate は仕込んでおいた正解で、実務データにはありません。

以下、コードです。

tau = df["effect"].values       # 効果の推定値
lo = df["lower"].values         # 効果の下限
truth = df["_true_cate"].values # 真の効果

# 各配布計画の利益を計算
plans = {
    "いまの配り方": df["coupon"].values == 1,
    "全員配布": np.ones(len(df), dtype=bool),
    "効果の上位30%": tau >= np.quantile(tau, 0.7),
    "効果 > 原価": tau > COST,
    "木": df["treat"].values == 1,
}

# 各配布計画の利益を計算
for name, m in plans.items():
    gain = (tau - COST)[m].sum() / 10000 # 利益の推定値
    gain_lo = (lo - COST)[m].sum() / 10000 # 利益の下限
    gain_true = (truth - COST)[m].sum() / 10000 # 真の利益
    print(
        f"{name:<8}\t配布率 {m.mean():<6.1%}, 期待利益 {gain:6.1f}万円, "
        f"下限で{gain_lo:5.1f}万円, 正解で{gain_true:6.1f}万円"
    )

 

以下、実行結果です。

いまの配り方  	配布率 30.2% , 期待利益   25.5万円, 下限で -0.9万円, 正解で  16.3万円
全員配布    	配布率 100.0%, 期待利益  140.4万円, 下限で 61.4万円, 正解で 156.3万円
効果の上位30%	配布率 30.0% , 期待利益   73.0万円, 下限で 48.5万円, 正解で  82.9万円
効果 > 原価 	配布率 90.2% , 期待利益  142.1万円, 下限で 73.5万円, 正解で 166.8万円
木       	配布率 81.7% , 期待利益  139.3万円, 下限で 80.1万円, 正解で 167.3万円
5つの配布案の期待利益の比較いまの配り方は配布率30%で期待利益25.5万円(下限-0.9万円・正解16.3万円)、全員配布は配布率100%で期待利益140.4万円(下限61.4万円・正解156.3万円)、効果の上位30%は配布率30%で期待利益73.0万円(下限48.5万円・正解82.9万円)、効果 > 原価は配布率90%で期待利益142.1万円(下限73.5万円・正解166.8万円)、木(ディシジョンツリー)は配布率82%で期待利益139.3万円(下限80.1万円・正解167.3万円)。正解で配った理想は168.3万円。誰に何枚配るかで、利益はここまで変わる:5つの配布案5,000人・原価100円。期待利益=配った人の(効果−原価)の合計。棒は因果フォレストの推定、印は区間の下限と仕込んだ正解050万円100万円150万円正解で配った理想 168万円いまの配り方配布率 30%(1,510人)25.5万円全員配布配布率 100%(5,000人)140.4万円効果の上位30%配布率 30%(1,500人)73.0万円効果 > 原価配布率 90%(4,512人)142.1万円木(ディシジョンツリー)配布率 82%(4,084人)139.3万円期待利益区間の下限で計算正解で計算(実務データには存在しない)同じ1,500枚でも、効果順なら25.5万→73.0万円。枚数も決めるなら「効果>原価」の人まで

読みどころは3つあります。

1つ目、いまの配り方は同じ枚数の上位30%の3分の1しか稼いでいない。現場は優良顧客に配っていて、その人たちはクーポンがなくても買います。いまの配布先のうち、効果の上位30%に入っている人は13%だけでした。

2つ目、全員配布と木(ディシジョンツリー)は、推定ではほぼ同じ。140万円と139万円です。ところが下限で計算すると61万円と80万円、正解で計算すると156万円と167万円。配らないほうがよい18%を外した分が、確実性と正解の両方で効いています。

3つ目、「効果>原価」で機械的に切ると推定は最大だが、下限では木(ディシジョンツリー)に負ける。点推定が原価をわずかに超える450人を含むためです。

  

何枚配るか:累積利益カーブ

枚数の決め方は1つの図で片づきます。効果の高い人から順に配っていき、そこまでの利益を積み上げたのが以下の図です。

効果の高い順に配ったときの累積利益カーブ因果フォレストの推定は配布率90%で最大142.1万円、正解は84%で最大167.9万円、区間の下限は81%で最大79.7万円。30%(いまと同じ枚数)では73.0万円、木(ディシジョンツリー)の82%では139.3万円、全員配布では140.4万円。何枚配るか:効果の高い順に配っていくと、利益はどこで頭打ちになるか横軸は配った人の割合(因果フォレストの効果が高い順)。縦軸はそこまでに配った人の(効果−原価)の合計(5,000人・原価100円)050万円100万円150万円0%20%40%60%80%100%区間の下限 61万円123451いまと同じ30%:73万円2木 82%:139万円3効果>原価 90%:142万円(推定の最大)4全員配布 100%:140万円5正解のピーク 84%:168万円正解区間の下限因果フォレストの推定効果=原価の人まで配ると利益は最大。その先は配るほど減る。上位30%は途中で止めた形

利益は「効果=原価」の人で頭打ちになり、その先は配るほど減ります。予算が決まっているなら効果の高い順に予算まで、枚数も決めてよいなら「効果>原価」の人まで。上位30%は、その途中で止めた形です。

この図は、アップリフト分析の Qini 曲線と同じ発想です。違いは、縦軸が「効果」ではなく「効果−原価」になっていること。原価を引くと、山の頂上が「何枚配るか」の答えになります。

  

裏で何が起きているのか

 符号を当てる木(ディシジョンツリー)

第6回の木(ディシジョンツリー)は「効果がいくらか」を当てていました。今回の木(ディシジョンツリー)が当てるのは、「効果−原価」がプラスかマイナスか、つまり符号です。

期待利益を最大にする配り方は、理屈の上では「効果−原価がプラスの人全員」です。ただ、5,000人分の判定はルールになりません。木(ディシジョンツリー)は、覚えられる行数で符号をできるだけ正しく分ける木(ディシジョンツリー)です。

policy_value_ にはその木(ディシジョンツリー)で配ったときの1人あたり利益、always_treat_value_ には全員配布の値が入っています(risk_level を付けたときは、どちらも下限で計算した値です)。

 なぜ下限で切るのか

売上は見込み、原価は確定額です。効果の点推定が原価をわずかに上回っていても、区間がマイナスにかかっていれば「配って損をする」可能性が残ります。

risk_level=0.05 は、効果の点推定を95%区間の下限に置き換えてから符号を判定する指定です。「下限で見ても原価を超える人にだけ配る」という、経営でいえば「見込みの弱い側で計画する」やり方です。

因果フォレストを経由せず、データから直接「配る/配らない」の木(ディシジョンツリー)を学習する DRPolicyTree もあります。今回は第5回からの流れで因果フォレストを使いました。

  

答え合わせ:点推定で切るとどうなるか

同じ因果フォレスト・同じ原価・同じ深さで、risk_level を付けない木(ディシジョンツリー)も作りました。

点推定で切った木(ディシジョンツリー)と下限で切った木(ディシジョンツリー)の答え合わせ点推定の木(ディシジョンツリー)は購入8回以上・休眠29日以上の205人(効果+122円、下限-131円)に配るが正解は-37円で赤字。下限の木(ディシジョンツリー)は購入7回の303人(正解+129円)を見送るが、代償は0.9万円にとどまる。答え合わせ:点推定で切った木(ディシジョンツリー)と、区間の下限で切った木(ディシジョンツリー)同じ因果フォレスト・同じ原価100円・同じ深さ2。risk_level を付けるかどうかだけが違う。正解は実務データにはない点推定で切る(risk_level なし)効果の点推定 − 原価 の符号で分ける購入回数 8回以上 → 休眠日数 29日以上?購入 7回以下配る4,387人 効果 +422円 下限 +274円正解 +483円 → 判断は正しい購入 8回以上・休眠 28日以下配らない408人 効果 +64円 下限 -148円正解 -123円 → 判断は正しい購入 8回以上・休眠 29日以上配る205人 効果 +122円 下限 -131円正解 -37円 → 配ると損 -2.8万円下限で切る(risk_level=0.05)効果の95%区間の下限 − 原価 の符号で分ける購入回数 7回以上 → 8回以上?購入 6回以下配る4,084人 効果 +441円 下限 +296円正解 +510円 → 判断は正しい購入 7回配らない303人 効果 +170円 下限 -28円正解 +129円 → 見送りの機会損失 −0.9万円購入 8回以上配らない613人 効果 +84円 下限 -142円正解 -94円 → 判断は正しい点推定は205人に配って−2.8万円。下限は303人を見送って−0.9万円。慎重な側の代償が小さい

点推定の木(ディシジョンツリー)は「購入8回以上・休眠29日以上の205人」に配る判断をしました。効果の点推定は122円で原価を超えていますが、下限は−131円、正解は−37円。配ると2.8万円の損です。

下限の木(ディシジョンツリー)にも取りこぼしはあります。購入7回の303人は正解が+129円で、配ったほうがよかった。ただし見送りの代償は0.9万円です。慎重な側の間違いのほうが安い。これが下限で切る理由です。

  

ありがちな失敗

1つ目:「上位k%」を先に決める。 30%という数字に根拠がなければ、山の途中で止めているだけです。予算がないなら「効果>原価」で枚数が決まり、予算があるなら予算で決まります。k%は結果であって、入力ではありません。

2つ目:原価を引き忘れる。 全員配布の効果の合計は190万円です。ここから原価50万円(5,000枚×100円)を引いた140万円が利益ですが、190万円のまま報告される資料をよく見ます。sample_treatment_costs を渡し忘れると、木(ディシジョンツリー)も同じ間違いをします。

  

まとめ

今回のポイントです。

  • 原価を入れると、問いは「効果はいくら」から「配ると、いくら残るか」に変わる。期待利益=配った人の(効果−原価)の合計
  • SingleTreePolicyInterpreter に sample_treatment_costs を渡すと、「配る/配らない」を直接決める木(ディシジョンツリー)ができる。このデータでは「購入6回以下に配る」の1行
  • 同じ枚数なら効果順に、枚数も決めるなら「効果>原価」まで。累積利益カーブの頂上が答え
  • 境目は点推定ではなく区間の下限で切る(risk_level)。慎重な側の間違いのほうが安い
  • 上位k%は結果であって入力ではない。原価を引き忘れない
Screenshot

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

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