この用語ページの主要トピックを一覧から飛べます。
🍰 まずはやさしく
複数の原因から結果を予想する道具です。
何がどれくらい影響しているか調べます。
テストの点数を勉強時間と睡眠時間で予想します。
この章では分析の結論を出す方法を学びます。
🍰 まずはやさしく
統計の分析でよく使われる基本の手法です。
複雑なデータの関係を整理するために使います。
地域の健康状態を色々なデータで分析します。
この章ではこの手法がどう使われるかを見ます。
論文や報告書で 「重回帰分析」「multiple regression」「OLS による多変量回帰」「偏回帰係数」「調整済み R²」「Ridge / Lasso 回帰」 といった表現が出てきたら、 このページが該当します。
本コンペでは、 都道府県や市町村の 死亡率・人口減少率・経済成長率 といったマクロ指標を、 高齢化率・産業構造・気候・所得・医療資源 など複数の説明変数で説明したいときに毎回登場します。 「単相関で見えていた関係が、 第3変数を入れたら消える」現象を 統計的に処理するための 基幹技術 です。
単相関は「2 変数の散布図に直線を引く」絵で済みますが、 重回帰は「3 次元以上の超平面」を引きます。 視覚化はできなくても、 数式の上では同じ「最小二乗法(OLS)」で解けます。 教育的には p=2(説明変数 2 個=3 次元)の絵を一度しっかり描く ことで、 高次元に対する直感を養いやすくなります。
なお、 本ページでは OLS による重回帰 を主軸に扱い、 Ridge/Lasso/Elastic Net などの正則化版や、 ロジスティック・GAM といった非線形化は 関連手法セクション でリンクします。 因果推論的な利用は パネル因果 へ。
🍰 まずはやさしく
データの世界に平らな面を引くイメージです。
見かけ上の関係にだまされないために使います。
気温と死亡率の関係を高齢化率で考え直します。
この章では図や例を使って仕組みを理解します。
単回帰は「散布図に 直線 を引く」絵で理解できます。 では説明変数が 2 個になったら? y, x₁, x₂ の 3 次元空間で 平面 を引くイメージです。 3 個以上だと視覚化はできませんが、 数式上は 超平面 を引いていることになります。
SSDSE-B-2026 で死亡率(A4200 系の代理指標)を 4 変数で説明したい:
単純に「死亡率 vs 求人倍率」の単相関を取ると、 たとえば r = +0.31 と出るかもしれません。 でも本当に「不景気 → 死亡率上昇」なのでしょうか? 実は東北など雪国では同時に高齢化が進んでいる。 つまり「高齢化率」という第 3 変数が両方を動かしている 交絡(confounding) の典型です。
そこで重回帰の出番。 x₁(高齢化率)を式に同時投入すると、 求人倍率の偏回帰係数 β₃ は「高齢化を一定にしたうえでの求人倍率の追加効果」になり、 多くの場合大きく縮みます。 偏回帰係数こそ、 政策議論で本当に欲しい数値。 これが重回帰の威力です。
47 都道府県を 4 次元空間に浮かべた 点雲とイメージしてください。 重回帰は、 この雲を 最も低い残差二乗和で貫く超平面 をハサミで探す作業。 ハサミで切った断面が「y の予測値 ŷ」になります。 各説明変数の β は その軸方向への平面の傾き に対応します。
理論を覚える前に、 もっと身近な例を 1 つ:
「単純に客数だけで回帰したら β = 950 円」だったとしても、 重回帰で気温・曜日を入れると β = 1,050 円かもしれない。 なぜなら、 暑い休日には客数だけでなく 客単価も上がる 傾向があり、 単回帰は「客数だけ」の効果ではなく「客数+それに付随するもの」を混ぜていたから。 重回帰で他要因を一定にして、 ようやく 客 1 人あたりの真の売上貢献が見えてくる。
$\hat{y} = 2 + 3x_1 - 1.5x_2$ を 3 次元空間で描くと、 切片 2 で、 x₁ 方向に 3 ずつ上がり、 x₂ 方向に 1.5 ずつ下がる「斜めの板」になります。 残差は各点から板までの「縦方向の距離」。 これらを二乗して合計した値が最小になる板を選ぶのが OLS です。 視覚化ツール(plotly や matplotlib の 3D 散布図)で一度自分の手で描いてみると、 偏回帰係数の「軸方向の傾き」という意味が腑に落ちます。
「マンション価格を予測する」のは典型的な重回帰タスク。 説明変数として築年数・面積・駅距離・階数・周辺所得・最寄り路線などを使う。 例えば
$\hat{\text{価格}} = 800 + 30 \cdot \text{面積}_{m^2} - 5 \cdot \text{築年数} - 2 \cdot \text{駅距離}_{分} + 10 \cdot \text{階数} + \dots$
これだと「同じ面積・駅距離・階数・所得帯のマンションで比べたとき、 築 1 年増えるごとに価格が 5 万円下がる」という解釈ができる。 査定エンジンの古典的バックボーン。 SUUMO や HOMES の自動査定 API はこの拡張版を使っている。
重回帰分析を「式」だけでなく「形」で掴むために、 4 つの観点から視覚化を整理する。 ここでは SSDSE-B-2026(47 都道府県)で実際に確認できる現象を中心に扱う。
単相関は「他の変数を一切無視した素朴な関係」、 偏相関は「他の変数を統計的に固定したうえでの純粋な関係」。 例: 死亡率 y と平均気温 x₄ の単相関は r = −0.44(寒い県ほど死亡率が高い)。 しかし高齢化率 x₁ を固定すると偏相関は ≈ 0 に縮む → 気温の効果は実は高齢化率の影づくり。
| 変数 | 単相関 r(y, xⱼ) | 偏相関 r(y, xⱼ | 残り) | 解釈 |
|---|---|---|---|
| x₁ 高齢化率 | +0.972 | +0.909 | 本物の効果 |
| x₂ 病院数 | +0.524 | −0.038 | 共通要因で見かけだけ |
| x₃ 新規求職申込件数 | −0.594 | +0.215 | 疑似相関(弱い) |
| x₄ 気温 | −0.442 | +0.051 | 疑似相関 |
残差 eᵢ = yᵢ − ŷᵢ を予測値 ŷᵢ の関数としてプロットしたとき、 ランダムに散らばっていれば線形性・等分散性が概ね成立。 曲線パターンが見えたら非線形項追加、 ラッパ状なら HC3 ロバスト SE か WLS。
| 残差プロットの形 | 診断 | 対処 |
|---|---|---|
| 無方向に散布 | 仮定OK | そのままで OK |
| U 字 / 逆 U 字 | 線形性違反 | 多項式項、 GAM |
| ラッパ状(広がる) | 等分散性違反 | HC3 ロバスト SE、 log(y) |
| 周期パターン | 独立性違反 | Newey-West、 ARIMA |
| 外れ値突出 | 影響観測 | Cook 距離、 ロバスト回帰 |
VIFⱼ = 1 / (1 − Rⱼ²) は「変数 xⱼ を残りの説明変数で回帰したときの説明されにくさの逆数」。 5 を超えたら警戒、 10 で深刻。 多重共線性が深刻な場合、 個別の β̂ は不安定だが予測そのものは依然有効、 という点を見落とさない。
単位が異なる x(人口、 温度、 倍率)を直接比較できないため、 z 標準化した X で回帰し直すか、 β*ⱼ = β̂ⱼ × (sₓⱼ / s_y) を計算する。 SSDSE 例では x₁: +1.02 が圧倒的、 x₂-x₄ は ≤ 0.07 で実質寄与しない。
これら 4 つの視点を毎回確認すれば、 重回帰の出力を「数字の羅列」から「物語」に変えられる。
このコードでやること: SSDSE-B-2026 を読み込み、 4 説明変数で重回帰し、 偏相関・残差プロット・VIF・標準化係数を一気に出す。
📥 入力: data/raw/SSDSE-B-2026.csv(cp932、 2023 年抽出)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | import pandas as pd, numpy as np from statsmodels.regression.linear_model import OLS from statsmodels.stats.outliers_influence import variance_inflation_factor as vif df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023].copy() y = df['A4200'] / df['A1101'] * 1000 # 死亡率 (千分率) X = pd.DataFrame({ 'age': df['A1303'] / df['A1101'] * 100, # 高齢化率 % 'hosp': df['I510120'] / df['A1101'] * 1e5, # 10万人あたり病院数 'job': df['F3101'], # 新規求職申込件数 'temp': df['B4101'], # 平均気温 }) X = pd.concat([pd.Series(1, index=X.index, name='const'), X], axis=1) res = OLS(y, X).fit() print(res.summary()) print('VIF:', {c: round(vif(X.values, i), 2) for i, c in enumerate(X.columns) if c != 'const'}) |
📤 実行例:
💬 高齢化率だけが圧倒的に有意。 他 3 変数は単相関では見えても重回帰では消える → 疑似相関。
このコードでやること: 偏相関係数を全変数ペアで計算し、 単相関 r と比較する。
📥 入力: 同じ X, y
1 2 3 4 5 6 7 8 | from pingouin import partial_corr for v in ['age', 'hosp', 'job', 'temp']: covars = [c for c in ['age','hosp','job','temp'] if c != v] r_simple = X[v].corr(y) pc = partial_corr(data=pd.concat([y.rename('y'), X.drop(columns='const')], axis=1), x=v, y='y', covar=covars) print(f'{v}: r={r_simple:+.3f} partial r={pc["r"].iloc[0]:+.3f}') |
📤 実行例:
💬 偏相関により「他変数を一定にした純粋な効果」が見える。 気温の効果は消える。
このコードでやること: 残差プロットと QQ プロットで仮定診断。
📥 入力: res(前段の OLS 結果オブジェクト)
1 2 3 4 5 6 7 8 9 10 11 12 13 | import matplotlib.pyplot as plt import scipy.stats as stats fig, ax = plt.subplots(1, 2, figsize=(10, 4)) ax[0].scatter(res.fittedvalues, res.resid) ax[0].axhline(0, color='red', ls='--') ax[0].set_xlabel('Fitted'); ax[0].set_ylabel('Residual') ax[0].set_title('Residual vs Fitted') stats.probplot(res.resid, dist='norm', plot=ax[1]) ax[1].set_title('Normal QQ Plot') plt.tight_layout(); plt.savefig('multireg_diagnostics.png', dpi=130) |
📤 実行例: 残差は方向性なく散布、 QQ プロットはほぼ直線 → 仮定 OK
💬 残差プロットで曲線や広がりが見えなければ、 線形性・等分散性は許容範囲。
このコードでやること: 標準化係数で重要度を可視化。
📥 入力: 同じ X, y
1 2 3 4 5 6 7 | from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LinearRegression Xs = StandardScaler().fit_transform(X.drop(columns='const')) ys = (y - y.mean()) / y.std() beta_std = LinearRegression().fit(Xs, ys).coef_ print(dict(zip(['age','hosp','job','temp'], beta_std.round(3)))) |
📤 実行例:
💬 標準化 β は age が圧倒的(1.02)、 他は ≤0.07 → 実質「高齢化率 1 変数モデル」と等価。
重回帰の核心は 正規方程式 β = (X'X)-1X'y。 これを SSDSE-B-2026 の 47 都道府県データで具体的に値を代入して計算する。 ここでは簡略化のため、 説明変数を 1 つだけ (高齢化率) に絞り、 切片を含む 2 次元線形回帰として手計算する。 47 件の実数値を全て展開すると紙面が膨大になるため、 最初の 5 県のみ 使用した縮小版で計算過程を示す (本来は 47 件で計算)。
最初の 5 都道府県の (高齢化率 [%], 死亡率 [千分率]) を取り出す:
これを X (5×2 行列、 切片列 + 高齢化率) と y (5×1 ベクトル) に並べる:
X' は X の転置 (2×5)。 X' × X は (2×5) × (5×2) = (2×2):
2×2 行列 [[a,b],[c,d]] の逆行列は (1/det) × [[d,-b],[-c,a]]、 det = ad - bc。
X' × y = (2×5) × (5×1) = (2×1):
解釈: 高齢化率が 1% 上がると、 死亡率は 約 0.69 千分率増加。 切片 -7.37 は (age=0 の理論値)、 外挿のため解釈に使わない。 これが手計算で得られた最初の 5 県だけの結果で、 47 県全体ではより精度高い推定 (本文の β₁ ≈ +0.649) になる。
このコードでやること: 上記 Step 1〜5 を numpy で機械的に実行し、 手計算の結果と完全に一致することを確認する。
📥 入力: 5 都道府県の age と death の数値
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import numpy as np # Step 1: データ準備 (5 都道府県の縮小版) age = np.array([33.0, 35.2, 35.0, 29.2, 39.1]) death = np.array([14.8, 17.6, 16.9, 12.7, 19.2]) # 切片列を追加した X 行列を作る X = np.column_stack([np.ones(5), age]) y = death # Step 2: X'X XtX = X.T @ X print("X'X =\n", XtX) # Step 3: (X'X)^(-1) XtX_inv = np.linalg.inv(XtX) print("(X'X)^-1 =\n", XtX_inv) # Step 4: X'y Xty = X.T @ y print("X'y =", Xty) # Step 5: β = (X'X)^-1 X'y beta = XtX_inv @ Xty print("β (切片, 高齢化率) =", beta) print(f"回帰式: death = {beta[0]:+.2f} + {beta[1]:+.2f} × age") |
📤 実行例 (出力):
💬 手計算 (上の Step 5) と完全に一致。 numpy の np.linalg.inv は内部で LU 分解を使うため、 5×5 以下の小行列なら手計算との誤差は無視できる。 ただし n が大きいと数値安定性のため np.linalg.solve(XtX, Xty) や QR 分解 (np.linalg.lstsq(X, y, rcond=None)) を推奨。
上の手計算は 5 県の縮小版。 47 県すべてで同じ計算をすると、 サンプルサイズが増えることで標準誤差が縮み、 推定値が安定する。 詳しい結果は本ページ前半 (5. 補強コード R282) の OLS 回帰結果を参照。 5 県 vs 47 県の β₁ を比較:
| データ範囲 | サンプル数 | β₁ (高齢化率 1% あたり死亡率 [千分率]) | 標準誤差 |
|---|---|---|---|
| 本セクション手計算 | 5 | +0.69 | (計算省略) |
| R282 補強コード OLS | 47 | +0.649 | 0.046 |
差 0.04 は 小標本誤差。 47 件で計算すれば、 標準誤差 0.046 を満たす中央推定 0.649 が得られる (推定値は ±2 SE で 0.56〜0.74 の範囲)。
🍰 まずはやさしく
数式を使って結果を予測するモデルです。
正確な予測のルールを決めるために使います。
スマホの利用時間と成績の関係を数式にします。
この章では計算に使う数式について学びます。
重回帰モデルの基本形:
$$ y_i = \beta_0 + \beta_1 x_{i1} + \beta_2 x_{i2} + \cdots + \beta_p x_{ip} + \varepsilon_i, \quad i = 1, \dots, n $$行列形式(設計行列 X は n×(p+1)、 先頭列は切片用の 1):
$$ \mathbf{y} = X\boldsymbol{\beta} + \boldsymbol{\varepsilon}, \quad \boldsymbol{\varepsilon} \sim \mathcal{N}(\mathbf{0}, \sigma^2 I_n) $$OLS(最小二乗法)の解析解:
$$ \hat{\boldsymbol{\beta}} = \arg\min_{\beta} \sum_{i=1}^n (y_i - X_i^\top \beta)^2 = (X^\top X)^{-1} X^\top \mathbf{y} $$推定量の分散共分散行列:
$$ \operatorname{Var}(\hat{\boldsymbol{\beta}}) = \sigma^2 (X^\top X)^{-1}, \quad \hat{\sigma}^2 = \frac{1}{n-p-1} \sum (y_i - \hat{y}_i)^2 $$決定係数と調整済み R²:
$$ R^2 = 1 - \frac{\mathrm{RSS}}{\mathrm{TSS}} = 1 - \frac{\sum (y_i - \hat{y}_i)^2}{\sum (y_i - \bar{y})^2}, \quad R^2_{\mathrm{adj}} = 1 - (1 - R^2)\frac{n-1}{n-p-1} $$全体検定(F検定):帰無仮説 H₀: β₁ = β₂ = … = βₚ = 0
$$ F = \frac{R^2 / p}{(1 - R^2) / (n - p - 1)} \sim F(p, n-p-1) $$個別係数の t検定:
$$ t_j = \frac{\hat{\beta}_j}{\mathrm{SE}(\hat{\beta}_j)} \sim t(n-p-1) $$VIF(分散拡大因子):x_j を他の説明変数で回帰したときの $R^2_j$ を用いて
$$ \mathrm{VIF}_j = \frac{1}{1 - R^2_j} $$標準化偏回帰係数 β*(変数を z 標準化してから推定):
$$ \beta_j^* = \hat{\beta}_j \cdot \frac{s_{x_j}}{s_y} $$単位の違う変数間で「影響度」を比較するには β* を使う。
Ridge / Lasso 推定量(参考、 正則化版):
$$ \hat{\boldsymbol{\beta}}_{\mathrm{ridge}} = (X^\top X + \lambda I)^{-1} X^\top \mathbf{y}, \quad \hat{\boldsymbol{\beta}}_{\mathrm{lasso}} = \arg\min \Big\{ \|\mathbf{y} - X\beta\|^2 + \lambda \sum_j |\beta_j| \Big\} $$Cook's distance(影響度指標):
$$ D_i = \frac{e_i^2}{p \hat{\sigma}^2} \cdot \frac{h_{ii}}{(1 - h_{ii})^2}, \quad h_{ii} = [X(X^\top X)^{-1} X^\top]_{ii} $$数式 $y_i = \beta_0 + \beta_1 x_{i1} + \cdots + \beta_p x_{ip} + \varepsilon_i$ は暗号ではなく、 1 文字 1 文字に明確な意味があります。 ここでは「重回帰の式を、 統計を知らない人にも説明できるレベルまで」分解します(4 段構え)。
「i 番目の県の死亡率 yᵢ は、 切片 β₀ に、 高齢化率の寄与 β₁x₁ と、 病院数の寄与 β₂x₂ と…… を足したものに、 説明しきれないズレ ε を加えたもの」。 つまり 「目的変数 = 直線的な足し算 + 誤差」 というレシピを書いた式です。 「直線的」とは「説明変数を 2 倍したら y への寄与も 2 倍」という関係。 これが線形モデルの定義。
この式は「料理のレシピ」だと思うと分かりやすい。 完成品(y、 たとえば死亡率)は、 ベースの量 β₀ から始めて、 素材ごとの分量 × 単価 β₁x₁、 β₂x₂、 … を足していくと組み上がる。 ただし、 どんなレシピも「実際に作るとちょっとズレる」。 そのズレが ε(誤差)です。
単回帰との 決定的な違いは、 β₁ が「単独で x₁ と y を見たときの傾き」ではないこと。 重回帰の β₁ は 「他の説明変数 x₂, x₃, …, xₚ を全部一定にした上で」の x₁ の効果。 これを partial(部分・偏)と呼ぶ理由がここにあります。 「高齢化率を一定にしたうえでの、 求人倍率の追加効果」— これは単純な相関よりも はるかに政策的に意味のある情報です。
直感的に 「補正後の効果」「条件付きの効果」 と読むと、 統計を知らない人にも伝わります。 「同じ高齢化率の県同士で比べたとき、 求人倍率の高い県の方が死亡率が 0.18 ポイント低い」のように。 これは Frisch-Waugh-Lovell の定理として正式に証明されており、 「重回帰の β₁ は、 (a) x₁ を他の x で回帰した残差と (b) y を他の x で回帰した残差で、 単回帰した時の傾きと一致する」という美しい性質を持つ。
公式 $\hat{\boldsymbol{\beta}} = (X^\top X)^{-1} X^\top \mathbf{y}$ は天から降ってきたものではありません。 「残差二乗和 RSS = Σ(y − ŷ)² を最小化する β を解析的に解いた結果」です。 RSS を β で微分してゼロと置くと、 正規方程式(normal equations) $X^\top X \hat{\boldsymbol{\beta}} = X^\top \mathbf{y}$ が出てくる。 これを左から $(X^\top X)^{-1}$ で掛けると公式が得られる。
なぜ「二乗和」を最小化するのか? — 大きく 3 つの理由があります:
重回帰の「美しい性質」(BLUE)は、 ガウス・マルコフ仮定 が成り立つ前提です。 仮定とは:
仮定が破れた時の症状と対処:
| 崩れた仮定 | 症状 | 対処 |
|---|---|---|
| 等分散性 | SE が間違う、 t/p 値が嘘になる | HC3 ロバスト SE、 WLS |
| 独立性(時系列) | SE が過小、 自己相関残差 | Newey-West SE、 ARIMA、 パネル固定効果 |
| 多重共線性 | β SE 爆発、 符号反転 | 変数除外、 Ridge、 Lasso、 PCA |
| 外生性違反 | β̂ にバイアス、 因果解釈不可 | 操作変数法(IV)、 DiD、 RDD |
| 非線形 | 残差プロットに曲線パターン | 多項式項、 GAM、 ツリーモデル |
つまり「数式を読む」とは、 単に記号を翻訳することではなく、 仮定が破れた時に何が壊れるか/どう直すかまで含めて理解すること。 ここまで読めて初めて、 重回帰を「使える」と言えます。
最後に重要な注意:βⱼ は 「効果」ではなく「条件付き連関」に過ぎません。 観察データから因果を読みたければ、 (i) 処置の無作為割り付け(RCT)、 または (ii) 適切な操作変数(IV)、 (iii) 差の差(DiD)、 (iv) 不連続回帰(RDD) のいずれかが必要。 「重回帰の係数」を「政策効果」と短絡しないのが、 統計リテラシーの第一歩です。
それでも観察研究で重回帰を使う価値はあります: (a) 予測モデルとして 高精度なら十分実用的、 (b) 仮説生成として どの変数が「効きそう」かを絞り込める、 (c) 政策シミュレーションの第一段階として 大まかな見当をつける、 などの用途で必要不可欠です。
SSDSE-B-2026 から 47 都道府県の死亡率系指標を 4 変数で重回帰した実例で、 数値を追います。 ここでは目的変数として 死亡数(A4200)を 総人口(A1101)で割った千分率を「死亡率」とし、 説明変数は高齢化率・病院数・新規求職申込件数・年平均気温の 4 つを取ります。
| 役割 | 列コード | 名称 | 派生処理 |
|---|---|---|---|
| y | A4200 / A1101 × 1000 | 死亡率(千分率) | 死亡数 ÷ 人口 ×1000 |
| x₁ | A1303 / A1101 × 100 | 高齢化率(%) | 65歳以上人口 ÷ 総人口 |
| x₂ | I510120 / A1101 × 100000 | 10万人あたり病院数 | 正規化 |
| x₃ | F3101 | 新規求職申込件数(件) | そのまま |
| x₄ | B4101 | 年平均気温(℃) | そのまま |
| 変数 | 平均 | 標準偏差 | 最小 | 最大 |
|---|---|---|---|---|
| y:死亡率(‰) | 14.1 | 2.1 | 9.7(東京) | 19.2(秋田) |
| x₁:高齢化率(%) | 31.6 | 3.3 | 22.8(東京) | 39.1(秋田) |
| x₂:人口10万あたり病院 | 6.9 | 2.8 | 3.1(神奈川) | 16.1(高知) |
| x₃:新規求職申込件数(件) | 60,404 | 53,020 | 15,329(鳥取) | 270,954(東京) |
| x₄:年平均気温(℃) | 16.8 | 2.0 | 11.0(北海道) | 23.8(沖縄) |
x₁: r = +0.972(強い正)/x₂: r = +0.52/x₃: r = −0.59/x₄: r = −0.44。 「高齢化率 1 変数で死亡率はほぼ説明できる」が見えます。 では他の 3 変数は「ついで」なのか? 重回帰で検証します。
| 変数 | β̂ | SE | t値 | p値 | 標準化 β* | VIF |
|---|---|---|---|---|---|---|
| 切片 β₀ | -6.80 | 2.08 | -3.26 | 0.002 | — | — |
| β₁(高齢化率) | +0.649 | 0.046 | 14.09 | <0.001 | +1.02 | 4.31 |
| β₂(病院数) | -0.010 | 0.038 | -0.25 | 0.804 | -0.01 | 2.07 |
| β₃(新規求職申込件数) | +2.9e-06 | 2.0e-06 | 1.43 | 0.161 | +0.07 | 2.10 |
| β₄(気温) | +0.017 | 0.052 | 0.33 | 0.743 | +0.02 | 2.09 |
R² = 0.947、 調整済み R² = 0.943、 F(4, 42) = 189.5、 p < 0.001。 モデル全体は強く有意。
沖縄県:x₁=23.8, x₂=5.2, x₃=43877, x₄=23.8 →
$\hat{y} = -6.80 + 0.649 \times 23.8 - 0.010 \times 5.2 + 2.9\text{e-}6 \times 43877 + 0.017 \times 23.8$
= -6.80 + 15.45 − 0.05 + 0.13 + 0.40 = 9.13(‰)。 実測値 ≈ 10.29。 残差 = +1.16。 やや過小予測。
秋田県:x₁=39.1, x₂=5.3, x₃=30175, x₄=13.7 →
$\hat{y} = -6.80 + 0.649 \times 39.1 - 0.010 \times 5.3 + 2.9\text{e-}6 \times 30175 + 0.017 \times 13.7$
= -6.80 + 25.38 − 0.05 + 0.09 + 0.23 = 18.85(‰)。 実測値 ≈ 19.17。 残差 = +0.32。 高精度。
R² = 0.947, p = 4, n = 47 →
$F = \frac{R^2 / p}{(1-R^2) / (n-p-1)} = \frac{0.947/4}{0.053/42} = \frac{0.2369}{0.00125} \approx 189.5$
自由度 (4, 42) で F の 1% 臨界値は約 3.83、 0.1% 臨界値は約 5.85。 190 >> 5.85 → p < 0.001、 モデル全体は極めて強く有意。
x₁(高齢化率)を x₂, x₃, x₄ で回帰 → R²₁ ≈ 0.77。 よって VIF₁ = 1/(1−0.77) ≈ 4.31。 5 を下回るので多重共線性は許容範囲。
β₁* = +1.02 は「高齢化率を 1 標準偏差(3.3 ポイント)動かすと、 死亡率が 1.02 標準偏差(≈ 2.14 ‰)動く」。 他の 3 変数の標準化 β は |0.01|, |0.07|, |0.02| なので、 高齢化率の 影響度は他を 15 倍以上上回る。 重回帰の標準化 β は、 政策議論で「どの要因が一番効くか」を語る最重要数値です。
残差の正規性を Shapiro-Wilk 検定で確認すると W = 0.946、 p = 0.030(n=47)。 0.05 をわずかに下回り、 帰無仮説「正規分布である」は 5% 水準で辛うじて棄却される(残差正規性はやや弱い)。 → t検定・F検定は大標本では頑健だが、 厳密には HC3 ロバスト SE の併用が無難。
BP 統計量 = 1.82、 自由度 4 のカイ二乗で p = 0.77。 等分散性の帰無仮説は棄却されず、 通常の OLS SE をそのまま信頼できる。
合成 5 サンプル 2 説明変数で重回帰係数を計算する。
| x1 | x2 | y |
|---|---|---|
| 1 | 2 | 5 |
| 2 | 1 | 4 |
| 3 | 3 | 9 |
| 4 | 2 | 9 |
| 5 | 4 | 14 |
1 2 3 4 5 6 7 | import numpy as np from sklearn.linear_model import LinearRegression X = np.array([[1,2],[2,1],[3,3],[4,2],[5,4]]) y = np.array([5,4,9,9,14]) m = LinearRegression().fit(X, y) print(f"切片: {m.intercept_:.2f}") print(f"係数: {m.coef_}") |
💬 手計算 (Step 2) [-0.40, 1.356, 1.889] と Python 出力が完全一致。
SSDSE-B-2026 を読み込み、 4 変数で重回帰を実施し p値・SE・F検定・VIF を一括取得する。 統計推論が目的のときの第一選択。
data/raw/SSDSE-B-2026.csv を cp932 でロード。 2023 年データに絞り、 派生変数(高齢化率 %、 10万人あたり病院数)を作成。
係数 β̂、 SE、 t値、 p値、 95%CI、 R²、 調整 R²、 F値、 各変数の VIF、 Breusch-Pagan 統計量、 Durbin-Watson、 残差プロット用配列。
β₁(高齢化率)が圧倒的に有意(t=14.09, p<.001)、 他の 3 変数は単相関では見えても重回帰では非有意。 VIF max=4.31 で共線性問題なし。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import pandas as pd import statsmodels.api as sm import statsmodels.formula.api as smf from statsmodels.stats.outliers_influence import variance_inflation_factor from statsmodels.stats.diagnostic import het_breuschpagan df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026']==2023].dropna(subset=['A4200','A1101','A1303','I510120','F3101','B4101']).copy() # 派生変数 df['death_rate'] = df['A4200'] / df['A1101'] * 1000 # 千分率 df['aging_rate'] = df['A1303'] / df['A1101'] * 100 # % df['hosp_per_100k'] = df['I510120'] / df['A1101'] * 100000 formula = 'death_rate ~ aging_rate + hosp_per_100k + F3101 + B4101' model = smf.ols(formula, data=df).fit() print(model.summary()) # VIF X = sm.add_constant(df[['aging_rate','hosp_per_100k','F3101','B4101']]) vif = {c: variance_inflation_factor(X.values, i) for i, c in enumerate(X.columns)} print('VIF:', {k: round(v, 2) for k, v in vif.items()}) # 等分散検定(Breusch-Pagan) bp_stat, bp_p, _, _ = het_breuschpagan(model.resid, model.model.exog) print(f'Breusch-Pagan p = {bp_p:.3f} ({"等分散OK" if bp_p>0.05 else "ヘテロ"})') # ロバスト標準誤差(HC3) robust = smf.ols(formula, data=df).fit(cov_type='HC3') print(robust.summary()) |
同じデータで「OLS」「Ridge」「Lasso」「Elastic Net」を Pipeline 化し、 5-fold 交差検証で汎化性能を比較する。
NumPy 配列の X (47, 4) と y (47,)。 KFold(shuffle=True, random_state=42)。 StandardScaler で z 標準化済み。
各モデルの CV R² 平均±SD、 標準化済み係数、 Lasso による自動変数選択結果(係数 0 になった変数)。
共線性が軽度のため OLS と Ridge はほぼ同等。 Lasso は β₂, β₃, β₄ を 0 に縮める → 「実質的に高齢化率 1 変数モデル」と等価。 これが Lasso の特徴。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline from sklearn.model_selection import cross_val_score, KFold import numpy as np X = df[['aging_rate','hosp_per_100k','F3101','B4101']].values y = df['death_rate'].values cv = KFold(5, shuffle=True, random_state=42) models = { 'OLS' : LinearRegression(), 'Ridge' : Ridge(alpha=1.0), 'Lasso' : Lasso(alpha=0.1, max_iter=10000), 'EN' : ElasticNet(alpha=0.1, l1_ratio=0.5, max_iter=10000), } for name, est in models.items(): pipe = Pipeline([('sc', StandardScaler()), ('m', est)]) scores = cross_val_score(pipe, X, y, cv=cv, scoring='r2') pipe.fit(X, y) coefs = pipe['m'].coef_ print(f'{name:6s} CV R² = {scores.mean():.3f} ± {scores.std():.3f} ' f'coef = {dict(zip(["aging","hosp","jobs","temp"], coefs.round(3)))}') |
$\hat{\beta} = (X^\top X)^{-1} X^\top y$ を行列演算で直接計算し、 SE・t値・p値・95%CI を組み立てる。
切片列を追加した設計行列 X_design (47, 5)、 目的変数 y (47,)。 残差から $\hat{\sigma}^2$ を計算。
β̂、 残差、 σ²、 Cov(β̂)、 SE、 t値、 p値、 95%CI。 statsmodels と完全一致を確認。
「公式は天から降ってきたものではなく、 13 行のコードで導出できる」と腑に落ちる。 教育的な価値の高いコード。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import numpy as np from scipy import stats, linalg X_design = np.column_stack([np.ones(len(df)), X]) # (n, p+1) beta = linalg.solve(X_design.T @ X_design, X_design.T @ y) # β̂ residuals = y - X_design @ beta n, p = X_design.shape sigma2 = (residuals @ residuals) / (n - p) cov_beta = sigma2 * linalg.inv(X_design.T @ X_design) se = np.sqrt(np.diag(cov_beta)) t_stats = beta / se p_values = 2 * (1 - stats.t.cdf(np.abs(t_stats), df=n-p)) t_crit = stats.t.ppf(0.975, df=n-p) ci_low = beta - t_crit * se ci_high = beta + t_crit * se names = ['intercept', 'aging', 'hosp', 'jobs', 'temp'] for nm, b, s, t_, p_, lo, hi in zip(names, beta, se, t_stats, p_values, ci_low, ci_high): print(f'{nm:10s} β={b:8.3f} SE={s:.3f} t={t_:6.2f} p={p_:.4f} 95%CI=[{lo:.2f}, {hi:.2f}]') |
残差 vs 予測値、 残差 QQ プロット、 影響度(Cook's distance)を可視化し、 線形性・等分散性・正規性・外れ値を診断する。
statsmodels モデルの residuals, fittedvalues, get_influence() 結果。
4 つのサブプロット:残差散布図、 QQ プロット、 スケール-ロケーション図、 leverage vs Cook's distance。
残差がランダム雲ならOK。 ファン状(広がり)なら等分散性違反、 曲線パターンなら非線形効果が残っている。 Cook's d > 4/n の点は影響大。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import matplotlib.pyplot as plt from scipy import stats as sst import numpy as np fig, ax = plt.subplots(2, 2, figsize=(12, 10)) # 残差 vs 予測値 ax[0,0].scatter(model.fittedvalues, model.resid) ax[0,0].axhline(0, color='red', ls='--') ax[0,0].set(xlabel='Fitted', ylabel='Residual', title='Residuals vs Fitted') # QQ プロット sst.probplot(model.resid, dist='norm', plot=ax[0,1]) # スケール - ロケーション ax[1,0].scatter(model.fittedvalues, np.sqrt(np.abs(model.get_influence().resid_studentized_internal))) ax[1,0].set(xlabel='Fitted', ylabel='√|Std. Residual|', title='Scale-Location') # Cook's distance infl = model.get_influence() cooks = infl.cooks_distance[0] ax[1,1].stem(range(len(cooks)), cooks) ax[1,1].axhline(4/len(df), color='red', ls='--', label='4/n') ax[1,1].set(xlabel='Observation', ylabel="Cook's D", title="Cook's distance") ax[1,1].legend() plt.tight_layout(); plt.savefig('mreg_diagnostics.png', dpi=120) |
正則化強度 α を変化させながら Lasso 係数の軌跡をプロットし、 「どの変数が最後まで残るか」を可視化する。
標準化済み X (47,4)、 y (47,)、 α のグリッド [0.001, 0.01, 0.1, 1.0, 10]。
各 α における 4 個の係数(aging/hosp/jobs/temp)の値。 横軸 log(α)、 縦軸 β。
α を上げるほど β は 0 に縮む。 一番粘るのが β₁(高齢化率)。 これは「予測に最も寄与する変数」と一致する。
1 2 3 4 5 6 7 8 9 10 11 12 | from sklearn.linear_model import lasso_path import matplotlib.pyplot as plt from sklearn.preprocessing import StandardScaler X_std = StandardScaler().fit_transform(X) alphas, coefs, _ = lasso_path(X_std, y, alphas=[0.001, 0.01, 0.05, 0.1, 0.5, 1.0, 5.0]) print('coefs shape:', coefs.shape) # (4, len(alphas)) for i, name in enumerate(['aging','hosp','jobs','temp']): plt.plot(-np.log10(alphas), coefs[i], label=name) plt.legend(); plt.xlabel('-log10(α)'); plt.ylabel('β'); plt.title('Lasso path') plt.savefig('lasso_path.png', dpi=120) |
各説明変数の「他変数を制御した後の x_j vs y の関係」を視覚化し、 偏回帰係数の意味を直感的に理解する。
statsmodels モデルオブジェクト、 説明変数の名前リスト。
4 つの partial regression plot。 x 軸は「x_j を他で回帰した残差」、 y 軸は「y を他で回帰した残差」。
各プロットの傾きが偏回帰係数 βⱼ。 aging_rate の傾きが急で、 ほぼ単相関と同じ傾向。 他は傾き ≈ 0 で「制御後は効果なし」が明示される。
1 2 3 | import statsmodels.api as sm fig = sm.graphics.plot_partregress_grid(model) fig.tight_layout(); fig.savefig('partregress.png', dpi=120) |
変数を追加するごとに AIC, BIC, 調整済み R² を比較し、 「どこまで変数を入れる価値があるか」を判断。 入れ子モデルの F検定も実施。
3 つの入れ子モデル:M1=高齢化率のみ、 M2=高齢化率+病院数、 M3=高齢化率+病院数+新規求職申込件数+気温。
各モデルの AIC, BIC, 調整済み R², F検定の p値。
M1→M2 で AIC は微減(病院数は限界的な貢献)。 M2→M3 では BIC が増加 → 「BIC 基準では M2 が最適」。 これが ペナルティ込みのモデル選択。
1 2 3 4 5 6 7 8 9 10 | m1 = smf.ols('death_rate ~ aging_rate', data=df).fit() m2 = smf.ols('death_rate ~ aging_rate + hosp_per_100k', data=df).fit() m3 = smf.ols('death_rate ~ aging_rate + hosp_per_100k + F3101 + B4101', data=df).fit() print(f'{"Model":<6}{"AIC":>8}{"BIC":>8}{"adjR2":>8}') for name, m in [('M1', m1), ('M2', m2), ('M3', m3)]: print(f'{name:<6}{m.aic:8.2f}{m.bic:8.2f}{m.rsquared_adj:8.3f}') from statsmodels.stats.anova import anova_lm print(anova_lm(m2, m3)) |
外れ値に強い HuberRegressor を試し、 通常の OLS と係数を比較。 影響度の高い県を除いても結果が安定するか確認。
X (47, 4), y (47,), StandardScaler 適用済み。 ε = 1.35(Huber の閾値)。
Huber と OLS の係数比較。 大きく違う変数があれば外れ値の影響を受けていた証拠。
沖縄県は気温・高齢化率・死亡率すべて極端値。 OLS では沖縄に引きずられる傾向あり。 Huber の方が他県平均的な傾向を反映する。
1 2 3 4 | from sklearn.linear_model import HuberRegressor huber = HuberRegressor(epsilon=1.35) huber.fit(StandardScaler().fit_transform(X), y) print('Huber β:', dict(zip(['aging','hosp','jobs','temp'], huber.coef_.round(3)))) |
R 流の式(formula)記法で、 交互作用項・多項式項・離散化を柔軟に組み込む。
pandas DataFrame と R 風の式 'y ~ x1 + I(x1**2) + x1:x2 + C(region)'。
Design matrix が自動生成され、 statsmodels に渡せる形式に。
多項式 I(x**2)、 交互作用 x1:x2、 カテゴリ C(...) を 1 行で表現。 学術論文の式そのまま書ける。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | # ── この抜粋で使うデータを用意します ── import patsy import statsmodels.api as sm import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 2023 年の 47 都道府県 df = df.copy() df['death_rate'] = df['A4200'] / df['A1101'] * 1000 # 人口千人あたり死亡数 df['aging_rate'] = df['A1303'] / df['A1101'] * 100 # 高齢化率 % df['hosp_per_100k'] = df['I510120'] / df['A1101'] * 100000 # 人口 10 万人あたり一般病院数 _E = {'北海道': '北', '青森県': '北', '岩手県': '北', '宮城県': '北', '秋田県': '北', '山形県': '北', '福島県': '北'} df['region'] = df['Prefecture'].map(_E).fillna('その他') # 2 水準の地域区分 y, X = patsy.dmatrices('death_rate ~ aging_rate + I(aging_rate**2) + aging_rate:hosp_per_100k + C(region)', data=df, return_type='dataframe') model = sm.OLS(y, X).fit() print(model.summary()) |
正規性仮定が怪しい時、 Bootstrap で経験的に信頼区間を推定する。 ノンパラメトリック法。
X (n, p), y (n,), n_boot = 1000 回のリサンプリング。
各係数の bootstrap 分布、 percentile 95%CI。
正規分布仮定の 95%CI とほぼ一致すれば仮定 OK。 違いが大きければ Bootstrap CI の方を採用する。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | # ── この抜粋で使うデータを用意します ── import numpy as np import pandas as pd from sklearn.utils import resample df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) df = df[df['SSDSE-B-2026'] == 2023] # 2023 年の 47 都道府県 # const を除く 4 変数(下の name 一覧と数を合わせる) X = np.column_stack([ df['A1303'] / df['A1101'] * 100, # aging: 高齢化率 df['I510120'] / df['A1101'] * 100000, # hosp : 人口10万人あたり一般病院数 df['F3103'].astype(float) / 1000, # jobs : 月間有効求人数(千件) df['B4101'].astype(float)]) # temp : 年平均気温 y = (df['A4200'] / df['A1101'] * 1000).to_numpy(dtype=float) # 死亡率 n_boot = 1000 betas = [] for _ in range(n_boot): Xb, yb = resample(X, y, random_state=None) Xd = np.column_stack([np.ones(len(Xb)), Xb]) beta_b = np.linalg.lstsq(Xd, yb, rcond=None)[0] betas.append(beta_b) betas = np.array(betas) ci_low = np.percentile(betas, 2.5, axis=0) ci_high = np.percentile(betas, 97.5, axis=0) print('Bootstrap 95%CI:') for i, name in enumerate(['const','aging','hosp','jobs','temp']): print(f' {name}: [{ci_low[i]:.3f}, {ci_high[i]:.3f}]') |
StandardScaler() + LinearRegression() で係数が直接 β* になる。基本の落とし穴 5 つは押さえたとして、 さらに見落としがちな「重回帰の罠」を 10 個追加します。
サブグループでは正の効果なのに、 全体で集計すると負になる現象。 重回帰でも「グループダミーを入れ忘れる」と、 サブグループの符号と全体の符号が逆転することがある。 必ずグループ別の散布図を確認すること。
学習データの x の範囲外に予測を伸ばすと、 線形の仮定が崩れる可能性大。 「築 50 年のマンション」のデータで学習して「築 0 年」を予測すると、 切片次第で異常値が出る。 必ず学習データの分布範囲を確認。
「両方の条件を満たすサンプル」のみ集めて回帰すると、 元集団には存在しない相関が出る。 病院に来た患者だけで「症状 A と病気 B の関係」を見ると、 母集団とは異なる結論になることがある。
アンケート回答者だけのデータで回帰すると、 「回答しなかった人」の特性が抜ける。 ヘックマンの 2 段階推定で補正可能だが、 手間がかかる。
説明変数 x に測定誤差があると、 β̂ は 0 方向に縮む(attenuation)。 教育年数の自己申告など、 報告誤差のある変数では β を過小評価しがち。 構造方程式モデル (SEM) や IV で補正可能。
y → x の因果関係が逆方向にも存在すると、 β̂ は混合された値になる。 「病院数→死亡率」の効果と「死亡率→病院数」(高齢化地域に病院誘致)の効果が同時に動く。 時間ラグを取る、 操作変数を使う、 で対処。
欠損が「ランダムに起きる」(MCAR) ではなく、 「ある条件で起きる」(MAR / MNAR) と、 単純な listwise deletion でバイアス。 多重代入や逆確率重み付けで補正。
20 個の変数を入れて 1 個でも p<.05 になれば、 偶然の可能性が高い(family-wise error rate)。 Bonferroni、 Holm、 FDR で補正する。 重回帰では「F検定が有意か」を最初にチェック。
説明変数に「未来の情報」「目的変数の派生」が紛れると、 R² が見かけ上跳ね上がる。 たとえば「翌月の売上を当月の利益で予測」など。 時系列ではデータ分割の時点を厳密に管理。
Judea Pearl の「因果のはしご」では、 (1) 連関 → (2) 介入 → (3) 反実仮想、 の 3 段階。 重回帰は (1) のレベル。 介入効果を語るには (2)、 「もし〜だったら」を語るには (3) が必要。 重回帰 1 本で「効果」「介入」「反実仮想」を語ると論理破綻する。
重回帰を中心に、 上位・下位・並列の概念をツリーで配置:
一般化線形モデル (GLM) ├─ 線形回帰 │ ├─ 単回帰 (p=1) │ ├─ 重回帰 (p≥2) ← この用語 │ │ ├─ OLS — 通常の最小二乗 │ │ ├─ WLS / GLS — 重み付き/一般化最小二乗 │ │ ├─ Ridge — L2 正則化 │ │ ├─ Lasso — L1 正則化 │ │ └─ Elastic Net — L1+L2 │ └─ 多項式回帰 ├─ ロジスティック回帰 (二値) ├─ ポアソン回帰 (カウント) └─ プロビット回帰 拡張: ├─ GAM (非線形) ├─ 混合効果モデル (階層構造) ├─ パネル固定効果モデル └─ Bayesian Regression 仮定が崩れた時: ├─ HC3 / Newey-West SE (等分散・自己相関違反) ├─ ロバスト回帰 (外れ値) ├─ 操作変数法 IV (内生性) └─ DiD / RDD (因果推論)
重回帰の系譜は意外に古く、 18 世紀の天文学にまで遡ります。
アドリアン=マリ・ルジャンドルが彗星軌道計算のために 最小二乗法(Méthode des moindres carrés)を発表(1805 年)。 同時期にガウスも独立に発見しており、 「ガウス・ルジャンドル法」とも呼ばれる。 これが重回帰の数学的基礎。
フランシス・ゴルトン(1885)が遺伝学の研究で 「回帰(regression to the mean)」という言葉を作る。 カール・ピアソン(1896)が相関係数を厳密化し、 ユードニー・ユール(1907)が 偏回帰係数(partial regression coefficient)を定義。 ここで現代的な重回帰の枠組みが完成。
R.A. フィッシャー(1922)が最尤法と F検定を確立。 ガウス・マルコフの定理が整理され、 OLS の BLUE 性質が証明される。 これにより重回帰は「推定 + 検定」の体系として完成した。
クリーヴ・グレンジャー、 ジェームズ・ヘックマンらにより、 内生性・自己相関・パネルデータといった現実データの問題点が体系化される。 ヘックマンの選択バイアス補正、 ヘンドリーの一般から特殊への方法論など、 計量経済学の現代スタイルが確立。
ロバート・ティブシラニが Lasso(L1 正則化)を発表(Tibshirani, 1996)。 高次元データへの対応と自動変数選択を可能にし、 機械学習との接続が始まる。 Elastic Net(Zou & Hastie, 2005)で完成形。
scikit-learn、 statsmodels、 PyMC、 Stan などの普及で、 重回帰は 「全データサイエンティストの基本ツール」になった。 SHAP・LIME などの解釈ツールで非線形モデルにも「βⱼ 的解釈」が広がる。 重回帰は古いが、 現代でも色褪せない。
日本では 1950 年代に統計数理研究所の北川敏男・赤池弘次らが計量経済学を発展させ、 AIC(赤池情報量基準、 1973)を生み出した。 これは重回帰のモデル選択における世界標準。 SSDSE データを使う本コンペは、 こうした半世紀以上の伝統の上に立つ。
「OLS で重回帰」と一言で言っても、 実装にはいくつもの細部があります。 ここでは「教科書には載りにくいが現場で大事」な詳細を整理します。
linalg.solve を使うべきか公式 $\hat{\beta} = (X^\top X)^{-1} X^\top y$ をそのままコード化するなら inv(X.T @ X) @ X.T @ y ですが、 これは数値的に不安定。 条件数(condition number)が大きいと丸め誤差で結果が壊れる。 推奨は:
linalg.solve(X.T @ X, X.T @ y) — LU 分解で解く(やや改善)linalg.lstsq(X, y) — SVD ベース、 最も安定(推奨)QR 分解:$X = QR$、 $\hat{\beta} = R^{-1} Q^\top y$ — statsmodels が内部で使用get_dummiesカテゴリ変数(地域、 業種など)は one-hot エンコーディングで扱う。 ただし注意:
drop_first=True で必ず 1 つ落とす(さもないと完全な多重共線性に)。「x₁ の効果が x₂ の値で変わる」場合、 x₁ × x₂ を新変数として追加する。 statsmodels では 'y ~ x1 * x2' が自動で x1, x2, x1:x2 の 3 つを入れてくれる。 解釈は:
y と x₁ の関係が U 字なら、 x₁ + x₁² を入れる。 経験年数と賃金の Mincer 型回帰など、 経済学で頻出。 注意:x₁ と x₁² は強く相関するので 中心化(x₁ − x̄)してから二乗するのが推奨。 sklearn なら PolynomialFeatures。
観測値ごとに信頼度が違うときは重み w_i を付ける。 重みは「分散の逆数」が理論的に最適:$w_i = 1 / \sigma_i^2$。 ヘテロスケダスティシティへの対処にも使える。 smf.wls(formula, data=df, weights=w)。
SSDSE データは時々欠損があります。 基本戦略:
IterativeImputer、 R の mice パッケージ。 統計的に最も望ましい。| タイプ | statsmodels の指定 | 適用場面 |
|---|---|---|
| 古典的 (homoscedastic) | cov_type='nonrobust' | 等分散仮定OK |
| HC0 (White) | cov_type='HC0' | ヘテロ性ありの基本 |
| HC1 | cov_type='HC1' | 小標本補正 |
| HC2 | cov_type='HC2' | leverage 補正 |
| HC3 | cov_type='HC3' | 小標本・leverage 両方(推奨) |
| クラスタロバスト | cov_type='cluster' | パネル・階層データ |
| Newey-West | cov_type='HAC' | 時系列・自己相関 |
本番運用での「壊れ方」と対処を整理。
学習時と本番で説明変数の分布が変わると β̂ は陳腐化する。 月次で平均・分散・KL ダイバージェンスを取り、 5% を超える変動があれば再学習。 不動産モデルなら「平均坪単価のシフト」、 マーケティングなら「広告予算の構成変化」が典型。
予測誤差(RMSE、 MAE)を本番ログから継続計算。 学習時の CV RMSE の 1.5 倍を超えたらアラート。 また、 残差の分布が変化していないか、 自動 KS 検定で確認する。
SSDSE データのような外部データを毎年更新するパイプラインは、 列名変更・コードの追加・欠損値の増加で簡単に壊れる。 schema validation(great_expectations、 pandera)を組み込み、 異常があれば deploy をブロックする。
単一の外れ値で β が大きく動くことを利用したデータポイズニング攻撃が存在する。 本番では Z-score > 5 の入力を自動拒否、 IQR ベースのチェックを通すなど、 入力検証を厳密に。
| 用途 | 予測 latency 要求 | 再学習頻度 | 典型構成 |
|---|---|---|---|
| バッチ予測(夜間) | 数十分OK | 週1〜月1 | Airflow + S3 |
| オンライン API | < 100 ms | 週1 | FastAPI + Redis |
| 広告入札 | < 10 ms | 時間単位 | C++/Rust 実装 |
| バックテスト | 数時間OK | 変更時のみ | Jupyter |
モデルは MLflow などで version 管理し、 旧版に即座に戻せるようにする。 A/B テストで段階リリース。 「全トラフィックの 1% で新モデル → 異常がなければ 10% → 50% → 100%」のカナリアリリース。
重回帰そのものの計算コストは極めて低い(O(np² + p³))が、 周辺コストが実は本番では支配的。
| データ規模 | 計算時間 | 必要メモリ | コスト目安 |
|---|---|---|---|
| n=47, p=5 (本コンペ規模) | < 0.01 秒 | 数 MB | ローカル PC で十分 |
| n=10,000, p=50 | < 1 秒 | 数十 MB | ローカル可 |
| n=1,000,000, p=100 | 数秒 | 数 GB | ローカル可 |
| n=100,000,000, p=1000 | 数分〜数十分 | 数百 GB | Spark MLlib / クラウド |
予測は p+1 個の乗算と足し算だけ。 1 件 ≈ ナノ秒オーダー。 1 億件でも 1 秒以内で処理可能。 これが「重回帰はインフラ的に楽」と言われる所以。
実は重回帰の総コストの 80% は 「データ前処理」と 「結果の解釈・報告」。 計算コストは無視できる。 アナリストが半日〜数日かけて:
重回帰を「正しく」使いこなすには、 大学レベルの統計学(OLS、 仮定診断、 多重共線性、 因果推論)を最低限学ぶ必要がある。 自社で internal training を入れる場合、 1 人あたり 20〜40 時間の研修コスト(社員時給 5,000 円換算で 10〜20 万円)。 e-learning や AtCoder / KaggleでのSkill Tier 化が有効。
scikit-learn (BSD)、 statsmodels (BSD)、 R (GPL) など主要ツールはオープンソース無料。 SAS や SPSS の商用利用は年数十万〜数百万円。 ただし金融や医療の規制業界では SAS validation の方が監査で楽、 という現実もある。
重回帰モデルを業務に使う際のガバナンス論点を整理。
重回帰の最大の強みは 「なぜこの予測か」を係数で説明できること。 これは EU の GDPR Article 22(自動化された意思決定の説明権)にも適合する。 顧客に対して「あなたのスコアが下がった理由は、 βⱼ × xⱼ の項目 X が原因」と回答可能。
説明変数に「性別」「人種」が入っていなくても、 「住所」「学歴」を経由して差別が忍び込む(disparate impact)。 EEOC や金融庁の公平性ガイドラインに照らし、 protected attribute での予測精度差をモニタリングする。
SSDSE-B-2026 は 47 都道府県の集計値なので個人情報問題は基本なし。 ただし、 マイクロデータ(個票)で重回帰する場合は匿名化(k-匿名性、 差分プライバシー)を必ず施す。 学習データの再特定化攻撃も検討事項。
Google が提唱するモデルカードを作成すべき項目:
いつ・誰が・どのデータで・どの結果を出したかを完全記録。 MLflow Tracking、 DVC、 Weights & Biases などのツールで自動化。 金融や医療では Part 11(FDA)/ J-SOX 対応が必須。
日本では総務省・経産省の AI 利活用ガイドライン、 政府の AI 戦略 2022。 重回帰のような「線形・透明」なモデルは high-risk 分類に該当しないことが多いが、 信用スコアや採用予測など用途次第で規制対象に。 詳細は AI ガイドライン 参照。
厚労省・自治体は、 47 都道府県の死亡率を高齢化率・医療体制・所得などで重回帰し、 政策の優先順位を決める。 単相関では混同される交絡を制御できるのが強み。 地域包括ケアシステムの設計や、 病床数の適正配置の議論で頻出。
築年数・面積・駅距離・階数・周辺所得など 10 数変数で価格を重回帰。 査定エンジンの古典的バックボーン。 最近は XGBoost に置き換わりつつあるが、 「なぜこの価格か」の説明用にいまも併用される。 SUUMO 等の自動査定 API の中核。
売上を TV/デジタル/チラシ/価格/季節要因の重回帰で分解し、 媒体ごとの ROI を推定。 Facebook の Robyn、 Google の LMM が代表的 OSS。 重回帰の現代版で、 ベイズ階層・時変係数まで拡張されている。
賃金 = β₀ + β₁ 教育年数 + β₂ 経験年数 + β₃ 経験² + ε。 半世紀来の労働経済学の主力ツール。 教育の収益率 β₁ は政策議論で必ず引用される。 Mincer (1974) 以来、 何万本もの論文がこの形を使う。
鉄鋼/半導体プロセスで、 製品歩留まりを温度・圧力・時間など工程パラメータで重回帰。 SHAP と組み合わせて要因分析するのが現代的トレンド。 Six Sigma の DoE と直結。
PM2.5 濃度を気温・湿度・風速・交通量・工場排出量で重回帰し、 各汚染源の寄与率を分解。 環境省・自治体の常套ツール。 ソース・アポーションメント(source apportionment)の主要手法。
「重回帰分析」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。
SSDSE-B-2026 で県民所得を被説明変数、 人口・高齢化率・就業者数を説明変数とする重回帰は、 多重共線性 (人口と就業者) と外れ値 (東京) の両方を考慮する必要がある。 標準化偏回帰係数で比較するのが安全。
どの正則化を使うべきか? どんな診断を回すべきか? — 状況別の意思決定ツリーをまとめます。
目的変数 y は何か?
├─ 連続値 → 重回帰(このページ)/GAM/RandomForest
├─ 二値 (0/1) → ロジスティック回帰
├─ カウント (0,1,2,...) → ポアソン回帰/負の二項回帰
├─ カテゴリ (3つ以上) → 多項ロジット
└─ 順序 (低/中/高) → 順序ロジット
変数数 p と n の関係は?
├─ n ≥ 10p → そのまま OLS で OK
├─ n < 10p かつ予測重視 → Lasso / Elastic Net
├─ n << p (高次元) → Lasso 強推奨、 もしくは PCA + 回帰
└─ 共線性あり (VIF > 10) → Ridge / 変数除外 / PCA
残差プロットを見て:
├─ ランダム雲 → 仮定 OK
├─ ファン状の広がり → ヘテロ性、 HC3 SE か WLS
├─ 曲線パターン → 非線形効果、 多項式項か GAM
├─ 周期パターン → 自己相関、 Newey-West SE か ARIMA
└─ 一部の点だけ外れ → 外れ値、 Cook's d 確認
因果を主張したい?
├─ 無作為割付できる → RCT(実験)
├─ 自然実験あり → IV / DiD / RDD
├─ パネルデータあり → 固定効果モデル
├─ 観察データのみ → 「条件付き連関」までしか言えない
└─ → 報告書に「因果ではない」と明記
何を基準に選ぶ?
├─ 予測重視 → 交差検証 RMSE、 CV R²
├─ 説明力重視 → 調整済み R²
├─ 「真モデル」探索 → BIC
├─ 予測 & スパース → Lasso CV
└─ 入れ子モデル比較 → F 検定 / 尤度比検定
本コンペで再現対象となる過去論文では、 重回帰が中心的な手法として頻出します。 代表的なものを 5 件紹介します(あくまで例。 実際の論文タイトルは本リポジトリの index.html を参照)。
目的:医療費(人口あたり)を高齢化率・所得・病院数・医師数・気候・産業構造・教育水準で重回帰。 結果:高齢化率と医師数が主要因。 ただし医師数と医療費の因果方向に注意が必要(医療費が高いから医師が来るのか、 医師が多いから医療費が高くなるのか)。
目的:5 年間の人口減少率を、 高齢化率・有効求人倍率・出生率・転出率・大学進学率で重回帰。 結果:転出率(特に若年層)が支配的。 政策提言:地方創生は「若年層の流出抑制」が最大のレバー。
目的:米の単収(10a あたり)を平均気温・日照時間・降水量・台風数・施肥量で重回帰。 結果:気温と日照が主要因。 ただし「気温」は二次関数的効果(暑すぎても寒すぎても減収)なので多項式項を入れた。
目的:高校進学率を世帯所得中央値・両親学歴・教育費補助・地域人口で重回帰。 結果:所得と両親学歴が支配的。 政策提言:奨学金拡充の効果量を試算する基礎データに。
目的:年齢調整自殺率を失業率・離婚率・所得格差・社会的孤立指標・寒冷地ダミーで重回帰。 結果:失業率と社会的孤立が主要因。 教訓:「自殺は個人の問題」ではなく社会的決定因子の影響大。 ただし因果は別途実証必要。
頭で読むだけでは身につきません。 ここでは「自分で手を動かす演習」を 5 ワーク用意します。 SSDSE-B-2026 を使う前提です。 各ワークには想定解と頻出ミスを併記しました。
課題:47 都道府県の死亡率を「高齢化率」と「年平均気温」だけで重回帰せよ。 β̂、 SE、 R² を報告すること。
想定解:高齢化率の β = 0.49(p<.001)、 気温 β = −0.03(p=0.27)、 R²=0.946。 単相関では気温が r=−0.68 だが、 重回帰では非有意。 これは「寒冷地は高齢化が進む」交絡が原因。
頻出ミス:encoding を指定し忘れて文字化け(cp932 を使う)、 skiprows=[1] を忘れて 1 行目を欠損として読む。
課題:「高齢化率」と「65歳以上人口数」(A1303)を両方説明変数に入れて重回帰せよ。 VIF を計算して結果を解釈すること。
想定解:両者は定義式上 100% 相関なので、 VIF が数百〜数千になる(実質的には推定不能)。 statsmodels はランク落ちを検出して片方の係数を NaN にするか、 警告を出す。 教訓:派生変数を作るときは「片方しか入れない」ように設計する。
課題:同じ 4 変数モデルを Ridge と Lasso で推定し、 α を 0.001〜10 まで変化させて係数の軌跡をプロットせよ。
想定解:Ridge は α を上げても全係数が滑らかに 0 に近づく。 Lasso は α=0.1 あたりで β₂, β₃, β₄ が完全にゼロになり、 「実質高齢化率 1 変数モデル」になる。 これが Lasso のスパース性。
課題:4 変数重回帰の CV R² を、 5-fold と LOO(Leave-One-Out)で比較せよ。
想定解:5-fold CV R² ≈ 0.92(学習時 R² = 0.948 より少し下がる)。 LOO CV R² も同程度。 過学習はほぼ起きていない。 もし大きく乖離していたら変数を絞るべき。
課題:4 変数モデルの (1) Residuals vs Fitted, (2) QQ plot, (3) Scale-Location, (4) Cook's distance を 4 枚プロットせよ。 各図から読み取れることを 1 文ずつ書け。
想定解:(1) ランダム雲、 線形性 OK。 (2) ほぼ直線上、 正規性 OK。 (3) 横軸に対して平坦、 等分散性 OK。 (4) Cook's d は全て < 4/47=0.085、 影響度の高い外れ値なし。 → 全仮定支持。
重回帰の核心は「他の変数を一定にしたときの効果(偏回帰係数)」です。 ここでは 2 つの説明変数 X₁・X₂ から目的変数 Y を作り、 最小二乗法で係数を推定する様子をその場で体感できます。 真の係数はドラッグ、 X₁ と X₂ の相関・ノイズはスライダーで動かしてください。 図と数値がリアルタイムで更新されます。
横軸は「他方の変数で説明した残り」、 縦軸は「Y から他方の変数の影響を除いた残り」。 この散布図の傾きがそのまま偏回帰係数になります(added-variable plot)。
β̂₁ は「X₂ を同じ値に固定したまま X₁ を 1 増やすと Y が平均どれだけ動くか」を表します。 上の偏回帰プロットで、 横軸を「X₂ で説明しきれなかった X₁ の残り」に置き換えているのはそのためです。 X₂ の影響をあらかじめ取り除いてから X₁ と Y の関係を見る、 これが「他を一定にする」の数学的な意味です。
説明変数が 3 個以上になると手計算の閉形式は現実的でなくなり、 行列表現 β̂ = (XᵀX)⁻¹Xᵀy で一般化します(このページ内でも既出)。 多重共線性が深刻なときは正則化(リッジ回帰・Lasso)で分散を抑え、 変数のスケールが揃わないときは標準化偏回帰係数で効き目の大きさを比較します。 このミニ実験の「r を上げると推定が暴れる」現象こそ、 それらの手法が必要になる出発点です。
関連ページ: 単回帰・相関分析・多重共線性・交絡・標準化・最小二乗法(OLS)・決定係数・自由度調整済み決定係数
本文では「単回帰と重回帰で係数が変わる」ことを繰り返し見ました。 ここでは一歩進んで、 そのズレ幅が偶然ではなく公式で 1 ‰ の桁まで正確に予言できること(脱落変数バイアスの分解式)と、 本文で名前だけ登場した Frisch-Waugh-Lovell(FWL)定理を SSDSE-B-2026 の実数値で「実演」します。 使うのは 2023 年・47 都道府県の死亡率(A4200/A1101×1000、‰)・高齢化率(A1303/A1101×100、%)・年平均気温(B4101、℃)です。 本文 #calc の 4 変数モデルとは違い、 代数を目で追えるようここでは 2 変数モデルに絞るので、 係数の値は #calc(β₁=+0.649)と少し異なります(本節では +0.6067)。
高齢化率を「入れ忘れた」短い回帰と、 入れた長い回帰を実データで並べます(すべて実測値)。
| 回帰 | 式(推定結果) | R² |
|---|---|---|
| 短い回帰(気温のみ) | 死亡率 = 21.702 − 0.4525×気温(r = −0.442) | 0.196 |
| 長い回帰(高齢化率+気温) | 死亡率 = −4.857 + 0.6067×高齢化率 − 0.0122×気温 | 0.945 |
| 補助回帰(脱落変数を気温で説明) | 高齢化率 = 定数 − 0.7258×気温(r = −0.445) | — |
OVB の分解式は次の 1 行です。
$$\underbrace{\hat\beta_{\text{短}}}_{\text{気温の見かけの効果}} = \underbrace{\hat\beta_{\text{長}}}_{\text{直接効果}} + \underbrace{\hat\beta_{\text{高齢化}}}_{\text{脱落変数の効果}} \times \underbrace{\hat\delta}_{\text{補助回帰の傾き}}$$
分解式 $\hat\beta_{\text{短}} = \hat\beta_{\text{長}} + \hat\beta_{\text{脱落}}\hat\delta$ の積の符号だけ見れば、 脱落変数を測定できていなくてもバイアスの向きが予言できます。
| 脱落変数の効果 β > 0 | 脱落変数の効果 β < 0 | |
|---|---|---|
| 脱落変数と x の相関 δ > 0 | 上振れ(過大評価) | 下振れ(過小評価) |
| 脱落変数と x の相関 δ < 0 | 下振れ(過小評価) | 上振れ(過大評価) |
上の実例は「β > 0(高齢化は死亡率を上げる)× δ < 0(暖かい県は高齢化率が低い)→ 下振れ」の左下セルで、 実際に −0.0122 が −0.4525 まで下振れしていました。 論文の考察で「未測定の交絡が残る」と書くとき、 この表で残ったバイアスの向きまで議論できると説得力が段違いです。
本文 #breakdown で「β₁ は残差同士の単回帰の傾きと一致する(FWL 定理)」と述べました。 実データで本当に一致するか確かめます。 手順は 3 ステップ:
① 高齢化率を気温で回帰し残差 $e_{x_1}$ を取る(高齢化率から「気温で説明できる分」を抜く)。 ② 死亡率を気温で回帰し残差 $e_y$ を取る。 ③ $e_y$ を $e_{x_1}$ で単回帰する。
1 2 3 4 5 6 7 8 9 | import pandas as pd, numpy as np df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]) d = df[df['SSDSE-B-2026']==2023] y = (d['A4200']/d['A1101']*1000).values # 死亡率 ‰ x1 = (d['A1303']/d['A1101']*100).values # 高齢化率 % x4 = d['B4101'].astype(float).values # 年平均気温 ℃ e_x1 = x1 - np.poly1d(np.polyfit(x4, x1, 1))(x4) # ① 残差 e_y = y - np.poly1d(np.polyfit(x4, y , 1))(x4) # ② 残差 print(np.polyfit(e_x1, e_y, 1)) # ③ 傾き 0.6067, 切片 ≈ 0 |
実行結果:残差回帰の傾きは 0.6067 で、 2 変数重回帰の β₁ = 0.6067 と完全一致。 切片は 4.3×10⁻¹⁵(数値誤差の範囲で 0)。 つまり p 変数の重回帰は「p 回の残差取り+単回帰」に分解でき、 偏回帰係数とは文字通り『他の変数の影響を差し引いた後に残る関係』の傾きだと計算レベルで確認できます。
この「partialling out(部分化)」の視点は現代の手法に直結します。 ① パネルデータの固定効果推定は「県ダミー 47 本の重回帰」を FWL で「県平均を引いた残差回帰」に置き換えたもの(within 変換)。 ② 因果推論の DiD も二方向の平均を引く FWL の応用。 ③ 機械学習では、 残差取りの部分を線形回帰でなくランダムフォレスト等に置き換える Double/Debiased Machine Learning(Chernozhukov ら, 2018)が、 高次元の交絡を柔軟に partialling out する標準手法になっています。 なお本節の R² 実測値が示すとおり、 気温は高齢化率の後に足しても R² を 0.945 から実質増やしません(y〜高齢化率のみでも R² = 0.945)——「変数を足す価値」は増分 R²(= 偏決定係数の視点)で測る、 という #python ⑦ のモデル比較にもつながります。