EconMLで実践する ビジネス因果推論
(第10回)
「どれだけやるか」を決める、そして現場に載せる
(連続処置の限界効果と実務チェックリスト10項目)

EconMLで実践する ビジネス因果推論(第10回)「どれだけやるか」を決める、そして現場に載せる(連続処置の限界効果と実務チェックリスト10項目)

営業部長がこう聞きます。「うちの営業は、年に何回訪問すべきなんだ?」 集計では、年9回以上訪問した顧客の契約金額は0〜2回の顧客の3倍。「多いほど良い」と答えたくなります。

これまでの9回は「やる/やらない」の2値の施策でした。最終回は「何回やるか」、連続量の施策です。答えの形も「効く/効かない」ではなく「何回目まで効く」に変わります。

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

  1. 直線で当てはめると「訪問1回あたり +18.5万円」。この数字は平均の傾きで、実際は8回で頭打ちです。直線は9回目以降の伸びを作り出します。
  2. カーブで当てはめると、増分は 0→2回 65万円、6→8回 13万円、8→10回 −4万円。伸びなくなる回数と、費用を引いた最適回数(6回)は別物です。
  3. 分析を現場に載せる前に、10項目のチェックリストに「はい」と言えるか確かめます。言えない項目は先に言います。

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

  • 訪問回数や値引き率のような連続量の施策の効果を推定できる
  • 「1回あたり」の直線と「何回目まで」のカーブの違いを説明できる
  • 限界効果と費用を比べて、最適な回数を決められる
  • 会議に出す前に、10項目で自己点検できる

事例とデータ

第8回と同じ法人営業の6,000社です。違いは施策の列で、年間の訪問回数 visits(0〜12回)が入っています。成果は今年度の契約金額 sales(万円)。

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

btob_visits.csv

現場のルールは「大口の顧客とA担当は多く訪問する」なので、訪問回数は企業規模と担当者ランクに引っ張られています。第8回と同じ交絡です。

単純平均の差は、0回と比較して 4回 +253万円、8回 +695万円、12回 +1,168万円。正解は 8回で +160万円、それ以降はわずかに下がります。

 

まず、動かしてみる:直線で当てはめる

discrete_treatment を付けなければ、LinearDML は施策を連続量として扱います。効果は ate(X, T0=0, T1=1) と「何回から何回へ」を指定して取り出します。

以下、コードです。

import pandas as pd
from sklearn.ensemble import GradientBoostingRegressor
from econml.dml import LinearDML

# データの読み込み
df = pd.read_csv("btob_visits.csv")

# 変数の定義
Y = df["sales"]     # 今年度の契約金額(万円)
T = df["visits"]    # 年間の訪問回数(0〜12回)
IND = {"製造": 0, "卸売": 1, "サービス": 2, "IT": 3}
X = pd.DataFrame({
    "size_class": df["size_class"],
    "industry": df["industry"].map(IND),
    "years": df["years"]
})
W = df[["rep_rank", "last_year_sales"]] # 担当者ランク、前年売上(交絡)

# モデルの学習
est = LinearDML(
    model_y=GradientBoostingRegressor(random_state=0),
    model_t=GradientBoostingRegressor(random_state=0),
    cv=5, random_state=0
)
est.fit(Y, T, X=X, W=W)
slope = est.ate(X, T0=0, T1=1)  # 1回増やしたときの効果
lo, hi = est.ate_interval(X, T0=0, T1=1, alpha=0.05) # 信頼区間

# 結果の表示
print(f"訪問1回あたりの効果: +{slope:.1f}万円 [{lo:.1f}, {hi:.1f}]")
print(f"直線で12回まで伸ばすと: +{slope * 12:.0f}万円")

 

以下、実行結果です。

訪問1回あたりの効果: +18.5万円 [17.6, 19.3]
直線で12回まで伸ばすと: +222万円

 

区間は狭く、いかにも確からしい数字です。でも「1回あたり」には「何回目でも同じ」という仮定が隠れています。12回で222万円は、正解の144万円より78万円多い。直線は頭打ちを表現できず、平均の傾きを最後まで伸ばします。

 

