この連載について
因果推論を「本を読んだ」で終わらせず、自分の言葉で説明でき、コードで再現できる状態まで落とす30日連載です。直前のDay 15では、DoWhyの4ステップで「仮定→識別→推定→反証」を1本のワークフローに束ねました。今日からPhase 3、計量経済のデザインに入ります。初回の道具は、深層学習でも傾向スコアでもなく、昔ながらの線形回帰です。なぜ因果推論の実務は今も回帰から始まるのか。その理屈を、合成データと実データ close_college(教育年数と賃金)の両方で確かめます。
TL;DR(3行)
-
回帰は因果推論の実務の主力。
y ~ w + C(g)の係数は層別比較の加重平均そのもので、合成例では素朴な差 5.03 に対し回帰 2.98(真値 3.0)。ただし重みは層の規模ではなく層内の処置の分散 $e(1-e)$ です。 - FWL定理:重回帰の係数=「残差×残差」の単回帰。 close_college では educ の係数 0.0746 が、統制変数で残差化した残差同士の単回帰と差 1.5e-16 で厳密に一致。「統制する」の正体は残差化で、Day 24 の DML はこの絵の機械学習版です。
- 統制変数の係数は因果と読めません(Table 2 fallacy)。 同じ回帰でも処置の係数は 2.02(真値 2.0)と当たり、統制変数の係数は 1.72(直接効果 1.0)と大外れ。調整集合は処置のために選んだものだからです。
今日の問い
深層学習の時代に、因果推論の実務はなぜ、いまだに線形回帰から始まるのか?
答えの半分は「回帰が層別比較の自動化だから」で、残りの半分は「統制の意味が残差化として厳密に分かっているから」です。今日のゴールは、この2つを式とコードで言語化することです。あわせて、回帰を因果に使うときの代表的な事故(統制変数の係数を因果と読む Table 2 fallacy)まで、真値を握った合成データで踏み抜いておきます。
概念① 回帰による調整:係数は層別比較の加重平均
Day 6 で、交絡因子 $X$ の層に切ってから処置群と対照群を比べると、素朴な差 4.92 が真値 3.0 に戻ることを見ました。あのとき回帰 y ~ w + x も 3.00 を返しましたが、なぜ回帰で層別と同じことが起きるのかは説明していませんでした。
種明かしは、回帰係数の正体が層内比較の加重平均であることです。処置 $W$ が2値で、離散の層 $g$ をダミー変数で入れた(飽和させた)回帰では、$W$ の係数が厳密に次の形になります。
$$
\hat\beta_w = \frac{\sum_g n_g, \hat e_g (1-\hat e_g), \hat\Delta_g}{\sum_g n_g, \hat e_g (1-\hat e_g)}
$$
ここで $\hat\Delta_g$ は層 $g$ の中での処置群と対照群の平均差、$n_g$ は層の規模、$\hat e_g$ は層内の処置割合です。つまり回帰は、層の中で比べて平均するという Day 6 の手作業を自動でやっています。ただし注意がひとつ。重みが層の規模 $n_g$ ではなく $n_g \hat e_g(1-\hat e_g)$、つまり層内で処置がどれだけ「混ざっている」かに比例します。処置が半々の層($e=0.5$)を最も重視し、ほぼ全員処置またはほぼ全員対照の層をほとんど無視するのです。比較の情報量が多い層を重視する、という意味で合理的な設計ですが、ATE の定義(層の規模で加重)とは別物です。この差は効果が層ごとに違うときに表に出ます。手を動かす①で数値で見ます。
概念② FWL定理:「統制する」の正体は残差化
重回帰にはもうひとつ、Frisch-Waugh-Lovell 定理(FWL)という強力な見方があります。重回帰 $y = \beta_w w + X\gamma + \varepsilon$ の係数 $\hat\beta_w$ は、次の3手順で得られる値と厳密に一致します。
- $w$ を統制変数 $X$ に回帰し、残差 $\tilde w$ を取る
- $y$ を統制変数 $X$ に回帰し、残差 $\tilde y$ を取る(この手順は省略しても係数は同じです)
- $\tilde y$ を $\tilde w$ に単回帰した傾きを読む
言葉にすると、「$X$ で統制する」とは「$w$ の変動のうち $X$ で説明できる部分を捨て、残った変動だけで効果を測る」ことです。何十個の統制変数が入った重回帰も、最後は残差同士の2次元の散布図に還元できます。この見方が効く場面は2つあります。ひとつは診断で、係数がどの変動から推定されているかを散布図で目視できます。もうひとつが Day 24 への伏線で、Double Machine Learning は手順1と2の回帰を機械学習に置き換えたものです。残差×残差という絵をここで頭に焼き付けておくと、DML が「新しい魔法」ではなく FWL の現代版だと分かります。
手を動かす①:Day 6 の 3.00 の正体、回帰=層別の分散加重平均
まず概念①の等式を、真値を握った合成データで確かめます。Day 6 と同じ構造(層が上の人ほど処置されやすく、素の $Y$ も高い)を、今日は3つの離散の層で作ります。真の効果は全員 $\tau = 3.0$ です。
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
rng = np.random.default_rng(16)
N = 6000
g = rng.choice([0, 1, 2], size=N, p=[0.5, 0.3, 0.2]) # 層(離散の共変量)
e = np.array([0.1, 0.5, 0.9])[g] # 層ごとの処置確率
w = rng.binomial(1, e) # 割付は層に依存=交絡
y0 = 5 + 2.0 * g + rng.normal(0, 1, N) # 層が上がるほどYも高い
y = y0 + 3.0 * w # 真の効果 τ = 3.0(全員同じ)
d = pd.DataFrame({"y": y, "w": w, "g": g})
# 層ごとの差 Δ_g と、2種類の重みの材料
delta = np.array([y[(g == k) & (w == 1)].mean() - y[(g == k) & (w == 0)].mean()
for k in range(3)])
n_g = np.bincount(g) # 層の規模
var_g = np.array([w[g == k].var() for k in range(3)]) # 層内の処置の分散 e(1-e)
print("素朴な差 = %.2f" % (y[w == 1].mean() - y[w == 0].mean()))
print("層別(規模で加重) = %.4f" % (delta @ n_g / n_g.sum()))
print("回帰 y ~ w + C(g) = %.4f" % smf.ols("y ~ w + C(g)", d).fit().params["w"])
print("層別(分散で加重) = %.4f" % (delta @ (n_g * var_g) / (n_g * var_g).sum()))
素朴な差 = 5.03
層別(規模で加重) = 2.9983
回帰 y ~ w + C(g) = 2.9774
層別(分散で加重) = 2.9774
素朴な差 5.03 に対し、層別も回帰も真値 3.0 のそばに戻ります。注目してほしいのは下2行です。回帰係数 2.9774 は、層ごとの差を $n_g \hat e_g(1-\hat e_g)$ で加重した平均と小数第4位まで一致しています(一致は偶然ではなく恒等式です。層をダミーで飽和させた回帰では厳密に成り立ちます)。効果が全員同じなら、どんな正の重みで平均しても答えは 3.0 付近なので、規模加重(2.9983)との違いは誤差の内です。Day 6 の 3.00 の正体は、この「自動層別」でした。
では、効果が層ごとに違ったらどうなるか。同じ割付のまま、真の効果を層0から順に 1.0、3.0、5.0 にします。層の規模は 0.5 : 0.3 : 0.2 なので、真の ATE は $0.5 \times 1 + 0.3 \times 3 + 0.2 \times 5 = 2.4$ です。
tau_g = np.array([1.0, 3.0, 5.0]) # 層ごとに効果が違う世界(真の ATE = 2.4)
y2 = y0 + tau_g[g] * w
d2 = pd.DataFrame({"y": y2, "w": w, "g": g})
delta2 = np.array([y2[(g == k) & (w == 1)].mean() - y2[(g == k) & (w == 0)].mean()
for k in range(3)])
print("層別(規模で加重)=ATEの推定 = %.2f" % (delta2 @ n_g / n_g.sum()))
print("回帰 y ~ w + C(g) = %.2f" % smf.ols("y ~ w + C(g)", d2).fit().params["w"])
print("回帰が各層に置く重み =", np.round(n_g * var_g / (n_g * var_g).sum(), 2))
層別(規模で加重)=ATEの推定 = 2.43
回帰 y ~ w + C(g) = 2.63
回帰が各層に置く重み = [0.31 0.56 0.13]
規模で加重した層別は 2.43 と真の ATE 2.4 に戻る一方、回帰は 2.63 を返します。回帰が半分近い重み(0.56)を、処置が半々に混ざった層1(効果 3.0)に置くからです。これは回帰のバグではなく、「分散加重平均という別の推定対象を答えている」だけです。効果が均一なら区別は消えますが、異質なら乖離します。ATE が欲しいときは規模で加重し直すか、IPW(Day 7)を使います。効果の異質性そのものは Day 22 の主役です。
手を動かす②:close_college で FWL、残差×残差は厳密に一致する
次は実データで FWL です。データは causaldata の close_college(Card 1995 の NLS 抽出、N=3010)で、教育年数 educ が対数賃金 lwage をどれだけ動かすかを問います。統制変数は職務経験(2乗項つき)・人種・南部・都市部の5本です。OLS の結果表の読み方(p値・信頼区間・頑健標準誤差)そのものは今日の主題から外し、係数の因果解釈だけに集中します。
import pandas as pd
import statsmodels.formula.api as smf
from causaldata import close_college
d = close_college.load_pandas().data # Card (1995) の NLS 抽出。N = 3010
controls = "exper + I(exper**2) + black + south + smsa"
short = smf.ols("lwage ~ educ", d).fit() # 統制なし
long = smf.ols("lwage ~ educ + " + controls, d).fit() # 統制あり(重回帰)
# FWL: educ と lwage をそれぞれ統制変数で残差化し、残差同士で単回帰
r_educ = smf.ols("educ ~ " + controls, d).fit().resid
r_lwage = smf.ols("lwage ~ " + controls, d).fit().resid
fwl = smf.ols("r_lwage ~ r_educ",
pd.DataFrame({"r_lwage": r_lwage, "r_educ": r_educ})).fit()
print("統制なしの educ 係数 = %.4f" % short.params["educ"])
print("統制ありの educ 係数 = %.4f" % long.params["educ"])
print("残差×残差の単回帰係数 = %.4f" % fwl.params["r_educ"])
print("両者の差 = %.1e" % abs(long.params["educ"] - fwl.params["r_educ"]))
統制なしの educ 係数 = 0.0521
統制ありの educ 係数 = 0.0746
残差×残差の単回帰係数 = 0.0746
両者の差 = 1.5e-16
重回帰の educ 係数 0.0746 と、残差×残差の単回帰の傾きは、差 1.5e-16、つまり浮動小数点の丸めの範囲で厳密に一致しました。5本の統制変数が入った重回帰が、2次元の散布図1枚に還元されたわけです。
係数の動きも見どころです。統制なしの 0.052 が、統制を入れると 0.075(教育1年あたり賃金 約+7.5%)へ、むしろ大きくなりました。交絡の調整は効果を小さくするとは限りません。このデータでは経験年数が exper = 年齢 - educ - 6 とほぼ機械的に定義されるため、教育が長い人ほど経験が短く、経験の賃金効果が educ の係数を押し下げていました。統制するとその混入が外れて係数が上がります。私は最初この結果を見たとき、統制を増やしたのに係数が大きくなるのはおかしいと思って、回帰式のタイポを疑いました。交絡の調整は係数を小さくする方向にしか働かない、と無意識に思い込んでいたからです。ちなみに下の図の右を見ると、残差化した educ の横の広がりが元の educ より縮んでいます。統制後の係数は、この残った変動だけから推定されています。統制変数を増やすほど、係数の推定に使える処置の変動は痩せていく。この感覚も FWL の散布図が教えてくれます。
手を動かす③:統制変数の係数は因果でない(Table 2 fallacy)
回帰表には educ 以外の係数もずらりと並びます。ここで事故が起きます。統制変数の係数まで因果効果として読んでしまう、疫学で Table 2 fallacy(論文の表2に回帰係数一覧が載ることから)と呼ばれる誤読です。真値を握った合成データで、この事故を安全に踏んでみます。処置 $W$ の交絡因子 $Z$ を統制する正しい回帰、ただし $Z$ 自身には観測できない要因 $U$ がぶら下がっている、という設定です。
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf
rng = np.random.default_rng(16)
N = 20000
u = rng.normal(0, 1, N) # 観測できない要因(Zだけの交絡)
z = 0.8 * u + rng.normal(0, 1, N) # 統制変数(Wの交絡因子)
w = rng.binomial(1, 1 / (1 + np.exp(-0.9 * z))) # 処置はZで決まる
y = 2.0 * w + 1.0 * z + 1.5 * u + rng.normal(0, 1, N) # Wの効果2.0・Zの直接効果1.0
m = smf.ols("y ~ w + z", pd.DataFrame({"y": y, "w": w, "z": z})).fit()
print("w の係数 = %.3f (真の効果 2.0)" % m.params["w"])
print("z の係数 = %.3f (Z の直接効果は 1.0)" % m.params["z"])
w の係数 = 2.024 (真の効果 2.0)
z の係数 = 1.724 (Z の直接効果は 1.0)
同じ1本の回帰から出た2つの係数の運命が、見事に分かれました。$W$ の係数 2.02 は真の効果 2.0 に一致します。$W$ のバックドアパス $W \leftarrow Z \rightarrow Y$ は $Z$ の統制で閉じているからです(Day 11)。ところが $Z$ の係数 1.72 は、$Z$ の直接効果 1.0 からも、$W$ 経由を含めた総合効果からも外れた、因果として読めない数字です。$Z$ 自身のバックドアパス $Z \leftarrow U \rightarrow Y$ は誰も閉じていないので、$U$ の交絡がまるごと乗っています。
理屈は単純で、調整集合は処置のために選んだものだからです。バックドア基準(Day 11)は「$W$ から $Y$ への効果を識別するには何を統制するか」に答えたのであって、$Z$ の効果の識別条件はまったく別の問題です。$Z$ を主役にするなら、$Z$ のバックドアを閉じる調整集合を選び直して、もう1本回帰を組む必要があります。1本の回帰表で因果を語ってよい係数は、原則として処置の1つだけです。
つまづき・誤解しやすい点
- 「統制変数は多いほど安全」ではありません。 合流点や中間変数を入れると、閉じていたパスが開きバイアスが生まれます(Day 11 の悪い調整)。close_college で言えば、educ の結果である職種や役職を統制に入れると、効果の通り道を自分で塞ぐことになります。統制してよいのは処置に先立つ変数だけ、選ぶ規則はバックドア基準です。
- 回帰の係数は ATE そのものではありません。 手を動かす①のとおり、回帰が返すのは分散加重平均で、効果が異質なら ATE(2.43)と回帰(2.63)は割れます。効果が均一に近ければ実害はありませんが、「誰に効くか」が論点になった瞬間に効いてくる差です(Day 22)。
- 統制しても、観測できない交絡は1ミリも消えません。 今日の 0.0746 も「観測した5変数で閉じられるバックドアを閉じた」だけの数字です。教育を長く受ける人はもともと賃金を稼ぐ能力が高いかもしれず、能力の列は回帰式の右辺のどこにもありません。非交絡性が検証できない事情は Day 6 のまま、回帰はそれを1行も解決していません。この続きが明日の主題です。
GISデータ実務での使い方
-
最初の1本はいつも統制つき OLS にしています。 自治体の施策評価でも漏水率の要因分析でも、いきなり凝った手法に行かず、
y ~ w + 統制変数をベースラインに置きます。係数・標準誤差・仕様が1行で共有でき、誰でも追試できるからです。凝った手法の出番は「OLS と答えがどうズレたか」を語れるようになってからで、そのズレの言語化に今日の分散加重と FWL がそのまま効きます。 - 回帰表の読み回しに Table 2 fallacy の線を引きます。 管路劣化の重回帰で、主役の管路年数と一緒に管種・土壌・水圧の係数が並ぶと、「土壌の係数が大きいから土壌対策を」という読みが自然に始まってしまいます。因果として設計した係数は処置の1つだけ、他は調整のための同乗者と、レポートに明記しておくと事故が減ります。
- 残差化は空間データの発想と地続きです。 座標や地域ダミーで処置と結果を残差化してから関係を見る、いわゆる空間トレンド除去は、FWL の手順そのものです。空間的に滑らかな交絡をどこまで残差化で外せるかは Day 28 で、残差化を機械学習に置き換える一般化は Day 24 で掘ります。
参考(本棚)
- Matheus Facure『Causal Inference for the Brave and True』第4章。今日のタイトルの出典。回帰・FWL・統制の直感。今日の下敷き
- Angrist & Pischke『Mostly Harmless Econometrics』第3章。回帰係数=分散加重平均(手を動かす①の等式)の導出と議論
- Wooldridge『Introductory Econometrics』。FWL(部分回帰)の教科書的な整理
- Westreich & Greenland (2013) The Table 2 Fallacy。統制変数の係数を読む事故の命名元
- データ: close_college(
causaldataパッケージ経由、MITライセンス、取得日2026-08-15)。Card (1995) の NLS 抽出。明日の Day 17 も同じデータを使います
次回予告(Day 17)
今日の 0.0746 には、まだ大物の交絡が残っています。能力です。能力が高い人ほど教育を長く受け、教育と無関係に賃金も高いなら、educ の係数は上振れします。しかも能力はどのデータセットにも列がありません。観測できない交絡の前では、回帰は手も足も出ない。ここで登場するのが、Card (1995) がこのデータで使った操作変数法です。「実家の郡に4年制大学があるか」という外生のてこ( nearc4 列)で、観測できない交絡ごと educ を動かして効果を取り出します。2SLS を2回の回帰で手組みするところから、明日も close_college で手を動かします。


