Mímisbrunnr知恵の泉

← 因果推論 一覧

🎓 レベル:標準 | 重要度:B(標準)

📎 前提:なぜRCTが黄金律か | 数理:フィッシャーの3原則乱塊法・ラテン方格(統計)

要点(BLUF)


1. フィッシャーの3原則を因果の言葉で

実験計画法の3原則(フィッシャーの3原則)は、そのまま RCT の設計指針になる。

原則統計(誤差の扱い)因果での意味
反復(replication)偶然誤差を測定可能にするATE の標準誤差を見積もり、検定・区間推定を可能にする
無作為化(randomization)系統誤差を偶然誤差に変換交換可能性を保証し、交絡バイアスを断つ(なぜRCTが黄金律か
局所管理(local control)既知の変動源を分離・除去既知の予後因子をブロック化・調整して ATE 推定の分散を下げる

このノートが扱うのは3番目の局所管理と、その分析版である共変量調整だ。無作為化(2番目)が「偏り」を断つのに対し、局所管理は「ばらつき」を縮める。役割が違う。


2. 共変量調整:不偏性は不変、分散が下がる

結果 YY を予測する共変量 XX(ベースラインの重症度、過去の購買額など)を測っているとする。RCT では割り当て TTXX と独立(TXT \perp X)なので、XX を回帰に入れても TT の係数は不偏のままだ。

flowchart LR
    R["ランダム化"] --> T["処置T"]
    X["予後因子X(Yを強く説明)"] --> Y["結果Y"]
    T --> Y

XX から TT への矢印が無い(ランダム化ゆえ独立)ので、XX は交絡ではない。にもかかわらず XX を調整する理由は精度にある。

なぜ分散が下がるか

素朴な差(difference in means)の分散は、結果のばらつき Var(Y)\operatorname{Var}(Y) をそのまま引きずる。

Var(τ^DM)=Var(YT=1)n1+Var(YT=0)n0\operatorname{Var}(\hat\tau_{\text{DM}}) = \frac{\operatorname{Var}(Y\mid T=1)}{n_1} + \frac{\operatorname{Var}(Y\mid T=0)}{n_0}

一方、共変量調整(ANCOVA)は次の回帰の τ\tau を推定する。

Y=α+τT+βX+εY = \alpha + \tau\, T + \beta\, X + \varepsilon

XXYY を説明する分だけ残差 ε\varepsilon のばらつきが小さくなるXX で説明される割合を R2R^2 とすると、推定量の分散はおおよそ

Var(τ^adj)(1R2)Var(τ^DM)\operatorname{Var}(\hat\tau_{\text{adj}}) \approx (1 - R^2)\,\operatorname{Var}(\hat\tau_{\text{DM}})

に縮む。標準誤差でいえば 1R2\sqrt{1-R^2} 倍。XX が結果をよく説明する(R2R^2 が大きい)ほど効く。重要なのは、TXT \perp X ゆえにこの調整で τ\tau の中心はずれない点だ。偏りを増やさずにばらつきだけ削れる。


3. コード:素朴な差 vs 回帰調整(分散低減の実証)

予後因子 XX が結果を強く規定する(係数 55、ノイズSD 11 なので R225/260.96R^2 \approx 25/26 \approx 0.96)RCT を多数回くり返し、素朴な差と回帰調整の「平均(偏り)」と「標準偏差(ばらつき)」を比べる。

import numpy as np
import statsmodels.api as sm

# === RCTでの「素朴な差」と「共変量調整(回帰)」を多数反復で比べる ===
# どちらも不偏。だが予後因子を調整すると分散(=標準誤差)が下がることを確かめる
rng = np.random.default_rng(0)
ATE_true = 2.0
n = 200          # 1実験のサンプルサイズ
n_rep = 3000     # 反復回数

dm_estimates = []     # 差分(difference in means)
adj_estimates = []    # 共変量調整(回帰)
dm_se = []            # 各推定の報告標準誤差
adj_se = []

for _ in range(n_rep):
    # 予後因子 X(結果を強く説明する。例:ベースラインの重症度や過去の購買額)
    X = rng.normal(0.0, 1.0, size=n)
    # 割り当て T はコイン投げ(X と独立=ランダム化)
    T = rng.binomial(1, 0.5, size=n)
    # 結果:X が結果を強く規定(係数5)。処置効果は ATE_true 一定
    Y = 10.0 + 5.0 * X + ATE_true * T + rng.normal(0.0, 1.0, size=n)

    # (1) 素朴な差:処置群平均 - 対照群平均
    dm = Y[T == 1].mean() - Y[T == 0].mean()
    dm_estimates.append(dm)
    # 差分の標準誤差(2標本の標準誤差)
    v1 = Y[T == 1].var(ddof=1) / (T == 1).sum()
    v0 = Y[T == 0].var(ddof=1) / (T == 0).sum()
    dm_se.append(np.sqrt(v1 + v0))

    # (2) 共変量調整:Y を T と X で回帰し、T の係数を効果とする
    design = sm.add_constant(np.column_stack([T, X]))
    model = sm.OLS(Y, design).fit()
    adj_estimates.append(model.params[1])   # T の係数
    adj_se.append(model.bse[1])

dm_arr = np.array(dm_estimates)
adj_arr = np.array(adj_estimates)

print(f"真の効果 ATE_true             = {ATE_true:+.3f}")
print(f"素朴な差     : 平均={dm_arr.mean():+.3f}  実測SD={dm_arr.std():.3f}  平均報告SE={np.mean(dm_se):.3f}")
print(f"共変量調整   : 平均={adj_arr.mean():+.3f}  実測SD={adj_arr.std():.3f}  平均報告SE={np.mean(adj_se):.3f}")
print(f"\n分散低減率(調整SD / 素朴SD)= {adj_arr.std()/dm_arr.std():.3f}")
print(f"理論値 sqrt(1 - R^2) の目安  = {np.sqrt(1 - 25/26):.3f}  (Var(5X)=25, Var(noise)=1)")

出力:

真の効果 ATE_true             = +2.000
素朴な差     : 平均=+1.998  実測SD=0.735  平均報告SE=0.722
共変量調整   : 平均=+2.000  実測SD=0.142  平均報告SE=0.142

分散低減率(調整SD / 素朴SD)= 0.192
理論値 sqrt(1 - R^2) の目安  = 0.196  (Var(5X)=25, Var(noise)=1)

出力の意味:素朴な差も回帰調整もどちらも真値 2.02.0 を中心にしている(偏りは増えていない)。違いはばらつきで、標準偏差が 0.7350.1420.735 \to 0.142 へ約 1/51/5 に縮んだ。実測の低減率 0.1920.192 は理論値 1R2=0.196\sqrt{1-R^2}=0.196 とほぼ一致する。さらに「実測SD」と「平均報告SE」がどちらの手法でも近い(0.1420.1420.142 \approx 0.142)ことから、報告される標準誤差が正しく精度を表していると分かる。同じ被験者数でより狭い信頼区間が得られる=必要サンプル数を減らせる、というのが共変量調整の実利だ。


4. ブロック化(局所管理):設計段階で釣り合わせる

共変量調整が「分析」で精度を上げるのに対し、ブロック化は「設計」で精度を上げる。既知の予後因子(病院・性別・地域など)でブロックを作り、ブロック内でランダム割り当てする。これがフィッシャーの局所管理=乱塊法乱塊法・ラテン方格)だ。

