統計コンサルの議事メモ

統計や機械学習の話題を中心に、思うがままに

Bayesian GLMで過適合を避ける

長らく知人に貸していた「みんなのR」が手元に戻ってきたのでパラパラと見ていたら、見逃していた面白いトピックを見つけたので紹介します。内容としてはこちらの記事とほぼ同じで、基本的には書籍の該当部分を再現しているだけですが、{purrr}のmapを使った書き方にしてみたり、少しだけ変更を加えています。

書籍はこちら。P320の「Bayesian 縮小」からです。

みんなのR -データ分析と統計解析の新しい教科書-

みんなのR -データ分析と統計解析の新しい教科書-

分析環境の準備

ライブラリの読み込み

まずは必要なライブラリの準備から。以下のライブラリを使用します。

library(MASS)
library(coefplot)
library(tidyverse)

今回使うBayesian GLMのための関数bayesglmは{arm}というライブラリにあるのですが、{coefplot}とnamespaceが衝突するためarm::bayesglmとして実行することにします。

データのロード

続いて「みんなのR」の著者が公開しているサンプルデータをこちらのリンク先から取得し、適当な場所に保存します(urlを使ってloadしたのですが、うまくいきませんでした)。

保存したデータを以下のようにloadします。

load("適当なフォルダ/ideo.rdata") # ideoというデータフレームが読み込まれる

フィッティング

まずは、(ベイズではない)通常のモデリングをした場合の問題点を明らかにするためにglmでフィッティングしてみます。ここでideo$Yearごとに同じモデルをあてはめるのですが、折角なのでサブグループごとにフィッティングを行うための方法をいくつか試してみましょう。

1. glmのsubsetで分割する

まずはそのまま、glmのオプションsubsetでYearを指定してデータを分割する方法です。

theYears          <- unique(ideo$Year)
results_01        <- vector(mode = "list", length = length(theYears))
names(results_01) <- theYears
for (i in theYears) {
   results_01[[as.character(i)]] <- 
      glm(Vote ~ Race + Income + Gender + Education, 
          data = ideo, subset = Year == i, family = binomial(link = "logit"))
}
2. purrr::mapを用いる

続いて、同じことを{purrr}ライブラリのmap関数を使って実行します。

results_02 <- 
   ideo %>% 
   split(.$Year) %>% 
   map(~glm(Vote ~ Race + Income + Gender + Education, 
            data = ., family = binomial(link = "logit")))

mapを使えばかなりスッキリ書けますね。なおsplitは{base}の関数です。

3. tidyr::nestと組み合わせる

さらに続いて、{tidyr}のnestと組合わせてみましょう。まずはmapに渡す関数を定義しておきます。

追記:この書き方は書籍「Rではじめるデータサイエンス」の20章「purrrとbroomによる多数のモデル」を参考にしています。

year_model <- function(df) {
   glm(Vote ~ Race + Income + Gender + Education, 
       data = df, family = binomial(link = "logit"))
}

次にideo$Yearでgroup_byし、nest()入れ子にします。

year_data <- 
   ideo %>% 
   group_by(Year) %>% 
   nest()

nestすると新しいデータフレームのdataにYearごとのデータフレームが格納されることになり、慣れないとかなり戸惑いがあります。

> year_data
# A tibble: 14 x 2
    Year                 data
   <dbl>               <list>
 1  1948   <tibble [374 x 7]>
 2  1952 <tibble [1,217 x 7]>
 3  1956 <tibble [1,245 x 7]>
 4  1960   <tibble [882 x 7]>
 5  1964 <tibble [1,100 x 7]>
 6  1968   <tibble [869 x 7]>
 7  1972 <tibble [1,560 x 7]>
 8  1976 <tibble [1,292 x 7]>
 9  1980   <tibble [829 x 7]>
10  1984 <tibble [1,343 x 7]>
11  1988 <tibble [1,172 x 7]>
12  1992 <tibble [1,304 x 7]>
13  1996 <tibble [1,004 x 7]>
14  2000 <tibble [1,049 x 7]>

このデータに対して先ほど定義した関数を適用します。

results_03 <- map(year_data$data, year_model)
names(results_03) <- as.character(unique(ideo$Year))

