ベイズマックスへようこそ。
統計に関するあれやこれやを自由気ままに投稿します。
全記事一覧はこちらから。

2020年09月20日

StanでWiener分布からの乱数を得る方法(+累積分布関数ほか)

 Stanには,Drift Diffusion Modelを実装するのにも使えるWiener分布の確率密度関数が実装されています。サンプリングステートメントとしてのy ~ wiener(alpha, tau, beta, delta)と対数確率密度を計算するwiener_lpdf(y | alpha, tau, beta, delta)を使えばデータをWiener分布に当てはめることは容易に行えますが,Wiener分布からの乱数を発生させる関数wiener_rng()は実装されていません。Stanで得たMCMCサンプルを使ってR側でRWiener::rwiener()関数を使えば乱数を得ることはできますが,事後予測分布を作りたいときにはStan側で実装できたほうが便利だと思います。そんな訳で,Stan用のwiener_rng()を自作してみました。2通りのやり方を思い付いたので2つ紹介します。1つ目はWiener過程そのものをStan内でシミュレートすることで乱数を得る方法で,2つ目はWiener分布の累積分布関数の逆関数を使う方法です。順番に紹介していきますが,先取りして結論を言ってしまうと,後者のやり方の方が高速かつ安定しそうです。※この記事で紹介しているやり方は必ずしも正しさが保証されたものではありませんので,実際にご自身の研究等で使用される際は自己責任でお願いします。

1. Wiener過程をシミュレートする方法(非推奨)

 RWiener::rwiener()の内部ではWiener過程をシミュレートする方法が用いられているようなので,それを真似してみました。Wiener過程はランダムウォークにおける各ステップの時間間隔Δtを0に近づけていったものなので,tの微小変化量として十分小さなdtの値を適当に与えてランダムウォークを作ります。このランダムウォークにトレンド(ドリフト率δ)を加えた確率過程が初期値αβからスタートし,その値がαを超えたときに上側反応,0を下回ったときに下側反応が生起すると考えます。Stanコードは以下の通り。

testmodel01.stan: wiener_rng()の実装その1(非推奨)
functions{
  //乱数生成: ウィーナー過程をシミュレートして求める方法
  vector wiener_rng(real alpha, real tau, real beta, real delta){
    vector[2] out = rep_vector(-999,2);
    real dt = 10^(-4); //1ステップの時間の微小変化量を指定;小さいほど正確だが時間がかかる
    int N = 1000000; //最大ステップ数;これを越えても閾値に到達しなければエラー
    vector[N] dis;
    for(i in 1:N) dis[i] = normal_rng(0,1);
    dis = delta*dt + sqrt(dt)*dis;
    dis = cumulative_sum(dis) + alpha*beta;
    for(i in 1:N){
      if(dis[i] > alpha){
        out[2] = 1;
        out[1] = i * dt + tau;
        break;
      } else if(dis[i] < 0){
        out[2] = 0;
        out[1] = i * dt + tau;
        break;
      }
    }
    
    return(out); //最大ステップ数以内に閾値を超えなければエラーとして(-999,-999)を返す
  }
}

data{ //今回はwiener_rng()の挙動の確認が目的なのでパラメータを外から与える。
  real<lower=0> alpha;
  real<lower=0> tau;
  real<lower=0,upper=1> beta;
  real delta;
}

parameters{
  real tmp; //エラー防止用の無意味なパラメータ
}

model{
  tmp ~ normal(0,1); //エラー防止用の無意味なサンプリング
}

generated quantities{
  vector[2] x_pred = wiener_rng(alpha,tau,beta,delta); //乱数生成
}

 なお,Wiener分布は反応 (上側か下側か) と時間(非負の実数)の同時分布なので,wiener_rng()も反応と時間の両方を返すように作ってあります。上記のコードではwiener_rng()からの乱数を受け取る生成量x_predが2次元のベクトルになっていることに注意してください。x_pred[1]が時間,xpred[2]が反応(上側なら1,下側なら0)を格納します。上記コードをRで実行するためのコードは以下の通り。

Rで実行する その1
library(tidyverse)
library(RWiener)
library(rstan)

# おまじない----
rstan_options(auto_write = TRUE)
options(mc.cores = parallel::detectCores())

# 上記モデルのコンパイル----
sm <- stan_model(file="testmodel01.stan")

# パラメータの指定----
data <- list(
  alpha = 1,
  tau = 0.5,
  beta = 0.6,
  delta = 1
)

# サンプリングの実行 (単に乱数を発生させるだけなのでwarmupは小さくしている) ----
fit <- sampling(sm
                 ,data = data
                 ,seed = 610
                 ,chains = 4
                 ,iter = 2520
                 ,warmup = 20
)

# 結果の集計 ----
x_pred_stan <- rstan::extract(fit)$x_pred1 %>% as.data.frame() %>% 
  dplyr::rename(q = V1, resp = V2) %>%
  mutate(resp = as.factor(ifelse(resp==1,"upper","lower"))) %>%
  mutate(resp = factor(resp,levels=c("upper","lower")))

# 比較のためにRWiener::rwiener()からの乱数を得る ----
set.seed(8823)
x_pred_rwiener <- rwiener(10000,data$alpha,data$tau,data$beta,data$delta)

# 要約 ----
x_pred_stan %>% summary()
#       q             resp     
# Min.   :0.5092   upper:8051  
# 1st Qu.:0.5833   lower:1949  
# Median :0.6555               
# Mean   :0.7127               
# 3rd Qu.:0.7829               
# Max.   :2.3316   

x_pred_rwiener %>% summary()
#       q             resp     
# Min.   :0.5105   upper:8085  
# 1st Qu.:0.5805   lower:1915  
# Median :0.6509               
# Mean   :0.7090               
# 3rd Qu.:0.7760               
# Max.   :2.2582     

# プロット ----
wiener_plot(x_pred_stan)
wiener_plot(x_pred_rwiener)

 上記のコードに記している通り,要約統計量の値はRWiener::rwiener()の結果とほぼ一致しました。下図は分布をプロットものですが,こちらも見た目には問題なさそうです。

comp_wiener_rng01.png

 ところが,このやり方には問題があって,単に乱数を発生させるだけなのに計算にかなり時間がかかってしまいます。自分の環境では20分近くもかかってしまいました。dt = 10^(-4)という甘めの設定ですらこんなに時間がかかってしまうのは問題ですし,データの単位に応じて適切なdtを設定するのもかなり面倒そうです。Wiener過程の中身を理解するのには役立ちますが,実用的にはいまいちです。もしかしたらもっとうまい書き方があるのかもしれませんが,それよりは別の方法を試してみようと思います。

2. 累積分布関数の逆関数を利用する方法

 もうひとつの乱数生成のやり方として,累積分布関数の逆関数を利用する方法を紹介します。累積分布関数というのは,ある確率分布f(x)があったときに,任意の値qを入れると確率P(x <= q)を返す関数のことです。その逆関数は確率pを入れると値qを返す関数です。確率pの範囲が[0,1]であることと,pとqが一対一対応することを利用すれば,[0,1]の一様分布から発生させた乱数をこの逆関数に代入することで,元の確率分布からの乱数qを得ることができます。つまり,Wiener分布の累積分布関数の逆関数が分かれば一様分布を使ってWiener分布からの乱数を得ることができるのですが,僕が調べた限り,Wiener分布の累積分布関数の逆関数の数式を見つけることはできませんでした。RWiener::qwiener()の内部でも直接qを求めるのではなくpwiener()を利用して近似的にqを計算しているようなので,おそらく解析的に解くのは難しいのだと思います。RWiener::qwiener()の内部のアルゴリズムを真似すれば,逆関数が未知でも元の累積分布関数を使って近似的にqを計算することができるので,その方針で関数を作ってみました。Stanには累積分布関数wiener_cdf()も実装されていないので,これも自作しました。Wiener分布にはlarge-time representation(長い時間のときに収束が速い)とshort-time representation(短い時間のときに収束が速い)の二通りがありますが,ここではlarge-time representationを採用しました。下側の累積分布関数は次の式で表されます (Wabersich & Vandekerckhove, 2014より)。

