相関から因果へ:効果を語ることば — 潜在結果・DAG・バックドア基準
この記事では、「効果があった」と言うために必要なことばを学びます。具体的には、潜在結果とATE、セレクションバイアス、DAGで交絡を見つけ、バックドア基準で調整すべき変数を決めます。以降の手法がすべてこの上に立つ、連載の土台となる回です。
by Shin
「LINEを追加した人は、しなかった人より購入率が高かった。だからLINE追加は効く」——この推論は因果の主張として成立しません。追加した人は、もともと買う気が強かっただけかもしれないからです。観測された差を効果と読み違えると、効いていない施策に予算を積みます。この回では、「効いた」と言うために必要なことば——反実仮想・潜在結果・ATE、そしてDAGとバックドア基準で「調整すべき変数」を決める方法——を、架空のD2C企業SOLNAの「LINE友だち追加 → 再購入」で組み立てます。
ここは連載の土台です。第5回以降の回帰調整・傾向スコア・DiD・RDDは、すべてこの回のことばの上に立ちます。
到達目標
- 反実仮想と潜在結果でATE(平均処置効果)を定義できる。
- セレクションバイアスがなぜ「観測された差」を効果から乖離させるか、データで説明できる。
- DAGで交絡・媒介・合流点(コライダー)を見分けられる。
- バックドア基準で「調整すべき変数」を選び、「調整してはいけない変数」を除外できる。
前提
第1回で相関と検定の読み方、第2回で真の成果と接合率(どこまで遡れるか)、第3回で予測と「予測 ≠ 因果」の伏線を置きました。本回はその伏線を回収します。第3回の予測モデルは「誰が買いそうか(相関)」を当てる道具でした。本回が問うのは「施策を打てば誰が買うようになるか(因果)」で、両者は別物です。
反実仮想という考え方
因果を主張するには、反実仮想——「もしその施策がなかったら、どうなっていたか」——という比較対象が必要です。私たちが観測できるのは「LINEを追加して、再購入した」という一本の事実だけで、同じ人が「LINEを追加しなかった世界」は永遠に見えません。
だから「追加群の購入率は非追加群より高い」という観測は、それ単独では効果を意味しません。効果とは「同じ人」の追加した世界と追加しなかった世界の差であって、「別の人どうし」の差ではないからです。この区別が、相関と因果を分ける最初の一線です。
潜在結果とATE
これを形式化したのが潜在結果(potential outcomes)です。顧客 について、LINEを追加した場合の結果を 、追加しなかった場合の結果を と書きます。個人にとっての効果は です。
ここに因果推論の根本問題があります。実際の世界では、各人について か のどちらか一方しか観測できません。追加した人の (追加しなかったら)は反実仮想で、欠測しています。個人の効果は原理的に観測不能なのです。
そこで諦めて、集団の平均に話を移します。平均処置効果ATEは で定義します。「集団全体に一律でLINE追加が起きたときと、起きなかったときの、平均購入率の差」です。観測できない個人差を、平均という形でなら推定の射程に入れられます。
問題は、ATEを「追加群の平均 − 非追加群の平均」で素朴に近似してよいか、です。結論から言えば、追加するかどうかが人によって偏っている限り、近似できません。
セレクションバイアスの正体
SOLNAのデータで素朴な差を出してみます。まず分析テーブルを組みます。ここで一つ目の実務落とし穴——交絡は必ず「処置より前」の期間で測ること——を最初から埋め込みます。処置後の行動を交絡に混ぜると、効果そのものを変数に取り込んでしまい(リーク)、推定が壊れます。
-- SOLNA: 「LINE友だち追加 → 再購入」の因果分析テーブル
-- 鉄則1: 交絡は "友だち追加より前" の期間で測る(処置後の行動を混ぜない)
-- 鉄則2: 成果は観測窓が満了したコホートだけ(未成熟コホートで過小評価しない)
WITH treat AS (
-- 友だち追加イベント(LINE/CRM 連携の friend-add から導出。初配信時刻で近似)
SELECT customer_id, MIN(sent_at) AS line_added_at
FROM messages
WHERE channel = 'line'
GROUP BY customer_id
),
base AS (
SELECT
c.customer_id,
c.acquisition_channel AS acq_channel,
-- 追加群は追加日、非追加群は first_order_at を擬似基準日 T0 とする
COALESCE(t.line_added_at, c.first_order_at) AS t0,
IF(t.customer_id IS NOT NULL, 1, 0) AS line_add
FROM customers c
LEFT JOIN treat t USING (customer_id)
),
conf AS (
-- 交絡: T0 より前 90 日の来訪頻度(=事前のアクティブさ)
SELECT b.customer_id, COUNT(s.session_id) AS visit_freq_pre
FROM base b
LEFT JOIN sessions s
ON s.customer_id = b.customer_id
AND s.session_start BETWEEN TIMESTAMP_SUB(b.t0, INTERVAL 90 DAY) AND b.t0
GROUP BY b.customer_id
),
y AS (
-- 成果: T0 後 90 日以内の再購入(確定済みのみ)
SELECT b.customer_id,
MAX(IF(o.order_type IN ('repeat','subscription'), 1, 0)) AS purchase
FROM base b
LEFT JOIN orders o
ON o.customer_id = b.customer_id
AND o.order_at BETWEEN b.t0 AND TIMESTAMP_ADD(b.t0, INTERVAL 90 DAY)
GROUP BY b.customer_id
)
SELECT b.customer_id, b.line_add, b.acq_channel,
conf.visit_freq_pre, y.purchase, b.t0
FROM base b
JOIN conf USING (customer_id)
JOIN y USING (customer_id)
-- 未成熟コホートを除外(観測窓 90 日が満了したものだけ)
WHERE b.t0 <= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 90 DAY);このテーブルで「追加群 − 非追加群」の購入率差を取ると、もっともらしい数字が出ます。
import pandas as pd
df = pd.read_gbq("SELECT * FROM mart.solna_line_effect") # 上のクエリ結果
rate = df.groupby("line_add")["purchase"].mean()
naive_diff = rate.loc[1] - rate.loc[0]
print(round(naive_diff, 3)) # 例: 0.18(追加群が 18pt 高い)
# だがこれは ATE ではない。追加群は事前来訪頻度が高い=もともと買いやすい層
print(df.groupby("line_add")["visit_freq_pre"].mean())
# line_add=1 の方が visit_freq_pre が明確に大きいなら、差の多くは交絡差が18ポイント出ても、それは効果ではありません。LINEを追加するのは、もともとブランドに前のめりで来訪頻度が高い人です。その人たちは追加しようがしまいが買いやすい。観測された差は「効果」と「もともとの買いやすさの偏り」が混ざったものです。この混入がセレクションバイアスで、相関 ≠ 因果が具体的なお金の判断に化ける瞬間です。
DAG入門:交絡・媒介・合流点
なぜ来訪頻度は調整し、別の変数は調整してはいけないのか。これを「気合い」ではなく「構造」で決めるのがDAG(有向非巡回グラフ)です。矢印は「原因 → 結果」を表します。SOLNAの「LINE追加 → 再購入」を描くと、登場人物は4種類に分かれます。
DAG(dot 表記)
visit_freq_pre ──▶ line_add ──▶ purchase ← 直接効果
visit_freq_pre ──▶ purchase ← 交絡(事前のアクティブさ)
acq_channel ──▶ line_add
acq_channel ──▶ purchase ← 交絡(流入チャネル)
line_add ──▶ line_click ──▶ purchase ← 媒介(LINE配信のクリック経由)
line_add ──▶ review
purchase ──▶ review ← 合流点(レビュー投稿)役割ごとに意味がまったく違います。
- 交絡(confounder): 処置と成果の両方に矢印を出す共通原因。ここでは事前来訪頻度(visit_freq_pre)と流入チャネル(acq_channel)。放置すると「もともとの偏り」が効果に混ざる。調整する。
- 媒介(mediator): 処置 → ◯ → 成果と、効果が通る経路上の変数。ここではLINE配信のクリック(line_click)。LINE追加の効果の多くは「配信が届いてクリックして買う」を通る。総効果を測るなら調整しない。
- 合流点(collider): 処置と成果の両方から矢印を「受ける」変数。ここではレビュー投稿(review)。買った人はレビューを書き、追加した人にはレビュー依頼が届くので、両方の影響を受ける。調整すると、ない関連を生み出す。
交絡は「共通の原因」、合流点は「共通の結果」。矢印の向きが逆で、扱いも正反対です。ここを取り違えると、調整すればするほど答えが歪みます。
バックドア基準で調整変数を決める
「どの変数を調整すべきか」を機械的に決めるのがバックドア基準です。直感的には、処置と成果の間にある「裏口(バックドア)の経路」——交絡を経由して両者を結ぶ経路——をすべて塞ぎ、かつ効果の通り道(媒介)は塞がない、という条件です。今回のDAGなら、塞ぐべきはvisit_freq_preとacq_channelの組だけです。
これを手で詰めるとミスが出るので、DoWhyにDAGを渡して導出させます。
from dowhy import CausalModel
dot_graph = """
digraph {
visit_freq_pre -> line_add;
visit_freq_pre -> purchase;
acq_channel -> line_add;
acq_channel -> purchase;
line_add -> line_click; // 媒介
line_click -> purchase;
line_add -> review; // 合流点
purchase -> review;
line_add -> purchase; // 直接効果
}
"""
model = CausalModel(
data=df,
treatment="line_add",
outcome="purchase",
graph=dot_graph,
)
# バックドア集合を自動導出 → {visit_freq_pre, acq_channel} だけが返る
# line_click(媒介)と review(合流点)は調整対象に含まれない
estimand = model.identify_effect()
print(estimand)
# 導出された調整集合で推定(傾向スコア重み付け)
estimate = model.estimate_effect(
estimand,
method_name="backdoor.propensity_score_weighting",
)
print(estimate.value) # 交絡を調整した後の ATE。素朴な 0.18 より小さくなるはずナイーブな18ポイントと、調整後のATEを並べて、差がどれだけ「偏り」だったかを見ます。多くの場合、調整後は目減りします。その目減り分が、効いていると勘違いしていた量です。
媒介と合流点を「調整してはいけない」理由
最後に、つい調整したくなる二つの罠を潰します。
媒介を調整する罠: line_click(配信クリック)を調整集合に足したくなります。「クリックの有無を揃えれば公平だ」と感じるからです。しかしLINE追加の効果はクリックを通って購入に届きます。クリックで層別すると、効果の通り道を塞いでしまい、残るのは「クリックを介さない直接効果」だけ。総効果を知りたいのに、過小評価した別物を測ることになります(直接効果と間接効果を分けたい場面は別にありますが、それは媒介分析という独立した問いです)。
合流点を調整する罠: review(レビュー投稿)でフィルタしたくなります。「レビューを書いた濃い顧客だけで見よう」という具合です。これが一番たちが悪い。レビューは追加と購入の共通の結果なので、ここで層別すると、本来関係のない追加と購入の間に見せかけの関連が生まれます。何もしなければ正しかった分析が、合流点を触った瞬間に歪む。「サンプルを絞って綺麗にした」つもりが、バイアスを注入しています。
対応書籍と読みどころ
この回のことばは、以下の書籍で深掘りできます。本連載は地図で、深さは書籍に預けます。
効果検証入門(ホクソエム) 潜在結果・セレクションバイアス・バックドア基準を、マーケに近い例で最短距離に通します。本回でやった「ナイーブな差はなぜ効果でないか」を、回帰と傾向スコアまで地続きで追えるので、第5回への助走に最適です。
金本『因果推論』前半 本連載が理論の軸に据えた一冊。前半は潜在結果からDAG、識別条件までを体系立てて積みます。本回の各概念が「なぜその順で必要になるか」を、式とともに確認したい人向け。
フレームとの接続
本回は診断フレームでいう「識別性」——因果を語れるかどうかのゲート——に当たります。「ユーザー単位で結合できるか」「処置・成果・交絡を一本に繋げるか」が、そもそも因果を語れるかの入口でした。成熟度のステップでは、記述 → 予測の上に乗る因果の段階の、最初の一歩です。
ただし、本回で学んだのは「ことば」であって「測定」ではありません。DAGとバックドア基準は、データ量に関係なく誰でも描けます。しかし描いた調整集合で実際にATEを推定できるかは、データ構造が決めます。SOLNAは取引件数が多く反復購入のある複数地域なので、次回の傾向スコアやDiDが素直に動きます。一方、取引件数が少なく単一市場のB2B(オフィス仲介)では、同じDAGを描けても対照群が作れず、推定が成立しないことがあります。「描ける」と「測れる」は別、という非対称が次回以降の主題です。
章末チェックリスト
- 「追加群と非追加群の差」を、そのまま効果と呼んでいないか(反実仮想を意識したか)。
- ATEを として、自分の施策に当てはめて言えるか。
- 交絡を「処置より前」の期間で測っているか(リーク・媒介の混入を防いだか)。
- 成果を観測窓が満了したコホートで見ているか(未成熟コホートを避けたか)。
- DAGで交絡・媒介・合流点を区別し、調整集合に交絡だけを入れたか。
- 「サンプルを絞って綺麗にする」操作が、合流点の調整になっていないか。
ここまでは「効果を語ることば」です。このことばで描いた調整集合を使って、回帰調整・傾向スコア・DiD・RDDで実際に因果を取りに行くのが第5回。そして「自社のデータ構造でどの手法が成立し、何をやらないか」の判断は、対になる因果編にまとめています。DAGは描けたが推定が成立しない——その線引きを、匿名化した取引件数が少ないB2B案件で確認できます。