餡子付゛録゛

ソフトウェア開発ツールの便利な使い方を紹介。

Rのリスト処理のざっとした説明

説明用です。他のプログラミング言語連想配列/マップ/ハッシュと同様に使えるわけですが、[ ]と[[ ]]の使い分けがややこしいですね。

# 空リストを作成
lst <- list()

# リストを作成
lst <- list("a"=123, "b"=456, "c"=789)

# ベクターをリストに変えて、名前をつけてもよい
lst <- as.list(c(123, 456, 789))
names(lst) <- c("a", "b", "c")

# 1番目だけを抽出したリストを作る
sub_1 <- lst[1]

# "b"と"c"のリストを作る
sub_bc <- lst[c("b", "c")] 

# 2番目の要素を得る
lst[[2]]

# 3番目の要素を得る
getElement(lst, 3)

# "a"の要素を得る
lst[["a"]]

# "d"を追加する
lst["d"] <- 0

# "b"を消す
lst["b"] <- NULL

# 名前"d"の要素が含まれているか調べる
if(!is.null(lst[["d"]])) print("名前dの要素は無い")

# 名前のベクトルを得る
names(lst)

# 要素のベクトルを得る
unlist(lst)

# リストの長さを得る
length(lst)

# 全部の要素から17を引いたリストをつくる
lst_minus_17 <- lapply(lst, function(x){ x - 17 })

# 全部の要素に31を足したベクトルを得る
vector_plus_31 <- sapply(lst, function(x){ x + 31 })

# ベクトルのリストをつくる
vlst <- list("a"=c(1, 2, 3), "b"=c(4, 5, 6), "c"=c(7, 8, 9))

# リストの各要素の2番目の値を取り出す
sapply(vlst, "[[", 2) # "[["はgetElementでもOK

# リストの各要素の3番目の値だけで、リストを作りなおす
vlst_sub <- lapply(vlst, "[[", 3)

前世紀風のRの日付処理

以前書いたエントリーを見直していて、操作を手早く紹介する例をつくっておきたくなりました。文字列型や数値型の日付データを、タイムゾーンUTCでのUNIX秒を保持するクラスPOSIXctか、日付構造体になっているリストPOSIXlt型に変換して操作して、文字列型に戻すだけの話なんですが、関数名をよく忘れるので。なお、最近はlubridateパッケージを使うのが一般的のようです。

# POSIXct → POSIXlt
now_ct <- Sys.time() # 現在時間を取得すると、POSIXct型で戻る
now_lt <- as.POSIXlt(now_ct)

# POSIXct or POSIXlt → numeric
now_unix_time <- as.numeric(now_ct)

# numeric → POSIXct, POSIXlt 
# tzの引数はOlsonNames()が返す値。tzを省略するとデフォルトのタイムゾーンになる
now_ct <- as.POSIXct(now_unix_time, origin="1970-01-01", tz="Japan")
now_lt <- as.POSIXlt(now_unix_time, origin="1970-01-01", tz="Japan")

# C言語のmktime風にPOSIXctに変換
now_ct <- ISOdate(1997, 4, 3, tz="Japan")
now_ct <- ISOdatetime(1997, 4, 3, 13, 34, 45, tz="Japan")
now_lt <- as.POSIXlt(now_ct)

# POSIXct or POSIXlt → 文字列
format(now_lt, "%Y/%m/%d %H:%M:%S") # helpはstrftimeを参照

# 文字列 → POSIXct
# formatを省略するとデフォルトのtryFormats(e.g. %Y/%m/%d)が試される
as.POSIXct("1997*4+3", format="%Y*%m+%d", tz="Japan")
as.POSIXlt("1997*4+3", format="%Y*%m+%d", tz="Japan")

# POSIXctと違いPOSIXltは30日後や100時間後のカレンダーの日付が得られる
# ただし$yearが{年-1900}(e.g. 2022年だと122),$monが{月-1}(e.g. 12月だと11)なのに注意
format(now_lt, "%Y/%m/%d %H:%M:%S")
now_lt$mday <- now_lt$mday + 30 # 30日後にセット
format(now_lt, "%Y/%m/%d %H:%M:%S")
now_lt$hours <- now_lt$hours + 100 # さらに100時間後にセット
format(now_lt, "%Y/%m/%d %H:%M:%S")

# roundで丸め処理ができる
round(as.POSIXlt("1997-4-3 12:34:56"), "mins")

# シーケンス生成もできる
seq(as.Date("2022/2/24"), as.Date("2022/10/13"), "weeks")
seq(as.POSIXlt("1997-4-3 12:34:56"), as.POSIXlt("1997-4-5 12:34:56"), "12 hours")

# 応用例:来年まであと何日?
now_lt <- as.POSIXlt(Sys.time())
# 今年(now_lt$year + 1900) + 1は来年
newyear_ct <- ISOdate(now_lt$year + 1900 + 1, 1, 1, tz="Japan")
# 差分をとる前にDate型にして端数を抑制
difftime(as.Date(newyear_ct), as.Date(now_lt), units="days")

Rで線形混合モデル×多重代入法

反復測定分散分析をlme4パッケージとmiceパッケージを使った線形混合モデル*1×多重代入法で行なう作業を確認したので公開します*2。以前のエントリー「反復測定分散分析を主観ベイズ推定に置き換えよう」で、こっちの方が楽と書いたのですが、欠損値補定がやや煩雑だったので。

1. データセット

前回とほとんど同じモノにしますが、比較のために欠損値なしのデータセットも作っておきます。細かい説明は以前のエントリーを参照してください。

set.seed(604)
rm(list=ls())

n <- 120 # 各時点のサンプルサイズ
t <- 1:3 # 時点
g <- c("C", "T") # 群

dimnames <- list(g, sprintf("t=%d", t))

# 以下のmu, sigma, betaは真の値になるので、本当は分からない
mu <- matrix(c(0+0*t, 0+1*(t-1)), ncol=length(t), byrow=TRUE, dimnames=dimnames)
sigma <- matrix(c(1+0*t, 1+0.1*(t-1)), ncol=length(t), byrow=TRUE, dimnames=dimnames)
beta <- 1