収穫逓減を捉える:treatment_featurizer

効果が曲がるなら、曲がる形を treatment_featurizer で渡します。PolynomialFeatures(degree=2) は施策 T を「T と T²」の2列に広げます。

以下、コードです。

from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import PolynomialFeatures

# モデルの学習
est = LinearDML(
    model_y=GradientBoostingRegressor(random_state=0),
    model_t=RandomForestRegressor(
        n_estimators=200, 
        min_samples_leaf=20,
        random_state=0
    ),
    treatment_featurizer=PolynomialFeatures(
        degree=2, include_bias=False), 
    cv=5, random_state=0
)
est.fit(Y, T.values.reshape(-1, 1), X=X, W=W)   # T は2次元にして渡す
truth = df.groupby("visits")["_true_gain"].mean()   # 答え合わせ用の列

# 結果の表示
print("訪問を2回増やしたときの効果(万円)")
for k in range(0, 12, 2):
    gain = est.ate(X, T0=k, T1=k + 2)
    lo, hi = est.ate_interval(X, T0=k, T1=k + 2, alpha=0.05)
    ans = truth[k + 2] - truth[k]
    print(
        f"{k:2d}回 → {k + 2:2d}回: {gain:6.1f} [{lo:6.1f}, {hi:6.1f}]"
        f"  正解 {ans:5.1f}"
    )

 

以下、実行結果です。

訪問を2回増やしたときの効果(万円)
 0回 →  2回:   65.2 [  61.0,   69.4]  正解  70.0
 2回 →  4回:   47.9 [  45.4,   50.3]  正解  50.0
 4回 →  6回:   30.5 [  28.6,   32.4]  正解  30.0
 6回 →  8回:   13.2 [  10.0,   16.4]  正解  10.0
 8回 → 10回:   -4.1 [  -9.2,    1.0]  正解  -8.0
10回 → 12回:  -21.4 [ -28.5,  -14.3]  正解  -8.0

 

0→2回は65万円、6→8回は13万円、8→10回は−4万円。効果は回を追うごとに減り、8回を超えると増えません。正解を区間がほぼ捉えています。1つ正直に書くと、10→12回は −21万円 [−29, −14] と、正解の −8万円より下に外れました。11回以上のデータが77社しかなく、2次のカーブは端で強く曲がるからです。データが薄い端の数字は信じすぎない。

訪問回数に対する契約金額の増分:直線・2次・正解の用量反応カーブ0回比の増分(万円)。0回:推定0.0・正解0.0、2回:推定65.2・正解70.0、4回:推定113.0・正解120.0、6回:推定143.6・正解150.0、8回:推定156.8・正解160.0、10回:推定152.7・正解152.0、12回:推定131.3・正解144.0。直線のLinearDMLは1回あたり18.5万円で伸び続ける。単純平均の差は4回253、8回695、12回1168万円と交絡で膨らむ。訪問を増やすと契約金額はどう伸びるか:直線・2次・正解横軸は年間訪問回数、縦軸は0回と比べた契約金額の増分(万円)。薄い青は2次の推定の95%区間0501001502000回2回4回6回8回10回12回0回比の増分(万円)正解は8回で頭打ち推定のピーク 9回直線:1回 +18.5万円のまま伸び続ける ↗正解直線の LinearDML2次の LinearDML単純平均の差(0回比・交絡あり)4回+253万円8回+695万円12回+1,168万円第1回と同じ罠大口ほど訪問が多い「1回あたり+18.5万円」は平均の傾き。実際は8回で頭打ち。直線は9回目以降の伸びを作り出す

 

何回目で止めるか:限界効果と費用

「8回で頭打ち」は答えの半分です。訪問には費用がかかります。ここでは1回10万円とします。決め方は、「もう1回増やしたときの効果(限界効果)」が費用を上回る間は続け、下回ったら止める。

以下、コードです。

COST = 10   # 訪問1回にかかる費用(万円)