wiener_cdf_formula.png

なお,この式のP0は下側選択確率です。これについては前回の記事で解説しているのでそちらをご覧ください。
Drift Diffusion Modelのパラメータから選択確率を計算する関数:RとStanによる実装

 累積分布関数の式にはシグマ記号が含まれており,kに1から+∞までの整数を代入したときの総和を取らなければなりません。しかし,許容する誤差をεと決めてやれば,+∞の代わりに高々有限のKLまでの総和を使って近似できることが分かっています。RWienerパッケージではε=10^(-10)に設定されています。今回実装する関数もこれに倣います。KLは以下の不等式を満たす整数であればOKです(再びWabersich & Vandekerckhove, 2014より)。

wiener_K_formula.png

 ここまでの内容をまとめると次のようになります:(1) Wiener分布の累積分布関数の逆関数を利用すればWiener分布からの乱数qを得ることが出来る。(2)逆関数を導出するのは難しいので累積分布関数を使って近似的にqを得ることにする。(3)累積分布関数もStanには実装されていないので自分で関数を実装する必要がある。(4)累積分布関数を計算するためには選択確率の関数が必要になる(前回の記事で作成済み)。ということで,全ての武器が揃ったのでStanに実装してみましょう。Stanコードは以下の通りです。

testmodel02.stan: wiener_rng()の実装その2
functions{
  //上側選択確率を返す関数
  real wiener_upper_cp(real alpha, real beta, real delta){
    real p_lower;
    if(delta == 0){
      p_lower = 1 - beta;
    } else{
      real tmp = -2*delta*alpha*(1-beta);
      p_lower = (1-exp(tmp)) / (exp(2*delta*alpha*beta) - exp(tmp));
    }
    return(1-p_lower);
  }
  
  //上側の累積確率を返す関数
  real wiener_upper_cdf(real x, real alpha, real tau, real beta, real delta){
    real p;
    if(x < tau){
      p = 0;
    } else{
      real beta2 = 1-beta;
      real delta2 = -delta;
      real epcilon = 10^(-10);
      real K1 = 1 / (x - tau) * (alpha / pi())^2;
      real K2 = -K1 * (
        log(
          0.5 * epcilon * pi() * (x - tau) * (delta2^2 + pi()^2 / alpha^2)
          ) 
          + delta2*alpha*beta2 + 0.5*delta2^2*(x-tau)
        );
      real k_max = fmax(fmax(sqrt(K1),sqrt(K2)),1.0);
      real P0 = 1 - wiener_upper_cp(alpha,beta2,delta2);
      real A = 2*pi() / alpha^2 * exp(-alpha*beta2*delta2 - (delta2^2 * (x-tau))/2 );
      real B = 0;
      int k = 1;
      while(k < (k_max + 1)){
        B += k * sin(pi()*k*beta2) / (delta2^2+(k*pi()/alpha)^2) * exp(-0.5 * (k*pi()/alpha)^2 * (x-tau));
        k += 1;
        if(k > 10000){
          P0 = -999; //1万回ループしても終わらなければエラーとして-999を返す。
          break;
        } 
      }
      p = P0 - A*B;
    }
    return(p);
  }
  
  //上側の累積確率を返す関数の逆関数
  real wiener_upper_cdf_inv(real p, real alpha, real tau, real beta, real delta){
    real q = 1;
    real pmin = 0;
    real pmax = wiener_upper_cp(alpha,beta,delta);
    real qmin = 0;
    real qmax = 10^(3); //時間の取り得る最大値を指定
    real epcilon = 10^(-10); //許容できる最大の誤差を指定
    
    if(p > pmax){
      q = -999; //与えられたpが選択確率を越えた場合にはエラーとして-999を返す
    } else{
      real pmid = wiener_upper_cdf(q,alpha,tau,beta,delta);
      int count = 0;
      while(fabs(p-pmid) > epcilon){
        if(p <= pmid){
          pmax = pmid;
          qmax = q;
          q = (qmax+qmin)/2;
        } else{
          pmin = pmid;
          qmin = q;
          q = (qmax+qmin)/2;
        }
        pmid = wiener_upper_cdf(q,alpha,tau,beta,delta);
        count += 1;
        if(count >= 10000){
          q = -9999; //10000回以内に収束しなかったらエラーとして-9999を返す
          break;
        }
      }
    }
    return(q);
  }
  
  //乱数生成: 逆累積確率関数を利用して求める方法
  vector wiener_rng(real alpha, real tau, real beta, real delta){
    vector[2] out;
    real rand = uniform_rng(0,1);
    real cp =  wiener_upper_cp(alpha,beta,delta);
    if(rand <= cp){
      out[2] = 1;
      out[1] = wiener_upper_cdf_inv(rand,alpha,tau,beta,delta);
    } else{
      out[2] = 0;
      out[1] = wiener_upper_cdf_inv(1-rand,alpha,tau,1-beta,-delta);
    }
    return(out);
  }
}

data{ //今回はwiener_rng()の挙動の確認が目的なのでパラメータを外から与える。
  real<lower=0> alpha;
  real<lower=0> tau;
  real<lower=0,upper=1> beta;
  real delta;
}

parameters{
  real tmp; ////エラー防止用の無意味なパラメータ
}

model{
  tmp ~ normal(0,1); //エラー防止用の無意味なサンプリング
}

generated quantities{
  vector[2] x_pred = wiener_rng(alpha,tau,beta,delta); //乱数生成
}

 実行するためのRコードは以下の通り。さっきのコードとファイル名以外全く同じです。

Rで実行する
library(tidyverse)
library(RWiener)
library(rstan)

# おまじない----
rstan_options(auto_write = TRUE)
options(mc.cores = parallel::detectCores())

# 上記モデルのコンパイル----
sm <- stan_model(file="testmodel02.stan")

# パラメータの指定----
data <- list(
  alpha = 1,
  tau = 0.5,
  beta = 0.6,
  delta = 1
)

# サンプリングの実行 (単に乱数を発生させるだけなのでwarmupは小さくしている) ----
fit <- sampling(sm
                 ,data = data
                 ,seed = 610
                 ,chains = 4
                 ,iter = 2520
                 ,warmup = 20
)

# 結果の集計 ----
x_pred_stan <- rstan::extract(fit)$x_pred1 %>% as.data.frame() %>% 
  dplyr::rename(q = V1, resp = V2) %>%
  mutate(resp = as.factor(ifelse(resp==1,"upper","lower"))) %>%
  mutate(resp = factor(resp,levels=c("upper","lower")))

# 比較のためにRWiener::rwiener()からの乱数を得る ----
set.seed(8823)
x_pred_rwiener <- rwiener(10000,data$alpha,data$tau,data$beta,data$delta)