完全ランダム化では、偶然により予後因子が片群に偏ることがある(小標本ほど深刻)。ブロック内で割り当てれば、その予後因子は設計上ぴったり釣り合うので、偶然の不均衡が生む余分なばらつきが消える。推定は層別推定量

τ^strat=bnbn(Yˉb,1Yˉb,0)\hat\tau_{\text{strat}} = \sum_{b} \frac{n_b}{n}\,\big(\bar Y_{b,1} - \bar Y_{b,0}\big)

で各ブロックの差を人数で重み付けして合算する。


5. コード:ブロック化の分散低減

予後因子 BB(2つの病院など、結果を +8+8 底上げ)について、(1) 完全ランダム化+単純差分 と (2) ブロック内ランダム化+層別推定量 を比べる。

import numpy as np

# === ブロック化(層内ランダム化)が分散を下げることを多数反復で確かめる ===
# 予後因子 B(例:2つの病院/性別)でブロックを作り、ブロック内でランダム割り当て
rng = np.random.default_rng(1)
ATE_true = 2.0
n_per_block = 100      # 各ブロックの人数
n = 2 * n_per_block
n_rep = 4000

# ブロック指標 B:前半100人が B=0、後半100人が B=1(B は結果を強く左右する)
B = np.r_[np.zeros(n_per_block), np.ones(n_per_block)].astype(int)

complete_est = []   # 完全ランダム化 + 単純差分
blocked_est = []    # ブロック内ランダム化 + 層別推定量