# 訪問を1回増やしたときの効果と費用の比較
print(f"1回追加したときの効果と、費用{COST}万円との比較")

# 訪問を1回増やしたときの効果
for t in range(1, 13):
    gain = est.ate(X, T0=t - 1, T1=t)
    lo, hi = est.ate_interval(X, T0=t - 1, T1=t, alpha=0.05)
    verdict = "続ける" if gain > COST else "止める"
    print(
        f"{t:2d}回目: {gain:6.1f}万円 "
        f"[{lo:6.1f}, {hi:6.1f}]  {verdict}"
    )

# 訪問を8回増やしたときの効果
print(f"\n8回 → 20回の推定: {est.ate(X, T0=8, T1=20):.0f}万円")

 

以下、実行結果です。

1回追加したときの効果と、費用10万円との比較
 1回目:   34.8万円 [  32.4,   37.1]  続ける
 2回目:   30.4万円 [  28.6,   32.3]  続ける
 3回目:   26.1万円 [  24.7,   27.5]  続ける
 4回目:   21.8万円 [  20.7,   22.8]  続ける
 5回目:   17.4万円 [  16.5,   18.4]  続ける
 6回目:   13.1万円 [  12.1,   14.2]  続ける
 7回目:    8.8万円 [   7.4,   10.2]  止める
 8回目:    4.4万円 [   2.6,    6.3]  止める
 9回目:    0.1万円 [  -2.2,    2.4]  止める
10回目:   -4.2万円 [  -7.0,   -1.4]  止める
11回目:   -8.5万円 [ -11.8,   -5.3]  止める
12回目:  -12.9万円 [ -16.7,   -9.1]  止める

8回 → 20回の推定: -284万円

 

6回目の13.1万円は費用を上回り、7回目の8.8万円は下回ります。費用を引いた最適回数は6回。正解のデータで計算しても6回です。「伸びなくなる回数」(正解8回、推定では9回目が+0.1万円で紙一重)と「最適回数」(6回)は違う数字です。伸びなくなるまで続けると、7・8回目の2回分で1社あたり約7万円の持ち出しです。

最後の行を見てください。「8回→20回で −284万円」。データは12回までなのに、20回を聞けば数字は返ってきます。カーブは端で急降下するので意味のない数字です。モデルはデータの外を聞かれても黙ってくれません。範囲を守るのは聞く側の仕事です。

訪問1回ごとの増分と費用の比較1回ごとの増分(万円)。1回目:推定34.8・正解37.5、2回目:推定30.4・正解32.5、3回目:推定26.1・正解27.5、4回目:推定21.8・正解22.5、5回目:推定17.4・正解17.5、6回目:推定13.1・正解12.5、7回目:推定8.8・正解7.5、8回目:推定4.4・正解2.5、9回目:推定0.1・正解-4.0、10回目:推定-4.2・正解-4.0、11回目:推定-8.5・正解-4.0、12回目:推定-12.9・正解-4.0。費用10万円を上回るのは推定・正解とも6回目まで。何回目まで訪問する価値があるか:1回ごとの増分と費用を比べる棒は「その回の訪問で契約金額がいくら増えるか」(2次の推定、縦線は95%区間)。破線は1回の訪問にかかる費用10万円-10010203040その回の増分(万円)1回目352回目303回目264回目225回目176回目137回目98回目49回目010回目-411回目-812回目-13費用 10万円/回6回目までは増分>費用7回目から費用割れ(正解も6回まで)推定(青は続ける、灰は止める)正解伸びなくなる回数(8回)と、費用を引いた最適回数(6回)は違う。原価を引いてから決める

 

裏で何が起きているのか:効果は「どこからどこへ」の差

2値の施策では、効果は「やる − やらない」の1つの差でした。連続量では「T0 から T1 に変えたときの差」になり、T0 と T1 を指定して初めて数字になります。ate(X, T0=6, T1=8) がその指定です。