# 要約 ----
x_pred_stan %>% summary()
#       q             resp     
# Min.   :0.5089   upper:8076  
# 1st Qu.:0.5805   lower:1924  
# Median :0.6498               
# Mean   :0.7080               
# 3rd Qu.:0.7769               
# Max.   :2.4453

x_pred_rwiener %>% summary()
#       q             resp     
# Min.   :0.5105   upper:8085  
# 1st Qu.:0.5805   lower:1915  
# Median :0.6509               
# Mean   :0.7090               
# 3rd Qu.:0.7760               
# Max.   :2.2582     

# プロット ----
wiener_plot(x_pred_stan)
wiener_plot(x_pred_rwiener)

 要約統計量はちゃんと一致しているようです。下に示す分布のプロットを見ても特に問題なさそうです。

comp_wiener_rng02.png

 また,今回は計算にかかる時間もかなり短かったです。僕の環境では25秒で終わりました。実装するならこちらのやり方の方が良さそうですね。

3. おまけ:Rで作ったWiener分布関連の自作関数もろもろ

 Stanのwiener_rng()を自作することを目的に文献を探したりシミュレーションをしたりした結果,だいぶWiener分布のことが分かってきたように思います。せっかくなので,その過程で生まれたRの自作関数をここに貼っておこうと思います。基本的にRWienerの関数の下位互換(計算速度が遅いし正確性も不明)なので実用性は皆無だと思いますが,Wiener過程の中身を知る上で勉強の役に立つかもしれません。もし変なところがあったらご指摘頂けると嬉しいです。

おまけのR関数
# ウィーナー分布の上側・下側選択率を求める関数 ----
wiener_cp <- function(alpha,beta,delta,resp="upper",tau=NULL){
  if(resp!="upper" & resp!= "lower"){
    resp <- "upper"
    print("Warining: resp must be either 'upper' or 'lower.' The probability at the upper boundary was calculated.")
  }
  if(delta == 0){
    p <- 1-beta
  } else{
    tmp <- -2*delta*alpha*(1-beta)
    p <- (1-exp(tmp)) / (exp(2*delta*alpha*beta) - exp(tmp))
  }
  
  if(resp=="upper"){
    p <- 1-p
  }
  return(p)
}

# ウィーナー分布の累積分布関数(RWiener::pwiener()の下位互換) ----
pwiener2 <- function(x,alpha,tau,beta,delta,resp="upper",epcilon=10^(-10)){
  if(resp!="upper" & resp!= "lower"){
    resp <- "upper"
    print("Warining: resp must be either 'upper' or 'lower.' The probability at the upper boundary was calculated.")
  }
  if(resp=="upper"){
    beta <- 1-beta
    delta <- -delta
  }
  
  if(x < tau){
    return(0)
  } else{
    K1 <- 1 / (x - tau) * (alpha / pi)^2
    K2 <- -K1 * (log(0.5 * epcilon * pi * (x - tau) * (delta^2 + pi^2 / alpha^2)) + delta*alpha*beta + 0.5*delta^2*(x-tau))
    k_max <- ceiling(max(sqrt(K1),sqrt(max(K2,1))))
    
    P0 <- wiener_cp(alpha,beta,delta,resp="lower")
    A <- 2*pi / alpha^2 * exp(-alpha*beta*delta - (delta^2 * (x-tau))/2 )
    B <- 0
    for(k in 1:k_max){
      B <- B + k*sin(pi*k*beta) / (delta^2+(k*pi/alpha)^2) * exp(-0.5 * (k*pi/alpha)^2 * (x-tau))
    }
    return(P0 - A*B)
  }
}

# Wiener分布の累積分布関数の逆関数 (RWiener::qwiener()の下位互換) ----
qwiener2 <- function(p,alpha,tau,beta,delta,resp="upper",epcilon=10^(-10)){
  if(resp!="upper" & resp!= "lower"){
    resp <- "upper"
    print("Warining: resp must be either 'upper' or 'lower.' The probability at the upper boundary was calculated.")
  }
  if(resp=="upper"){
    beta <- 1-beta
    delta <- -delta
  }
  
  q <- 1
  pmin <- 0
  pmax <-  wiener_cp(alpha,beta,delta,resp="lower")
  qmin <- 0
  qmax <- 10^(10)
  
  if(p > pmax){
    q <- NA
  } else{
    pmid <- pwiener2(q,alpha,tau,beta,delta,resp="lower")
    
    while(abs(p-pmid) > epcilon){
      pmid <- pwiener2(q,alpha,tau,beta,delta,resp="lower")
      if(p <= pmid){
        pmax <- pmid
        qmax <- q
        q <- qmin + (qmax-qmin)/2
      } else{
        pmin <- pmid
        qmin <- q
        q <- qmin + (qmax-qmin)/2
      }
    }
  }
  
  return(q)
}

# Wiener分布からの乱数をシミュレーションで発生させる関数 (RWiener::rwiener()の下位互換;計算時間めっちゃ長い) ----
rwiener2 <- function(n,alpha,tau,beta,delta){
  dt <- 0.0001 #1ステップの時間[s]
  t_max <- 5 #非決定時間以降の最長時間[s]
  q <- rep(0,n)
  resp <- q
  
  N <- (t_max - tau)/dt
  
  for(i in 1:n){
    dis <- delta*dt + sqrt(dt)*rnorm(N, 0, 1);
    dis <- cumsum(dis) + alpha*beta;
    
    upper_first <- ifelse(sum(dis > alpha) > 0, which.max(dis > alpha) * dt, t_max*10)
    lower_first <- ifelse(sum(dis < 0) > 0, which.max(dis < 0) * dt, t_max*10)
    
    q[i] <- min(upper_first,lower_first,t_max)
    resp[i] <- which.min(c(lower_first,upper_first)) - 1 #upper = 1, lower = 0
    if(q[i] == t_max){
      q[i] <- NA
      resp[i] <- NA
    } else{
      q[i] <- q[i] + tau
    }
  }
  out <- data.frame(
    q = q,
    resp = resp
  ) %>%
    mutate(resp = as.factor(ifelse(resp==1,"upper","lower"))) %>%
    mutate(resp = factor(resp,levels=c("upper","lower")))
  return(out)
}

# Wiener分布からの乱数を累積分布関数の逆関数から得る関数 (なぜかRWiener::rwiener()より高速) ----
rwiener3 <- function(n,alpha,tau,beta,delta){
  S <- 1/alpha^2
  alpha2 <- alpha*sqrt(S)
  delta2 <- delta/sqrt(S)
  tau2 <- tau*S
  
  p_upper <- wiener_cp(alpha2,beta,delta2,resp="upper")
  rand <- runif(n,0,1)
  
  q <- rep(0,n)
  resp <- as.numeric(rand < p_upper)
  for(i in 1:n){
    q[i] <- ifelse(resp[i]==1,
                   qwiener2(rand[i],alpha2,tau2,beta,delta2,resp="upper"),
                   qwiener2(1-rand[i],alpha2,tau2,beta,delta2,resp="lower"))
  }
  
  out <- data.frame(
    q = q / S,
    resp = resp
  ) %>%
    mutate(resp = as.factor(ifelse(resp==1,"upper","lower"))) %>%
    mutate(resp = factor(resp,levels=c("upper","lower")))
  return(out)
}
posted by mutopsy at 21:49 | 統計

2020年09月13日

