⛷️

多重共線性をチェックする方法

2021/08/30に公開

はじめに

多重共線性は相関行列を見てもチェックすることはできない。そして Ridge や Lasso などの正則化付き線形モデルも多重共線性をどうにかしてくれるわけではない(これらの方法でどうにかなると思っている人はもうちょっと深く考え直したほうがよい)。

私も普段明示的にはチェックしていない。それはよろしくないと思ったので多重共線性チェッカーを作った。

仕組み的には VIF(variance inflation factor)による多重共線性チェックと同じことで、VIF を計算するだけなら statsmodel ライブラリの variance_inflation_factor でもできる。

今回作成したコードでは、どの変数とどの変数が多重共線性を持っているかの情報まで可視化できる。Google Colabで作成したサンプルは以下。

https://colab.research.google.com/drive/1-pSs6p6NPL8EWl3bvcokd_2gDqnGCTzT?usp=sharing

参考文献

多重共線性の定義

データの入力x \in \mathbb{R} ^ n

x = (x _ 1 \,\, x _ 2 \,\, \cdots \,\, x _ n)

で表すものとする。k番目のデータの入力は

x ^ {(k)} = (x _ 1 ^ {(k)} \,\, x _ 2 ^ {(k)} \,\, \cdots \,\, x _ n ^ {(k)})

で表記するものとする。多重共線性とは、あるベクトルa \in \mathbb{R} ^ nとスカラーb \in \mathbb{R}が存在して、どんなk番目の入力に対しても

a \cdot x ^ {(k)} + b = 0

が成り立つことをいう。実際のデータでは入力に含まれるノイズの影響で上の式は完全には成立しないので、kに依存する誤差\varepsilon ^ {(k)}を用意して

a \cdot x ^ {(k)} + b = \varepsilon ^ {(k)}

を成り立たせたとき、誤差のデータセット全体に渡る平均

\overline{\varepsilon} = \frac{1}{N} \sum _ {k = 1} ^ N \varepsilon ^ {(k)}

を最小化させることを考える。このとき、\overline{\varepsilon}が小さければ多重共線性が大きい、大きければ多重共線性が小さいという。

多重共線性のチェック方法

多重共線性の定義

a \cdot x ^ {(k)} + b = \varepsilon ^ {(k)}

を適当に式変形すると

x _ i ^ {(k)} = a ^ \prime _ 0 + \sum _ {j = 1 \,(j \neq i)} ^ N a ^ \prime _ j x _ j ^ {(k)} + \varepsilon ^ {(k)}

となる。ややこしい見た目をしているが、要するに変数x _ iを他の変数で線形回帰してみて、決定係数(R ^ 2スコア)が1に近ければそれだけ多重共線性が強いということだ。じゃあ実際やってやればいいではないか。

import numpy as np
from sklearn.linear_model import Ridge
from sklearn.metrics import r2_score

def collinearity_check(Xc, model=None, alpha=1.0, emph=False):
    """
    Parameters
    ----------
    Xc : np.ndarray(m, n)
        Input data (each data stored in a row).
    model
        Regression model (default Ridge).
    alpha : float
        Hyper parameter of Ridge (default 1.0),
        ignored if model is not None.
    emph : bool
        Emphasize the result by R2 score or not.

    Returns
    -------
    rc : np.ndarray(n, n)
        Regression coefficient, emphasized by R2 score if emph is True.
    scores : np.ndarray(n)
        R2 scores.
    """
    
    if model is None:
        model = Ridge(alpha=alpha)

    m, n = Xc.shape
    if n < 2:
        raise ValueError()

    # 戻り値
    rc = np.empty((n, n)) # 回帰係数
    scores = np.empty(n) # R2スコア

    X = np.copy(Xc)
    for i in range(n):
        y = np.copy(X[:,i]) # 自分を他の変数で回帰させる
        X[:,i] = 1 # 自分は多重共線性の定数項に対応させる

        model.fit(X, y)
        y_calc = model.predict(X)

        score = r2_score(y, y_calc)
        if score < 0:
            # R2 スコアが 0 以下なら線形性なしとみなす
            scores[i] = 0
            rc[i] = 0
        else:
            scores[i] = score
            if emph:
                # 係数が大きくても R2 スコアが 0 に近ければ 0 になるように加工
                rc[i] = model.coef_ * score
            else:
                rc[i] = model.coef_

        X[:,i] = y
    
    return rc, scores

デフォルトで Ridge にしたのは線形回帰が解けない場合でも解けるからである。Lasso でやったら本当に関係のありそうなやつだけ残してくれるのでは、と期待したが、Lasso は多重共線性を考慮するわけではなく「使用する変数の個数を絞ってください」という命令に忠実に従うだけであり、試してみたら本当に関係があるやつまで削りやがったので使わないほうがいい。

ハイパーパラメータの\alphaは、今回のケースではチューニングとかできない気がするので人間の手で指定してやるべきだと思う。気に食わなければチューニングまで行うパイプラインを作って model に渡してやればいい。

Ridge 以外にも多項式回帰とかのモデルを作って渡してやれば、線形回帰よりも強い関係性を一応は抽出できるはずである。