直線モデルは効果 = a×T で、a が「1回あたり」の傾き。何回目でも同じなので頭打ちを表現できません。treatment_featurizer は T を「T, T²」に広げ、効果 = a×T + b×T² を推定します。b が負なら増分は減り続け、頂点を過ぎると下がります。中身は第2回と同じ LinearDML です。変わったのは、施策の列を1本から2本に広げたことだけ。

第1回で「効果とは差である」と書きました。連続量でも同じです。「どこからどこへの差か」を言わない数字は、まだ効果ではありません。

 

ありがちな失敗

1つ目:T の形と model_t を変え忘れる。 treatment_featurizer を使うとき、T を1次元のまま渡すと ValueError: Expected 2D array, got 1D array instead で止まります。

T.values.reshape(-1, 1) で列にします。もう1つ、施策が「T, T²」の2列になるので、model_t は2列を同時に予測できるモデルが必要です。GradientBoostingRegressor は ValueError: y should be a 1d array, got an array of shape (4800, 2) で止まります。RandomForestRegressor は多出力に対応しています。

2つ目:データにない回数を聞く。 20回の推定は返ってきますが、根拠になる顧客は1社もいません。報告する回数はデータがある範囲(今回は実質10回まで)に限ります。範囲の外を知りたければ、推定ではなく小さく試すのが答えです。

 

現場に載せる:実務チェックリスト10項目

会議に出す前の自己点検10項目です。連載のどの回で扱ったかを添えています。

実務チェックリスト10項目1 施策と成果の定義、2 施策後の変数を入れない、3 交絡の洗い出し、4 層の重なり、5 サンプル数と処置比率、6 信頼区間の報告、7 再現性、8 意思決定の言葉に翻訳、9 実施後のモニタリング、10 小規模A/Bテスト。実務チェックリスト10項目:施策の効果を出す前に、出したあとに連載10回ぶんを1枚に。右の番号は、その項目を扱った回 1. 施策と成果を、誰が読んでも同じに解釈できる形で定義したか第1回 2. 施策より後に発生した変数を、説明変数に入れていないか第4回 3. 交絡になりうる要因を、業務側の人と一緒に洗い出したか第4回・第9回 4. 施策を受けた層と受けていない層に、比較できる重なりがあるか第8回 5. 効果を推定するのに十分なサンプル数と処置比率か第8回・第9回 6. 点推定だけでなく、信頼区間もセットで報告したか第2回・第9回 7. random_state を固定し、同じ結果を再現できるか第2回 8. 推定結果を「1人あたり◯円」「何回まで」の意思決定の言葉に翻訳したか第6回・第7回・第10回 9. 施策実施後にモニタリングして、答え合わせをする段取りがあるか第9回10. 可能なら小規模なA/Bテストで検証する計画を立てたか第7回10個に「はい」と言えない分析は、まだ会議に出さない。出すなら、言えない項目を先に言う

10個に「はい」と言えない分析は、まだ会議に出しません。出すなら、言えない項目を先に言います。「④の重なりが薄いので、大口顧客の効果は幅が広いです」と自分から言う人が信用されます。

 

まとめ

今回のポイントです。

  • 連続量の施策は、効果を「T0→T1の差」で定義する。「1回あたり」の直線は頭打ちを表現できない
  • treatment_featurizer に PolynomialFeatures(degree=2) を渡してカーブを当てはめる。T は reshape(-1, 1)、model_t は多出力対応のモデルにする
  • 伸びなくなる回数(8回)と、費用を引いた最適回数(6回)は別。限界効果>費用の間だけ続ける
  • データの外(20回)を聞けば数字は返るが意味はない
  • 10項目に「はい」と言えるか。言えない項目は先に言う

連載を振り返ると、第1回で「効果とは差」と定義し、第2〜4回で交絡と変数の仕分け、第5〜7回でばらつきを木と政策に、第8回で手法選び、第9回で頑健性、最終回で「何回やるか」を扱いました。全部に共通するのは「比べる相手を作る」ことです。

Screenshot

【月1 特定テーマ講座(11月)】
POS売上分析Copilotを作る
(POSデータ×PYTHON×生成AI)

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