Drift Diffusion Modelのパラメータから選択確率を計算する関数:RとStanによる実装

 Drift Diffusion Modelのパラメータから選択確率(一方の反応が生じる確率;choice probability)を計算する方法について調べたので,備忘録として,かつ,日本語で書かれた資料がWeb上にはなさそうなので,記事にすることにしました。

 Drift Diffusion Modelは反応時間分布を説明するときによく使われるモデルの1つです。2つのうちいずれの反応を実行するかについての情報(証拠)が時間とともに蓄積され,情報量が閾値を越えたときに反応が生じることを仮定します。反応時間分布と書きましたが,正確にいえば<いずれの反応が生じるか>と<反応時間>の同時分布です。このうち,<いずれの反応が生じるか>だけに注目すると,これは2値変数なので,その周辺分布は選択確率Pupperをパラメータとするベルヌーイ分布に従います。この選択確率Pupperを,Drift Diffusion Modelのパラメータから計算する方法についてこの記事で説明します。といっても導出は難しそうなので,計算式とRとStanで実装する方法だけを説明します。これを知っていれば,例えばStanで選択確率の事後予測分布を生成したいときなどに役立つと思います。なお,Drift Diffusion Modelについては初学者向けの優れた解説が既にあるので(e.g.,【RとIATとBayesian hierarchical diffusion model】拡散過程モデルとは?(西村友佳さんによる解説), Stanで階層diffusionモデル(小林穂波さんによる解説)),ここでは改めて解説はしません。モデル式は原著論文 (Ratcliff, 1978) に詳しく書かれていますが,実用上はWiener過程の分散パラメータを1に固定したり (RWienerパッケージやStanでは1に固定している;see Wabersich & Vandekerckhove, 2014),より効率的な計算法を用いたり(e.g., Blurton, Kesselmeier, & Gondan, 2012; Navarro & Fuss, 2009)します。

 Drift Diffusion Modelでは,情報が上側(正の方向)の閾値に到達するか下側(負の方向)の閾値に到達するかのいずれかが生じたときに反応が生じると考えるので,上側選択確率Pupperと下側選択確率Plowerの両方を考えることができます。もちろんPupper + Plower = 1なので,どちらか一方が分かれば十分です。Wiener過程の分散パラメータを1に固定すると,これらは以下の式で表せます。

ddf_p_formula.png

この式はWabersich & Vandekerckhove (2014)のAppendixに載っているものです。ここで,αは閾値を決めるパラメータ(≒慎重さ),βは初期値を決めるパラメータ(≒反応バイアス),δはドリフト率パラメータ(情報集積の速さ)を表します。非決定時間パラメータのτは選択率には影響しないのでこの式には含まれていません。αが大きいほどδの符号と同じ方向の選択確率が高くなること,βが大きいほど上側選択確率が高くなること,δが大きいほどその符号の方向の選択確率が高くなることがこの式から読み取れます。直感にも合いますね。なお,δ=0のときは元の式が0/0の不定形になってしまうので,そうならないようにifで場合分けされています。これらの式を使って選択確率をRで計算するための関数は以下の通りです。

選択確率を計算するR関数
## 関数の定義 ------
wiener_cp <- function(alpha,beta,delta,resp="upper",tau=NULL){
  if(resp!="upper" & resp!= "lower"){
    resp <- "upper"
    print("Warining: resp must be either 'upper' or 'lower.' The probability at the upper boundary was calculated.")
  }
  if(delta == 0){
    p <- 1-beta
  } else{
    tmp <- -2*delta*alpha*(1-beta)
    p <- (1-exp(tmp)) / (exp(2*delta*alpha*beta) - exp(tmp))
  }
  
  if(resp=="upper"){
    p <- 1-p
  }
  return(p)
}

## 関数を使ってみる ------
alpha <- 1
beta <- 0.6
tau <- 0.5
delta <- 1

wiener_cp(alpha,beta,delta, resp="upper") #[1] 0.8081812
wiener_cp(alpha,beta,delta, resp="lower") #[1] 0.1918188

## 合っているかを確認する ------
library(RWiener)
pwiener(100000,alpha,tau,beta,delta,resp="upper") #[1] 0.8081812
pwiener(100000,alpha,tau,beta,delta,resp="lower") #[1] 0.1918188

 Stanで選択確率の事後予測分布を生成するには次のように書けば良いです。関数は,使うことの多い上側選択確率を返すように設定しています。

選択確率の事後予測分布を生成するStanコード
functions{
  //上側選択確率を返す関数
  real wiener_cp_upper(real alpha, real beta, real delta){
    real p_lower;
    if(delta == 0){
      p_lower = 1 - beta;
    } else{
      real tmp = -2*delta*alpha*(1-beta);
      p_lower = (1-exp(tmp)) / (exp(2*delta*alpha*beta) - exp(tmp));
    }
    return(1-p_lower);
  }
}

data{
  int N; //データの数
  real<lower=0> RT[N]; //反応時間
  int resp[N]; //反応:0(下側) or 1(上側)
}

transformed data{
  real minRT = min(RT); //tauの上限
}

parameters{
  real<lower=0> alpha;
  rea<lower=0, upper=minRT> tau;
  real<lower=0,upper=1> beta;
  real delta;
}

model{
  for(i in 1:N){
    if(resp[i] == 1){ //上側反応のとき
      RT[i] ~ wiener(alpha,tau,beta,delta);
    } else{ //下側反応のとき
      RT[i] ~ wiener(alpha,tau,1-beta,-delta);
    }
  }
}

generated quantities{
  real p_upper = wiener_cp_upper(alpha,beta,delta); //上側選択確率
  real p_lower = 1- p_upper; //下側選択確率
  int resp_pred = bernoulli_rng(p_upper); //選択確率の事後予測分布
}

 以上です。ちょっとしたメモのつもりだったのになんやかんや書くのに2時間ぐらいかかってしまいました。Enjoy!

posted by mutopsy at 17:07 | 統計

2020年03月06日

MCMCとともだちになろう [スライド紹介]

 この記事は,ベイズ統計学勉強会2020年春合宿で使用したスライドの紹介記事です。

 『社会科学のためのベイズ統計モデリング』の第4章「MCMC」の内容をベースにMCMCの仕組みについて解説しているスライドです。StanやJAGSのような確率的プログラミング言語を使えば理屈を知らずともMCMCを簡単に実行するができますが,MCMCの仕組みを知っていれば,うまく収束しないときに解決策を見つけやすくなるかもしれません。以下,スライド内で紹介しているRコードを(コピペしやすいように)掲載しておきます。


 まずはメトロポリス・アルゴリズムのコードから (スライド15枚目)。このコードは『社会科学のためのベイズ統計モデリング』のpp.52-53で紹介されているコードを改変し,メトロポリス・アルゴリズムの各ステップをより分かりやすくしています。

Rコード01: メトロポリス・アルゴリズム
## つのドリルデータ ##
data <- rep.int(c(1,0),times=c(18,46-18))

## 確率モデル (尤度) ##
lik <- function(x,theta) theta^sum(x)*(1-theta)^(length(x)-sum(x))

## 事前分布 ##
prior <- function(theta) dbeta(theta,1,1)

## メトロポリス・アルゴリズム ##
n_iter <- 1000 #反復回数

#Step0: 初期値を設定
theta0 <- 0.10

MCMCsample <- 1:(n_iter+1) #結果を格納する変数 (初期値を含める)
MCMCsample[1] <- theta0