x <- rep(runif(n), length(t))

df01 <- data.frame(
    id=rep(1:n, length(t)),
    time=rep(t, each=n),
    group=rep(rep(g, each=n/length(g)), length(t)),
    x=x,
    z=x/2 + rnorm(n, sd=1),
    score=numeric(n*length(t))
)

rm(x)

for(j in t){
    for(i in 1:length(g)){
        s <- n*(j-1)+n/length(g)*(i-1) + 1
        e <- n*(j-1)+n/length(g)*i
        df01$score[s:e] <- rnorm(n/length(g), mean=mu[i, j], sd=sigma[i, j]) + beta*df01$x[s:e]
    }
}

# xに欠損値をつくる
df00 <- df01
df01$x[df01$id %in% sample(1:n, n/2)] <- NA

2. 補定と推定

miceパッケージのお手軽関数は、lme4パッケージが出力するオブジェクトを現時点で処理できません。miceが補定*3したデータセットに逐次推定をまわし、その結果となる係数と分散共分散行列の推定量をリストにまとめて、miceaddsパッケージ、mitoolsパッケージに渡して推定結果を統合します。こう書くと煩雑で大変そうですが、偉い人が情報をまとめておいてくれました*4

# 利用パッケージを呼び出す
library(lme4)
library(mice)
library(miceadds)
library(mitools)

# miceで補定したm個のデータセットをつくる
# 欠損値があるのは変数xのみ
mice.out <- mice(df01, blocks=c("x"), print=FALSE, m=100)

# 補定したm個のデータセットをそれぞれ推定する
mods <- lapply(1:mice.out$m, function(i){
    # m個の補定データセットの中からi番目を取り出す
    dfm <- complete(mice.out, action=i)
    # i番目で推定をかけて結果を戻すとリストに格納される
    lmer(score ~ x + factor(time)*factor(group) + (1|group), data = dfm)
})

# 推定結果の集合から係数を抜き出す
cmod <- MIextract(mods, fun=function(r_lmer){
    # lme4の癖でcoef(r_lmer)が複数行返ってくるため
    summary(r_lmer)$coefficients[,1]
})

# 推定結果の集合から分散共分散行列を抜き出す
vmod <- MIextract(mods, fun=function(r_lmer){
    # lme4の癖でvcov(r_lmer)が行列にならないため
    as.matrix(vcov(r_lmer))
})

# 自由度を計算
df <- with(summary(mods[[1]]), length(residuals) - length(coefficients[,1]))

# 推定結果を統合
r_pool <- pool_mi(qhat=cmod, u=vmod, dfcom=df)

# coef(r_pool)とvcov(r_pool)ができるので、後はまとめる

欠損値補定後にクロス項をつくっていますが、今回はクロス項に使う変数は補定されていないので問題ないです。unable to evaluate scaled gradientや Model failed to converge: degenerate Hessian with 1 negative eigenvaluesと言う警告が出るのですが、どうも内部で生成している乱数が原因で、推定結果を大きく左右するものでは無さそうなので無視します。

3. 結果の比較

欠損値補定、劇的に推定結果が代わるというものではないのですが、欠損値なしの場合と、listwise法の場合と、miceの場合で比較してみましょう。

# 欠損値なしデータでの推定(比較用)
mixed.lmer.c <- lmer(score ~ x + factor(time)*factor(group) + (1|group), data = df00)

# listwise法(比較用)
mixed.lmer.m <- lmer(score ~ x + factor(time)*factor(group) + (1|group), data = df01)

cmp_beta <- cbind(summary(mixed.lmer.c)$coefficients[,1], summary(mixed.lmer.m)$coefficients[,1], coef(r_pool))
cmp_se <- cbind(summary(mixed.lmer.c)$coefficients[,2], summary(mixed.lmer.m)$coefficients[,2], sqrt(diag(vcov(r_pool))))
colnames(cmp_beta) <- colnames(cmp_se) <- c("No Missing", "listwise", "MI/mice")

listwise法よりも欠損値なしに近づけば改善ですが、係数の比較は改善しているか、悪化しているのか良く分かりません。

標準誤差は改善していると言えるでしょう。

A. Ameliaを使う

lme4パッケージとmiceパッケージを併用するにはどうしたらいいかと言う質問に、Ameliaパッケージを使えと回答がついていたのを見かけて試してみたのですが、微妙でした。Amelia向きのデータ生成のはずですが。

library(Amelia)
library(merTools)
library(plyr)

# Ameliaで補定したm個のデータセットをつくる
amelia.out <- amelia(df01, idvars="id", ts="time", cs="group", m=100)

# 補定したm個のデータセットをそれぞれ推定する
mods <- lapply(amelia.out$imputations, function(dfa){
    lmer(score ~ x + factor(time)*factor(group) + (1|group), data=dfa)
})

# 推定結果の集合から係数を抜き出す
beta <- sapply(mods, function(r_lmer){
    summary(r_lmer)$coefficients[, 1]
})

# 推定結果の集合から標準誤差を抜き出す
se <- sapply(mods, function(r_lmer){
    summary(r_lmer)$coefficients[, 2]
})

# 合成する(分散共分散行列でなくて良いのか感があるが、良いらしい)
r_mi <- mi.meld(q=beta, se=se, byrow=FALSE)

# 比較表に追加
cmp_beta <- cbind(cmp_beta, c(r_mi$q.mi))
cmp_se <- cbind(cmp_se, c(r_mi$se.mi))
colnames(cmp_se)[4] <- colnames(cmp_beta)[4] <- "Amelia"

*1:線形混合モデルとその応用例 - Qiita

*2:データセットの都合で、一般化線形混合モデルではなくて、線形混合モデルになっています。lme4パッケージの場合、lmerをglmerにすれば一般化線形混合モデルになるので、だいたい同じノリで処理できますが。

*3:階層構造を指定したりと利用がややこしいので、今回は使いませんでしたが、miceaddsパッケージのmice.impute.ml.lmerを使うと、線形混合モデルを使った補定が可能になるようです。

