因果推論編・卒業回 — 分析を施策に落とし、診断フレームを自分で回す
連載の集大成。分析を予算配分・営業優先順位・配信対象に接続し、holdoutを最初から設計し、SHAPで説明責任を果たす実装の型。最後に6軸→接合率→手法カタログ→やらない判断を、取引件数の多いD2C(SOLNA)と取引件数が少ないB2Bの両方で回し、フレーム自走を確認します。
by Shin
分析を作ったのに、意思決定が一つも変わらなかった——これが連載で最も避けたい結末です。卒業回の答えは単純で、「その出力がどの意思決定を動かすか」から逆算して手法を絞ります。本記事では、意思決定への接続 → holdout(配信を止めて残す対照群)の設計 → 解釈 → 自動化の順に実装の型を示し、最後に 6軸 → 接合率 → 手法カタログ → やらない判断 のフレームを、過去の受託案件をもとに設計した、取引件数の多い架空のD2C企業SOLNAと取引件数が少ないB2Bの両方で回して、卒業を確認します。
到達目標
- 分析の出力を「どの意思決定を動かすか」で逆算し、手法を絞り込める。
- ランダム化できる面(holdout)を、施策設計の最初から仕込める。
- SHAPで予測の寄与を説明しつつ、「予測に効く」と「因果がある」を混同せずに説明責任を果たせる。
- 新規案件を、6軸 → 接合率 → 手法カタログ → やらない判断のフレームで自走して仕分けられる。
前提
第2回で記述(真CAC・接合率)、第3回で予測と評価指標、第4回で因果の言語(潜在結果・DAG)、第5回で観測データの因果(傾向スコア・DiD・RDD)、第6回で因果ML(CATE・uplift)、第7回で時系列・ベイズ・MMMの素養を扱いました。本回はその総まとめなので、SOLNAのテーブル(orders customers ad_spend sessions messages subscriptions 等)に慣れている前提で進めます。
分析を意思決定に接続する
最後のフィルタはいつも同じです。その分析の出力は、予算配分・営業優先順位・配信対象のどれを実際に動かすのか。動かさないなら作りません。各成熟段は、それぞれ別の意思決定に接続します。
| 段 | 代表的な出力 | 動かす意思決定 | SOLNAでの主データ |
|---|---|---|---|
| 記述 | チャネル別の真CAC・早期ROAS | 予算配分・チャネル選別 | orders×ad_spend×customers |
| 予測 | 解約・F2転換の確率 | 引き止め配信対象・通知 | sessions×customers×orders |
| 因果 | upliftスコア(誰に効くか) | 配信対象の選定 | messages(holdout) |
土台になるのは記述です。媒体が報告するCVではなく、orders の実購入を成果に置き、リード作成(初回購入)コホートで真CACと早期ROASを出して予算配分に繋ぎます。第2回の総仕上げとして、未成熟コホートとリークを避けた最小構成を置きます。
-- BigQuery想定。媒体CVではなく orders(実購入)を成果に置く。
with acq as ( -- 新規獲得を「初回購入コホート」で定義
select
customer_id,
acquisition_channel as channel,
first_order_at,
date_trunc(date(first_order_at), month) as cohort_month
from customers
where first_order_at is not null
),
ltv90 as ( -- 獲得後90日の累計購入額(早期LTVの代理)
select a.channel, a.cohort_month, a.customer_id,
sum(o.amount) as rev_d90
from acq a
join orders o
on o.customer_id = a.customer_id
and o.order_at < timestamp_add(a.first_order_at, interval 90 day) -- 観測窓を固定=リーク防止
group by 1, 2, 3
),
spend as (
select channel, date_trunc(date_day, month) as cohort_month, sum(spend) as spend
from ad_spend
group by 1, 2
)
select
l.channel, l.cohort_month,
count(distinct l.customer_id) as new_customers,
any_value(s.spend) as spend,
safe_divide(any_value(s.spend), count(distinct l.customer_id)) as true_cac, -- 真CAC
safe_divide(sum(l.rev_d90), any_value(s.spend)) as roas_d90 -- 早期ROAS
from ltv90 l
join spend s using (channel, cohort_month)
where l.cohort_month <= date_sub(current_date(), interval 90 day) -- 未成熟コホート除外
group by 1, 2
order by 1, 2;SOLNAでは、媒体報告CVベースの見かけのCPAと、実購入ベースの真CACで、チャネルの優劣がしばしば入れ替わります(想定レンジで、真CACは見かけのCPAの1.3〜2.0倍に開くチャネルが出ます)。この一枚で予算配分の会話が変わるなら、それが「意思決定を動かす分析」です。
ランダム化できる面を作る — holdoutは最初から
因果手法のうち最も強いのは、自分でランダム化を仕込んだときに開きます。SOLNAの messages には holdout_flag(配信しない群をランダムに確保した印)があります。これがある時点で、配信の平均効果(ATE)も、誰に効くか(uplift/CATE)も、正しく取りに行けます。
import pandas as pd
# holdout_flag = 1 が「ランダムに配信を止めた群」。因果の生命線はこの一列。
df = pd.read_gbq("""
select customer_id, campaign, holdout_flag, converted
from messages
where channel = 'line' and campaign = 'winback_2026q2'
""")
treat = df.loc[df.holdout_flag == 0, 'converted'].mean() # 配信した群
control = df.loc[df.holdout_flag == 1, 'converted'].mean() # holdout(配信しない群)
ate = treat - control # 配信の平均効果。holdoutが無ければ、この引き算自体ができない
print(f"配信群CVR={treat:.3f} / holdout CVR={control:.3f} / ATE={ate:.3f}")
# 「誰に送ると効くか」(uplift/CATE)は、このランダム割付の上でだけ正しく学習できる
from causalml.inference.meta import BaseTClassifier
from lightgbm import LGBMClassifier
learner = BaseTClassifier(learner=LGBMClassifier())
# X=顧客特徴量(membership_tier, recency, frequency...), treatment=1-holdout_flag, y=converted
# learner.fit(X=X, treatment=treatment, y=y); cate = learner.predict(X)ここから第6回の結論が効いてきます。upliftスコアの上位群だけに配信を寄せると、全配信より少ない配信量で同等以上の増分を取れます(SOLNAの想定レンジで、配信量を3〜5割削っても増分はほぼ維持、という形になります)。
解釈で説明責任を果たす — ただし予測≠因果
ステークホルダーに「なぜこのリードを優先するのか」を説明できなければ、予測モデルは現場で使われません。第3回で作った予測モデル(先行指標を目的変数に、リーク列を除いたもの)に、SHAPで寄与を可視化します。
import shap
import lightgbm as lgb
# 第3回の解約予測モデル(y = 90日以内の解約フラグ等。未来情報の列は学習前に除外済み)
model = lgb.LGBMClassifier().fit(X_train, y_train)
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_valid)
shap.summary_plot(shap_values, X_valid) # どの特徴量が予測をどちら向きに動かしたかここで連載を通した二つ目の誤解——予測≠因果——を最後に回収します。SHAPが説明するのは「モデルの予測にどの特徴量が効いたか」であって、「その特徴量を動かせば成果が変わるか」ではありません。たとえば「product_views が多いほど解約予測が下がる」と出ても、それは「商品ページを見せれば解約が減る」を意味しません。前者は予測の寄与、後者は介入の因果効果で、後者を知りたいなら前節のholdout+CATEに戻る必要があります。
自動化はいつ狙うか — 最初からは狙わない
成熟度は 記述 → 予測 → 因果 → 自動化 の順に上がり、下の段が上の段の土台になります。自動化(予算の自動配分、配信の自動最適化)は最上段で、いきなり狙うものではありません。較正された予測(「確率70%」が実際に7割当たる予測)と、holdoutで検証された因果ループが回っていない状態で自動化すると、相関に最適化し続ける装置を作ることになります。発動条件は、較正済みの予測・holdout基盤・因果検証のループ——この三つが揃ってからです。
診断フレーム総まとめ
判断レイヤーの言語(6軸・接合率・やらない判断の考え方そのもの)は対の因果編に譲り、ここでは実装視点で要点だけ再掲します。
- 6軸: 量/単価×サイクル/識別性/反復性/比較可能な単位/マッチング性。手法の成立可否は業態名ではなくこの構造で決まる。
- 接合率(spine coverage): 広告接触 → ID → 成果を一本で辿れる成果の割合。各手法が使える実弾の上限を画定する。
- 手法カタログ: 各手法を「成立条件」と「成立しないときの発動条件」のセットで持つ。
- やらない判断: 成立しない手法は、発動条件(再評価のトリガー)とセットで記録する。
このフレームは、コードに落とせる程度に明確です。6軸のプロファイルを入れると手法の発動可否が返る最小例を示します。
# 6軸プロファイル → 手法の発動可否(フレームをコードに落とす最小例)
def triage(p: dict) -> dict:
return {
"MMM": "OK" if (p["weekly_history_y"] >= 2 and p["volume"] == "high") else "NG",
"DiD/geo": "OK" if p["markets"] >= 2 else "NG",
"RDD": "OK" if p["has_threshold"] else "NG",
"uplift/CATE": "OK" if p["has_holdout"] else "PENDING", # holdoutを仕込めば解禁
"CausalImpact": "OK" if p["has_clear_intervention"] else "PENDING",
}
solna = dict(volume="high", weekly_history_y=2, markets=5, has_threshold=True,
has_holdout=True, has_clear_intervention=True)
b2b = dict(volume="low", weekly_history_y=0.5, markets=1, has_threshold=False,
has_holdout=False, has_clear_intervention=True)
# triage(solna) と triage(b2b) で、同じ関数から逆の答えが返る実演A: SOLNA(取引件数が多く反復購入のある複数地域)の仕分け
SOLNAは取引件数が多く、反復購入・会員識別・複数地域・holdout ありの構造です。triage() の結果を、全テーブルに対応づけて広げると次のようになります。
| 手法 | 使う主データ | 成立 | 発動条件・備考 |
|---|---|---|---|
| 記述(真CAC/LTV/コホート) | orders customers ad_spend | ◎ | 常時。意思決定の土台 |
| 予測(初回CV・F2・解約) | sessions customers orders | ◎ | リーク防止・確率較正が前提 |
| 傾向スコア・回帰調整 | sessions customers | ◎ | 観測交絡の調整 |
| DiD | ad_spend(region) orders(region) | ◎ | 複数地域があるので成立(催事・地域配信のon/off) |
| RDD | customers(membership_tier) | ◎ | 会員ランク閾値の特典がある |
| uplift・CATE | messages(holdout_flag) | ◎ | holdout設計済みで解禁 |
| CausalImpact | 日次KPI+無影響系列 | ◎ | LP刷新・配信開始などの離散介入 |
| MMM | ad_spend(週次)×売上 | △ | 週次2年規模が貯まれば◎。それまでは時期尚早 |
| レコメンド | orders sessions reviews | ◎ | 商品×行動 |
| テキストNLP | reviews | ◎ | 不満・要件の構造化 |
取引件数が多く多地域で holdout 基盤があるという構造が、ほぼ全ての手法を開いています。MMMだけは履歴の蓄積待ちで、これが唯一の「やらない(今は)+発動条件」です。
実演B: 対照ケース(取引件数が少ない・高単価B2B)でなぜ別の答えになるか
同じ手法カタログを、取引件数が少なく高単価で、商圏が単一で営業が介在するB2B(匿名化した実在のオフィス仲介案件・数値はレンジ)に当てると、答えが反転します。手法は同じでも、構造が違えば成立条件が逆になる——これが本連載の中心命題です。
| 手法 | B2B(取引件数が少なく単一市場) | SOLNAと逆になる理由 |
|---|---|---|
| 記述(真CAC) | ◎ | むしろ最優先。成約・手数料が「お金の真実」 |
| 予測(スコアリング) | △ | 成約ラベルが少ない。先行指標を目的変数に置く |
| DiD | ✕ | 単一商圏で対照地域が作れない |
| RDD | ✕ | 明確な閾値(会員ランク等)が無い |
| uplift | ✕ → △ | ランダム化配信の面が無い。新チャネルにholdoutを仕込めば△に上がる |
| CausalImpact | ◎ | 介入時点は自分で作れる。取引件数が少ないB2Bで最初に効く因果手法 |
| MMM | ✕ | 件数・履歴ともに不足 |
SOLNAでは複数地域があるのでDiDが成立しますが、B2Bは対照地域が作れず不成立です。逆に、取引件数が少ないB2Bで現実的に最初に効くのはCausalImpactで、これは比較地域や閾値が無くても「明確な介入時点」を自分で作れるからです。同じ非対称を判断レイヤーの言葉で整理したのが因果編で、本実演Bはその供給源と呼応しています。
対応書籍と読みどころ
- 施策デザインのための機械学習入門(ホクソエム): upliftと介入設計の実装書。読みどころは、holdoutを前提としたuplift評価の手順と、「効果を測る」から「効く相手に介入する」へ繋ぐ設計の発想。本回のholdout節・CATE節が下敷きにしています。
- 森下『機械学習を解釈する技術』: PFI・PD・ICE・SHAPを体系的に扱う一冊。読みどころは、解釈量が「予測への寄与」であって因果効果ではない、という線引きを実装とともに掴めるところ。本回のSHAP節の注意点はここに対応します。
フレームとの接続
本回はフレーム全体の卒業課題にあたります。6軸で構造を把握し、接合率で適用範囲の上限を見積もり、手法選択フローで仕分け、章末チェックリストで自走を確認する——この一巡を、SOLNA(実演A)と取引件数が少ないB2B(実演B)の両方で回せたなら、連載の目的は達成です。
卒業チェックリスト
- 作ろうとしている分析が、予算配分・営業優先順位・配信対象のどれを動かすか言えるか
- 成果は媒体CVではなく実購入(
orders)で、未成熟コホートを除外して測っているか - 新しい配信面にholdout(ランダム割付)を最初から設計したか
- SHAP等の解釈量を「予測の寄与」として扱い、因果効果と取り違えていないか
- 自動化を、較正済み予測・holdout基盤・因果ループが揃う前に狙っていないか
- 新規案件を6軸で見立て、成立する手法と「やらない+発動条件」を表にできるか
- 同じ手法でも構造が違えば答えが反転することを、SOLNAとB2Bで説明できるか
ここまでが「分析をどう実装し、どの意思決定に接続するか」というHOWの卒業課題でした。自社のデータ構造でどの手法が成立し、何をやらないか——というWHAT/WHYの判断そのものは、対の因果編で扱っています。新しい案件に出会ったら、まず因果編で構造を見立て、本連載に戻って実装する。この往復ができれば、フレームは自走しています。