肺ガンデータセットに一般化線形モデルや一般化線形混合モデルを適用してみる

肺ガンデータセットに一般化線形モデルや一般化線形混合モデルを適用してみる


あけましておめでとうございます。新年1発目の投稿です。

今後の業務で一般化線形混合モデル(GLMM)や状態空間モデルを使うかもしれないということで、再び統計モデリングを勉強しています。

今まで担当してきた業務は自然言語処理やレコメンドなどの機械学習モデルを扱ったものばかりで、GLMMや状態空間モデルに関して実務で使うことはありませんでした。

今回は簡単ではありますがscikit-learnの肺ガンデータセットをもとに、GLMやGLMMによるモデリングをやってみます。

コードはGitHubにあげています。

環境

本記事で紹介しているコードはDockerコンテナ上での動作を想定しています。
なお、Dockerコンテナではなくても以下のライブラリをインストール済みであれば動作するはずです。
※依存ライブラリについてはGitHubのpyproject.tomlやpoetry.lockを確認してください。

  • Python 3.7
  • pandas==’0.25.3′
  • numpy==’1.18.1′
  • sklearn==’0.22.1′
  • seaborn==’0.9.0′
  • matplotlib==’3.1.2′
  • statsmodels==’0.12.1′
  • pystan==’2.19.1.1′
  • arviz==’0.10.0′

データ読み込みと可視化

scikit-learnの肺ガンデータセットをロードし、目的変数と説明変数のboxplot(箱ヒゲ図)を作図します。

import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
sns.set()
from sklearn.datasets import load_breast_cancer

# 肺ガンデータセットのロード
data = load_breast_cancer()
# 出力結果: (569, 30)
print(data.data.shape)

# 目的変数と説明変数のboxplot
df_viz = pd.concat(
    [pd.DataFrame(data.target, columns=['is_benign']), pd.DataFrame(data.data, columns=data.feature_names)],
    axis=1
)
plt.figure(figsize=(20, 30))
for i, feature in enumerate(data.feature_names):
    plt.subplot(6, 5, i+1)
    sns.boxplot(x='is_benign', y=feature, data=df_viz)


横軸は肺ガンの有無(1なら良性、0なら悪性)で、縦軸は各説明変数の値です。
目的変数である肺ガンの有無によって、四分位範囲が大きく異なる説明変数がいくつか存在することがわかります。

次に説明変数間の関係を確認します。
説明変数は全部で30個あるため、seabornライブラリのpairplot()でまとめて30×30の散布図を可視化するのは現実的ではありません。
なので、以下のように説明変数間の相関行列をもとに、高い相関係数の変数ペアが存在するかどうかを確認します。

import numpy as np

def get_high_corr_pairs(df, thred=0.8):
    """
    相関係数が閾値以上の変数の組み合わせを取得する
    """
    pairs = list()
    variables = list(df.columns)
    for num, row in enumerate(np.array(df.corr())):
        over_index = np.where(row[num + 1:] > thred)[0]
        if len(over_index) > 0:
            pairs.extend([(variables[num], variables[index + 1]) for index in over_index])
    return pairs

df_feature = pd.DataFrame(data.data, columns=data.feature_names)
# 出力結果: 44
print(len(get_high_corr_pairs(df_feature, thred=0.8)))

相関係数が0.8を超える説明変数のペアが44組存在することがわかりました。
このように説明変数間の相関係数が高くなる事象を多重共線性(マルチコ)といいます。
30個の説明変数全てを回帰モデルに入れると、多重共線性(マルチコ)によって推定したいパラメータの分布が不安定になります。
※多重共線性(マルチコ)については以前にブログで書きました。

多重共線性への対応

多重共線性の原因となっている説明変数を取り除くことで、パラメータの推定値の分布が安定します。
今回は多重共線性を評価する指標の1つであるVIF値に応じて説明変数を一つずつ取り除いていきます。
ちなみにVIF値とは、ある説明変数を目的変数とし、それ以外の全ての説明変数で線形回帰を行った場合の決定係数 \(R^2\) をもとに、\(1/(1-R^2)\) で与えられます。