*4:[R][初心者の質問] mice で多重代入法の結果を統合できない時の対処法 - ill-identified diary

OpenBLASをリンクしたWindows版R 4.2 PR

標準のNetlib BLASよりも高速な線形代数ライブラリOpenBLASをリンクしたR 4.1.xを使ってきたのですが、今年から4.2系が標準になるようです。古いRを使い続けていると、更新されたパッケージを使うときに警告で出て目障りなので、バージョンアップをしてみました。ちょっとだけチートペーパーと手順を変えたので、記録しておきます。

1. 補助ツールのインストール

1.1. Inno Setupのインストール

公式サイトからインストーラーinnosetup-6.2.1.exeをダウンロードしてきて、Inno Setupをインストール*1。インストール先はデフォルトで*2

1.2. MikTeXのインストール

公式サイトからbasic-miktex-22.3-x64.exeをダウンロードしてきて、MikTeXをインストール。付属ユーティリティMiKTeX Consoleで、administration modeを選択し、Check for updates、Update now、メニューのTasksのRefresh file name database、Refresh font map files、Update package databaseを順番に実行。

1.3. QPDFのインストール

SourceForgeのQPDFのリポジトリからバイナリqpdf-10.6.3-bin-mingw32.zipをダウンロードしてきて、C:で展開。

2. Rtools42のインストールから、R 4.2PRのコンパイルまで

Rの公式サイトのRtools42のフォルダーからRtools42 installer(rtools42-5253-5107.exe)をダウンロードしてきてインストールします。
まず、Rtools42 Bashを起動して、wgetコマンドを追加して、Rtoolsのアップデートをします。

pacman -Sy wget
pacman -Syuu

自動終了するので、Rtools42 Bashを再び起動して、(一時的に使う)PATHを設定します

export PATH=/c/rtools42/x86_64-w64-mingw32.static.posix/bin:/c/rtools42/usr/bin:$PATH
export PATH=/c/Program\ Files/MiKTeX/miktex/bin/x64:$PATH
export TAR="/usr/bin/tar"
export TAR_OPTIONS="--force-local"

Rtools42 Bashを終了したら、再設定になるので注意してください。
リリース候補版の最新ソースコードをダウンロードしてきて、C:に置きます*3

cd /c
wget https://cran.r-project.org/src/base-prerelease/R-latest.tar.gz
tar zxf R-latest.tar.gz --force-local
# 「シンボリックリンクが作れません」とエラーが出た場合は、展開したファイルを残したまま、同じコマンドで展開すると誤魔化せる*4

C:\R-rcが出来ます*5

Rの公式サイトのRtools42のフォルダーにあるTcl/Tkのソースコードをダウンロードしてきて、C:\R-rcに展開します。

export wdir=/c/R-rc # 展開先がC:\R-rcでない場合は、ここを修正
cd $wdir
wget https://cran.r-project.org/bin/windows/Rtools/rtools42/files/tcltk-5253-5175.zip
unzip tcltk-5253-5175.zip

なお、ファイル名tcltk-5253-5175.zipは、今後、変わっていく可能性があるので、適時変更してください。
そして、src/gnuwin32に移動して、MkRules.localをつくります。qpdfのフォルダ名に注意してください。

cd $wdir/src/gnuwin32
cat <<EOF >MkRules.local
USE_ATLAS = YES
EOPTS = -march=native -pipe -mno-rtm -mno-fma
LTO = -flto -ffat-lto-objects -fuse-linker-plugin
LTO_OPT = -flto -ffat-lto-objects -fuse-linker-plugin
LTO_FC_OPT = -flto -ffat-lto-objects -fuse-linker-plugin
QPDF = C:/qpdf-10.6.3
OPENMP = -fopenmp
EOF

QPDFのパスはインストール先にあわせてください。
src/extra/blas/Makefile.winをsedで置換します。notepadで編集したいのですが、UNIX改行コードを認識しないので。

cd $wdir/src/extra/blas
mv Makefile.win Makefile.win.old
sed 's/-L"$(ATLAS_PATH)" -lf77blas -latlas/-fopenmp -lopenblas/' < Makefile.win.old > Makefile.win

Makefile.winが変わっているとパターンマッチしないので、cat Makefile.winをして、しっかり書き換わっているかチェックしてください。

PATHが通っているか確認します。

which make gcc pdflatex tar

エラーが出なければ問題なしです。
コンパイルを開始します。

cd $wdir/src/gnuwin32
make distribution

無事、終われば C:\R-rc\src\gnuwin32\installerにR-4.2.1rc-win.exe*6が出来上がっています。私はMikTeXのアップデート後の処理が抜けて!pdfTeX error: pdflatex.exe (file ts1-zi4r): Font ts1-zi4r at 540 not foundと言われたり、QPDFのパス指定を誤ったりして、修正後、make distributionをやり直しました。

3. 動作確認

Windows版ではsessionInfo()をしても使っているBLASの種類を教えてくれないのですが、マルチコアの計算機で以下のように行列演算をさせて、ユーザー時間が経過時間よりも大きければ並行処理ができているので、OpenBLASとリンクしているのが分かります。

set.seed(1013)
n <- 5000
M1 <- matrix(runif(n^2, min=1, max=10), n, n)
M2 <- matrix(runif(n^2, min=1, max=10), n, n)
system.time({ M3 <- M1 %*% M2 })

*1: 本当は古いバージョンのInno Setupが残っていたので、それで済ましています。

*2: インストール先をカスタマイズした場合は、Mkrules.localに、ISDIR = /path/to/Inno を付け加える必要があります。

*3: https://cran.r-project.org/src/base/R-latest.tar.gzを落としてくれば、リリース最新版がコンパイルできます。

*4: 無視すると「make[2]: *** 'stamp-recommended' に必要なターゲット 'MASS.ts' を make するルールがありません. 中止.」のようなことを言われて止まる.

*5: リリース版だと-rc無しで-4.2.1のようなバージョン番号がつきます。4.2リリース後は、R-patchedに展開されるようになりました。

*6:4.2リリース後はR-4.2.1patched-win.exe

反復測定分散分析を主観ベイズ推定に置き換えよう