for(i in 1:n_iter){
  #Step1: 一様乱数からtheta1を生成
  theta1 <- runif(1,0,1)
  
  #Step2: 事後オッズrを計算
  posterior0 <- lik(data,theta0)*prior(theta0)
  posterior1 <- lik(data,theta1)*prior(theta1)
  r <- posterior1 / posterior0
  
  #Step3: 遷移確率alphaを計算
  alpha <- min(1,r)
  
  #Step4: 次のtheta0を決定
  theta0 <-ifelse(alpha>=runif(1,0,1),theta1,theta0)
  
  #MCMCサンプルとして保存
  MCMCsample[i+1] <- theta0
}

 このコードを実行することで,R上で(メトロポリス・アルゴリズムによる)MCMCを実行することができます。確率モデル(尤度関数)や事前分布,データを変えて遊んでみてください。

 続いて,スライド28枚目にあるマルコフ連鎖のシミュレーション用のRコードを掲載します。

Rコード02: マルコフ連鎖のシミュレーション
## 時点tの最大数 ##
max_t <- 10

## 推移確率行列 ##
P <- matrix(c(1/5,3/5,1/5,
              1/3,1/3,1/3,
              1/2,1/4,1/4),nrow=3,byrow=T)

## 初期の確率ベクトル ##
pi_t <- matrix(c(1,0,0),ncol=3)

## 全時点の確率ベクトルを格納する箱 ##
pi_all <- data.frame(t=0:max_t,ramen=0,chahan=0,bread=0)
pi_all[1,-1] <- pi_t 

## マルコフ連鎖開始 ##
for (i in 1:max_t){
  pi_t <- pi_t %*% P
  pi_all[i+1,-1] <- pi_t
}

 このコードを走らせればマルコフ連鎖が定常分布に収束することを確認できます。初期値や推移確率行列などを変えてみても楽しいです。

 このスライドで解説しているMCMCのアルゴリズム(メトロポリス・アルゴリズムとMHアルゴリズム)は非効率的なのであまり実用的ではありませんが,MCMCの基本的な仕組みとして知っていれば何かと役立つと思います。

タグ:ベイズ MCMC
posted by mutopsy at 19:35 | 統計

2019年12月15日

どの方略が優勢か?:0%と100%を含むパーセントデータの分析 with JAGS

 この記事はStan Advent Calendar 2019の15日目のエントリー記事です。この記事では,「あなたは何%の試行で方略Aを用いましたか?」といった質問項目を実施したときに得られるパーセントデータの分析事例を紹介します。

1. 分析に用いるデータ

 今回分析するデータのヒストグラムを図1に示します。

adcl.hist1.png
図1. 今回の分析に使用するパーセントデータのヒストグラム

 このデータは,筆者が以前行った研究 (Muto, Matsushita, & Morikawa, 2019)で収集したデータの一部です (OSFで公開しています)。詳細は省きますが,この実験では60名の実験参加者 (12名×実験5つ) に2種類の認知課題を遂行してもらったあとで,それぞれの課題において,研究者があらかじめが想定していた方略Aと方略Bについて実験参加者が使用した割合を0%〜100%の範囲で評定してもらいました。測定には図2のような視覚アナログスケール (visual analogue scale) を使用しました。参加者は,0〜100%の線分上の当てはまる位置に縦線を引くことで割合を回答します。データは0〜100の範囲の整数値として記録されました。

adcl.vas.png
図2. 視覚アナログスケールの例 (実際に用いた質問文は論文参照)。

 60名の参加者が課題1・課題2のそれぞれについて方略A・方略Bの使用頻度を評定したため方略Aのパーセントデータは60名×2=120個の要素から成る反復測定データですが,今回は簡単のために対応のないデータとして扱います (つまり,120名の参加者が1回ずつ評定した場合と同じとみなす)。図1のヒストグラムはこれらの120個の値のヒストグラムです。 (ちなみに,ここでいう方略Aとは論文中でegocentric transformation strategyと呼んでいる方略ですが,この記事では単に方略Aと呼ぶことにします。方略Bことmental scanning strategyに関してはこの記事では扱いませんが,方略Bで同じ分析をしても類似した結果が得られると予想されます。)

 図1のヒストグラムの両端 (濃い棒) は評定値がちょうど0%または100%であった参加者の割合を表し,それ以外の薄い棒は[1%, 99%]の範囲のデータについてbinの幅を9としてそれぞれの割合をプロットしたものです。右端の100%のバーがその隣の[91%, 99%]のバーよりも低いのはbinの幅が異なるためであって,100%と回答した参加者が99%と回答した参加者よりも少なかったことを意味する訳ではないので注意。このヒストグラムを見ると,方略Aが用いられた割合は0%付近と100%付近にピークを持ち,かつ0%と100%の値を含んでいることが分かります。つまり,方略Aを使う参加者はほとんどの試行で方略Aを用いるが,方略Aを使わない参加者は一貫して使わない,といった傾向がありそうです (※実際には,参加者の違いではなく実験状況の違いによって方略の使用頻度が二極化すると考えられます)。このようなデータの生成メカニズムを考えるときに真っ先に思い浮かぶのがベータ分布でしょう (図3)。ベータ分布は(0,1)の範囲の実数を実現値として返す確率分布で,非負のパラメータ αβ によって分布の形が様々に変化します。特に 0 < α < 1 かつ 0 < β < 1 のときにはU字型の分布となり,α = β であれば左右対称の分布,α < β であれば0%側に偏った分布,α > β であれば100%側に偏った分布になります。

adcl.betad.png
図3. いろいろなベータ分布。

 この分布を使えば今回のデータをうまく説明できそうな気がしますが,残念ながら両端の0と1に対しては確率密度を定義できない (無限大に発散してしまう) ため,今回のように0%と100%を含むデータに対してはそのまま当てはめることができません。代替案としてゼロワン過剰ベータ分布を使えば恐らくフィッティングはうまくいきますが解釈が困難になります (単純にパラメータが多くなり,しかも評定値が0%になるメカニズムと1%になるメカニズムが質的に異なることを仮定してしまうので,今回のデータには恐らく不適切)。今回のデータをうまく説明するためには一工夫必要なようです。このようなデータの背後にある生成過程を考えてモデルを構築してみましょう。

2. モデル

 今回用いる評定値データは,その前に行った320試行から成る認知課題において方略Aを用いた割合の評定値です。そこでまず,ある実験参加者が認知課題において方略Aを用いる確率を θ と置くと,実際に方略Aを用いた試行の数 ν は,二項分布を用いて

ν ~ Binomial(n = 320, p = θ)

と表現できます。νθ も直接観測することができない未知のパラメータです。さて,もし実験参加者が,自分が方略Aを用いた試行の数 (= ν) と総試行数 (= 320) を正確に知っていれば,実験参加者の評定値は ν / 320 になるはずです。ところが現実的には記憶の曖昧さや評定値の記入の不正確さなどに由来する誤差が生じるはずなので,実際に観測される評定値 Y

Y ~ Normal(μ = ν / 320, σ)