この方法では結果のlistにYearがnamesとして渡らないため後からつけているのですが、データと名前があっているのか不安になってしまいますね。

結果の確認

では結果を見てみましょう。multiplotはlistの各要素にlmglmの結果を持つオブジェクトを渡し、関心のある変数の回帰係数を比較することができます。

いずれも同じ結果となっています。

multiplot(results_01, coefficients = "Raceblack", secret.weapon = TRUE) +
   coord_flip(xlim = c(-20, 10))
multiplot(results_02, coefficients = "Raceblack", secret.weapon = TRUE) +
   coord_flip(xlim = c(-20, 10))
multiplot(results_03, coefficients = "Raceblack", secret.weapon = TRUE) +
   coord_flip(xlim = c(-20, 10))

f:id:ushi-goroshi:20180115164702p:plain
(同じグラフなので二枚は省略)

さてここで結果を見てみると、1964年の係数とその標準誤差がおかしなことになっています。multiplotでplot = FALSEとし、数値を確認してみます。

> (multi_01 <- multiplot(results_01, coefficients = "Raceblack", plot = FALSE))
          Value Coefficient   HighInner     LowInner   HighOuter    LowOuter Model
1    0.07119541   Raceblack   0.6297813   -0.4873905   1.1883673   -1.045976  1948
2   -1.68490828   Raceblack  -1.3175506   -2.0522659  -0.9501930   -2.419624  1952
3   -0.89178359   Raceblack  -0.5857195   -1.1978476  -0.2796555   -1.503912  1956
4   -1.07674848   Raceblack  -0.7099648   -1.4435322  -0.3431811   -1.810316  1960
5  -16.85751152   Raceblack 382.1171424 -415.8321655 781.0917963 -814.806819  1964
6   -3.65505395   Raceblack  -3.0580572   -4.2520507  -2.4610605   -4.849047  1968
7   -2.68154861   Raceblack  -2.4175364   -2.9455608  -2.1535242   -3.209573  1972
8   -2.94158722   Raceblack  -2.4761518   -3.4070226  -2.0107164   -3.872458  1976
9   -3.03095296   Raceblack  -2.6276580   -3.4342479  -2.2243631   -3.837543  1980
10  -2.47703741   Raceblack  -2.1907106   -2.7633642  -1.9043839   -3.049691  1984
11  -2.79340230   Raceblack  -2.4509285   -3.1358761  -2.1084548   -3.478350  1988
12  -2.82977980   Raceblack  -2.4609290   -3.1986306  -2.0920782   -3.567481  1992
13  -4.45297040   Raceblack  -3.4433048   -5.4626360  -2.4336392   -6.472302  1996
14  -2.67827784   Raceblack  -2.2777557   -3.0788000  -1.8772336   -3.479322  2000

何と1964年の回帰係数は文字通り桁が違っており、誤差も100倍といったスケールで異なっています。

では、このような結果が得られたときにどうしたら良いでしょうか?その答えの一つが、他の年の結果などから「係数はこの辺りにあるだろう」といった事前知識をモデルに取り込むことです。弱情報事前分布をモデルに取り入れることは、ベイズの観点からは縮小と見做すことができ、Bayesian Shrinkageベイズ縮小)と呼ばれます。

Bayesian GLMによるフィッティング

では実際にやってみましょう。関数は{arm}ライブラリのbayesglmで、glmと同じように使えますが、事前分布を指定することが可能です。まずは先ほどのモデルに、回帰係数の事前分布として尺度パラメータ2.5のコーシー分布(=自由度1のt分布)を用いて当てはめてみます。

results_04 <- 
   ideo %>% 
   split(.$Year) %>% 
   # {arm}はlibrary(arm)とせず、arm::bayesglmとして直接呼び出す
   map(~arm::bayesglm(Vote ~ Race + Income + Gender + Education, 
                      data = ., family = binomial(link = "logit"),
                      prior.scale = 2.5, prior.df = 1))
結果の確認

1964年の異常値が解消されたことを確認してみましょう。

multiplot(results_04, coefficients = "Raceblack", secret.weapon = TRUE) +
   coord_flip()

f:id:ushi-goroshi:20180115165807p:plain

係数とその誤差がかなり改善されていますね!

