【ベンチマーク】多重共線性のある回帰

中級

2.1.22

【ベンチマーク】多重共線性のある回帰

最終更新 2026-03-06 読了時間 5 分
まとめ
  • x₂ ≈ 0.95×x₁, x₄ ≈ 0.9×x₃ の多重共線性を持つ5変数データで、6つの回帰モデルを比較する。
  • OLS の係数は CV フォールドごとに大きく変動し、解釈が困難になる。
  • Ridge は係数を安定化し、Lasso は冗長な変数をゼロに縮小して変数選択を行う。

身長と座高、GDP と人口、気温と日照時間——実務データでは特徴量同士が強く相関していることが多い。多重共線性が存在すると OLS の係数推定が不安定になり、符号が反転したり、CV フォールドごとに大きく変動したりする。ここでは意図的に共線性を埋め込んだ合成データで、正則化と次元削減の効果を検証する。


1. データの特徴 #

  • n=200、5変数回帰
  • 真のモデル: y = 3x₁ + 2x₃ + 1.5x₅ + ε
  • x₂ ≈ 0.95×x₁ + noise(VIF > 10)
  • x₄ ≈ 0.90×x₃ + noise(VIF > 10)
  • x₅ は独立

2. 合成データの生成 #

 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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

rng = np.random.default_rng(42)
n = 200

x1 = rng.normal(0, 1, n)
x2 = 0.95 * x1 + rng.normal(0, 0.3, n)  # x1と強相関
x3 = rng.normal(0, 1, n)
x4 = 0.90 * x3 + rng.normal(0, 0.4, n)  # x3と強相関
x5 = rng.normal(0, 1, n)                  # 独立

X = np.column_stack([x1, x2, x3, x4, x5])
y = 3 * x1 + 2 * x3 + 1.5 * x5 + rng.normal(0, 1, n)
feature_names = ["x₁", "x₂", "x₃", "x₄", "x₅"]

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

# 相関行列
corr = np.corrcoef(X.T)
im = axes[0].imshow(corr, cmap="RdBu_r", vmin=-1, vmax=1)
axes[0].set_xticks(range(5))
axes[0].set_xticklabels(feature_names)
axes[0].set_yticks(range(5))
axes[0].set_yticklabels(feature_names)
for i in range(5):
    for j in range(5):
        axes[0].text(j, i, f"{corr[i,j]:.2f}", ha="center", va="center", fontsize=8)
axes[0].set_title("特徴量の相関行列")
fig.colorbar(im, ax=axes[0], shrink=0.8)

# x1 vs x2 散布図
axes[1].scatter(x1, x2, alpha=0.4, s=15, color="#2563eb")
axes[1].set_title(f"x₁ vs x₂(r={np.corrcoef(x1, x2)[0,1]:.3f})")
axes[1].set_xlabel("x₁")
axes[1].set_ylabel("x₂")
axes[1].grid(alpha=0.2)

# VIF計算
from numpy.linalg import inv
corr_matrix = np.corrcoef(X.T)
try:
    vif = np.diag(inv(corr_matrix))
except np.linalg.LinAlgError:
    vif = np.full(5, np.nan)

colors = ["#ef4444" if v > 10 else "#10b981" for v in vif]
axes[2].barh(feature_names, vif, color=colors, edgecolor="white")
axes[2].axvline(10, color="#1e40af", linestyle="--", linewidth=1, label="VIF=10 基準")
axes[2].set_title("VIF(分散膨張係数)")
axes[2].set_xlabel("VIF")
axes[2].legend(fontsize=8)
axes[2].grid(alpha=0.2, axis="x")

fig.suptitle("多重共線性データの構造", fontsize=13)
fig.tight_layout()
plt.show()

多重共線性データの構造

3. 比較するパイプライン #

前処理説明
なし全5変数をそのまま使用
VIFベースで除外VIF > 10 の変数を逐次除外
PCA(n=3)主成分3つに圧縮
StandardScaler標準化のみ
モデル特徴
OLS最小二乗法(共線性に弱い)
RidgeL2正則化(係数を縮小)
LassoL1正則化(変数選択)
ElasticNetL1+L2(Lasso+Ridge)
PLS潜在変数で共線性を吸収
PCR主成分回帰

4. 実験と結果 #

  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
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
from sklearn.model_selection import cross_val_score, KFold
from sklearn.linear_model import LinearRegression, Ridge, Lasso, ElasticNet
from sklearn.cross_decomposition import PLSRegression
from sklearn.decomposition import PCA
from sklearn.preprocessing import StandardScaler
from sklearn.pipeline import Pipeline
from sklearn.base import clone
import seaborn as sns

cv = KFold(n_splits=5, shuffle=True, random_state=42)

def make_pcr(n_components=3):
    return Pipeline([
        ("scaler", StandardScaler()),
        ("pca", PCA(n_components=n_components)),
        ("reg", LinearRegression()),
    ])

models = {
    "OLS": LinearRegression(),
    "Ridge": Ridge(alpha=1.0),
    "Lasso": Lasso(alpha=0.1),
    "ElasticNet": ElasticNet(alpha=0.1, l1_ratio=0.5),
    "PLS(3)": PLSRegression(n_components=3),
    "PCR(3)": make_pcr(3),
}

results = []
coef_records = []