TJO氏にふられた質問で、反復測定分散分析(repeated measures anova)をベイズ推定に置き換えられないかと言う話があったので、質問文から想定される状況を整理し、欠損値補定をFully Bayesian Imputation Method *1で、モデル比較をBayes Factorで行なうコードを描いて試してみました。

結論はRのlme4パッケージあたりで時点ダミー×介入群ダミーを入れた線形混合モデルを推定した方がコード量も計算時間も1/100で済むといった感じなのですが *2、各時点各群の平均と分散をそれぞれ推定できること、欠損値の補定方法のパラメーターも同時推定できる利点もあるので、ファンがうぃーんと言うのが好きな人には良いかもです。うぃーん。

1. 分析する状況の想定

反復測定分散分析が使われているところなので、医療系の実験で、薬剤を与える介入群Tと、偽薬を与える操作群Cの2群を、第1時点、第2時点、第3時点それぞれ調べ、介入群と操作群の違いが出る時点を探すことを考えます。
数式で書くとこんなモデルになります。

\mbox{score}_{gti} = \mu_{gt} + \beta x_i + \epsilon_{gt}

 \mbox{score}は検査指標、 \muは群の各時点の状態、 xは個体ごとの欠損値もある変数、 gは介入群と操作群を現し、 tは時点、 iは個体、 \epsilonは誤差項を示します。
こう書くと簡素なモデルが1つしかないと思うかも知れませんが*3 \mu_{gt} \mbox{VAR}(\epsilon_{gt}) で評価して、

  •  \mbox{T}_{t=1}=\mbox{C}_{t=1}, \mbox{T}_{t=2}=\mbox{C}_{t=2}, \mbox{T}_{t=3}=\mbox{C}_{t=3}(T群とG群にずっと差異なし)
  •  \mbox{T}_{t=1}=\mbox{C}_{t=1}, \mbox{T}_{t=2}=\mbox{C}_{t=2}, \mbox{T}_{t=3}\ne\mbox{C}_{t=3}(T群とG群は第3時点で差異が出た)
  •  \mbox{T}_{t=1}=\mbox{C}_{t=1}, \mbox{T}_{t=2}\ne\mbox{C}_{t=2}, \mbox{T}_{t=3}\ne\mbox{C}_{t=3}(T群とG群は第2時点から差異)
  •  \mbox{T}_{t=1}\ne\mbox{C}_{t=1}, \mbox{T}_{t=2}\ne\mbox{C}_{t=2}, \mbox{T}_{t=3}\ne\mbox{C}_{t=3}(T群とG群は第1時点から差異)

の4パターンのモデルを評価することができます。平均だけではなく分散も異なること、平均が同じでも分散が異なれば \mbox{T}_{t}\ne\mbox{C}_{t}となるとしましょう。また、どちらの群でも個体の状態が一定でないこと、 T_{t=1}\ne T_{t=2}\ne T_{t=3},C_{t=1}\ne C_{t=2}\ne C_{t=3}を仮定します。
欠損値の入り方は想像がつかなかったのですが、MCARとしておきます。原理的にはMARでも大丈夫ですし、欠損値が入るパターンが分かっていれば、推定モデルを工夫すればMNARにも対応できます。

2. データセット

真のモデルは以下の表の通りです。

パラメーター t=1 t=2 t=3
C  \mu_{Ct} 0.0 0.0 0.0
C  \mbox{VAR}(\epsilon_{Ct}) 1.0 1.0 1.0
T  \mu_{Tt} 0.0 1.0 2.0
T  \mbox{VAR}(\epsilon_{Tt}) 1.0 1.1 1.2

t=2時点からT群とC群に差があるとしています。
この設定を元に、架空のデータを生成します。練習感あふれてしまいますが、真のパラメーターが分かることから、推定結果が妥当なものか検討できるので、手法の確認には実データを使うより良いとも言えます。

set.seed(604)
rm(list=ls())

n <- 120 # 各時点のサンプルサイズ
t <- 1:3 # 時点
g <- c("C", "T") # 群

dimnames <- list(g, sprintf("t=%d", t))

# 以下のmu, sigma, betaは真の値になるので、本当は分からない
mu <- matrix(c(0+0*t, 0+1*(t-1)), ncol=length(t), byrow=TRUE, dimnames=dimnames)
sigma <- matrix(c(1+0*t, 1+0.1*(t-1)), ncol=length(t), byrow=TRUE, dimnames=dimnames)
beta <- 1

x <- rep(runif(n), length(t))

df01 <- data.frame(
    id=rep(1:n, length(t)),
    time=rep(t, each=n),
    group=rep(rep(g, each=n/length(g)), length(t)),
    x=x,
    z=x/2 + rnorm(n, sd=1),
    score=numeric(n*length(t))
)

rm(x)

for(j in t){
    for(i in 1:length(g)){
        s <- n*(j-1)+n/length(g)*(i-1) + 1
        e <- n*(j-1)+n/length(g)*i
        df01$score[s:e] <- rnorm(n/length(g), mean=mu[i, j], sd=sigma[i, j]) + beta*df01$x[s:e]
    }
}

# xに欠損値をつくる
df01$x[df01$id %in% sample(1:n, n/2)] <- NA

こんな感じのデータセットが出来上がります。

id time group x z score
1 1 C 0.03264839 -0.39328775 0.1545386
2 1 C 0.07093954 -0.07395381 -0.3282276
3 1 C 0.96846472 0.70290536 1.6737583
4 1 C NA -0.79364909 1.0485097
5 1 C 0.40517678 -0.50145817 0.7247011
6 1 C 0.09162524 -1.43459105 0.5523630

3. 推定コード

ベイズファクターの計算の都合上、推定はこれでもかと言うぐらいループが繰り返されます。

library(MCMCpack)

# ある点の確率密度を得る(Parzen Window)
get_density <- function(m, samp, n=200){
    max <- max(samp)
    min <- min(samp)
    range <- (max - min)/n
    sum(samp > (m - range) & samp < (m + range))/length(samp)/2/range
}