事前分布の仮定が他の年の係数に影響を与えてはいないでしょうか?それも確認してみましょう。

multi_04 <- multiplot(results_04, coefficients = "Raceblack", plot = FALSE)
> cbind(sprintf("%.3f", multi_01$Value), sprintf("%.3f", multi_04$Value))
      [,1]      [,2]    
 [1,] "0.071"   "0.073" 
 [2,] "-1.685"  "-1.635"
 [3,] "-0.892"  "-0.867"
 [4,] "-1.077"  "-1.050"
 [5,] "-16.858" "-5.100"
 [6,] "-3.655"  "-3.524"
 [7,] "-2.682"  "-2.654"
 [8,] "-2.942"  "-2.864"
 [9,] "-3.031"  "-2.971"
[10,] "-2.477"  "-2.446"
[11,] "-2.793"  "-2.744"
[12,] "-2.830"  "-2.779"
[13,] "-4.453"  "-4.151"
[14,] "-2.678"  "-2.620"

1964年は5行目ですが、その他の年の係数に大きな変動はないようです。また異常値が解消されたことで係数のバラつきはかなり小さくなりました。

plot(cbind(multi_01$Value, multi_04$Value))

f:id:ushi-goroshi:20180115170220p:plain

> apply(cbind(multi_01$Value, multi_04$Value), 2, sd)
[1] 4.037096 1.335844

また異常値となっていた5行目を除外すると、相関係数はほぼ1となります。

> correlate(cbind(multi_01$Value[-5], multi_04$Value[-5])) %>% fashion()
  rowname   V1   V2
1      V1      1.00
2      V2 1.00  
plot(cbind(multi_01$Value[-5], multi_04$Value[-5]))

f:id:ushi-goroshi:20180115170528p:plain

おまけ

bayesglmのオプションにあるprior.scaleとprior.dfを変更することで様々な事前分布を仮定することができます。ここをどちらもInfとすると正規分布となり、glmの結果と一致するそうです。

ということで試してみたのですが、Yearごとのデータで当てはめるとエラーとなってしまったので、全体のデータに対して当てはめた結果を比較してみます。

results_05 <- glm(Vote ~ Race + Income + Gender + Education, 
                  data = ideo, family = binomial(link = "logit"))
results_06 <- arm::bayesglm(Vote ~ Race + Income + Gender + Education, 
                            data = ideo, family = binomial(link = "logit"),
                            prior.scale = Inf, prior.df = Inf)

# 小数点以下7桁ぐらいまでは一致する
> cbind(sprintf("%.7f", coef(results_05)), sprintf("%.7f", coef(results_06)))
      [,1]         [,2]        
 [1,] "0.1592985"  "0.1592984" 
 [2,] "-0.2833290" "-0.2833290"
 [3,] "-2.4806024" "-2.4806025"
 [4,] "-0.8534029" "-0.8534029"
 [5,] "-0.3919568" "-0.3919568"
 [6,] "-0.5816898" "-0.5816898"
 [7,] "-0.3790876" "-0.3790876"
 [8,] "-0.0601803" "-0.0601803"
 [9,] "0.0230734"  "0.0230734" 
[10,] "0.1551473"  "0.1551473" 
[11,] "0.7373030"  "0.7373030" 
[12,] "0.1753482"  "0.1753482" 
[13,] "0.0954255"  "0.0954255" 
[14,] "0.3491240"  "0.3491240" 
[15,] "-0.3277760" "-0.3277760"
[16,] "-0.1064615" "-0.1064615"
[17,] "0.1802366"  "0.1802366" 
[18,] "-0.1227574" "-0.1227574"
> correlate(cbind(coef(results_05), coef(results_06))) %>% fashion()
  rowname   V1   V2
1      V1      1.00
2      V2 1.00
plot(cbind(coef(results_05), coef(results_05)))

f:id:ushi-goroshi:20180115170829p:plain

ほとんど一致していますね!

終わりに

この記事では、bayesglm関数を使ったBayesian GLMの当てはめについて紹介しました。また事前分布を変更することでbayesglmの結果がglmと一致することを確認しました。

ベイズでは事前分布のバラつきの程度によってパラメータに対する分析者の「確信度」が表されます。この確信度とデータを観測した時の影響について、追ってブログで紹介したいと思っています。