この連載について
因果推論を「本を読んだ」で終わらせず、自分の言葉で説明でき、コードで再現できる状態まで落とす30日連載です。Day 1・Day 2・Day 3 で、「相関が因果に化ける罠」「潜在的結果とATE」「3つの流派は同じ調整の別名」までを見てきました。また、ものの見事に3日目で更新が止まってしまい、三日坊主となっていましたが、仕事も連休もひと段落したため、再開したいと思います。30日まずはやり切ります。
Day 3では「正しい変数 $Z$ で交絡を調整すれば因果が取れる」と分かりました。今日はその一歩手前、そもそも交絡が生まれないように割付を"設計"するRCT(無作為化比較試験)に入ります。図はできるだけ実際のデータで見せます。
TL;DR(3行)
- RCTが「黄金律」なのは、無作為割付が「潜在的結果 ⊥ 処置」を強制的に成立させるから。交絡は調整で消すのではなく、割付の設計で最初から生まない。
- だから RCT では 単純な引き算がATEの不偏推定になる。Thornton (2008) のHIV検査インセンティブ実験で、告知率は 33.9% → 78.9%(+45.1ポイント)。しかも
t検定 = 回帰係数はぴたり一致。 - 効いているのは**「調整」ではなく「設計」**。共変量を回帰に足しても効果は 0.4496 → 0.4488 とほぼ動きません(Day 3の観察データが 81→51 と激変したのと対照的)。
今日の問い
Day 3で「正しい $Z$ で調整すれば因果が取れる」と分かった。でも現実には「何が交絡か」を全部知るのは難しい。
調整に頼らずに、そもそも交絡を生まない方法はないのか? RCTの「黄金律」とは何が魔法なのか?
結論から言うと、RCTの魔法は 「割付をコインで決める」ことそのものにあります。今日はその1点がなぜ因果を取り出せるのかを言語化し、本物のRCTデータで「引き算=因果」を確かめます。
無作為割付:交絡を「調整」ではなく「設計」で消す
Day 3の交絡構造を思い出します。もともとの意欲・スキル $Z$ が、処置 $D$ と結果 $Y$ の両方を押し上げるので、単純な差はバイアスを含みました。
観察データでは、この裏口 $D \leftarrow Z \to Y$ を $Z$ で塞ぐ(調整する)必要がありました。ところが RCT は、処置 $D$ をコイン投げで決めて $Z \to D$ の矢印そのものを断ち切ります。
$Z$ から $D$ への矢印が消えれば、裏口経路は存在しません。観測できていない交絡(性格・遺伝・その日の気分…)まで含めて、すべての背景要因が処置と無関係になります。これが「調整いらず」の正体です。
潜在的結果の言葉で言うと、無作為割付は
$$
(Y(0),, Y(1)) \perp D
$$
を設計によって保証します(Day 2で定義した反事実 $Y(0), Y(1)$ が、割付 $D$ と独立)。Day 3のRubin流はこれを「条件付き無視可能性」と呼び、$Z$ で条件付けて近似的に成り立たせようとしました。RCTは条件付けすら要らず、無条件で成立させます。だから「黄金律(gold standard)」と呼ばれます。
なぜ「引き算」がATEになるのか
$(Y(0),Y(1)) \perp D$ が成り立つと、処置群で観測する平均は「全員が処置を受けたときの平均」に、対照群で観測する平均は「全員が受けなかったときの平均」に一致します。
$$
E[Y \mid D=1] = E[Y(1)],\qquad E[Y \mid D=0] = E[Y(0)]
$$
したがって、観測できる単純な差が、そのまま定義どおりのATEになります。
Day 2で「反事実は観測できない(因果推論の根本問題)」と言いました。RCTは、その観測できない反事実の平均を、別の人たちの平均で正当に埋める仕組みなのです。
手を動かす①:無作為割付は共変量をそろえる
題材は Thornton (2008) のフィールド実験です。マラウイでHIV検査を受けた人に、「結果を聞きに来たら現金をあげる」というインセンティブを無作為に配りました(causaldata に収録)。処置=インセンティブ付与、アウトカム=「結果を受け取りに来たか」です。
まず、無作為割付が本当に2群をそろえているかを確認します。事前共変量(年齢・VCTまでの距離・2004年のHIV陽性)の標準化平均差(群間の平均差を標準偏差で割った値)を見ます。慣習的に $|値| < 0.1$ なら実質的にバランスしているとみなします。
import numpy as np, pandas as pd
from causaldata import thornton_hiv
df = thornton_hiv.load_pandas().data
d = df.dropna(subset=["any", "got"]).rename(columns={"any": "incentive"})
d["incentive"] = d["incentive"].astype(int) # 処置: 1=インセンティブあり
d["got"] = d["got"].astype(int) # アウトカム: 1=結果を受け取りに来た
print("処置群(あり) =", (d.incentive==1).sum(), " 対照群(なし) =", (d.incentive==0).sum())
# 事前共変量の標準化平均差(|値| < 0.1 なら実質バランス)
for col in ["age", "distvct", "hiv2004"]:
g = d.dropna(subset=[col])
mt, mc = g[g.incentive==1][col].mean(), g[g.incentive==0][col].mean()
print(f"{col:9s} 標準化平均差 = {(mt-mc)/g[col].std():+.3f}")
処置群(あり) = 2211 対照群(なし) = 623
age 標準化平均差 = +0.118
distvct 標準化平均差 = +0.054
hiv2004 標準化平均差 = +0.000
年齢がわずかに +0.118、距離が +0.054、そしてHIV陽性率はぴたり ±0.000。どれも小さく、2群は「比べられる」状態です。図にすると一目で分かります。
左のラブプロットは、3つの共変量がすべて青い ±0.1 の帯(=実質バランス)付近に収まっていることを示します。右は年齢分布で、処置群(オレンジ)と対照群(青)がほぼ重なっています。誰も意図してそろえていないのに、コイン投げだけで背景がそろう——これが無作為化の効き目です。
手を動かす②:単純な差=ATE、t検定と回帰は同じもの
バランスが取れていれば、あとは引き算するだけです。平均の差、その差のt検定、そして回帰 got ~ incentive の係数——この3つが同じ数字になることを確かめます。
import statsmodels.formula.api as smf
from scipy import stats
t = d[d.incentive==1].got # 処置群のアウトカム
c = d[d.incentive==0].got # 対照群のアウトカム
# ① 単純な差(引き算)
print("単純な差 = %.4f (%.1f pt)" % (t.mean()-c.mean(), (t.mean()-c.mean())*100))
# ② 平均の差の t 検定
tt = stats.ttest_ind(t, c, equal_var=False)
print("t検定 t=%.1f p=%.2g" % (tt.statistic, tt.pvalue))
# ③ 回帰 got ~ incentive の係数(=単純な差と一致)
m = smf.ols("got ~ incentive", data=d).fit()
ci = m.conf_int().loc["incentive"]
print("回帰係数 = %.4f 95%%CI [%.3f, %.3f]" % (m.params["incentive"], ci[0], ci[1]))
単純な差 = 0.4506 (45.1 pt)
t検定 t=21.6 p=1.3e-83
回帰係数 = 0.4506 95%CI [0.413, 0.488]
告知率は 33.9% → 78.9%、その差 +45.1ポイント。インセンティブが結果受領をおよそ2.3倍に押し上げた、と読めます。そして 単純な差 0.4506 = 回帰係数 0.4506 はぴったり同じ。二値処置を1本だけ入れた回帰は、群平均の引き算そのものなのです(この「回帰=比較」の一般化はDay 16で掘ります)。
右は単純な差をブートストラップ(データを取り直すシミュレーション)で5000回計算した分布です。推定量は +45pt 付近に偏りなく集まり、95%区間は約 [40.8, 49.1] pt。RCTでは、この単純な差が「たまたま」ではなく因果効果を捉えている、と胸を張って言えます。
手を動かす③:効いているのは「調整」ではなく「設計」
RCTの本質を最も感じられるのが、共変量で調整しても効果がほとんど動かないことです。Day 3の観察データでは、調整の有無で 81 → 51 と激変しました。RCTではどうなるでしょうか。
# 共変量を調整しても、RCTでは効果はほとんど動かない
da = d.dropna(subset=["age", "distvct", "hiv2004"])
unadj = smf.ols("got ~ incentive", data=da).fit().params["incentive"]
adj = smf.ols("got ~ incentive + age + distvct + hiv2004", data=da).fit().params["incentive"]
print("調整なし = %.4f" % unadj)
print("調整あり = %.4f" % adj)
調整なし = 0.4496
調整あり = 0.4488
0.4496 → 0.4488。ほとんど動きません。 無作為割付が最初から交絡を消しているので、共変量を足しても「消すべき交絡」がもう残っていないからです。観察データでは調整が"バイアス除去"の主役でしたが、RCTでは調整は"精度をわずかに上げる脇役"に格下げされます。この落差こそが、RCTを黄金律たらしめている理由です。
つまづき・誤解しやすい点
- 「無作為化=2群がぴったり同一」ではありません。 今日も年齢は +0.118 とわずかにズレていました。無作為化が保証するのは「期待値としてそろう(偏りがない)」ことで、1回の実験で完全一致はしません。だからサンプルが小さいと偶然の不均衡が残り、共変量調整(③)で精度を補う価値が出ます。
- 「有意差が出た=効果が大きい」ではありません。 t検定のp値が極小($10^{-83}$)でも、それは「差がゼロではない」の確からしさにすぎません。大きさを語るのは効果量(+45.1pt)と信頼区間の役目です。両者を混同しないようにします。
- RCTでも万能ではありません。 割付はランダムでも、脱落・不遵守(割り当てられた処置を受けない)・波及効果があると単純な差は歪みます。「割り当てた効果(ITT)」と「実際に受けた効果(LATE)」の違いはDay 20前後で扱います。今日の前提は「割付どおりに全員が動いた理想のRCT」です。
実務での使いどころ(空間データの文脈)
- A/BテストはミニRCT。 プロダクトのUI変更やアラート閾値の出し分けを無作為に割り振れば、今日の「引き算=因果」がそのまま使えます。難しい調整を考える前に「割付を設計できないか?」を先に問うのが筋の良い順番です。
- 衛星xインフラでは「完全な無作為化」が難しい。 どの管路を先に更新するか、どの自治体で新機能を試すかは、予算・老朽度・政治で決まり、コイン投げにできません。割付が非ランダム=Day 3の交絡構造に戻る、というのが現場の出発点です。だからこそ Day 6以降の傾向スコア・マッチングで「観察データを実験に近づける」技術が要ります。
- せめて"部分的な無作為化"を仕込む。 全面ランダムが無理でも、候補地区の一部だけ無作為に先行導入すれば、そこだけは黄金律の推定が効きます。「どこをランダムにできるか」を設計段階で1つ確保するだけで、後段の因果推論はぐっと楽になります(空間的な波及=干渉の扱いはDay 28で深掘り)。
参考(本棚)
- Imbens & Rubin『Causal Inference for Statistics, Social, and Biomedical Sciences』第4章 — 無作為化実験・Fisher/Neymanの推論(今日の下敷き)
- 安井翔太『効果検証入門』第1章 — RCTと「バイアスのない比較」(A/Bテストの実務視点)
- 伊藤公一朗『データ分析の力』 — RCTがなぜ信頼できるかを数式なしで
- データ出典:Thornton, R. (2008) "The Demand for, and Impact of, Learning HIV Status", American Economic Review.
causaldataパッケージthornton_hiv(MIT License)。取得日 2026-07-13。
次回予告(Day 5)
今日の「引き算=因果」の背後には、実は2つの異なる推論哲学が隠れています。「効果がない」とは全員でゼロ(Fisherの鋭い帰無仮説)なのか、平均でゼロ(Neymanの反復サンプリング)なのか——同じRCTデータを2通りに検定してみます。並べ替え検定(permutation test)を自作して、p値が"絵"で見える体験をします。