from statsmodels.stats.outliers_influence import variance_inflation_factor

def drop_col_by_vif(df, thred=10):
    """
    VIF値が閾値を超える変数を削除する
    """
    def get_max_vif(df):
        """
        VIF値が最大の変数とVIF値を取得する
        """
        vif = np.array([variance_inflation_factor(df.values, i) for i in range(df.shape[1])])
        variable = np.array(df.columns)[np.argmax(vif)]
        max_vif = np.max(vif)
        
        return variable, max_vif
    
    df_ = df.copy()
    # 閾値未満のVIF値が存在しなくなれば処理終了
    while True:
        variable, max_vif = get_max_vif(df_)
        if max_vif > thred:
            print(f'drop variable:{variable}, VIF:{max_vif}')
            df_.drop(columns=variable, inplace=True)
        else:
            break
    
    return df_

# VIF値が閾値を超える変数を削除する
df = drop_col_by_vif(df_feature, thred=10)

OUTPUT:

drop variable:mean radius, VIF:63306.17203588469
drop variable:worst radius, VIF:7573.943486033555
drop variable:mean perimeter, VIF:3901.901687119607
drop variable:worst perimeter, VIF:668.3854404127386
drop variable:mean fractal dimension, VIF:508.08682464149285
drop variable:worst smoothness, VIF:368.0533791867144
drop variable:worst texture, VIF:309.54444960438434
drop variable:worst fractal dimension, VIF:184.67972071700538
drop variable:worst symmetry, VIF:167.30971478504884
drop variable:mean concavity, VIF:142.29904340088856
drop variable:radius error, VIF:104.99215955661566
drop variable:worst concave points, VIF:100.94649021325061
drop variable:mean smoothness, VIF:86.99658368431041
drop variable:mean compactness, VIF:74.72314541276282
drop variable:mean area, VIF:67.47169344522399
drop variable:worst compactness, VIF:49.02308700997905
drop variable:perimeter error, VIF:43.72833047786977
drop variable:mean symmetry, VIF:36.0757931560618
drop variable:mean texture, VIF:23.709901129257826
drop variable:concave points error, VIF:18.16312090582923
drop variable:compactness error, VIF:15.728368747925318
drop variable:worst area, VIF:13.976625992787914
drop variable:mean concave points, VIF:11.176654130350768

23個の説明変数が削除され、以下の7個の説明変数だけが残りました。

print(f'selected variables:{list(df.columns)}')
selected variables:['texture error', 'area error', 'smoothness error', 'concavity error', 'symmetry error', 'fractal dimension error', 'worst concavity']

PyStanでモデリング

VIF値をもとに多重共線性を引き起こす説明変数を取り除いたので、いよいよモデリングをしていきます。
本記事では、MCMCサンプリングによるベイズ統計モデリングを実装できるPyStanを使用して、一般化線形モデル(GLM)や一般化線形混合モデル(GLMM)を構築します。
なお、Stanの使い方については以下の書籍が詳しいです。
PyStanではPythonからStanを呼んでいるだけなので、基本的にはStanの使い方がわかっていれば使いこなせるはずです。

一般化線形モデル(GLM)

まずは一般化線形モデル(GLM)でパラメータを推定します。
以下のようにdataブロックに入力するデザイン行列、目的変数、行列・ベクトルサイズを指定します。

# デザイン行列
X = df
X['intercept'] = 1
#目的変数
Y = data.target
# 行列・ベクトルサイズ
N, K = df.shape

また、一般化線形モデルで指定する線形予測子、リンク関数、確率分布はそれぞれ以下とします。

  • 線形予測子: 1次線形結合
  • リンク関数: ロジット関数
  • 確率分布: ベルヌーイ分布