# Chib (1995)に習って、MCMCを繰り返して確率密度を計算する
get_posterior_density <- function(objf, init_theta, post.samp, estimated, log=TRUE){
    # 周辺尤度計算用の確率密度関数
    pdensity <- numeric(length(init_theta))
    len <- length(estimated)
    pdensity[len] <- get_density(estimated[len], post.samp[,len])
    for(i in 1:(len - 1)){
        l <- len - i
        post.samp.cnd <- MCMCmetrop1R(objf, theta.init=init_theta[1:l], force.samp=TRUE, estimated=estimated)
        pdensity[l] <- get_density(estimated[l], post.samp.cnd[,l])
    }
    ifelse(log, sum(log(pdensity)), prod(pdensity))
}

# xの係数と欠損値補定用の計4個のパラメーター
# t*g*2=12個のパラメーター
theta2param <- function(theta){
    theta <- theta[-(1:4)] # xと欠損値ダミーの係数のパラメーターは不要
  mlen <- length(t)*length(g)*2 # 制約なしの長さ
  tlen <- length(theta) # 引数の長さ
  slen <- (mlen - tlen)/2
  if(0 < slen){
    # 制約がついている
    # slen=kならば、t=k時点まではCとTは同じ
    len <- tlen/2
    m <- c(theta[1:length(t)], theta[1:slen])
    s <- c(theta[(1+len):(length(t)+len)], theta[(1+len):(slen+len)])
    if((length(t)+1)<=len){
      m <- c(m, theta[(length(t)+1):len])
      s <- c(s, theta[(length(t)+1+len):(2*len)])
    }
    theta <- c(m, s)
  }
  len <- length(theta)/2
  mu <- matrix(theta[1:len], ncol=length(t), byrow=TRUE, dimnames=dimnames)
  sigma <- matrix(theta[(len+1):(2*len)], ncol=length(t), byrow=TRUE, dimname=dimnames)
  list(mu=mu, sigma=sigma)
}
# theta2param(c(beta, 0, 0, 1, t(mu), t(sigma)))

# 尤度関数
llf.nd <- function(theta){
    p <- theta2param(theta)
    beta <- theta[1]
    alpha <- theta[2:4]
    theta <- theta[-(1:4)]
    len <- length(theta)/2
   # 補定処理の係数の尤度を計算
    xi <- df01$x
    s <- sum(dnorm(df01$x[!is.na(xi)] - alpha[1] - alpha[2]*df01$z[!is.na(xi)], sd=alpha[3], log=TRUE))
    # 線形の補定処理を行なう
    xi[is.na(xi)] <- alpha[1] + alpha[2]*df01$z[is.na(xi)]
    for(j in t){
        for(i in g){
            # i群j時点のscoreを説明するモデルの方の尤度を計算して加算
            s <- s + sum(dnorm(with(df01, { score[group==i & time==j] - beta*xi[group==i & time==j] })
                , mean = p$mu[i, j]
                , sd = p$sigma[i, j]
                , log = TRUE))
        }
    }
    s
}

# 事前分布(期待値には正規分布,偏差にはガンマ分布)
prior.nd <- function(theta){
    beta <- theta[1]
    alpha <- theta[2:4]
    theta <- theta[-(1:4)]
  len <- length(theta)/2
  mu <- 1:len
  sigma <- (1+len):(2*len)
  sum(dnorm(mu, mean=1, sd=1, log=TRUE)
    ,dgamma(sigma, shape=1.5, scale=2, log=TRUE)
        ,dnorm(beta, mean=1, sd=1, log=TRUE)
        ,dnorm(alpha[1], mean=0, sd=1, log=TRUE)
        ,dnorm(alpha[2], mean=0, sd=1, log=TRUE)
    ,dgamma(alpha[3], shape=1.5, scale=2, log=TRUE))
}

# 正規分布モデルの目的関数
objf.nd <- function(theta, estimated=theta){
    # thetaの長さが短ければ、周辺尤度の計算過程なので、estimatedの値で補完する
    len <- length(estimated)
    d <- len - length(theta)
    if(0 < d){
        p <- (len-d+1):len
        theta[p] <- estimated[p]
    }
    llf.nd(theta) + prior.nd(theta)
}

calcPosterior <- function(l=16){
    MCMCmetrop1R(objf.nd, theta.init=rep(1, l))
}

getMerginalLikelihood <- function(post.nd, l){
    mean.nd <- summary(post.nd)$statistics[, "Mean"]
    pdensity.nd <- get_posterior_density(objf.nd, rep(1, l), post.nd, mean.nd)
    llf.nd(mean.nd) - pdensity.nd
}

ptrn <- seq(2*max(t), 0, -2) + 10 # パラメーターの数は16(t=1に変化), 14(t=2に変化), 12(t=3に変化), 10(変化なし)でモデルの違いも意味する

# 並行処理の準備
library(parallel)
library(doParallel)
library(foreach)
cl <- makeCluster(max(1, detectCores() - 1))
registerDoParallel(cl)

# 対数周辺尤度を作る
# モデルごとに(パラメーター数, 事後分布, 周辺尤度)のリストを返す
r <- foreach(i=ptrn, .packages = c("MCMCpack")) %dopar% {
    post.name <- "posterior"
    mll.name <- "marginal likelihood"
    r <- list(i, calcPosterior(i))
    names(r) <- c("nop", post.name)
    r[mll.name] <- getMerginalLikelihood(r[[post.name]], i)
    return(r)
}

# 並行処理の終了
stopCluster(cl)

GetBayesFactor <- function(mll.a, mll.b){
    exp(mll.a - mll.b)
}

printMeans <- function(r){
    cat(sprintf("パラメーターの数:%d\n", r[[1]]))
    cat(sprintf("パラメーターの点推定値(mean):\n"))
    print(theta2param(summary(r[[2]])$statistics[,"Mean"]))
}