という正規分布に従うと考えることにします。このモデルには離散パラメータの ν が含まれるため,今回はStanではなくJAGSを用いてパラメータの推定を試みます。が,回答誤差のパラメータ σ がうまく収束せず,試行錯誤もむなしく本記事の執筆までに解決することができなかったので,今回は σ を推定せず,σ = 0.0001と決め打ちして αβ を推定することにします (回答の誤差がほとんどないという強い仮定を置くことを意味します)。うまく収束しなかった原因として考えられるのは,(1) σ だけでなく方略Aが用いられる確率 θ にも測定誤差に関する情報が反映されてしまって識別できなかった,(2) 正規分布が誤差分布として適切ではなかった (例えば両端では誤差は小さく50%付近で誤差が最大となるといった仮定を含めたり,[0,1]の範囲に収まるように工夫したりする必要がある?),(3) 筆者がJAGSの仕様をきちんと理解していないためモデルの書き方が悪かった,などがあり得ると思います (原因が分かる方がおられましたら教えて頂けると嬉しいです)。とりあえずこの点には目をつむり,以下のJAGSコードを書いて分析してみました。ちなみにJAGSはStanと比べると収束効率が悪いので,バーンイン期間を (Stanで実装するときよりも) 若干長めにしたり,初期値を明示したりといった工夫をしたほうがよい場合があります。今回はn.chains=4, n.iter=5000, n.burnin=1000, n.thin = 2とし,αの初期値を(0.1,0.3)の一様分布,βの初期値を(0.2,0.4)の一様分布から発生させました。

jagsmodel.txt
model{
	#prior
	alpha ~ dunif(0.0001,2)
	beta ~ dunif(0.0001,2)
	sigma <- 0.0001
	tau <- 1/pow(sigma,2)
	
	#model
	for (i in 1:N){
		theta[i] ~ dbeta(alpha, beta)T(0.00001,0.99999)
		nu[i] ~ dbinom(theta[i], 320)
		Y[i] ~ dnorm(nu[i] / 320, tau)
	}

	#pred
	theta_pred ~ dbeta(alpha, beta)
	nu_pred ~ dbinom(theta_pred, 320)
	Y_pred ~ dnorm(nu_pred / 320, tau)
}

3. 分析結果

 図4は,今回のデータのヒストグラムに事後予測分布を重ねたプロットです。予測分布の[73%,90%]のあたりが実測値よりも若干低めですが,全体的なU字のパターンはうまく表現できているようです。パラメータのEAP推定値はα = 0.15 (95% CI = [0.08, 0.22]),β = 0.26 (95% CI = [0.18, 0.36])でした。α < β なので,100%付近の参加者よりも0%付近の参加者のほうが人数が多いと解釈できます。

adcl.hist2.png
図4. データのヒストグラムとモデルの事後予測分布 (赤の破線)。

4. 発展編:実験条件間の比較

 これだけで終わってしまうのは物足りないので,もうちょっと踏み込んだ分析もしてみました。今回の実験では,理論的に方略Aが優勢になると予想される実験条件Aと,方略Aではなく方略Bが優勢になると予想される実験条件Bの2通りを設定していました。そこで,実際にこの予想が当たっているかどうかを確かめるために,実験条件Aと実験条件Bのそれぞれについて別々に αβ を推定してみました。コードは以下の通り。n.chains=4, n.iter=5000, n.burnin=1000, n.thin = 2とし,初期値は特に明示しませんでした。

jagsmodel_2cond.txt
model{
	#prior
	alpha1 ~ dunif(0.0001,2)
	beta1 ~ dunif(0.0001,2)
	alpha2 ~ dunif(0.0001,2)
	beta2 ~ dunif(0.0001,2)

	sigma <- 0.0001
	tau <- 1/pow(sigma,2)
	
	#model
	for (i in 1:N1){
		theta1[i] ~ dbeta(alpha1, beta1)T(0.00001,0.99999)
		nu1[i] ~ dbinom(theta1[i], 320)
		Y1[i] ~ dnorm(nu1[i] / 320, tau)
	}
	for (i in 1:N2){
		theta2[i] ~ dbeta(alpha2, beta2)T(0.00001,0.99999)
		nu2[i] ~ dbinom(theta2[i], 320)
		Y2[i] ~ dnorm(nu2[i] / 320, tau)
	}

	#pred
	theta_pred1 ~ dbeta(alpha1, beta1)
	nu_pred1 ~ dbinom(theta_pred1, 320)
	Y_pred1 ~ dnorm(nu_pred1 / 320, tau)

	theta_pred2 ~ dbeta(alpha2, beta2)
	nu_pred2 ~ dbinom(theta_pred2, 320)
	Y_pred2 ~ dnorm(nu_pred2 / 320, tau)

	#generated quantities
	phi1 <- log(alpha1 / beta1)
	phi2 <- log(alpha2 / beta2)
}

 実験条件AとBのそれぞれにおける評定値のヒストグラムと予測分布を以下の図5に示しました。ヒストグラムから,条件Aでは方略Aの使用頻度のピークが100%付近にあるのに対し,条件Bでは逆になっていることが分かります。事後予測分布もその特徴をうまく捉えているように見えます。EAP推定値は,条件Aでは α = 1.18 (95% CI = [0.70, 1.77]),β = 0.36 (95% CI = [0.21, 0.52]),条件Bでは α = 0.06 (95% CI = [0.004, 0.14]),β = 0.49 (95% CI = [0.30, 0.75])でした。ここで,αβ の大小関係から分布の偏り度合いが分かるということを利用して,φ = ln(α/β) という生成量を作ってみます。この生成量 φ は,α = β のとき,すなわち方略Aの使用頻度が0%付近と100%付近の両方に等しくピークを持ち分布が左右対称となるときに0と一致します。また,φ が0より小さければ小さいほど0%側に,0より大きければ大きいほど100%側に偏った分布を表します。EAP推定値は条件Aで φ = 1.18 (95% CI = [0.77,1.61]),条件Bで φ = -2.32 (95% CI = [-4.50, -1.33]) でした。これらの結果から,先ほどの予想通り,条件Aでは多くの参加者が方略Aを一貫して用いるのに対し,条件Bでは多くの参加者が方略Aをほとんど使わないということを定量的に示すことができました。

adcl.cond.png
図5. 条件ごとのデータのヒストグラムとモデルの事後予測分布 (赤の破線)。

5. まとめ

 という訳で,この記事では0%と100%を含むパーセントデータのモデリングを試みました。方略Aが課題中に用いられる確率がベータ分布に従うと仮定し,実際に方略Aが何試行で使われたかのメタ認知によって評定値が決まるという発想です。フィッティングするだけならゼロワン過剰ベータ分布や切断正規分布の混合分布などを使うやり方もありますし,例えば0%と100%のデータをそれぞれ1%と99%に置き換えるといった小細工をすれば単なるベータ分布モデルでも似たような結果を得ることはできるのですが,ドメイン知識を利用することで倹約的かつ解釈しやすいモデルを無理なく作ることができうることをこの記事でお示しできたのではないかと思います。σ の推定がうまくいかないという問題もありますし,まだまだ改善の余地はあるはずなので,もう少し吟味してみようと思います。

 以前,某研究会で講演者の方がご自身の論文で報告したデータを再分析してモデリングされていたのを見て,楽しそうだなーと思ったのがこの記事を執筆するきっかけになりました。元の論文 (Muto et al., 2019) では,今回の評定値データはメインの行動データの解釈に役立てるための補助的なデータという位置づけに過ぎなかったので,条件間の平均値差の検定だけしてお茶を濁していました。それはそれで問題ないとは思うのですが,もっと上手な分析方法があるのではないかとずっともやもやしていたというのもあって,この機会に再分析&モデリングを試みたという訳です。この論文のデータはオープンにしているので実は誰でもモデリングをすることが可能です。オープンデータの営みがもっと普及すればモデリングの可能性も広がる気がしてワクワクしますね。

 そしてこの記事を書き終えてから気付いたのですが,この記事は「Stan」 Advent Calendarの記事のはずなのにStan全く使ってねえ……。StanでできないことをJAGSにやってもらったということで許してクレメンス。