数式で表現すると以下となります。
$$ \log \frac{p_i}{1-p_i} = X \cdot \beta $$
$$ Y_i \sim Bernouli(p_i) $$
上記のStanコードを記述し、PyStanを実行します。

import pystan

# 一般化線形モデル
glm = """
data{ 
  int N;
  int K;
  int Y[N];
  matrix[N, K] X;
}

parameters{
  vector[K] b;
}

model{
  vector[N] p = X * b;
  Y ~ bernoulli_logit(p);
}
"""
fit = pystan.stan(model_code=glm, data={'N': N, 'K': K, 'Y': Y, 'X': X}, warmup=1000, iter=2000, chains=4)

PyStanの実行には少し時間がかかります。
モデリング結果は以下となります。
INPUT:

print(fit)

OUTPUT:

Inference for Stan model: anon_model_761a478f1c9b45406c71aeb93621ae8e.
4 chains, each with iter=2000; warmup=1000; thin=1; 
post-warmup draws per chain=1000, total post-warmup draws=4000.

       mean se_mean     sd   2.5%    25%    50%    75%  97.5%  n_eff   Rhat
b[1]  -0.14    0.01   0.55  -1.16  -0.52  -0.15   0.23   0.93   2519    1.0
b[2]  -0.23  8.0e-4   0.03  -0.29  -0.25  -0.23  -0.21  -0.17   1532    1.0
b[3] -382.6    2.95 124.97 -624.9 -466.8 -383.1 -295.8 -135.4   1794    1.0
b[4]  110.8    0.68  26.13  66.55  92.27 108.64 127.51 167.26   1464    1.0
b[5]  -8.47    0.68  36.38 -86.12 -30.96  -6.79  15.84  59.24   2876    1.0
b[6] 732.07    4.87 226.53 301.16 580.38  726.4 877.75 1187.0   2166    1.0
b[7] -30.08    0.13   4.14  -38.8 -32.82 -29.91 -27.14 -22.58    986    1.0
b[8]  13.23    0.05   1.74  10.11  11.98  13.17  14.38  16.79   1109    1.0
lp__ -65.03    0.06   2.09 -69.99  -66.1 -64.68 -63.54  -62.0   1369    1.0

Samples were drawn using NUTS at Sat Jan 16 03:42:16 2021.
For each parameter, n_eff is a crude measure of effective sample size,
and Rhat is the potential scale reduction factor on split chains (at 
convergence, Rhat=1).

Rhatが1.1未満なので収束は問題がなさそうです。
最後にパラメータのMCMCサンプルの分布を確認します。

import arviz

arviz.plot_trace(fit)


デザイン行列でdataブロックを記述したためパラメータが配列表記になっています。
b[0](texture erro)とb[4](symmetry error)についてはサンプリングで0が含まれるため、これらの変数はベイズ推論の枠組みにおいては肺ガンの良性悪性に寄与しないと言えそうです。

一般化線形混合モデル(GLMM)

最後に一般化線形混合モデル(GLMM)でパラメータを推定します。

GLMMは平均値に応じて分散が増大していく(過分散が生じる)、ポアソン分布などにしたがうデータに対するモデリング手法になります。
ポアソン分布は平均と分散が等しい確率分布です。平均が増加すれば分散も増加します。

なお、ベルヌーイ分布に従う確率変数については過分散の概念は存在しないので、良性か否かを目的変数とするこのデータセットに対してGLMMでモデリングするのは無意味です。

With respect to binomial random variables, the concept of overdispersion makes sense only if n>1 (i.e. overdispersion is nonsensical for Bernoulli random variables).
出典: https://en.wikipedia.org/wiki/Overdispersion

なので、今回はあくまでGLMMでモデリングをやってみるという建て付けでこの章を書いています。

個人ごとに観測値に誤差が存在する想定して、個人単位に正規分布に従うランダム効果を付与することにします。

dataブロックに入力するデザイン行列、目的変数、行列・ベクトルサイズはGLMの場合と同様です。