len <- length(ptrn)
bf <- matrix(NA, len, len, dimnames=list(c(ptrn), c(ptrn)))
for(i in 1:len){
    rownames(bf)[i] <- sprintf("numerator:t=%d", (max(ptrn) + 2 - r[[i]][[1]])/2)
    for(j in 1:len){
        colnames(bf)[j] <- sprintf("denominator:t=%d", (max(ptrn) + 2 - r[[j]][[1]])/2)
        bf[i, j] <- GetBayesFactor(r[[i]][[3]], r[[j]][[3]])
    }
}
# 最後の行と列は介入群と対照群の変化なし
rownames(bf)[len] <- colnames(bf)[len] <- "no change"
print(bf)

# Bayes Factorsからモデル選択
choiced_model_no <- which(bf[,1]==max(bf[,1]))
choiced_model <- r[[choiced_model_no]]
# 選択されたモデルの点推定値を表示
printMeans(choiced_model)
# 選択されたモデルの要約
summary(choiced_model[[2]])
# 選択されたモデルのパラメーターのプロット
plot(choiced_model[[2]])

長い? — 4つあるモデルを1つの処理で片付けようとしたために、ほんの少し複雑になっていますが、大したことありません。時点ダミー×介入群ダミーを入れた線形混合モデルだと10行以内に終わるとしてでもです。なお、ベイズファクターを用いているので、事前分布は絶対に省略しないでください。ところで記事を書いてから気付いたのですが、推定式に zを含めるのを忘れていますが、まぁ、含めても結果は変わらないと思うので気にしないでください。

4. 推定結果

各時点120とそれなりのサンプルサイズなのですが、ベイズファクターで選択されたモデルの推定結果はt=3で変化となりま・・・と最初書いていたのですが、コードにミスがあったので直したらt=1で変化となりました。真のモデルはt=2で変化のはずなので妙な気も済ますが、t=1とt=2のモデルのベイズ・ファクターを比較すると1.03とほとんど1であり、両者に差が無い事が分かります。t=2のモデルの方が簡素なので、第2時点で変化があったと解釈して良いでしょう。

ベイズファクターで選択されたモデル以外、今回の場合はt=2もチェックした方が良さそうです。今回のコードではprintMeans(r[[2]])で、もしくはMCMCPackを使っているので、summary(r[[2]][[2]])で数字を見ることができます。

なお、set.seed(611)にしておくと、t=2で変化と言う結果になります。例に使うシード値を間違えた感。

*1:"Bayesian methods for missing data Bayes Pharma"と言う文書を参考にしました。

*2:欠損値処理をしなければ、library(lme4); mixed.lmer <- lmer(score ~ x + factor(time)*factor(group) + (1|group), data = df01); print(summary(mixed.lmer))で終わります。多重代入法による欠損値処理も容易です( Rで一般化混合線形モデル×多重代入法 - 餡子付゛録゛ )。

*3:ごく普通に期待値と分散を線形モデルで説明する事も出来ます。

ベイズ推定の事前分布で多重共線性に対処しよう

線形回帰などでベイズ統計学を用いる利点は何なのかと言う御題が流れていました。ざっと思いつくところで、

  • 主観的事前分布で、データ以外の情報を加味できる
  • (同じことだが)逐次ベイズ推定にできる
  • 信頼区間よりも信用区間の方が解釈がしやすい
  • 有意水準のギリギリ前後で解釈を変えなくてよい
  • モデル選択でベイズファクターが使える

と言ったことがあげられますが、「データ以外の情報」のありそうな例を用意していなかったことを思い出しました。先行するデータ分析結果や、理論から演繹される情報を入れるのが一般的なようですが、もっと素朴に分析者が係数の符号の向きを知っている状態を考えてみましょう。

理屈から効果の正負の方向に予想がついている一方で、サンプルサイズが小さいために多重共線性で、推定結果の符号が予想と逆になることは多々あります。例えば、年収と(学部までの)学歴で既婚率を説明したときに、年収はプラスの効果があるのに、学歴はマイナスの効果が計測されたら、年収と学歴の多重共線性を疑うべきでしょう。

y = ꞵ₀ + ꞵ₁x₁ + ꞵ₂x₂ + ϵが真のモデルで、x₁とx₂で多重共線していて、かつ理論的にꞵ₁>0でꞵ₂<0と言う制約がある場合の推定をしてみましょう。

1. データセット

真のモデルを設定して、多重共線性のあるデータセットを作ります。

set.seed(105)
n <- 30
beta <- c(1, 2, -3)
names(beta) <- c("(Intercept)", "x1", "x2")
x1 <- runif(n, min=0, max=1)
x2 <- x1 + rnorm(n, mean=0, sd=0.2)
y <- beta[1] + beta[2]*x1 + beta[3]*x2 + rnorm(n, mean=0, sd=1)
cat(sprintf("cor(x1, x2)=%.3f\n", cor(x1, x2)))

もちろん、OLSが上手くいかないシード値を探した結果です。

2. OLSで推定

一般最小二乗法(OLS)で推定すると、x₁の係数が真のモデルと逆になっています。

r_lm <- lm(y ~ x1 + x2)
summary(r_lm)

なお、VIFは4.23と大きくは無いのですが、LOOCVをかけるとx2だけで回帰した方がよくなります*1

3. 事前分布に正規分布を置いてベイズ推定

誤差項を正規分布とし、x1とx2の係数の事前分布を平均aと-a, 標準偏差a/2の正規分布としてベイズ推定します。つまり、予想の逆の符号になるのは平均よりも2σ以上外側と言う主観を置きました。

library(MCMCpack)

a <- 5
post.nd <- MCMCregress(y ~ x1 + x2, b0=c(0, a, -a), B0=1/(a/2)^2)
summary(post.nd)

係数の事前分布に正規分布を置く線形回帰は標準的な関数なので簡単に推定できます。しかし、符号条件を満たす推定結果が得られますが、aを幾つにするか恣意的に思えるかも知れません。実際、aを変えると推定結果は大きく変化します。

4. 事前分布に一様分布を置いてベイズ推定

恐らく[0 a]と[-a 0]の一様分布を事前分布に置く方が説明は簡単になります。aは一定以上あれば、推定結果を左右しません。

library(MCMCpack)

llf_nd <- function(theta){
# concentrated loglikelihood functionにして分散の推定は省略
  e <- y - theta[1] - theta[2]*x1 - theta[3]*x2
  sd <- sqrt(sum((e - mean(e))^2)/length(e))
  sum(dnorm(e, mean=0, sd=sd, log=TRUE))
}

