概要
結論から言うとヘルプドキュメントを読めばどうすべきか分かる問題であるが, 初心者が初心者である所以はそこが分からないことなので (誰でも最初は初心者だったのでそれが悪だとは言わない) 書いておくことにもある程度価値はあるだろう.
R-wakalang にて最近 mice
パッケージを使った多重代入法 (multiple imputation) がうまくいかないという質問を何度か見かけた. mice
を使った多重代入法について日本語で書かれた教科書として高橋 and 渡辺 (2017)によるものがあるが, 今回のようなケースには詳しくないためここに解決法を書いておく (パッケージの使い方はヘルプドキュメントに書いてあるためこの教科書に問題があるわけではない. ちゃんとこの教科書などを読んで多重代入法の使い方を理解してから使ってほしい).
注意: この記事のパッケージ解説は公式ドキュメントではありません. パッケージは常に改良されているため, なるべく最新の公式ドキュメントを参考にしてください.
問題
類似する質問を投稿した人間が複数いるが, 共通するのは多重代入法で補完したデータにモデルや検定を当てはめて, pool
関数で結果を統合しようとしたらエラーが出たというもの. 例えばこんなコードを実行したとする.
require(mice) imp <- mice(nhanes) fit <- with(imp, t.test(bmi ~ hyp)) pool(fit)
するとこのようなエラーが出るはず.
エラー: Problem with `summarise()` input `ubar`. x Column `std.error` not found in `.data` i Input `ubar` is `mean(.data$std.error^2)`. Run `rlang::last_error()` to see where the error occurred.
今回は基本パッケージの t.test
を適用した結果だが, 生存時間分析の timereg::aalen
を使ってうまくいかなかったという投稿もあった. こちらの場合は以下のようなエラー.
エラー: No tidy method for objects of class aalen 追加情報: 警告メッセージ: get.dfcom(object, dfcom) で: Infinite sample size assumed.
解説と解決方法
確かにこれらのエラーメッセージだけでは原因がどこにあるのか少し分かりにくいかもしれない. まずは mice::pool
の内部の挙動を解説する必要がある. この関数は 「Rubin のルール」に基づいて結果の統合をする関数であるが, 内部では broom::tidy
関数を使ってモデルの推定結果を取り出すという処理がある. この tidy
関数は, 従来テキストで表示されていた回帰分析などの結果を表形式に変換してくれるものである. たとえば lm
関数の結果は従来なら以下のようにして表示できる.
fit2 <- lm(mpg ~ wt + qsec, data = mtcars) summary(fit2)
Call: lm(formula = mpg ~ wt + qsec, data = mtcars) Residuals: Min 1Q Median 3Q Max -4.3962 -2.1431 -0.2129 1.4915 5.7486 Coefficients: Estimate Std. Error t value Pr(>|t|) (Intercept) 19.7462 5.2521 3.760 0.000765 *** wt -5.0480 0.4840 -10.430 2.52e-11 *** qsec 0.9292 0.2650 3.506 0.001500 ** --- Signif. codes: 0 ‘***’ 0.001 ‘**’ 0.01 ‘*’ 0.05 ‘.’ 0.1 ‘ ’ 1 Residual standard error: 2.596 on 29 degrees of freedom Multiple R-squared: 0.8264, Adjusted R-squared: 0.8144 F-statistic: 69.03 on 2 and 29 DF, p-value: 9.395e-12
これを tidy
関数で変換するとこうなる.
broom::tidy(fit2)
term | estimate | std.error | statistic | p.value |
---|---|---|---|---|
(Intercept) | 19.746 | 5.252 | 3.759 | 0.000 |
wt | -5.047 | 0.483 | -10.429 | 0.000 |
qsec | 0.929 | 0.265 | 3.506 | 0.001 |
モデルオブジェクトのデータ構造は実装によってバラバラなので, 出力がこの形式で統一されることで pool
関数は様々なモデルに対して結果の統合処理をすることができる. summary
関数の出力をコピーしてレポートや卒論にそのまま貼り付ける人はいないだろうから, tidy
関数は単体でも結果のレポーティングに便利である (もちろん, stargazer
とかを使っても良い).
それはさておき, tidy
関数はあらゆる統計モデルの結果を表形式に変換できるわけではない. これは tidy
のヘルプドキュメントに書いてあるとおりで, 対応しているモデルの一覧に, Aalen モデル (timereg::aalen
) は含まれていない. つまり aalen
のエラーは tidy
に対応していないモデルを使ったのが原因である*1. 一方で t.test
の場合は表形式にすることができる(表 2).
tidy(t.test(bmi ~ hyp, data = nhanes))
estimate | estimate1 | estimate2 | statistic | p.value | parameter | conf.low | conf.high | method |
---|---|---|---|---|---|---|---|---|
-0.483 | 26.441 | 26.925 | -0.309 | 0.7616 | 13.961 | -3.835 | 2.868 | Welch Two Sample t-test |
しかし表 1 と比較すると項目が異なる. エラー内容が Column ‘std.error‘ not found in ‘.data‘
だったことからも, これは標準誤差の列が存在しなかったのがエラーの原因だとわかる.
では tidy
関数や pool
関数が対応していない場合, どうやって結果を統合するか, いくつか方法がある.
pool.scalar
を使うpool
関数が対応している等価な別のモデルに置き換える自分で処理を書く (そして
broom
パッケージにコミットする)
たぶん (1 - 2) が簡単で, (3) はそれなりに複雑なモデル, 例えば標準誤差を代数的に計算できない場合とかだろう.
(1) の方法
pool
が対応していない場合, 1変数バージョンの pool.scalar
関数を使うのが最も簡単な方法だろう. 名前の通り, 単一の統計量に対して結果の統合を行う関数である. 必要なのは (1) 多重代入ごとの推定値, (2) それらに対応する分散 (標準誤差を直接与えてはいけない), (3)元のデータのサンプルサイズ, (4) モデルの推定するパラメータの数である.
試しに lm
に対して結果が一致するか見てみる. lm
は pool
でも統合可能だが, ならば pool.scalar
関数でもできるはずである. 公式のサンプルコードを参考に, 以下のような回帰モデルの, age
のパラメータについてのみ比較してみる.
imp <- mice(nhanes, maxit = 2, m = 2, print = FALSE, seed = 42) fit3 <- with(data = imp, lm(bmi ~ age)) pool(fit3)[2, ]
Class: mipo m = NA term m estimate ubar b t dfcom df riv lambda fmi 2 age 2 -1.875604 0.8647848 0.01895132 0.8932118 23 20.13587 0.03287174 0.03182557 0.1155202
次に, pool.scalar
で age
の結果を統合する. Rubinのルールによる計算に対応する処理は pool.scalar
関数でのみなされており, それ以降の処理は結果を表形式に整形するためのものである.
pool_age <- pool.scalar( Q = unlist(lapply(fit3$analyses, function(x) coef(x)["age"])), U = unlist(lapply(fit3$analyses, function(x) summary(x)$coefficients["age", "Std. Error"]))^2, n = NROW(nhanes), k = 2 ) pool_age <- append(list(term = "age"), pool_age) out <- as.data.frame(pool_age)[1, -(3:4)] colnames(out)[c(3, 8)] <- c("estimate", "riv") out
fit3$analyses
はリストオブジェクトで, その各要素は多重代入ごとに適用したモデルの結果である. そこで上記では lappy
関数を使って必要な値を取りだす処理を並列して行っている. lm
オブジェクトならば coef
関数で回帰係数を取り出せる. そして標準誤差は suumary
関数の結果から取りだし, 忘れずに2乗する. この辺は何通りかやり方があり, モデルオブジェクトによって全く方法が変わってくるはずなので, 推定値と分散 (or 標準誤差) をどうやって取りだすかは自分で考えねばならない.
表 3 は pool.scalar
関数による結果. pool
関数と同様に, summary
関数を使えば p 値を出してくれる. 6桁まで一致しているので同じ結果が出ていると見て良いだろう.
term | m | estimate | ubar | b | t | df | riv | fmi |
---|---|---|---|---|---|---|---|---|
age | 2 | -1.875604 | 0.8647848 | 0.0189513 | 0.8932118 | 20.13587 | 0.0328717 | 0.1155202 |
(1) の補足: その他の拡張パッケージについて
mice
以外のパッケージも探してみる. miceadds
パッケージは名前の通り mice
にない機能を補うパッケージで, mitools
パッケージも同様のモチベーションで作られている. mitools::MIcombine
関数と miceadds::pool_mi
関数は mice::pool.scalar
の多変量バージョンと言える. 補完データごとのパラメータ推定値をリストオブジェクトにまとめて結果を与えることで計算できる. 以下, 比較に使ったコード. より一般的に, パラメータが多くても動作することを確認するため先ほどよりモデルに使用する変数を増やしてみる.
require(mitools) # ver.2.4 require(miceadds) # ver. 3.10-28 imp <- mice(nhanes, maxit = 2, m = 10, seed = 42) models <- with(data = imp, exp = lm(bmi ~ hyp + chl)) summary(pool(models), conf.int = T) summary(pool_mi( qhat = lapply(models$analyses, function(x) summary(x)$coefficients[, "Estimate"]), se = lapply(models$analyses, function(x) summary(x)$coefficients[, "Std. Error"]), dfcom = 22 )) summary(MIcombine( results = lapply(models$analyses, function(x) summary(x)$coefficients[, "Estimate"]), variances = lapply(models$analyses, function(x) summary(x)$coefficients[, "Std. Error"]^2), df.complete = 22 ))
MIcombine
関数はt統計量とp値を表示してくれないようだ. これも推定値と標準誤差, そして検定統計量は6桁まで一致するが, MIcombine
のみ区間推定の誤差が大きい. デフォルトではどれも95%信頼区間を表示するので, それ以外の点で違いがあることになる*2.
また, 細かい解説をしておくと, 上記の書き方はやや不正確である. pool.scalar
同様に1つ1つが独立したパラメータなら問題ないが, 実際にはそうとは限らない. 重回帰モデルであっても共分散行列を求めるのが正確である. 今回の例では結果が変わらないが, 以下のように書くのが適切である.
pool_mi( ..., u = lapply(models$analyses, vcov) ) MIcombine( ..., variances = lapply(models$analyses, vconv), )
さらに, これら2つの関数は, 「完全データの自由度」がデフォルトで無限大になっているため, 手動で指定しないと pool
関数とp値や区間推定の結果が一致しない*3*4.
というわけで, 現状では単変量ならpool.scalar
関数を使い, 2つ以上のパラメータがあるのならmiceadds::pool_mi
関数を使うのが簡単なやり方だろう.
なお, 多重代入法に関連するパッケージには Amelia
というのもあるが, これは mice
で実装されていない新しい多重代入法を提供するだけで結果の統合機能はない (Amelia
のサポートする手法の解説は 高橋 and 渡辺 (2017) を参考に).
(2) 別のモデルに置き換える
これは適用できるモデルが限定されるやり方である. 例えば Heymans and Eekhout (2019) の 5章 に書いてあるように, t.test
関数のデフォルトは Welch の検定で, 単回帰モデルの係数の有意検定と同じ値になる. よって 1元配置分散分析 (1-way ANOVA) の結果は lm
関数を使って統合できる. それ以外でも変わったモデルのように見えて実質的に線形回帰モデルということはよくあるので, lm
関数で同じことができないか考えてみるべきだろう.
(3) 自分で計算処理を書く
(1), (2) でうまくいかない, あるいは (1) で計算できない, モデル全体の統計量も正確に求めたいなら, Rubin のルールを自分で実装する必要がある. もしやりたいのなら, それは次の節が参考になる.
まとめ
mice
パッケージでpool
関数を使った場合に起こる冒頭のようなエラーは,mice
パッケージがあなたの使用している統計モデルを提供しているパッケージに対応していないのが原因- モデルのパラメータの推定値と標準誤差がわかっているならば, モデルのパラメータが1つなら
pool.scalar
関数を, 2つ以上のパラメータがあるならmiceadds
パッケージのpool_mi
関数を使う - そうでないなら, Rubin のルールを理解して自分で計算する
補足: Rubin のルール
Rubin のルールに関しては 1987年の Donald Rubin の教科書が毎度引用されるが, 古いし高いので大学図書館に自由にアクセスできる身分でない私には確認する気がおきない. mice
パッケージなども紹介している高橋 and 渡辺 (2017) でも, Rubin のルールについて大まかなアイディアは説明しているが, 細かい手順までは書いていない. なので調べるのが少し面倒なのだが, mice
パッケージの開発者の一人である Buuren の教科書 (mice
パッケージのリポジトリからもリンクされている) van Buuren (2018) “Flexible Imputation of Missing Data” の2章と5章や Heymans and Eekhout (2019) “Applied Missing Data Analysis With SPSS and RStudio” の9-10章では, 最近の修正された方法も含め, それなりに詳しく書かれている. これらはWeb上で公開されておりこれとヘルプドキュメントを読んでねで済む話で, これをコピペして書く意味もないのだが, まあ今回は「初心者の質問」への回答なので書く.
(これを書いてから気づいたのだが, 高井, 星野, and 野間 (2016) は計算できるソフトウェアの解説はないものの, 4章で Rubin のルールの詳しい話に言及している)
まず, Rubin のルールは多重代入ごとに求めたパラメータ が正規分布するという前提である*5 (よってモデルによっては Rubin ルールで統合するのが不適切な場合もありえる). ここから, 統合後 (pooled) のパラメータは単純にこれらの標本平均になる.
各標準誤差 も, 群間 (Between)・郡内 (Within) 分散を考慮して次のように統合した標準誤差を求められる.
さらに, の帰無仮説に対して, 以下のような検定統計量が帰無仮説のもとで近似的にt分布に従う.
信頼区間の計算もここからできるので, あとはt分布の自由度 の計算だけだが, これが少し複雑である. 当初は Rubin によって以下のを丸めた値を自由度としていた.
しかしこの方法では, 特にサンプルサイズの小さい時に多重代入法の個別の補完結果に対して過大になる (欠損値が一切ない場合よりも大きくなることすらありうる) 傾向があることから, pool
関数では後に発表された Barnard and Rubin (1999) による修正方法を採用している. よって, この関数が表示する df
は以下で計算される.
は Proportion of total variance due to missingness で, は RIV (relative increase in variance) を表し, 上記 の丸める前の値とともにそれぞれ pool
関数で lambda
と riv
と df
して出力される. , , はそれぞれ以下で与えられる.
は既に出てきたサンプルサイズとパラメータの数である. つまり は, サンプルが仮に欠損のない完全データ (complete data) だった場合の自由度を意味する (これも pool
関数で dfcom
として表示される). これで計算ができるようになった.
この定義では, のとき となり, サンプルサイズの増加に従い漸近的に古いルールに一致することが分かる. 一方で, の場合は となる. 自由度がゼロの分布というのは意味不明である. これはの定義から, 欠損値補完のばらつきが非常に大きい場合に起こりうる. つまり欠損値によって失われた情報が多すぎるため, この補完方法では信頼できる推定ができない可能性があるということになる.
これが多変量の場合も自然に拡張できる. 補完ごとのパラメータベクトルを , 対応する共分散行列を とすると,
以降は を以下の に置き換えた以外は同じである.
なお RIV は以下のようになる.なお, SPSS で使用できる統合処理はさらにその後提案された別の方法に従っているため, これとも少し違う値になるらしい. また, 正規分布の仮定が適切でないケースの解説は Buuren の5章で言及がある.
参考文献
欠測データの統計科学――医学と社会科学への応用 (調査観察データ解析の実際 第1巻)
- 発売日: 2016/04/20
- メディア: 単行本(ソフトカバー)
*1:より正確には, tidy または glance 関数に対応していないとエラーになる.
*2:p値が表示されないので, 自由度の計算が違うのではないかと疑っている. しかしヘルプドキュメントが書きかけだったり, どの理論に準拠した実装なのかとかを書いてないのでどういう仕様なのかわからない. 他の方法もあることだしソースコードを確認するまでの手間はかけたくない.
*3:ヘルプによれば pool 関数の場合は, サンプルサイズ - パラメータ数で計算する. この数値は (1) glance 関数で取りだした値を使う, (2) df.residual 関数で取りだした値を使う (3) いずれも不可能な場合, その旨を表示し, 自由度を999999 とする. また, この値は ‘dfcom‘として保存されるし, 引数で任意に指定もできる.
*4:サンプルサイズが大きければこのような近似で問題ないのだが, デフォルトが無限大なのは不親切な気がする. 必須引数にしてくれたほうがミスが少なくなると思うのだが...
*5:仮説検定が不要なら, この仮定は緩和できる