引用文献:
Muto, H., Matsushita, S., & Morikawa, K. (2019). Object's symmetry alters spatial perspective-taking processes. Cognition, 191, 103987. https://doi.org/10.1016/j.cognition.2019.05.024

posted by mutopsy at 00:00 | 統計

2019年12月03日

今までに買ったチョコボールの箱数の推定:JAGS・Stan・数値積分による離散パラメータの推定

「おまえは今まで買ったチョコボールの数をおぼえているのか?」

 ……こんなことを聞かれたら,あなたならどう答えますか?「覚えとらんわ」と一蹴するのは簡単ですが,答えられないのはちょっと悔しい。という訳で,この記事では僕が今までに買ったチョコボールの箱数の推定を試みます。

※この記事は,Stan Advent Calendar 2019の3日目のエントリー記事です。

1. チョコボールとエンゼルについて

 改めまして,こんにちは。mutopsyです。冒頭でも説明した通り,この記事では僕が今までに買ったチョコボールの箱数を推定することを目指します。

 チョコボールは森永製菓から発売されている国民的チョコレート菓子の1つです。チョコボールの箱の「くちばし」と呼ばれる開口部には,ごくまれにエンゼルのイラストが印刷されており,銀のエンゼルなら5枚,金のエンゼルなら1枚で「おもちゃのカンヅメ」と交換することができます。金のエンゼルと銀のエンゼルが当たる確率は企業秘密とのことですが,以前12,283箱分のチョコボールデータを用いて推定してみたところ,銀のエンゼルが当たる確率は約3.60% (3.13〜4.10%程度),金のエンゼルが当たる確率は約0.05% (0.02〜0.12%程度) であると推定されました(「たのしいベイズモデリング2:事例で拓く研究のフロンティア」の第1章;過去の記事も参照:[その1] [その2])。今回の分析では,銀のエンゼルが当たる確率についての情報を利用します。

1. 利用可能な情報とモデル

 当然ながら,今までに買ったチョコボールの箱数を推定するためには何らかの情報が必要です。今回僕が注目したのは,今までに当てた銀のエンゼルの数です。これまでに買ったチョコボールの総数そのものはさすがに覚えていませんが,銀のエンゼルが当たるというイベントはそれなりに記憶に残る事象であり,かつ生起頻度も少ないので,それぐらいなら覚えているのも難しくないでしょう。さらにいえば,僕は以前に銀のエンゼルを5枚集めておもちゃのカンヅメと交換してもらったこともあり,そしておそらくそれ以来銀のエンゼルを見かけていないので,これまでに当てた銀のエンゼル数が5枚であることがはっきりしています。そういう訳で,今回の分析では「今までに当てた銀のエンゼルの数は5枚」という情報と「銀のエンゼルが当たる確率は約3.60%」という情報を使って今までに当てたチョコボールの箱数の推定を試みます。ちなみに金のエンゼルを当てたことはありませんが,金のエンゼルが当たる確率は非常に低いため,「今までに当てた金のエンゼルの数は0枚」という情報はあまり役に立ちません。簡単のためにも今回の分析では金のエンゼルの情報については用いないことにします(反対に,「金のエンゼルをn (≧1) 枚当てたことがある」という情報は有用なので積極的に利用すべきです)。

 銀のエンゼルが当たる確率は約3.60%と述べましたが,先に紹介した書籍ではベイズ推定を用いて銀のエンゼルが当たる確率θsilverの確率分布 (事後分布)を計算しました (3.60%という値は事後分布のMAP推定値)。せっかくなので今回は点推定値ではなく分布の持つ情報をフル活用しましょう。この確率分布は以下のベータ分布とほぼ一致します (※書籍の分析では弱情報事前分布を用いたので厳密には一致しませんがほとんど無視できる程度の差です)。

θsilver ~ Beta(α = 351 + 1, β = 9404 + 1)

この分布は,9755 (= 351 + 9404) 箱のチョコボールから351枚の銀のエンゼルを見つけたときに二項分布モデルを用いて推定したθsilverの分布に相当します (一様事前分布を想定した場合)。チョコボールをν箱開封したときに銀のエンゼルがNsilver枚得られる確率は,二項分布を用いて

Nsilver ~ Binomial(n = ν, p = θsilver)

と表現することが可能です。このνが今回推定したいパラメータですね。ただし,νは自然数をとる離散パラメータなので,離散パラメータを直接推定することができないStanを使う場合は一工夫必要になります。そこでこの記事では,まず始めに離散パラメータを推定することができるJAGSを用いた推定法を紹介し,続いてStanで同様の分析を行う方法を説明します。そして最後に,数値積分を用いて正確な事後分布を得る方法も簡単に紹介します。ちなみに今回はNsilver = 5の場合の推定結果を示しますが,他の値を使うことももちろんできますし,Nsilverを確率分布で表現できるようにモデルを拡張することも可能です (例えば,「正確には覚えていないが3〜5枚のいずれかのはず」といった記憶の曖昧さも表現できます)。

3-1. JAGSによる推定

 JAGSはStanと違って離散パラメータを直接推定することができるので,上記のモデルをそのままコードに起こすだけで分析を実行することができます。分析に使用するJAGSコードは以下の通り。

angels_postmodel_jags.txt
model{
	#モデル
	N_silver ~ dbinom(theta_silver, nu)

	#銀のエンゼルが当たる確率の事前分布
	theta_silver ~ dbeta(alpha, beta)

	#今までに買ったチョコボールの数の事前分布(離散一様分布)
	nu ~ dcat(p_each[])
	for (m in 1:Max_nu){
	  p_each[m] <- 1/Max_nu
	}
}

ベータ分布と二項分布を用いて上記のモデルをそのまま書いています。Stanとは異なり変数の宣言は不要です。νの事前分布として十分幅の広い離散一様分布を指定しています。今回は上限 (Max_nu) を1000とします (データとしてRから渡す)。以下のRコードを実行すれば走ります。

JAGSで分析するためのRコード
#以下の環境で動作確認済み
#JAGS4.3.0, R2jags0.5-7, R3.5.2, Windows 10 Pro

#JAGSを下記URLからダウンロードしインストールする必要があります。
# http://mcmc-jags.sourceforge.net/
library(rjags)
library(R2jags)

#銀のエンゼルが当たる確率の事後分布 (ベータ分布) のパラメータ
alpha <- 351 + 1
beta <- 9404 + 1

#今回の分析に必要な情報
N_silver = 5 #今までに当てた銀のエンゼルの数
Max_nu = 1000 #今までに買ったチョコボールの数の上限 (十分大きな値にしておく)

#データの作成
data.post <- list(N_silver = N_silver, Max_nu = Max_nu, 
                  alpha = alpha, beta = beta)

#モデルの読み込みとサンプリング
fit.jags <- jags(data=data.post, model.file="angels_postmodel_jags.txt", parameters.to.save=c("nu"), 
             n.chains=4, n.iter=10500, n.burnin=500, n.thin = 1, DIC=TRUE)
  #MAPやHDIといった要約統計量の推定精度がどのくらい必要かに応じてn.iterを変える。
  #n.iter=250500ぐらいまで増やせばそれなりの精度になります (時間はかかりますが)。

maphdi(fit.jags) #MAPとHDIを計算するための自作関数;過去記事参照
#結果は以下の通り:
#                MAP   Lower95    Upper95   Lower99   Upper99
#deviance   3.531314  3.438523   7.376154  3.438523  10.26979
#nu       141.986992 46.000000 296.000000 36.000000 369.00000