for mdl_name, mdl in models.items():
    maes, r2s = [], []
    fold_coefs = []

    for train_idx, test_idx in cv.split(X):
        X_tr, X_te = X[train_idx], X[test_idx]
        y_tr, y_te = y[train_idx], y[test_idx]

        m = clone(mdl)
        try:
            if mdl_name.startswith("PLS"):
                m.fit(X_tr, y_tr.reshape(-1, 1))
                pred = m.predict(X_te).ravel()
                coefs = m.coef_.ravel()
            else:
                m.fit(X_tr, y_tr)
                pred = m.predict(X_te)
                if hasattr(m, "coef_"):
                    coefs = m.coef_.ravel()
                elif hasattr(m, "named_steps"):
                    coefs = np.full(5, np.nan)  # PCR: coefficients not directly comparable
                else:
                    coefs = np.full(5, np.nan)

            maes.append(np.mean(np.abs(y_te - pred)))
            r2 = 1 - np.sum((y_te - pred)**2) / np.sum((y_te - np.mean(y_te))**2)
            r2s.append(r2)
            if len(coefs) == 5:
                fold_coefs.append(coefs)
        except Exception:
            maes.append(np.nan)
            r2s.append(np.nan)

    results.append({
        "モデル": mdl_name,
        "MAE": np.nanmean(maes),
        "R²": np.nanmean(r2s),
    })

    if fold_coefs:
        coef_arr = np.array(fold_coefs)
        for j, fname in enumerate(feature_names):
            coef_records.append({
                "モデル": mdl_name, "特徴量": fname,
                "係数平均": coef_arr[:, j].mean(),
                "係数std": coef_arr[:, j].std(),
            })

df_res = pd.DataFrame(results)

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# MAE / R² 棒グラフ
mdl_order = ["OLS", "Ridge", "Lasso", "ElasticNet", "PLS(3)", "PCR(3)"]
df_plot = df_res.set_index("モデル").reindex(mdl_order)

colors_mae = ["#ef4444" if v > df_plot["MAE"].median() else "#10b981" for v in df_plot["MAE"]]
bars = axes[0].barh(df_plot.index, df_plot["MAE"], color=colors_mae, edgecolor="white")
for bar, v in zip(bars, df_plot["MAE"]):
    if not np.isnan(v):
        axes[0].text(bar.get_width() + 0.02, bar.get_y() + bar.get_height() / 2,
                     f"{v:.2f}", va="center", fontsize=9)
axes[0].set_title("MAE 比較(低いほど良い)")
axes[0].set_xlabel("MAE")
axes[0].grid(alpha=0.2, axis="x")

# 係数の安定性(std)
df_coef = pd.DataFrame(coef_records)
if len(df_coef) > 0:
    pivot_std = df_coef.pivot_table(index="モデル", columns="特徴量", values="係数std")
    pivot_std = pivot_std.reindex(
        index=[m for m in mdl_order if m in pivot_std.index],
        columns=feature_names
    )
    sns.heatmap(pivot_std, annot=True, fmt=".2f", cmap="RdYlGn_r",
                linewidths=0.5, ax=axes[1],
                cbar_kws={"label": "係数の標準偏差(低いほど安定)"})
    axes[1].set_title("CV間の係数変動(std)")
    axes[1].set_xlabel("")
    axes[1].set_ylabel("")

fig.suptitle("多重共線性データ: モデル別精度と係数安定性", fontsize=13)
fig.tight_layout()
plt.show()

精度と係数安定性

読み方のポイント #

  • OLS の x₁, x₂ の係数は CV フォールドごとに大きく変動する(std が大きい)。共線性により係数の分配が不安定になるため。
  • Ridge は係数を縮小して安定化する。x₁ と x₂ に似た大きさの係数を割り当てる傾向がある。
  • Lasso は冗長な x₂, x₄ をゼロ近くに縮小し、実質的に変数選択を行う。

5. 誤差パターン分析 #

 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
fig, axes = plt.subplots(2, 3, figsize=(15, 8))

true_coefs = np.array([3, 0, 2, 0, 1.5])  # 真の係数

for idx, mdl_name in enumerate(["OLS", "Ridge", "Lasso", "ElasticNet", "PLS(3)"]):
    ax = axes[idx // 3, idx % 3]
    df_sub = df_coef[df_coef["モデル"] == mdl_name]
    if len(df_sub) == 0:
        continue

    x_pos = np.arange(5)
    ax.bar(x_pos - 0.15, true_coefs, width=0.3, color="#94a3b8", label="真の係数", alpha=0.7)
    ax.bar(x_pos + 0.15, df_sub["係数平均"].values, width=0.3, color="#2563eb", label="推定(平均)")
    ax.errorbar(x_pos + 0.15, df_sub["係数平均"].values, yerr=df_sub["係数std"].values,
                fmt="none", color="#ef4444", capsize=3, label="±1σ")
    ax.set_xticks(x_pos)
    ax.set_xticklabels(feature_names)
    ax.set_title(mdl_name, fontsize=10)
    ax.legend(fontsize=7, loc="upper right")
    ax.grid(alpha=0.2)
    ax.set_ylabel("係数値")

axes[1, 2].axis("off")
fig.suptitle("真の係数 vs 推定係数(平均±標準偏差)", fontsize=13)
fig.tight_layout()
plt.show()

係数比較

OLS では x₁ と x₂ の係数合計は真の値(3)に近いが、個々の係数は不安定。Lasso は x₂=0, x₄=0 に近い値を出し、真のスパース構造に近い解を見つける。


6. よくある失敗パターン #

  • VIF が高いからといって変数を手動で除外する: どの変数を残すかの判断が恣意的になりやすい。Ridge / Lasso に判断を任せるほうが再現性が高い。
  • 共線性を無視して OLS の係数を解釈する: 「x₂ の係数が負 → x₂ は y を下げる」のような解釈は、x₁ との共線性を考慮しないと誤りになる。