ちなみにR^2スコアをrとおくと

T = 1 - r

で定義される値をトレランス(tolerance: 許容範囲)という。入力ノイズがあってR^2スコアは0.9くらいになるだろうから、トレランスが0.1以下であれば多重共線性ありと言えるだろう、と評価に使う。

なぜかは知らないが、トレランスの逆数を取った

{\rm VIF} = \frac{1}{T} = \frac{1}{1 - r}

を VIF(variance inflation factor)というらしい。確かに予測誤差の「分散」みたいなものであるR ^ 2スコアを1から引いて逆数を取って見かけ上大きく見えるように「膨張」させた「数値」だからそういう名前がついているのも頷ける。VIF が 10 以上で多重共線性ありらしい[1]が、だったら普通にR ^ 2スコア 0.9 以上って言ったほうがわかりやすくない……?

多重共線性の可視化

今回の可視化コードを通常の相関係数のプロットと比較しておく。

疑似データの生成

疑似データ X を生成する。実際に分析するときはこの X をお手元のデータに置き換えてもらえればよい。

このあと、どの変数に多重共線性があるのかを可視化するが「多重共線性があるインデックスの表示」には正解が書いてあるので、そこは最後に見ると楽しい。

疑似データの生成
import numpy as np

np.random.seed(0)

# データ数
m = 100

# データの次元数
n = 18

# 多変量ガウス分布(各変数独立)
mu = np.random.normal(0, 150, n)
Lam = np.diag(np.random.uniform(1.0, 100, n))
X_indep = np.random.multivariate_normal(mu, Lam, m)

# いくつを選んで多重共線性を付加するか
k = 5
# 多重共線性のある変数をいくつ追加するか
N = 2

# 多重共線性のある変数を作成する
def make_collinear_vals(X, k, N):
    X_crop = X[:, -k:]

    A = np.random.normal(0, 1, (k, N))
    b = np.random.normal(0, 10, N)

    return X_crop.dot(A) + b

# 多重共線性のある変数を付加
X_origin = np.concatenate([X_indep, make_collinear_vals(X_indep, k, N)], axis=1)

# 加法ノイズを加えて多重共線性が完璧には成り立たないようにする
X_origin += np.random.normal(0, 1e-1, (m, n+N))

# ひと目でわからないようにシャッフル
idx = np.arange(0, n+N, 1)
idx_shuffle = np.random.choice(idx, n+N, replace=False)
X = X_origin[:, idx_shuffle]
多重共線性があるインデックスの表示
print(idx[idx_shuffle >= n - k])
output
[ 0  2  3  9 13 14 18]

前処理

入力データは標準化しておくと可視化がうまくいきやすい。

from sklearn.preprocessing import StandardScaler

Xsc = StandardScaler().fit_transform(X)

相関係数の可視化(よくない例)

相関係数だけで判断しようとすると痛い目を見るかもしれない。多重共線性がある変数はいくつで、どれなのかを読み取ろうとしてみるとよい。

from matplotlib import pyplot as plt
import seaborn as sns
import pandas as pd

plt.figure(figsize=(10,8))
sns.heatmap(pd.DataFrame(Xsc).corr(), vmin=-1, vmax=1, cmap='bwr', cbar=True)
plt.ylabel('index of variables', fontsize=16)
plt.xlabel('index of variables', fontsize=16)
plt.show()

今回作った可視化アルゴリズム

cc, cc_scores = collinearity_check(Xsc, emph=True)
# どの変数とどの変数に多重共線性があるのかを可視化する
from matplotlib import pyplot as plt
import seaborn as sns
import pandas as pd

plt.figure(figsize=(10,8))
sns.heatmap(pd.DataFrame(cc), vmin=-1, vmax=1, cmap='bwr', cbar=True)
plt.ylabel('target variable', fontsize=16)
plt.xlabel('used variables to explain the target', fontsize=16)
plt.show()

このコードでプロットされる図はi番目の変数について他の変数で回帰したときの回帰係数をi行目に格納した行列のヒートマップである。collinearity_check関数を実行するときにemph=Trueを指定しておくと、重みにR^2スコアをかけて強調表示する(多重共線性がなければR^2スコアは0に近づくので色が白に近くなる)。対角成分は多重共線性の定数項に該当するが、定数項は標準化で0になるので真っ白になっている。

# どの変数に多重共線性があるのかを可視化する
from matplotlib import pyplot as plt

plt.figure(figsize=(8,5))
plt.plot(cc_scores)
plt.xticks(np.arange(0, len(cc_scores), 1))
plt.ylabel('R2 score', fontsize=16)
plt.xlabel('selected variables', fontsize=16)
plt.show()

このコードでプロットされる図は各変数を他の変数で回帰したときのR^2スコアを変数ごとにプロットする。1.0 に近いほど多重共線性が強く、0.5 以下であればほぼ多重共線性はないと思ってよい。

相関行列からは読み取れないだろうが、実は 0 番目の変数にも多重共線性がある。今回作成したアルゴリズムではかなり明確に読み取れている。

脚注
  1. 文献によって 5 以上と書いてたりするので適当だと思う。 ↩︎

Discussion