prior.u <- function(theta){
  a <- 100
  sum(
    dunif(theta[1], min=-a, max=a, log=TRUE)
    ,dunif(theta[2], min=0, max=a, log=TRUE)
    ,dunif(theta[3], min=-a, max=0, log=TRUE))
}

post.u <- MCMCmetrop1R(function(theta){
  v <- llf_nd(theta) + prior.u(theta)
  # -Infを返すとエラーが出るので絶対値が大きな負の数を戻す(大きすぎても特異になってエラー)
  if(!is.finite(v)){
    return(-1e+8)
  }
  return(v)
}, theta.init=c(0, 1, -1), optim.method="L-BFGS-B", optim.lower=c(-Inf, 1e-12, -Inf), optim.upper=c(Inf, Inf, -1e-12))
summary(post.u)


5. 比較

推定された係数を比較してみましょう。

cmp_coef <- rbind(beta, coef(r_lm), summary(post.nd)$statistics[1:3, 1], summary(post.u)$statistics[, 1])
rownames(cmp_coef) <- c("True Model", "OLS", "Bayes(gauss)", "Bayes(unif)")
cmp_coef

分析者の知識を事前確率として織り込むことで、OLSよりもベイズ推定の方が上手くいっていることが分かります*2。ただし主観的事前分布をどう置くかはやはり問題になります。正規分布を使う方がコードの量は減らせますが、裁量の余地は増えます。一様分布を置くと、裁量の余地は減るわけですが、不連続な点が出てくるためにコードの量が増えてしまいます。悩ましいですね。

*1:VIFの計算とLOOCVの具体的な方法は、「多重共線性を出してみよう」を参照してください

*2:Ridge回帰を使えば良い気がしますが、パラメーターの信用区間が出ることが利点になります。なお、主観的事前分布が受け入れ難い人には、主成分回帰でごちゃごちゃやる手があります。

完全情報最尤推定法(FIML)で操作変数のある連立方程式モデルを推定

計量経済学では同時方程式を考えることが多く、そのために一般最小二乗法(OLS)の発展である操作変数法(IVM)とそのバリエーションである二段階最小二乗法(2SLS)*1や三段階最小二乗法(3SLS)、モーメント法の発展である一般化モーメント法(GMM)がよく使われているのですが、これらは尤度函数を用いないのでベイズ推定にできません。頻度主義者がベイジアンになるのは難しいです。
しかし、完全情報最尤推定法(FIML)と言うのがあり、実際のところは簡単に最尤法で同時方程式を推定できます。FIMLはRでは欠損値処理アルゴリズムのように扱われている気がするのですが、教科書的な手法なので確認しておきましょう*2。最尤法だけに不均一分散や欠損値処理の対処も工夫できますし、ベイズ推定しない場合でも、便利に使える局面は多そうです。

理論モデルとOLS推定量に入る同時性バイアス

推定の例をつくるわけですが、需要曲線と供給曲線を考えましょう*3


需要関数: d = \alpha_{10} + \alpha_{11} p + \xi_1
供給関数: s = \alpha_{20} + \alpha_{21} p  + \alpha_{22} z + \xi_2
均衡条件: d=s
 dは需要、 sは供給、 pは価格、 zは天候などの操作変数、 \alphaは係数、 \xiは誤差項です。ここで均衡価格を計算すると、

均衡価格: p^*=\frac{\alpha_{20} - \alpha_{10}}{\alpha_{11} - \alpha_{21}} + \frac{ \alpha_{22}}{\alpha_{11} - \alpha_{21}} z  + \frac{ \xi_2 - \xi_1 }{\alpha_{11} - \alpha_{21}}
となります。観測データは均衡価格 p^*になるわけですが、例えば需要関数の推定で p^*を説明変数に用いると、 p^*の第3項に \xi_1があるため、 COV(p^*, \xi_1) \neq 0となりOLSの前提を満たしません。需要関数の価格の係数の推定量 \hat{\alpha_{01}}_{OLS}=\alpha_{01}+COV(p^*, \xi_1) とバイアスが入ります。

完全情報最尤推定

需要関数と均衡価格式を同時推定することを考えましょう。推定する係数を \beta、誤差項を \muと置きなおし、方程式一本ごとに誤差項があると考え、二本の方程式を行列にまとめて書くと、次のようになります。 nは観測数です。

 \begin{pmatrix}
d_1 & p_1 \\
d_2 & p_2 \\
\vdots & \vdots \\
d_n & p_n \end{pmatrix}  = \begin{pmatrix}
1 & z_1 & p_1\\
1 & z_2 & p_2\\
\vdots & \vdots & \vdots \\
1 & z_n & p_n
\end{pmatrix}  \begin{pmatrix}
\beta_{11}  & \beta_{12}  \\
0  & \beta_{22} \\
\beta_{31}  & 0 \end{pmatrix} +  \begin{pmatrix}
\mu_{11}  & \mu_{12}\\
\mu_{21}  & \mu_{22}\\
\vdots & \vdots \\
\mu_{n1} & \mu_{n2} \end{pmatrix}

前節の係数とは、 \beta_{11}=\alpha_{10} \beta_{12}=(\alpha_{20} - \alpha_{10})/(\alpha_{11} - \alpha_{21})と言った対応関係があります。
右辺の内生変数と外生変数を分離します。

 \begin{pmatrix}
d_1 & p_1 \\
d_2 & p_2 \\
\vdots & \vdots \\
d_n & p_n \end{pmatrix}  = \begin{pmatrix}
1 & z_1 \\
1 & z_2 \\
\vdots & \vdots \\
1 & z_n
\end{pmatrix}  \begin{pmatrix}
\beta_{11}  & \beta_{12}  \\
0  & \beta_{22} \end{pmatrix} + \begin{pmatrix}
p_1\\
p_2\\
\vdots \\
p_n
\end{pmatrix}  \begin{pmatrix}
\beta_{31}  & 0 \end{pmatrix} +  \begin{pmatrix}
\mu_{11}  & \mu_{12}\\
\mu_{21}  & \mu_{22}\\
\vdots & \vdots \\
\mu_{n1} & \mu_{n2} \end{pmatrix}