一般化線形混合モデルで指定する線形予測子、リンク関数、確率分布はそれぞれ以下とします。

  • 線形予測子: 1次線形結合 + ランダム切片(平均 \(0\) 、標準偏差 \(\sigma\) の正規分布に従う)
  • リンク関数: ロジット関数
  • 確率分布: ベルヌーイ分布

数式で表現すると以下となります。
$$ \log \frac{p_i}{1-p_i} = X \cdot \beta + r_i $$
$$ r_i \sim N(0, \sigma^2) $$
$$ Y_i \sim Bernouli(p_i) $$
上記のStanコードを記述し、PyStanを実行します。

# 一般化線形混合モデル
glmm = """
data{
  int N;
  int K;
  int Y[N];
  matrix[N, K] X;
}

parameters{
  vector[K] b;
  vector[N] r;
  real sigma;
}

transformed parameters{
  vector[N] p = X * b + r;
}

model{ 
  r ~ normal(0, sigma);
  Y ~ bernoulli_logit(p);
}
"""
fit_glmm = pystan.stan(model_code=glmm, data={'N': N, 'K': K, 'Y': Y, 'X': X}, warmup=1000, iter=2000, chains=4)

PyStanの実行には少し時間がかかります。
PyStan実行完了後、モデリング結果を確認します。

# 表示するパラメータをランダム効果が従う正規分布の標準偏差sigmaと係数ベクトルに限定する
print(fit_glmm.stansummary(pars=['b', 'sigma']))
Inference for Stan model: anon_model_baf824ebc8a4da7810996238f5cd4276.
4 chains, each with iter=2000; warmup=1000; thin=1; 
post-warmup draws per chain=1000, total post-warmup draws=4000.

        mean se_mean     sd   2.5%    25%    50%    75%  97.5%  n_eff   Rhat
b[1]   26.59   18.14   83.9 -137.4 -18.96  12.82  72.36 219.97     21   1.16
b[2]  -33.65    11.3  19.29 -70.25 -48.92 -32.63 -14.85  -0.45      3   2.98
b[3]  -6.3e4   2.1e4  4.1e4 -1.5e5 -9.0e4 -5.8e4 -2.9e4 -829.2      4   1.93
b[4]   1.6e4  5489.3  1.0e4 201.41 7315.2  1.5e4  2.3e4  3.8e4      3   2.29
b[5]   -1985  1133.2 6080.8 -1.6e4  -5191 -713.2 1199.0 9631.5     29   1.18
b[6]   1.0e5   3.3e4  6.4e4 1430.0  4.8e4  1.0e5  1.4e5  2.4e5      4   1.76
b[7]   -4354  1486.1 2530.6  -8950  -6528  -4096  -2321 -59.47      3   2.95
b[8]  1946.8  663.07 1125.5  25.62 983.73 1853.2 2950.9 3893.2      3   3.11
sigma 275.96   94.89  157.2   2.96 144.51 255.45 410.32 535.41      3   3.72

Samples were drawn using NUTS at Sat Jan 16 04:39:17 2021.
For each parameter, n_eff is a crude measure of effective sample size,
and Rhat is the potential scale reduction factor on split chains (at 
convergence, Rhat=1).

Rhatが1.1未満になっておらず、収束しているとは言えません。
最後にパラメータのMCMCサンプルの分布を確認します。

arviz.plot_trace(fit_glmm, var_names=('b', 'sigma'))


Rhatが1.1より大きいため、MCMCサンプルの分布はめちゃくちゃになっています。

最後に

今回は肺ガンデータセットに対して、一般化線形モデル(GLM)、一般化線形混合モデル(GLMM)を使って分析してみました。
GLMMは本来、目的変数がポアソン分布にしたがっているなどの過分散が疑われるデータ構造に対して適用されるので、今回のようなベルヌーイ分布に従うデータ構造に対してGLMMでモデリングするのは不適当です。
次はGLMMによるモデリングが活かせるデータセットを対象に分析してみようと思います。

参考