MAPとHDIの計算部分は以前の記事で紹介した自作関数を使用しています。νの事後分布を可視化すると以下のようになります。

post_jags.png

ということで,MAP推定値で見ると,僕はどうやら今まで約139箱のチョコボールを購入したようです。体感的にもそんなもんかなといったところです。ただし事後分布の幅は広めなので注意が必要です。また,iterationの数が少ないと点推定値はかなりぶれます。ちなみに今までに買ったチョコボールの数が100個を超える確率を計算したところ約84%となりました。

3-2. Stanによる推定

 前節ではJAGSによる推定方法を紹介しましたが,今回のモデルの場合は実は負の二項分布からの乱数を発生させるだけでνの事後分布を得ることが可能です。Stanを使うまでもないのですが,例えば銀のエンゼルが当たる確率をデータから推定した上でその値を使ってνを推定したい,という場合は全部Stanで書いた方が楽なので,そういった場合を念頭に置いてStanによる推定法を紹介します。今回のモデルでは

Nsilver ~ Binomial(n = ν, p = θsilver)

という二項分布を用いていますが,Nsilverが定数でνが確率変数のとき,νの事後分布は負の二項分布を用いて

ν ~ NegBinomial(r = Nsilver + 1, p = θsilver)

と表現することができます (負の二項分布は,成功率がpのときにr回成功するまでに要する試行数が従う分布です)。したがって,既知のNsilverθsilverを用いれば,MCMCするまでもなくνの事後分布を得ることができます。ただし,Stanのneg_binomial関数では2番目の引数が成功確率ではなくオッズ (成功確率と失敗確率の比) になっているので書き方を工夫する必要があります。Stanコードは以下の通りです。

angels_postmodel_stan.stan
data{
  int alpha;
  int beta;
  int N_silver;
}

parameters{
  real theta_silver;
}

model{
  theta_silver ~ beta(alpha,beta);
}

generated quantities{
  int nu;
  nu = neg_binomial_rng(N_silver+1,theta_silver/(1-theta_silver)) + N_silver;
}

 モデル記述部ではθsilverをベータ分布から発生させているだけです。生成量として負の二項分布から乱数を発生させればνの事後分布を得ることができます。上記コードを実行するためのRコードは以下の通り。ただし前処理の部分はJAGSコードと同じなので省略しています。

Stanで分析するためのRコード
#以下の環境で動作確認済み
#rstan2.18.2, Rtools3.5, R3.5.2, Windows 10 Pro
library(rstan)

... #JAGSのときと同じ前処理

stanmodel_post <- stan_model(file="angels_postmodel_stan.stan")
fit.stan <- sampling(stanmodel_post, data=data.post, seed=123,
                     chains=4, iter=250500, warmup=500, thin=1
                     ) #JAGSよりも速いのでiterを多めにしている。

round(maphdi(fit.stan),2)
#結果は以下の通り:
#                  MAP  Lower95  Upper95  Lower99  Upper99
#theta_silver     0.04     0.03     0.04     0.03     0.04
#nu             135.62    46.00   296.00    32.00   367.00
#lp__         -1514.99 -1516.88 -1514.96 -1518.29 -1514.96

そしてνの事後分布は以下の通り。

post_stan.png

 多少誤差はありますが,JAGSの結果とほぼ一致していることが分かります。

3-3. 数値積分による推定

 これまでは,二項分布のパラメータνを推定する方法として(1)JAGSで離散パラメータとして推定する方法と(2)Stanを使って (あるいは使わなくても) 負の二項分布から乱数を発生させる方法を紹介しました。ただし,これらの方法は乱数を用いた方法のためiterationが小さいと(少ないと)推定結果が安定せず,たとえばMAP推定値を1の位まで正確に求めるためにはiterationをかなり大きく設定する必要があります。そこでこの節では,乱数に頼らずに事後分布を数値積分で求める方法を簡単に紹介します。いろいろなやり方があると思いますが,ここでは高校数学で習う区分求積法を用いたコードを紹介します。Rコードは以下の通り。

数値積分で推定するためのRコード
Max_nu = 1000

#θがベータ分布に従うときの二項分布モデル(r|ν,θ)の,nの尤度を返す関数を定義 (区分求積法でθを消去)
cal.lik.nu <- function(nu,r,alpha,beta,bin.n=2000){
  output.lik.nu<-0
  bin.w <- 1/bin.n
  for (x in 0:(bin.n-1)){
    p.t <- x/bin.n+bin.w/2
    output.lik.nu <- output.lik.nu + bin.w * dbinom(r,nu,p.t) * dbeta(p.t,alpha,beta)
  }
  return(output.lik.nu)
}

#全てのnについて尤度を計算しベクトルとして格納
lik.nu <- 1:Max_nu
for(n in 1:Max_nu){
  lik.nu[n] <- cal.lik.nu(n,N_Silver,prior_pars$alpha,prior_pars$beta)
}

#尤度を正規化して確率に直す (nの事前分布に離散一様分布を仮定した場合)
prob.nu <- lik.nu / sum(lik.nu)

#MAPとHDIの計算と格納 ※参照:http://bayesmax.sblo.jp/article/185297395.html
accum<-1:length(prob.nu)
for(i in 1:length(prob.nu)){
  if (i ==1){
    accum[i] <- sort(prob.nu)[i]
  } else{
    accum[i] <- accum[i-1] + sort(prob.nu)[i]
  }
}
Interval95 <- grep(TRUE,prob.nu>max(sort(prob.nu)[accum<=(min(abs(accum-0.05))+0.05)]))
Interval99 <- grep(TRUE,prob.nu>max(sort(prob.nu)[accum<=(min(abs(accum-0.01))+0.01)]))
HDI95 <- c(min(Interval95),max(Interval95))
HDI99 <- c(min(Interval99),max(Interval99))
Result.intg <- data.frame(MAP=which.max(prob.nu),Lower99=HDI99[1],
                      Lower95=HDI95[1],Upper95=HDI95[2],Upper99=HDI99[2],
                      row.names = "n_total")

#MAPとHDIの表示
print(Result.intg) #MCMCサンプルから計算した値よりも正確
#        MAP Lower99 Lower95 Upper95 Upper99
#n_total 138      34      50     300     369

#今までに買ったチョコボールの数が100を超える確率
print(sum(prob.nu[101:length(prob.nu)])) #MCMCサンプルから計算した値よりも正確
#[1] 0.8419658

事後分布は次の通り。

post_intg.png

 この方法を使うと高速かつ正確に事後分布を求めることができます。iterationをものすごく増やせばJAGSやStanでも同じ結果が得られます。モデルによってはMCMCより数値積分したり解析的に解いたりしたほうが楽な場合もあるということですね。

4. まとめ

 という訳で,僕が今までに買ったチョコボールの数を推定することにぶじ成功しました。離散パラメータを推定するためにJAGSを使うもよし,Stanで乱数を発生させるのもよし,数値積分に頼るのもよし。手段はどうあれ目的は達成できましたね。これでもう大丈夫。「おまえは今まで買ったチョコボールの数をおぼえているのか?」と聞かれたら,僕ならこう答えます。

「おぼえていないが どうやら50〜300箱の ようだ──ッ」

※この記事の内容はもともと「たのしいベイズモデリング2」の第1章に含めるつもりでいたのですが,紙面の都合で泣く泣くカットせざるを得なかったためアドカレのネタとして使い回すことにしました。という裏話。

posted by mutopsy at 00:00 | 統計