内生変数を左辺にまとめます。

 \begin{pmatrix}
d_1 & p_1 \\
d_2 & p_2 \\
\vdots & \vdots \\
d_n & p_n \end{pmatrix} - \begin{pmatrix}
p_1\\
p_2\\
\vdots \\
p_n
\end{pmatrix}  \begin{pmatrix}
\beta_{31}  & 0 \end{pmatrix} = \begin{pmatrix}
1 & z_1 \\
1 & z_2 \\
\vdots & \vdots \\
1 & z_n
\end{pmatrix}  \begin{pmatrix}
\beta_{11}  & \beta_{12}  \\
0  & \beta_{22} \end{pmatrix} +  \begin{pmatrix}
\mu_{11}  & \mu_{12}\\
\mu_{21}  & \mu_{22}\\
\vdots & \vdots \\
\mu_{n1} & \mu_{n2} \end{pmatrix}

左辺第1項と第2項をまとめましょう。

 \begin{pmatrix}
d_1 & p_1 \\
d_2 & p_2 \\
\vdots & \vdots \\
d_n & p_n \end{pmatrix} \begin{pmatrix}
1 & 0 \\
 - \beta_{31}   & 1 \end{pmatrix} = \begin{pmatrix}
1 & z_1 \\
1 & z_2 \\
\vdots & \vdots \\
1 & z_n
\end{pmatrix}  \begin{pmatrix}
\beta_{11}  & \beta_{12}  \\
0  & \beta_{22} \end{pmatrix} +  \begin{pmatrix}
\mu_{11}  & \mu_{12}\\
\mu_{21}  & \mu_{22}\\
\vdots & \vdots \\
\mu_{n1} & \mu_{n2} \end{pmatrix}

この行列をいちいち書いていられないので、書き直します。

 Y\Gamma = W B + U

FIMLでは、この構造方程式を推定します。こう書くとOLSで入るバイアスが入らない理由が分かりづらいですが、この構造方程式の尤度函数は説明変数に内生変数が入らない誘導形 Y = W B \Gamma^{-1} + U \Gamma^{-1} = W\Pi + Vの尤度函数から導かれます*4。また、数理的な特性を使って整理すると以下のようなconcentrated loglikelihood function*5にできることが知られています*6


 \Sigma = \frac{1}{n} (Y\Gamma - WB)^{t} (Y\Gamma - WB)
 \log L(\beta) = \frac{-gn}{2}(\log 2\pi + 1)  + n \log | \Gamma | - \frac{n}{2} | \Sigma |
 gは内生変数(もしくは左辺にまとめられた被説明変)の数で、ここでは2です。 \beta = ( \beta_{11},  \beta_{12},  \beta_{22}, -\beta_{31} ) の4つのパラメーターを上の対数尤度函数で推定すればよいです。

データ

理論モデルに沿った式で、乱数を使ってデータを生成します。

set.seed(1231)
n <- 300
a10 <- 100
a11 <- -1
a20 <- 2
a21 <- 3
a22 <- 4
z <- runif(n, min=0, max=3)
mu <- rnorm(n, mean=0, sd=2)
nu <- rnorm(n, mean=0, sd=1)
p <- (a20 - a10 + a22*z + nu - mu)/(a11 - a21)
d <- a10 + a11*p + mu
s <- a20 + a21*p + a22*z + nu

推定

concentrated loglikelihood functionをoptim関数で推定します。

Y <- cbind(d, p)
W <- cbind(rep(1, n), z)
g <- 2

llf <- function(p){
    B <- matrix(c(p[1], 0, p[2], p[3]), 2, 2)
    G <- matrix(c(1, p[4], 0, 1), 2, 2) # Γ
    E <- Y%*%G - W%*%B
    S <- t(E)%*%E/n # ∑
    -g*n/2*(log(2*pi) + 1) + n*log(det(G)) - n/2*log(det(S))
}

r_optim <- optim(c(1, 0, 0, 0), function(p){ -llf(p) }, method="BFGS")

r_optim$parが推定された \hat{\beta}になりますが、内生変数の係数となる4番目のパラメーターの符号が逆になることに注意してください。なお、optim関数の引数にhessian=TRUEをつけるとヘッセ行列がとれるので、標準誤差を計算することができます*7

OLSとIVとの比較

推定結果の比較をしておきましょう。FIMLはOLSよりはかなりマシ、IVよりは僅かに劣るといった感じでしょうか。

係数 真の値 OLS IV FIML
 a_{10} 100 75.95501 100.11595 100.1228135
 a_{11} -1 0.03949 -1.01101 -1.0113085

なお、最尤法は漸近的に有効だけに、サンプルサイズが小さいとIVとの差が広がる傾向があります。IV推定量は以下のように計算できるので、nを変えて試してみてください。

zm <- matrix(c(rep.int(1, n), c(z)), n, 2)
xm <- matrix(c(rep.int(1, n), c(p)), n, 2)
solve(t(zm) %*% xm) %*% (t(zm) %*% d)

*1:操作変数が1つの場合はIVMと同値なのですが、一応、別なので。

*2:昔読んだGreene (2003)やWooldridge (2002)では最尤法で連立方程式モデルの推定の仕方は書いていなかったのですが、Davidson and MacKinnon (2003)では第12章で詳しく説明されています。

*3:Hayashi (2000)にある例を踏襲しています。

*4:Davidson and MacKinnon (2003)もしくはオンラインで配布している2021年版のpp.503–505とpp.544–545を参照。誘導形はSURと同じ多変量正規分布の尤度函数になるので、 \Gammaによる変形の影響を整理すれば構造形の尤度函数が導出できます。

*5:定訳不明

*6:一階条件の節の一階条件に直接関係のないExercise 12.24を \Sigma \Sigma^{-1}=Iに注意して \Sigma^{-1}の成分を考えて解くことで得ることができる・・・ハズ。

*7:Rと手作業で覚える最尤法 - 餡子付゛録゛