for _ in range(n_rep):
    Y0 = 10.0 + 8.0 * B + rng.normal(0.0, 1.0, size=n)   # B=1 は底上げ +8
    Y1 = Y0 + ATE_true

    # (1) 完全ランダム化:ブロックを無視して全体をコイン投げ
    T_c = rng.binomial(1, 0.5, size=n)
    Y_c = np.where(T_c == 1, Y1, Y0)
    if T_c.sum() > 0 and (1 - T_c).sum() > 0:
        complete_est.append(Y_c[T_c == 1].mean() - Y_c[T_c == 0].mean())

    # (2) ブロック化:各ブロック内でちょうど半数を処置に割り当て
    T_b = np.zeros(n, dtype=int)
    for b in [0, 1]:
        idx = np.where(B == b)[0]
        chosen = rng.choice(idx, size=n_per_block // 2, replace=False)
        T_b[chosen] = 1
    Y_b = np.where(T_b == 1, Y1, Y0)
    # 層別推定量:ブロックごとの差分を人数で重み付け平均
    tau_b = 0.0
    for b in [0, 1]:
        m = (B == b)
        diff_b = Y_b[m & (T_b == 1)].mean() - Y_b[m & (T_b == 0)].mean()
        tau_b += (m.sum() / n) * diff_b
    blocked_est.append(tau_b)

complete_arr = np.array(complete_est)
blocked_arr = np.array(blocked_est)

print(f"真の効果 ATE_true                  = {ATE_true:+.3f}")
print(f"完全ランダム化 + 単純差分 : 平均={complete_arr.mean():+.3f}  SD={complete_arr.std():.3f}")
print(f"ブロック化 + 層別推定量   : 平均={blocked_arr.mean():+.3f}  SD={blocked_arr.std():.3f}")
print(f"\n分散低減率(ブロックSD / 完全SD)= {blocked_arr.std()/complete_arr.std():.3f}")

出力:

真の効果 ATE_true                  = +2.000
完全ランダム化 + 単純差分 : 平均=+1.991  SD=0.575
ブロック化 + 層別推定量   : 平均=+2.002  SD=0.139

分散低減率(ブロックSD / 完全SD)= 0.242

出力の意味:こちらも両方とも真値 2.02.0 を中心にする(不偏)。だが予後因子 BB を設計で釣り合わせたブロック化は、標準偏差が 0.5750.1390.575 \to 0.139 へ約 1/41/4。完全ランダム化では「たまたま B=1B=1 の人が処置群に多く入る」偶然の不均衡が毎回ばらつきを生むのに対し、ブロック化はそれを断つ。測れる予後因子は、調整するより先に設計で釣り合わせておくのが上策(事後の調整も併用できる)。


6. 直観:偏りとばらつきは別物

なぜRCTが黄金律か で見たように、無作為化が解決するのは偏り。このノートの共変量調整・ブロック化が削るのはばらつき。両者は独立した問題で、対処法も違う。

予後因子で説明できる分を取り除くと、残った「説明できないノイズ」だけが推定のばらつきになる。これは フィッシャーの3原則 の局所管理(誤差からブロック間差を除く)と完全に同じ発想だ。


⚠️ よくある誤解・落とし穴

(1)処置の「後」に決まる変数で調整してはいけない(過剰調整) 共変量調整が安全なのは、調整する XX処置より前に確定したベースライン変数だから(TXT \perp X)。処置の結果として変化する中間変数(媒介変数)を調整に入れると、効果の一部を消したり、衝突点バイアスを生んだりする。これは因果ダイアグラムとd分離の衝突点・媒介の話で、観察データの調整でも同じ罠がある(回帰による調整とその限界)。

(2)「調整したら効果が変わった」=交絡があった、ではない RCT では調整しても τ\tau の中心は変わらない(変わるのは精度だけ)。1回の実験で点推定が動いて見えるのは偶然の不均衡の補正であって、観察データのような交絡の補正とは意味が違う。

(3)単純な線形回帰調整の微小バイアス(Freedman 批判と Lin の修正) 処置効果が共変量によって異なる(異質効果)と、YT+XY \sim T + X の単純な回帰調整は小標本でわずかに偏りうる、と Freedman (2008) が指摘した。Lin (2013) は処置と中心化共変量の交互作用 T×(XXˉ)T\times(X-\bar X) を入れることでこの問題が漸近的に解消し、調整が精度を下げることは決してないと示した。実務では交互作用込みの調整が無難。

(4)層を細かく切りすぎる ブロック/層を細分しすぎて各層の人数が極端に少ないと、層別推定量自体が不安定になる。連続的な予後因子は無理に層別せず、回帰で調整する方が素直なことが多い。


関連ノート