ねこすたっと

ねこの気持ちと統計について悩む筆者の備忘録的ページ。

データ構造を要約・説明する(2):検証的因子分析(lavaanパッケージなど)[R]

因子分析とは

因子分析には以下の2種類がある。

  • 探索的因子分析(exploratory FA, EFA):何も仮説を考えないまま、データの背景にありそうな因子構造を推定するもの
  • 検証的因子分析(confirmatory FA, CFA):先行する理論をもとに因子・観察変数の関係性をあらかじめ設定して、データがその因子構造に当てはまるのかを検証するもの。確認的因子分析とも呼ばれる。

検証的因子分析の手順

  1. データの確認
  2. 因子構造の指定
  3. モデルの当てはめ
  4. 推定結果の確認
  5. モデルの改善

使用するパッケージとデータセット

CFAは主にlavaan*1パッケージを使うが、他に関連するパッケージもまとめて読み込んでおく。

  • lavaanパッケージ:Latent variable analysis = 潜在変数を扱う解析、つまりCFAや構造方程式モデリング(Structural Equation Modeling, SEM)を行える
  • MVNパッケージ:正規性(後述)の確認
  • semToolsパッケージ:信頼性(後述)の確認
  • semPlot:パス図を描く
library(lavaan)
library(MVN)
library(semTools)
library(semPlot)

lavaanパッケージのHolzingerSwineford1939データセットを用いる。

> head(HolzingerSwineford1939)
  id sex ageyr agemo  school grade       x1   x2    x3       x4   x5        x6       x7   x8       x9
1  1   1    13     1 Pasteur     7 3.333333 7.75 0.375 2.333333 5.75 1.2857143 3.391304 5.75 6.361111
2  2   2    13     7 Pasteur     7 5.333333 5.25 2.125 1.666667 3.00 1.2857143 3.782609 6.25 7.916667
3  3   2    13     1 Pasteur     7 4.500000 5.25 1.875 1.000000 1.75 0.4285714 3.260870 3.90 4.416667
4  4   1    13     2 Pasteur     7 5.333333 7.75 3.000 2.666667 4.50 2.4285714 3.000000 5.30 4.861111
5  5   2    12     2 Pasteur     7 4.833333 4.75 0.875 2.666667 4.00 2.5714286 3.695652 6.30 5.916667
6  6   2    14     1 Pasteur     7 5.333333 5.00 2.250 1.000000 3.00 0.8571429 4.347826 6.65 7.500000

含まれる変数は以下のとおり。

  • id:ID
  • sex:性別
  • ageyr:年齢の年の部分
  • agemo:年齢の月の部分
  • school:児童の所属する学校
  • grade:学年
  • x1:visual perception(視覚)
  • x2:cubes(立方体?)
  • x3:lozenges(菱形?)
  • x4:paragraph comprehension(パラグラフ理解)
  • x5:sentence completion(文章補完)
  • x6:word meaning(語彙)
  • x7:speeded addition(速く足す)
  • x8:speeded counting of dots(速く数える)
  • x9:speeded discrimination straight and curved capitals(速く区別する)

x1x9だけ使う。

data <- HolzingerSwineford1939[-c(1:6)]

1. データの確認

まずはデータ(観察変数)の正規性を確認する。MVNパッケージのmvn( )で一気に確認できる。

> mvn(data)
$multivariateNormality
             Test        Statistic              p value Result
1 Mardia Skewness 344.905276936947 8.86473544030093e-15     NO
2 Mardia Kurtosis 2.83021589156651  0.00465166039260279     NO
3             MVN             <NA>                 <NA>     NO

$univariateNormality
          Test  Variable Statistic   p value Normality
1 Shapiro-Wilk    x1        0.9928  0.1582      YES   
2 Shapiro-Wilk    x2        0.9697  <0.001      NO    
3 Shapiro-Wilk    x3        0.9523  <0.001      NO    
~~~~~(省略)~~~~~

$Descriptives
     n     Mean  Std.Dev   Median       Min       Max     25th     75th       Skew    Kurtosis
x1 301 4.935770 1.167432 5.000000 0.6666667  8.500000 4.166667 5.666667 -0.2543455  0.30753382
x2 301 6.088040 1.177451 6.000000 2.2500000  9.250000 5.250000 6.750000  0.4700766  0.33239397
x3 301 2.250415 1.130979 2.125000 0.2500000  4.500000 1.375000 3.125000  0.3834294 -0.90752645
~~~~~(省略)~~~~~

$multivariateNormalityで多変量正規分布と見なせるかどうかを検定している。結果は"NO"なので正規分布とは見なせない。 多変量正規分布と見なせるならば最尤法を使うが、見なせないならロバスト最尤法を用いる。推定方法は、モデルの当てはめのときに指定する。

2. 因子構造の指定

モデル式を書いて因子構造を指定する。SEMで指定するような複雑なモデルを記述できるように構文が用意されている。4種類の式のタイプがあるみたいですが、詳細はまた勉強します。

  • =~:潜在変数の定義
  • ~:回帰
  • ~~:残差の分散・共分散
  • y ~ 1:切片

ここでは3つの潜在因子F1F3に対して、観察変数を以下のように割り当てた。

model = "
F1 =~ x1 + x2 + x3
F2 =~ x4 + x5 + x6
F3 =~ x7 + x8 + x9
"

図1:想定したモデル(3因子9項目)

ちなみに因子間に独立を想定する場合は、以下のようにすると因子間相関係数=0になる。

model = "
F1 =~ x1 + x2 + x3
F2 =~ x4 + x5 + x6
F3 =~ x7 + x8 + x9
F1 ~~ 0*F2
F1 ~~ 0*F3
F2 ~~ 0*F3
"

さらに、

model = "
F1 =~ x1 + x2 + x3
F2 =~ x4 + x5 + x6
F3 =~ x7 + x8 + x9
F1 ~~ 1*F1
F2 ~~ 1*F2
F3 ~~ 1*F3
"

とすると、各因子の分散を1に固定する(cfa( )でstd.lv = TRUEを指定するのと同じだと思う)。

3. モデルの当てはめ

モデル式とデータをcfa( )に渡して、因子負荷や相関を推定する。 指定する主な引数は、

  • estimator:推定方法。最尤法なら"ML"、ロバスト最尤法なら"MLR"を指定。デフォルトは"ML"みたい。
  • std.lv:TRUEを指定すると因子分散が1となるように標準化される。TRUEを指定しないとデフォルトのauto.fix.first = TRUEが指定され、各因子の中で1つ目に指定した変数のパラメータが1に固定される。因子分散を1に固定すると因子のバラツキ具合について比較できないが、1に固定して表示するのが普通なのかな。
fit <- cfa(model = model, data = data, estimator = "MLR", std.lv = TRUE)

4. 推定結果の確認

結果は以下のコードで表示。

  • fit.measures:適合度指標を表示したいときはTRUEにする
  • standardized:結果でStd.lvStd.allを表示したいときはTRUEにする。
summary(fit, fit.measures=FALSE, standardized=TRUE)

結果は長いので分割しながら見ていく。

4-1. 推定の概要

使用されたlavaanのバージョン、収束までの回数、パラメータ数、観測数などが表示される。

lavaan 0.6-8 ended normally after 20 iterations

  Estimator                                         ML
  Optimization method                           NLMINB
  Number of model parameters                        21
                                                      
  Number of observations                           301  

"ended normally"とあるので、ここでは推定は正常に収束したみたい。

4-2. モデルの適合度

ここではRobustの方を見る。

Model Test User Model:
                                               Standard      Robust
  Test Statistic                                 85.306      87.132
  Degrees of freedom                                 24          24
  P-value (Chi-square)                            0.000       0.000 #
  Scaling correction factor                                   0.979
       Yuan-Bentler correction (Mplus variant)                     

Model Test Baseline Model:

  Test statistic                               918.852     880.082
  Degrees of freedom                                36          36
  P-value                                        0.000       0.000
  Scaling correction factor                                  1.044

User Model versus Baseline Model:

  Comparative Fit Index (CFI)                    0.931       0.925 #
  Tucker-Lewis Index (TLI)                       0.896       0.888 #
                                                                  
  Robust Comparative Fit Index (CFI)                         0.930 #
  Robust Tucker-Lewis Index (TLI)                            0.895 #

Loglikelihood and Information Criteria:

  Loglikelihood user model (H0)              -3737.745   -3737.745
  Scaling correction factor                                  1.133
      for the MLR correction                                      
  Loglikelihood unrestricted model (H1)      -3695.092   -3695.092
  Scaling correction factor                                  1.051
      for the MLR correction                                      
                                                                  
  Akaike (AIC)                                7517.490    7517.490
  Bayesian (BIC)                              7595.339    7595.339
  Sample-size adjusted Bayesian (BIC)         7528.739    7528.739

Root Mean Square Error of Approximation:

  RMSEA                                          0.092       0.093 #
  90 Percent confidence interval - lower         0.071       0.073
  90 Percent confidence interval - upper         0.114       0.115
  P-value RMSEA <= 0.05                          0.001       0.001 
                                                                  
  Robust RMSEA                                               0.092 #
  90 Percent confidence interval - lower                     0.072
  90 Percent confidence interval - upper                     0.114

Standardized Root Mean Square Residual:

  SRMR                                           0.065       0.065 #
  • P-value (Chi-square):最尤統計量から得たカイ2乗分布に基づくP値。Model Test User Modelのところを見る。>0.05ならOKと言われるが、サンプルサイズが大きいと感度が良すぎる(75-200がちょうどよく、400以上ではいつも有意になってしまう*2)。この「感度が良すぎる問題」があるので、普通は以下の指標が使われる。
  • 相対的な適合との指標:ベースラインモデル(最も簡単なモデル)との相対的な比較を行う。Model Test User ModelModel Test Baseline Modelの情報を使っている。
    • Comparative Fit Index (CFI) :0〜1の値を取る。大きいほど良い。>0.95(甘くしても>0.9)ならOK。多分ここでもRobust CFIの方を見るのかな、と。
    • Tucker-Lewis Index (TLI):0〜1の値を取る(1以上になるときは1に丸める)。>0.90なら適合度OK。
  • 絶対的な適合度の指標:
    • SRMR:Standard root mean squareで、≦0.08ならOK
    • RMSEA:root mean square error of approximationの略。RMSEAが大きいほど、適合度が悪い。≦0.05ならぴったり適合、≧0.10なら適合良くない。ロバスト最尤法を使ったのなら、Robustの方を見るのかな。

4-3. 推定値

Parameter Estimates:

  Standard errors                             Sandwich
  Information bread                           Observed
  Observed information based on                Hessian

Latent Variables:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
  F1 =~                                                                 
    x1                0.900    0.100    8.973    0.000    0.900    0.772
    x2                0.498    0.088    5.681    0.000    0.498    0.424
    x3                0.656    0.080    8.151    0.000    0.656    0.581
  F2 =~                                                                 
    x4                0.990    0.061   16.150    0.000    0.990    0.852
    x5                1.102    0.055   20.146    0.000    1.102    0.855
    x6                0.917    0.058   15.767    0.000    0.917    0.838
  F3 =~                                                                 
    x7                0.619    0.086    7.193    0.000    0.619    0.570
    x8                0.731    0.093    7.875    0.000    0.731    0.723
    x9                0.670    0.099    6.761    0.000    0.670    0.665

Covariances:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
  F1 ~~                                                                 
    F2                0.459    0.073    6.258    0.000    0.459    0.459
    F3                0.471    0.119    3.954    0.000    0.471    0.471
  F2 ~~                                                                 
    F3                0.283    0.085    3.311    0.001    0.283    0.283

Variances:
                   Estimate  Std.Err  z-value  P(>|z|)   Std.lv  Std.all
   .x1                0.549    0.156    3.509    0.000    0.549    0.404
   .x2                1.134    0.112   10.135    0.000    1.134    0.821
   .x3                0.844    0.100    8.419    0.000    0.844    0.662
   .x4                0.371    0.050    7.382    0.000    0.371    0.275
   .x5                0.446    0.057    7.870    0.000    0.446    0.269
   .x6                0.356    0.047    7.658    0.000    0.356    0.298
   .x7                0.799    0.097    8.222    0.000    0.799    0.676
   .x8                0.488    0.120    4.080    0.000    0.488    0.477
   .x9                0.566    0.119    4.768    0.000    0.566    0.558
    F1                1.000                               1.000    1.000
    F2                1.000                               1.000    1.000
    F3                1.000                               1.000    1.000

Std.lvは潜在変数だけが標準化されていて、Std.allは潜在変数と観測変数の両方が標準化されている。

因子負荷量(factor loading, FL)

  • Latent VariableStd.allを見る
  • FLは0〜1の範囲に収まっているはずで、>1の場合はHeywood caseと呼ばれる*3
  • 0.5なら意味のある関連と考えていい

  • 全ての因子負荷量のP値は統計学的に有意でなくてはならない。

因子間相関(factor correlation)

  • CovariancesStd.allを見る
  • <0.85であれば因子が明確に区別されていることを示す。反対に>0.85だと多重共線性の問題ありかも。

因子分散および独自因子の分散

  • VariancesEstimateを見る
  • ここでは因子分散を1に固定しているので上記の結果。

4-4. パス図を描く

semPlotパッケージのsemPaths( )を使ってパス図を描く。

第1引数はcfa( )で作成したオブジェクト。その後、"path", "std"を指定。前者だけだと矢印しか描かれない。 styleは分散を表現するときに、双頭矢印(両方とも同じノードに向かう)なら"ram", "mx", "OpenMx"、一方向矢印なら"lisrel"を指定する。 デフォルトだと矢印の色がグレーなので、黒にしたいときはedge.colorで指定。

semPaths(fit, "path", "std", 
         style = "lisrel", edge.color = "black", intercepts = FALSE)

図2:semPaths( )を使ってCFAのパス図を描いた

先程のパラメータ推定値のStd.allが書き込まれていることを確認。

5. モデルの改善

5-1. 当てはまりの悪い部分を探す

モデルの当てはまりが悪かった場合、どこを変えたらいいのかを見るために、当てはまりの悪い部分を探す。

1つ目はmodification index(MI)で、>3.84を超えるところは改善の余地ありと判断。

> mis_fit <-  modificationIndices(fit)
> subset(mis_fit, mi>3.84)
   lhs op rhs     mi    epc sepc.lv sepc.all sepc.nox
10  F1 ~~  F2 26.552  0.388   0.388    0.388    0.388
11  F1 ~~  F3 22.633  0.393   0.393    0.393    0.393
12  F2 ~~  F3 12.152  0.248   0.248    0.248    0.248
25  F1 =~  x4  4.653  0.112   0.112    0.097    0.097
27  F1 =~  x6  9.912  0.157   0.157    0.144    0.144
28  F1 =~  x7  4.725 -0.145  -0.145   -0.133   -0.133
30  F1 =~  x9 40.044  0.399   0.399    0.396    0.396
31  F2 =~  x1 31.475  0.354   0.354    0.304    0.304
36  F2 =~  x9 10.298  0.175   0.175    0.173    0.173
37  F3 =~  x1 10.504  0.224   0.224    0.192    0.192
39  F3 =~  x3  4.612  0.142   0.142    0.125    0.125
45  x1 ~~  x4  6.547  0.111   0.111    0.197    0.197
50  x1 ~~  x9 14.396  0.200   0.200    0.263    0.263
55  x2 ~~  x7  6.279 -0.148  -0.148   -0.166   -0.166
59  x3 ~~  x5  4.487 -0.095  -0.095   -0.186   -0.186
63  x3 ~~  x9  4.651  0.109   0.109    0.164    0.164
66  x4 ~~  x7  7.095  0.106   0.106    0.198    0.198

もう1つはstandardized residuals(SR)で、|SR|>2.58は改善の余地ありと判断。

> sr <- residuals(fit, type = "standardized")
> sr
$type
[1] "standardized"

$cov
   x1     x2     x3     x4     x5     x6     x7     x8     x9    
x1  0.000                                                        
x2  0.000  0.000                                                 
x3  0.000  0.000  0.000                                          
x4  5.497  2.517  2.784  0.000                                   
x5  4.688  2.394  1.309  0.000  0.000                            
x6  5.119  2.996  3.126  0.000  0.000  0.000                     
x7  1.084 -1.430  1.247  2.928  1.662  2.031  0.000              
x8  3.531  1.682  3.081  1.798  2.228  2.358  0.000  0.000       
x9  6.325  3.613  5.462  3.138  3.849  3.025  0.000  0.000  0.000

5-2. 因子の信頼性を評価する

semToolsパッケージのreliablity( )でRaykov’s rhoを計算する。

> rel <- reliability(fit) 
> print(rel, digits = 3)
          F1    F2    F3
alpha  0.626 0.883 0.688
omega  0.633 0.886 0.696
omega2 0.633 0.886 0.696
omega3 0.633 0.886 0.696
avevar 0.369 0.723 0.438

omegaがRaykov’s rhoに相当。≧0.70なら信頼性あり。 alphaはCronbachのα係数。

おわりに

  • モデル式を使ってパラメータとか分散を自在に固定できるようになるには、まだまだ訓練が必要だが...私には使う機会がなさそう。
  • しばらくウェットの餌が続くと、ドライを出したときに「これじゃないでしょ?」って顔で見てきます。

参考資料

  • lavaanパッケージのチュートリアルです。

lavaan.ugent.be

  • 適合度指標の計算方法については、以下の資料を参考にしました。

stats.oarc.ucla.edu

Kaplan-Meier生存曲線をキレイに描く(ggplot系jskmパッケージ)[R]

ggplot系コードで生存曲線を描く方法については以前まとめました。

necostat.hatenablog.jp

今回はjskmパッケージを使って生存曲線を描く方法のまとめ。
まずは必要なパッケージを準備する。

library(survival)
library(tidyverse)
library(jskm)

ここではsurvivalパッケージ内のデータセットpbcを使う。
元々のアウトカム変数statusでは

  • 0 = censored
  • 1 = transplant
  • 2 = dead

となっているが、ここではtrandplantとdeadを合わせた複合アウトカム変数eventを作成して使用する(イベント発生の有無を示す変数は文字列ではダメ)。

> data(pbc)
> d <- pbc %>% mutate(event = if_else(status==0,0,1))
> str(d)
'data.frame':  418 obs. of  21 variables:
 $ id      : int  1 2 3 4 5 6 7 8 9 10 ...
 $ time    : int  400 4500 1012 1925 1504 2503 1832 2466 2400 51 ...
 $ status  : int  2 0 2 2 1 2 0 2 2 2 ...
 $ trt     : int  1 1 1 1 2 2 2 2 1 2 ...
 $ age     : num  58.8 56.4 70.1 54.7 38.1 ...
 $ sex     : Factor w/ 2 levels "m","f": 2 2 1 2 2 2 2 2 2 2 ...
~~~
 $ event   : num  1 0 1 1 1 1 0 1 1 1 ...

jskm( )を使ってKaplan-Meier生存曲線を描く

まずはsurvivalパッケージのsurvfit( )を使って生存時間データに当てはめるモデルを指定する。

fit <- survfit(Surv(time, event)~trt, data = d)

これをjskm( )に渡すだけでよい。

jskm(fit)

図1:jskm( )を使ってKaplan-Meier生存曲線を描く

デフォルトでは上記のような生存曲線(survival curve, S(x,t))が描かれるが、cumhaz = TRUEを指定することで累積発生関数(cumulative incidence function, C(x,t))を描くことができる。

ちなみにS(x,t), C(x,t), H(x,t)およびハザード関数h(x,t)には以下のような関係がある。

\begin{align} S(x,t) &= 1 - C(x,t) \\\ S(x,t) &= \exp \left[ -\int_0^t h(x,u) du \right] \\\ &= \exp \left[ - H(x,t) \right] \end{align}

※ 生存時間にまつわる関数をまとめてみました(2022-07-17 追加)

necostat.hatenablog.jp

信頼区間をつける

ci=TRUEを指定すればよい。

jskm(fit, ci = TRUE)

図2:信頼区間を付けた

色パレットを変更する

linecolsで色パレットを指定できる。デフォルトはlinecols = "Set1"になっている。白黒で表示したいときは"black"を指定する。

jskm(fit, ci=TRUE, linecols = "black")

図3:白黒で描いた

白黒にすると自動で線種が点線になるみたい。

線の種類を変更する

dashed = TRUEを指定すると層毎に線種が変更される。

jskm(fit, dashed = TRUE)

出力は割愛。

打ち切りマークを変更する

デフォルトでは打ち切りマークがつく。
付けたくないときはmarks = FALSEと指定する。

jskm(fit, marks = FALSE)

マーカーの種類を指定したいときは、

jskm(fit, shape = "C")

とする(出力割愛)。

Risk tableをつける

引数table=TRUEと指定すると、at risk人数(number at risk)の表を生存曲線の下につけることができる。
At risk人数表については次の引数も指定できる。

  • label.nrisk:表のタイトル。デフォルトは"Number at risk"
  • size.label.nrisk:デフォルトは10
jskm(fit, table = TRUE,
     label.nrisk = "no. at risk", 
     size.label.nrisk = 12) 

図4:at risk人数表を付けた

軸について色々

軸の目盛り間隔を変更する

時間軸の目盛りの間隔は、引数timebyで変更できる(デフォルトは0と最大近くを含めて7つ)。
例えば、時間を500刻みで区切りたいときは、

jskm(fit, timeby = 500) 

図5:時間目盛りの間隔を指定した

生存割合の単位をパーセントにしたい場合は、

jskm(fit,surv.scale = "percent")

とする(出力割愛)。

軸の範囲・ラベルを指定する

軸の範囲はxlims, ylimsで指定する。

jskm(fit, xlims = c(0,2000))

軸のラベルはxlabs, ylabsで指定できる。ちなみに図のメインタイトルはmainで指定できる(出力割愛)。

検定結果を表示する

図内にP値を表示したい場合はpval = TRUEを指定する。他に指定できる引数は、

  • pval.size:フォントサイズ(デフォルトは5)
  • pval.coord:表示位置
  • pval.testname:検定名を表示するかどうか

がある。

jskm(fit, data=d, 
     pval = TRUE,
     pval.size = 6, #デフォルト=5
     pval.coord = c(700,0.5),
     pval.testname = TRUE)

図6:検定結果を表示した

凡例を変更する

凡例を表示するときはlegend = TRUEを指定する。凡例については他に以下の引数を指定できる。

  • legendposition:表示場所。両軸ともに0から1の範囲で指定する。
  • ystrataname:凡例のタイトル
  • ystratalabs:凡例の層ラベル
jskm(fit, 
     legend = TRUE, 
     legendposition = c(0.2,0.5), 
     ystrataname = "Treatment",
     ystratalabs = c("Treatment A", "Treatment B")) 

図7:凡例を追加する

日本語が白い四角に文字化けしてしまう場合は次を参照。

日本語の文字化けを直す

Macでは日本語が豆腐化することがある。
そのようなときは、RStudioの設定を以下のように変更する。

  1. PreferenceGraphic と進み、 BackendAGGに変更する。
  2. raggパッケージをインストールするか聞かれるので、インストールする。

おわりに

  • survminerより指定できることは少ないが、シンプルで使いやすいかも。
  • cut.landmarkを使うとランドマーク解析の生存曲線が描ける。
  • こたつに足を突っ込んで、猫を蹴ってしまい、猫より人間の方がびっくりすることがよくあります。

参考資料

  • 開発者のvignetteページ:

jinseob2kim.github.io

ブートストラップ(bootstrap)法で信頼区間を求める(bootパッケージ)[R]

サンプルを用いて母集団の特性を推定する際は誤差がつきもの。そのため点推定値だけではなく信頼区間をつけるのが当たり前になっているわけだが、正規分布など特定の分布を仮定することが怪しいときがある。

ブートストラップ法は、元サンプル(original sample)から重複を許した復元抽出を繰り返して作成した標本(ブートストラップサンプル, bootstrap sample)を使って推定誤差を分析する方法。 元サンプルの平均・標準偏差を有する正規分布(など)から乱数を発生させる「パラメトリック・ブートストラップ法」という方法もあるが、ブートストラップと言えば大抵は次の「ノンパラメトリック・ブートストラップ法」のこと。

  • 元サンプルを母集団に見立てて
  • 元サンプルと同じサンプル数Nを*1
  • 重複を許して*2抽出することを
  • 何回も繰り返す(1000-2000回)

B個のブートストラップサンプルを作れば、興味ある統計量(平均値とか中央値とか)もB個できて、その中にはたまたま大きい値・小さい値になったものがあるわけです。計算される統計量の幅を使って推定の幅を求めてやろうという方法なので、あまり詳しい理論とか計算を知らなくても(PCが頑張ってくれれば)信頼区間が計算できるという便利な方法です。

例示のための元データの作成

ここではブートストラップ法の提唱者であるEfron先生・Tibshirani先生の教科書に載っているデータを例に使う。 15人のLSATとGPA(成績のスコア)の相関係数を信頼区間付きで推定してみる。

元データは次のコードで作成。

lsat <- c(576,635,558,578,666,
          580,555,661,651,605,
          653,575,545,572,594)
gpa <- c(3.39,3.30,2.81,3.03,3.44,
         3.07,3.00,3.43,3.36,3.13,
         3.12,2.74,2.76,2.88,2.96)
data <- data.frame(lsat,gpa)

相関係数rの標準誤差が


SE(r) = \sqrt{\frac{1-r^2}{n-1}}

95%信頼区間が


CI(r) = r \pm t_{df=n-2} SE(r)

となることを使って手計算すると、相関係数rは0.776(95%CI: 0.399, 1.154)と計算される。

bootパッケージを使って信頼区間を計算する

大まかな流れは以下のとおり。

  1. boot( )を使ってブートストラップサンプルを作成し、それぞれのブートストラップサンプルで統計量を計算する
  2. boot.ci( )を使ってブートストラップ信頼区間を計算する

boot( )を使ってブートストラップサンプル毎の統計量を計算する

元サンプルから母集団の分布のパラメータを推定して、想定した特定の確率分布から乱数を発生させる「パラメトリック・ブートストラップ法」を実装することもできる(sim="parametric")が、使う機会がほとんどなさそう(ちなみに`sim="ordinary"だと、「n個の観測値があるサンプルから、重複を許してn個サンプリングする」という「いつもの」方法を指定)。

知っておいた方がいい引数は3つ。

  • data:元サンプル(ベクトル、行列、データフレーム)
  • statistic:データに対して計算したい統計量。関数を定義して与える。この関数では第1引数はデータ、第2引数はブートストラップサンプルを定義する方法(index, frequency, weightのいずれか)。stypeという引数で使用するサンプリング方法を指定できるが、デオフォルトのindexを使う方法を知っていれば大体事足りそう。
  • R:サンプリング回数。1000-2000回は必要だが、元サンプルが少なかったらいくら増やしても頭打ちになる。

統計量を計算する関数を書くところが少しハードルが高そう。例えば、LSATとGPAの相関係数を求める関数は次のとおり。シンプルに2つしか引数を使わないようにして"get_cor"と名前をつけた。

get_cor <- function(d,i){
  x <- data[i,"lsat"]
  y <- data[i,"gpa"]
  r <- cor(x,y)
  return(r)
}

下のコードのように、boot( )に必要な3つの引数を指定する。

set.seed(1234)
boot_out <- boot(data,
                 statistic = get_cor,
                 R = 2000)

boot.ci( )を使ってブートストラップ信頼区間を計算する

前述のboot( )で作ったオブジェクト(boot_out)からboot.ci( )を使って信頼区間を計算する。信頼区間を計算するアルゴリズムは以下の5種類が用意されている。

  • "norm":正規分布への近似を使った方法(the first order normal approximation)
  • "basic":ベーシック法(the basic bootstrap interval)
  • "stud":スチューデント化t法(the studentized bootstrap interval)
  • "perc":パーセンタイル法(the bootstrap percentile interval)
  • "bca":BCa(=Bias Corrected and acceralated)法

それぞれの方法の大まかな説明は後述するが、分からなかったらBCa法を選んでおけばよい。コードは次のとおり。

boot_out <- boot(data,
                 statistic = get_cor,
                 R=2000)

どの方法を使うかは引数typeで指定。type="all"としておけばスチューデント化t法を除いた全ての方法の結果が表示される(スチューデント化t法では統計量だけではなく、統計量の分散もブートストラップ法で推定しないといけないから)。

boot.ci(boot_out, type = "all")
> boot.ci(boot_out,type = "all")
BOOTSTRAP CONFIDENCE INTERVAL CALCULATIONS
Based on 2000 bootstrap replicates

CALL : 
boot.ci(boot.out = boot_out, type = "all")

Intervals : 
Level      Normal              Basic         
95%   ( 0.5187,  1.0371 )   ( 0.5905,  1.0955 )  

Level     Percentile            BCa          
95%   ( 0.4573,  0.9622 )   ( 0.3138,  0.9397 )  
Calculations and Intervals on Original Scale
Some BCa intervals may be unstable
Warning message:
In boot.ci(boot_out, type = "all") :
  bootstrap variances needed for studentized intervals

ちなみに、元サンプルにおける統計量は$t0、ブートストラップサンプルにおける統計量は$tで取り出せる。

> boot_out$t0
[1] 0.7763745

> mean(boot_out$t)
[1] 0.7748804

> sd(boot_out$t)
[1] 0.1322489

以下のコードでヒストグラムを描いてみる。

library(tidyverse)
boot_out$t %>% as.data.frame() %>%
  rename(statistics=V1) %>%
  ggplot(aes(x=statistics)) +
  geom_histogram(fill="navy", alpha=0.25) +
  geom_vline(xintercept=boot_out$t0, colour="navy")+
  theme_bw()

図1:ブートストラップサンプルにおける相関係数の分布(ヒストグラム)と元サンプルの相関係数(垂直線)

信頼区間を計算するアルゴリズム色々

「ひと口にブートストラップ信頼区間といっても5種類あること」と「よく分からなかったらBCa法でよいこと」さえ押さえていれば十分な気もしますが、何となく分かったつもりで各アルゴリズムを説明してみる(詳細は末尾の資料を参照のこと)。

表記のルール

なるべく数式は使わないつもりだが、似たような用語が多くて使い分けも混乱するので表記ルールを決めておく。

  •  N:元サンプルのサンプルサイズ
  •  B:抽出するブートストラップサンプルの個数
  •  \theta:興味ある統計量(前述の例では相関係数)の真(=母集団)の値
  •  \hat{\theta}_0:元サンプルにおける統計量の値
  •  \hat{\theta}^*_i i 番目のブートストラップサンプルにおける統計量の値(ブートストラップサンプルについて一般的に述べるときは  i を省略するときがある)。
  •  E [ \hat{\theta}^* ]:B個のブートストラップ統計量の平均
  •  var [ \hat{\theta}^* ]:B個のブートストラップ統計量の分散

最後の2つは、

 \begin{align}
E \left[ \hat{\theta}^* \right]
&= \frac{1}{B}\sum_{i=1}^B \hat{\theta}^*_i \\

var \left[ \hat{\theta}^* \right]
&= \frac{1}{B-1} \sum_{i=1}^B \left( \hat{\theta}^*_i - E \left[ \hat{\theta}^* \right] \right)^2
\end{align}

と計算される。

バイアス

ブートストラップサンプルから計算したB個の 統計量の平均値と、元サンプルから計算された統計量の差をバイアスと呼ぶ。

 \begin{align}
bias &= E \left[ \hat{\theta}^*_i \right] - \hat{\theta}_0
\end{align}

つまり、前の例でのバイアスは、

> mean(boot_out$t) - boot_out$t0
[1] -0.001494062

と計算される。

統計量の推定誤差

統計量の推定誤差は標準誤差(standard error, SE)で表される。これは「ブートストラップ毎に統計量の推定がどれくらいバラついているか」ということなので、ブートストラップ分散推定量  var [ \hat{\theta}^* ] を使う。この分散の平方根をとると標準誤差になる(手計算で確認)。

> sd(boot_out$t)
[1] 0.1322489

正規分布への近似法

95%信頼区間といえば「平均±1.96SE」だが、この平均とSEにブートストラップサンプルから推定したものを使おうという方法。 「正規分布に近似が難しそうだからブートストラップしようと思ったのに、なぜまた正規分布なの?」あるいは「そもそもこれはブートストラップ信頼区間といっていいのか」という声もある(かも)。

前述の例では信頼区間上限値が相関係数の取りうる範囲を超えており、ブートストラップしたのに問題が解決されていない。

ベーシック法

ベーシック法での信頼区間は、以下のように定義される。


CI_\alpha = \left( 2\hat{\theta}_0 - q_{1-\alpha/2},  2\hat{\theta}_0 - q_{\alpha/2} \right)

ここで、[tex: q{\alpha/2}] と [tex: q{1-\alpha/2}] はそれぞれブートストラップサンプルの分布から得られる統計量の分位点(95%信頼区間なら2.5パーセンタイル値と97.5パーセンタイル値)。

こちらも正規分布への近似と同様、信頼区間上限値が相関係数の取りうる範囲を超えてしまっている。

スチューデント化t法

スチューデント化とは「推定値のとの差を推定誤差で割る」ということ。正規分布の標準化とおんなじようなものだが、このときは母集団の分散は既知である。これに対して、サンプルから平均だけでなく分散も推定して、推定した分散から得た値を分母に使おうというのがt分布。

スチューデント化t法では、下の式で計算されるブートストラップt推定量の分布を使って信頼区間の限界値を求める。


t^* = \frac{\hat{\theta}^* - \hat{\theta}_0}{\hat{SE_{\hat{\theta}}}}

この方法では前述のように、統計量のブートストラップ推定値(「代表」推定値とか言った方が誤解がないかもね)だけではなく、その推定誤差が必要になる。推定誤差がブートストラップ分散推定量から導出できるときはそれを使えばいいが、その計算が簡単にできないときはさらにブートストラップを使うことになる(nested bootstrap algprism)。こうなると計算負荷がはるかに大きくなることは想像に難くない。

標準誤差の推定のことも考えないとboot.ci()で計算されないので実装が一番大変そうだが、計算負荷が高い割に幅が広くなりがちだったり、取りうる範囲外の限界値が返ってきうるので、使う機会はあまりないかも。

パーセンタイル法

得られたB個のブートストラップ統計量を小さい方から並べて、2.5パーセンタイル値と97.5パーセンタイル値を信頼区間の限界値のする方法。非常に分かりやすいし、取りうる範囲外の数値が選ばれることもない。

BCa法

Bias corrected and accelerated(BCa)法は、バイアス定数  z_0 *3と加速定数  a の2種類を使って(推定して)、信頼区間の限界値に採用すべきパーセンタイル値を補正する方法。参考文献の数式を見てみると、この2つの定数が0のときは補正なしに等しくなる模様。

バイアス定数は中心位置のずれに関して、加速定数は歪度(左右方向の歪み)に関して正規分布に近似しても大丈夫なように補正している(と思う)。

結論から言うと、計算負荷も対してかからず性能も良いBCa法を使っておけば間違いないと理解した。

おわりに

  • BCa法を使う(短絡的)

参考資料

  • index以外にfrequency, weightを使うときのコード例が紹介されています。

ryamada22.hatenablog.jp

  • いわゆる「手計算」で何をしているかが書かれているので、それぞれのアルゴリズムの中身がよくわかりました。

qiita.com

  • 英語ですが、この記事もアルゴリズムの中身が分かりやすかった。

https://blog.methodsconsultants.com/posts/understanding-bootstrap-confidence-interval-output-from-the-r-boot-package/blog.methodsconsultants.com

  • 色々なところで参考にされています。

https://www.cis.doshisha.ac.jp/mjin/R/44/44.htmlwww.cis.doshisha.ac.jp

*1:サンプリング数を変化させる方法もあるらしい

*2:同じ症例が2回以上選ばれても良い

*3:ωを当てている記事もある

マルチレベルデータの解析方法(2):混合効果モデル(lme4パッケージ)

マルチレベルデータとは

「通常の」解析*1では、「ある患者のアウトカムは、他の患者のアウトカムから影響を受けない」と考えて解析している。もう少し正確に言えば、アウトカムの変化のうち、説明変数で説明できない誤差の部分に関しては「互いに独立である(mutually independent)」という前提で解析している。

では、「アウトカムの誤差が互いに独立である」とは考えにくい状況とはどういったものだろうか。

1つ目は、アウトカムが類似する傾向がある亜集団(=クラスター)が存在する場合である。例えば、研究対象集団がいくつかので世帯で構成されているとき、食事や生活習慣などは世帯内で類似する傾向があるだろう。このようなとき、世帯が異なる研究参加者同士は独立と考えてよいが、同一世帯内の参加者を独立と考えるのは無理がある。世帯の他にも、学校や地域などがクラスターになりうる。

2つ目は、同一対象者において反復してアウトカムが測定される場合である。この場合のデータの単位は「人」ではなく「評価時点」になる。同じ人から測定されたアウトカムは類似した傾向を持つと考える方が普通だろう。眼や四肢・指趾といった「臓器単位」あるいは個々の腫瘍といった「病変単位」でアウトカムをみるような研究の場合も、疾患のメカニズムによっては反復測定と同様に考える必要がある。各測定に対して、人がクラスター単位となっていると考えることもできる。

アウトカムの誤差の出どころが、「測定単位(観測単位)」とその上層の「クラスター単位」の複数箇所にわたる、という意味でマルチレベルデータと呼ばれることがある。解析手法の観点からは、以下の用語はいずれも同じものを指していると考えて差し支えない。

  • クラスターデータ(clustered data)
  • 反復測定データ(repeated-measurement data)= パネルデータ(panel data)
  • マルチレベルデータ(multi-level data)

マルチレベルデータの解析

マルチレベルデータを解析する際には、誤差の相関構造や階層性を考慮する必要である。それは以下の理由のため。

データの情報量を過大評価しないため:
アウトカムの誤差に相関があるということは、観測単位間で情報の一部が重複しているということ。これを考慮しないと情報量を水増ししていることになってしまう。

クラスターを考慮した適切なモデルを使うことで検出力アップ:
観測値の誤差の中でクラスター効果として説明できることがあれば、クラスター単位での異質性を考慮することで検出力が上がるかもしれない。

解析する方法としては大きく以下の2種類がある。

  1. 一般化推定方程式(Generalized Estimating Equation, GEE)
  2. 混合効果モデル(Mixed-effects model, MEM)

前者は「誤差間の相関構造を定義して解析する方法」で、個々の観測単位(の誤差)間にある相関構造行列を与えることで対応するアプローチ。
後者は「誤差の発生源を階層化する方法」で、上層(例:世帯)が与えられた条件下では下層(例:世帯構成員)の誤差は独立であると考えるアプローチ。
この記事では後者のみ解説する。

混合効果モデルの概要

混合効果モデルといってもモデルの基本は一般化線形モデル(Generalized Linear Model, GLM)なので、分布族(family)やリンク関数(link function)についての説明は割愛。

混合効果モデル = 固定効果 + 変量効果

固定効果(fixed effect, FE)と変量効果(random effect, RE)の両方を含んだモデルを混合効果モデルと呼ぶ。現実的には固定効果を含まないモデルはないので、変量効果を含むモデルを混合効果モデルと呼んでいる。

では固定効果と変量効果の違いは何か。実は統一された定義はないみたい。なので固定効果・変量効果の話をするときはお互いにどういう意味で使っているのか注意が必要(特に計量経済学分野と疫学分野は違う気がする)。

Gelman先生によると、次のような定義・解釈があるとのこと。

  1. FEは対象者間で共通の効果、REは対象者間で変動する
  2. FEは興味ある効果、REはそれ自体に興味がないもの
  3. 集団の大部分に関連するならFE、無視できるくらいごく一部にしか関連しないならRE
  4. 効果が変数の実現値だと仮定できるのであればRE
  5. FEは最小二乗法・最尤法で推定されるが、REはshrinkageで推定される

この後の混合効果モデルの説明を読むときには、定義1をイメージするのが分かりやすくなっていいと思う。あと定義4に関連して、「FEのパラメータは確かにモデルの係数(coefficient)だが、REは厳密に言えばパラメータではなく観察されていない変数(unobserved random variable)である」という点は重要。

クラスター効果をモデルに含める

傾向が類似する亜集団をここではクラスターと呼んでいるので、その亜集団の違いがもたらすアウトカムへの影響の違いを「クラスター効果」と呼ぶことにする。*2

同一対象者に複数回アウトカムを測定したデータを想定する(対象者=クラスター)。 対象者  i の時点  t における説明変数を  X_{it} 、アウトカム変数を  Y_{it} として、次の式のような回帰モデルを当てはめる。


Y_{it} = \beta_0 +  \beta_1 X_{it} + \varepsilon_{it}

ここで、 \beta_0 は「切片」、 \beta_1 は「傾き」、 \varepsilon_{it} はモデルから期待されるアウトカム  E[Y_{i}]と測定値  Y_i の間の「誤差」を表している。 このモデルでは、集団全体におけるXとYの関係性を1つの直線で表している。

図1:クラスター効果を考慮しない場合

この場合は個人間の差は無視して、集団全体で画一的な切片と傾きを設定して推定した。でも、個人毎にスタート地点(=切片)は異なっているし、増加速度(=傾き)にも緩急がある。これを考慮してやる方が、適切な推定値がえられるかもしれないし、推定誤差が小さくて済むかもしれない。

このクラスターによる影響を変量効果としてモデルに組み込んだものが、切片だけにクラスター効果を考慮した「ランダム切片モデル(random intercept model)」や、切片と傾きの両方にクラスター効果を考慮した「ランダム切片+傾きモデル(random intercept and slope model)」である。

図2:切片にのみクラスター効果を考慮した場合

図3:切片と傾きにクラスター効果を考慮した場合

クラスター効果は変量効果に含めるものとして説明してきたが、必ずしも決まっているわけではない。固定効果としてもモデルに組み入れることができる。例えば、切片のみにクラスター効果を含めたモデルを考える。

固定効果として含めたときのモデル式は次のように書ける。

\begin{align}
Y_{it} &= (\beta_0 + \beta_{0,i}^{'}) +  \beta_1 X_{it} + \varepsilon_{it} \\\
 \varepsilon_{it} &\sim N(0, \sigma^2)
\end{align}

このモデルでは、

  • n人の対象者のクラスター効果はn-1 個の  \beta_{0,i}^{'} として導入されていて*3 \beta_0 と合わせてn個分の切片を別々に設定している
  • 誤差は1種類だけ

ある対象者のクラスター効果  \beta_{0,i}^{'} は、その対象者の測定値のみから推定される。他の対象者のデータに制約を受けないが、新しく加わった対象者については全く予想のしようがない。

これに対して、変量効果として含めたときのモデル式は次のように書ける。

\begin{align}
Y_{it} &= (\beta_0 + b_{i}) +  \beta_1 X_{it} + \varepsilon_{it} \\\
b_i &\sim N(0, \sigma_b^2) \\\
\varepsilon_{it}|b_i &\sim N(0, \sigma^2)
\end{align}

このモデルでは、

  • n人の対象者のクラスター効果がn個の  b_i として導入されている*4
  • 誤差を「対象者間誤差  b_i 」と「対象者内誤差  \varepsilon_{it}」に分解している。誤差の合計が最小になるような値を推定する。
  • クラスター効果が与えられた(=同一対象者内である)条件下では、対象者内誤差は互いに独立

前述の固定効果モデルと違い、ある対象者のクラスター効果  b_i は他の対象者のデータからも緩やかに制約を受ける。他の対象者の情報を適度に借りているとも考えられる。また、新しく加わった対象者についても、 \sigma_b^{2} のお陰で多少予想が立つ。

lme4パッケージで混合効果モデルを使う

サンプルデータの準備

ChickWeightというデータセットを使う。ヒヨコの餌と体重変化の関係性をみたデータらしい(ヒヨコ50匹、測定数578回)。

> head(ChickWeight)
Grouped Data: weight ~ Time | Chick
  weight Time Chick Diet
1     42    0     1    1
2     51    2     1    1
3     59    4     1    1
4     64    6     1    1
5     76    8     1    1
6     93   10     1    1

ヒヨコ毎に体重は類似していると考える方が自然だろう。なのでChickについてクラスター効果を考える。

glmer( )を使ってモデルを当てはめる

lme4パッケージのglmer( )を使う。「GLMじゃなくてLMでいいならlmer( )を使ってよ!」とエラーメッセージが出るが、無視してここではglmer( )で統一しておく。

変量効果については、指定したいクラスターを|の後ろに書いて指定する。どこにクラスター効果を設定するかについて3パターンだけ紹介する(複数のクラスター効果の階層化についてはまた別の機会に)。 固定効果の部分の書き方はglm( )と同じだが、切片があることを意識して1を明示的に書いた。

ランダム切片モデル(random intercept model)

切片部分にだけクラスター効果を入れたければ、固定効果の式の後に"1|クラスター"を追加する。固定効果と区別するために変量効果は"()"で囲む方がよい。

library(lme4)
fit_1 <- glmer(weight ~ 1+Time + (1|Chick),
             data = ChickWeight,
             family = gaussian(link="identity"))

結果は次のとおり。

> summary(fit_1)
Linear mixed model fit by REML ['lmerMod']
Formula: weight ~ 1 + Time + (1 | Chick)
   Data: ChickWeight

REML criterion at convergence: 5619.4

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.0086 -0.5528 -0.0888  0.4975  3.4588 

Random effects:
 Groups   Name        Variance Std.Dev.
 Chick    (Intercept) 717.9    26.79   
 Residual             799.4    28.27   
Number of obs: 578, groups:  Chick, 50

Fixed effects:
            Estimate Std. Error t value
(Intercept)  27.8451     4.3877   6.346
Time          8.7261     0.1755  49.716

Correlation of Fixed Effects:
     (Intr)
Time -0.422

まず、当てはめられたモデルや推定に関する概要が表示される。次に、Random effects:で変量効果についての結果(説明は次のモデルで)、Fixed effects:で固定効果についての結果が表示されている。

各クラスターに割り当てられた変量効果を確認したいときはranef( )を使う。

> head(ranef(fit_1)$Chick)
   (Intercept)
18   0.2754554
16 -26.3026906
15 -25.2830406
13 -50.5775531
9  -38.3765089
20 -40.8929742

ランダム切片+傾きモデル, REに相関あり(random intercept and slope model, RE correlated)

Timeの係数についてもクラスター効果を入れたければ、変量効果のカッコの中に1Timeを入れる。

fit_2 <- glmer(weight ~ 1+Time + (1+Time|Chick),
               data = ChickWeight,
               family = gaussian(link="identity"))
> summary(fit_2)
Linear mixed model fit by REML ['lmerMod']
Formula: weight ~ 1 + Time + (1 + Time | Chick)
   Data: ChickWeight

REML criterion at convergence: 4827.5

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.6731 -0.5563 -0.0277  0.5010  3.4939 

Random effects:
 Groups   Name        Variance Std.Dev. Corr 
 Chick    (Intercept) 140.54   11.855        
          Time         14.14    3.761   -0.95
 Residual             163.51   12.787        
Number of obs: 578, groups:  Chick, 50

Fixed effects:
            Estimate Std. Error t value
(Intercept)  29.1780     1.9573   14.91
Time          8.4531     0.5408   15.63

Correlation of Fixed Effects:
     (Intr)
Time -0.871

変量効果の部分にCorr(相関係数)が表示されている。これは「切片に対するRE」と「Timeの傾きに対するRE」の相関係数で、強い負の相関がある(つまり、「ベースラインが高い方が時間による増加が緩やか」)ことがわかる。

ResidualStd.Dev.は、対象者が特定された条件下で、各測定値がどれくらいバラついているかを示している。

ランダム切片+傾きモデル, REに相関なし(random intercept and slope model, RE uncorrelated)

1つ前のモデルでは、切片と傾きのREの間に相関があることを想定したが、クラスター効果の間に関連がないことを想定したい場合は次のいずれかの方法で書く。

  • 誤:(1 | Chick) + (Time | Chick)
  • 正:(1 | Chick) + (Time-1 | Chick)
  • 正:(1 | Chick) + (0+Time | Chick)

切片は含まれることがデフォルトなので、切片を含まないことを明示しなければいけない。

fit_3 <- glmer(weight ~ 1+Time + (1|Chick)+(0+Time|Chick),
             data = ChickWeight,
             family = gaussian(link="identity"))
> summary(fit_3)
Linear mixed model fit by REML ['lmerMod']
Formula: weight ~ 1 + Time + (1 | Chick) + (0 + Time | Chick)
   Data: ChickWeight

REML criterion at convergence: 4890.5

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-2.5601 -0.5614 -0.0059  0.4823  3.4968 

Random effects:
 Groups   Name        Variance Std.Dev.
 Chick    (Intercept) 114.95   10.722  
 Chick.1  Time         12.29    3.506  
 Residual             166.06   12.886  
Number of obs: 578, groups:  Chick, 50

Fixed effects:
            Estimate Std. Error t value
(Intercept)   29.048      1.825   15.92
Time           8.466      0.507   16.70

Correlation of Fixed Effects:
     (Intr)
Time -0.079

おわりに

  • GEEとmixed modelのどちらを使うべきか、という話はいずれ別の機会に。

※ GEEとmixed modelのどちらを使うべきかについてまとめてみました(2022-07-17 追加)

necostat.hatenablog.jp

  • 猫同士で追いかけっこして遊んでいることが増えて、こちらが振り回す猫じゃらしにはあまり飛びかからなくなりました。

参考資料

*1:「マルチレベルを考慮しない」としたいところだがtautology回避のため

*2:他にも「ブロック効果」とか「個別効果」という呼び方もあるかもしれないが、微妙なニュアンスの違いまで含めては分かっていません

*3:推定のために「1人目が係数=0(corner-point constraint)」または「係数の合計=0(sum-to-zero constraint)」という制約が必要

*4:2番目の関係式で互いに制約を受けるので、n-1個ではなくn個でよい

マルチレベルデータの解析方法(1):一般化推定方程式GEE(geepackパッケージ)

マルチレベルデータとは

「通常の」解析*1では、「ある患者のアウトカムは、他の患者のアウトカムから影響を受けない」と考えて解析している。もう少し正確に言えば、アウトカムの変化のうち、説明変数で説明できない誤差の部分に関しては「互いに独立である(mutually independent)」という前提で解析している。

では、「アウトカムの誤差が互いに独立である」とは考えにくい状況とはどういったものだろうか。

1つ目は、アウトカムが類似する傾向がある亜集団(=クラスターcluster、あるいはブロックblock)が存在する場合である。例えば、研究対象集団がいくつかので世帯で構成されているとき、食事や生活習慣などは世帯内で類似する傾向があるだろう。このようなとき、世帯が異なる研究参加者同士は独立と考えてよいが、同一世帯内の参加者を独立と考えるのは無理がある。世帯の他にも、学校や地域などがクラスターになりうる。

2つ目は、同一対象者において反復してアウトカムが測定される場合である。この場合のデータの単位は「人」ではなく「評価時点」になる。同じ人から測定されたアウトカムは類似した傾向を持つと考える方が普通だろう。眼や四肢・指趾といった「臓器単位」あるいは個々の腫瘍といった「病変単位」でアウトカムをみるような研究の場合も、疾患のメカニズムによっては反復測定と同様に考える必要がある。各測定に対して、人がクラスター単位となっていると考えることもできる。

アウトカムの誤差の出どころが、「測定単位(観測単位)」とその上層の「クラスター単位」の複数箇所にわたる、という意味でマルチレベルデータと呼ばれることがある。解析手法の観点からは、以下の用語はいずれも同じものを指していると考えて差し支えない。

  • クラスターデータ(clustered data)
  • 反復測定データ(repeated-measurement data)= パネルデータ(panel data)
  • マルチレベルデータ(multi-level data)

マルチレベルデータの解析

マルチレベルデータを解析する際には、誤差の相関構造や階層性を考慮する必要である。それは以下の理由のため。

データの情報量を過大評価しないため:
アウトカムの誤差に相関があるということは、観測単位間で情報の一部が重複しているということ。これを考慮しないと情報量を水増ししていることになってしまう。

クラスターを考慮した適切なモデルを使うことで検出力アップ:
観測値の誤差の中でクラスター効果として説明できることがあれば、クラスター単位での異質性を考慮することで検出力が上がるかもしれない。

解析する方法としては大きく以下の2種類がある。

  1. 一般化推定方程式(Generalized Estimating Equation, GEE)
  2. 混合効果モデル(Mixed-effects model, MEM)

前者は「誤差間の相関構造を定義して解析する方法」で、個々の観測単位(の誤差)間にある相関構造行列を与えることで対応するアプローチ。

後者は「誤差の発生源を階層化する方法」で、上層(例:世帯)が与えられた条件下では下層(例:世帯構成員)の誤差は独立であると考えるアプローチ。

この記事では前者のみ解説する。後者は別の機会に。

一般化推定方程式(GEE)の概要

GEEといってもモデルの基本は一般化線形モデル(Generalized Linear Model, GLM)なので、分布族(family)やリンク関数(link function)についての説明は割愛。

前述のとおり、GEEでは上記に加えて「観測単位の誤差間にどのような相関構造を想定するか」を指定する。代表的な型は以下のとおり。

独立型:independent

観測単位間に無相関(=独立)を仮定するもの。GEEを使おうと思ったときにはこれは使わない。*2

図1:クラスター内・間を問わず独立を仮定した構造

交換可能型:exchangeable

同一クラスター内では同一の相関係数をもった構造(最もよく使われる)。

図2:クラスター内相関係数が全て同じになっている構造

1次自己相関型:AR(1)

近い時点ほど相関が強くなる構造。同一対象者において3回以上繰り返して測定したときに使われることがある。

図3:自己相関を仮定した構造

非構造化型:unstructured

それぞれのデータの組み合わせごとに個別に相関係数を設定した構造。*3

図4:特定の相関構造を指定しない構造

geepackパッケージで一般化推定方程式(GEE)を使う

サンプルデータの準備

ChickWeightというデータセットを使う。ヒヨコの餌と体重変化の関係性をみたデータらしい(ヒヨコ50匹、測定数578回)。

> head(ChickWeight)
Grouped Data: weight ~ Time | Chick
  weight Time Chick Diet
1     42    0     1    1
2     51    2     1    1
3     59    4     1    1
4     64    6     1    1
5     76    8     1    1
6     93   10     1    1

ヒヨコ毎に体重は類似していると考える方が自然だろう。

geeglm()を使ってモデルを当てはめる

glm()の使い方が分かっていれば、追加で指定する引数は以下の3つくらい。

  • id:クラスターを識別する変数
  • corstr:相関構造("independence", "exchangeable", "ar1", "unstructured")を指定する引数(ユーザーが直接指定したい場合はzcorという引数で指定できるみたい)。
  • std.err:使用する分散推定方法を指定。
    • "san.se":sandwich variance estimate(デフォルト)。
    • "jack":approxymate jackknife variance estimate
    • "j1s":1-step jackknife variance estimate
    • "fij":fully iterated jackknife variance estimate

3番目の分散推定方法について少しだけ。 推定された分散は係数βの標準誤差(推定誤差)に使われる。

デフォルトで使われるsandwitch法*4(White-Huber法)。通常は、誤差の分散は観測単位間で等しくないといけない(等分散性の仮定, homoskedasticity)のだが、この方法で推定された残差を使って誤差の分散を補正することで非等分散性(heteroskedasticity)を許容できる。 クラスター数が30以下と少ないときはデフォルトのsandwich法ではバイアスが生じうる。

Jackknife法は再サンプリング法の1つ*5で、元サンプルから反復して計算することで特定の仮定を置くことなく分散(など)を推定する方法。なので計算負荷が高くなりやすい(多分 "j1s"<"jack"<"fij"の順?)。

コードは以下のとおり。

fit <- geeglm(weight ~ Time, 
              data = ChickWeight,
              id=Chick,
              family = gaussian(link="identity"),
              corstr = "exchangeable",
              std.err = "san.se")

結果を以下のとおり。

> summary(fit)

Call:
geeglm(formula = weight ~ Time, family = gaussian(link = "identity"), 
    data = ChickWeight, id = Chick, corstr = "exchangeable", 
    std.err = "san.se")

 Coefficients:
            Estimate Std.err Wald Pr(>|W|)    
(Intercept)   27.845   1.983  197   <2e-16 ***
Time           8.726   0.522  280   <2e-16 ***
---
Signif. codes:  0***0.001**0.01*0.05 ‘.’ 0.1 ‘ ’ 1

Correlation structure = exchangeable 
Estimated Scale Parameters:

            Estimate Std.err
(Intercept)     1509     259
  Link = identity 

Estimated Correlation Parameters:
      Estimate Std.err
alpha    0.475  0.0228
Number of clusters:   50  Maximum cluster size: 12 

Timeが1増えるとweightは約8.7増えていると解釈できる。

指数変換が必要なとき

リンク関数がlogとかlogitのときは、係数の推定結果を解釈する際に指数変換が必要になる。

方法1:broomパッケージのtidy( )を使う

library(broom)
tidy(fit, exponentiate = TRUE, conf.int = TRUE)
> tidy(fit, exponentiate = TRUE, conf.int = TRUE)
# A tibble: 2 x 7
  term        estimate std.error statistic p.value     conf.low conf.high
  <chr>          <dbl>     <dbl>     <dbl>   <dbl>        <dbl>     <dbl>
1 (Intercept)  1.24e12     1.98       197.       0 25435648463.   6.04e13
2 Time         6.16e 3     0.522      280.       0        2216.   1.71e 4
Warning message:
In tidy.geeglm(fit, exponentiate = TRUE, conf.int = TRUE) :
  Exponentiating coefficients, but model did not use a log or logit link function

ここではlink="identity"だったので指数変換は不要ですよ、とエラーメッセージが表示されている。

方法2:biostat3パッケージのeform( )を使う

library(biostat3)
eform(fit, level=0.95, method="Delta")
            exp(beta)    2.5 %   97.5 %
(Intercept)  1.24e+12 2.54e+10 6.04e+13
Time         6.16e+03 2.22e+03 1.71e+04

おわりに

  • geeglmはロバスト分散推定をしたいときにも使える。

参考資料

  • 開発者の文献:Halekoh, U.; Højsgaard, S. and Yan, J (2006) The R Package geepack for Generalized Estimating Equations. Journal of Statistical Software, 15, 2, 1-11"

www.jstatsoft.org

  • CoxモデルにGEEアプローチを使いたいときはgeecureパッケージを参照のこと

cran.r-project.org

*1:「マルチレベルを考慮しない」としたいところだがtautology回避のため

*2:しかし、geeglmでロバスト分散推定したいだけなら使うかも

*3:使ってるところをまだ見たことがない

*4:計算の過程で登場する式で、分散共分散行列をはさんでいるように見えるから

*5:他にブートストラップ法やクロスバリデーション法など

罰則付き回帰モデル(LASSO回帰, Ridge回帰, Elastic Net)で過学習を抑える(glmnetパッケージ)[R]

回帰モデルの説明変数は多いほど良いという訳ではない。
15組の(x,y)を2次式で回帰した場合(赤・点線)と10次式で回帰した場合(青・実線)を比べると、後者の方が15のデータからのズレが小さいが、新しいデータには前者の方が上手く当てはまる。

図1:2次式(赤・点線)と10次式(青・実線)を当てはめた

このようにデータに過度に適合しすぎて返って使い物にならなくなることを、過剰適合(overfitting)とか過学習という。

罰則付き回帰モデル(penalized regression model)で過学習を抑える

xが少し変化しただけでyが大きく変化するモデル、つまり、xの係数βの絶対値が大きすぎるモデルは過学習を起こしている可能性があるので、|β|の大きさに応じてペナルティーを与えることで適正なモデルを探そうという話。 正則化回帰モデル(regularized regression model)ともいう。

下のように、「モデルからのズレ(=損失関数)」と「ペナルティー(=正則化項)」の和(=目的関数)を小さくするモデルを選ぶ。

glmnet( )のヘルプには

The objective function for "gaussian" is
  1/2 RSS/nobs + λ*penalty,
and for the other models it is
  -loglik/nobs + λ*penalty.

とあるので、正規分布モデルでは残差平方和(residual sum of squares, RSS)、その他のモデルでは対数尤度を「ズレの指標」に使っているみたい。

LASSO回帰, Ridge回帰, Elastic Netは正則化項(ペナルティー)が違う

ラッソ回帰(LASSO*1 regression)ではパラメータの絶対値の和  \Sigma |\beta| が、リッジ回帰(Ridge regression)ではパラメータの2乗和  \Sigma \beta ^2 が正則化項として用いられる *2。 Elastic Netは下のように両者を混合したもの(凸結合*3したもの)。

 \displaystyle
\begin{align}
(penalty) = \sum_{j=1}^p \left\{ \alpha | \beta_j | + (1-\alpha) \beta_j^2 \right\}, 0\leq \alpha \leq 1
\end{align}

α=1のときはLASSO回帰、α=0のときはRidge回帰になる。
Lasso回帰では影響の小さい変数が除かれていく(=係数βが0になる)ので変数選択が可能だが、Ridge回帰では基本的に全ての変数が残るので変数選択には使えない。

glmnet( )でペナルティーの重みλを色々変えて係数βを推定する

glmnetパッケージのQuickStartExampleデータを使う。

library(glmnet)
data(QuickStartExample)
x <- QuickStartExample$x
y <- QuickStartExample$y 
> str(QuickStartExample)
List of 2
 $ x: num [1:100, 1:20] 0.274 2.245 -0.125 -0.544 -1.459 ...
 $ y: num [1:100, 1] -1.275 1.843 0.459 0.564 1.873 ...

従属変数yは連続変数、説明変数xは20個の連続変数・100観察分のmatrixになっている。

説明変数xと従属変数y以外に、glmnet( )の引数で使いそうなものは次のとおり。

  • family:モデルの種類。
    • "gaussian":正規分布モデル(デフォルト)
    • "binomial":二項分布モデル
    • "poisson":ポアソン分布
    • "multinomial":多項分布モデル
    • "cox":Coxモデル
    • "mgaussian":多変量正規分布モデル(アウトカムが複数の連続変数で構成される)
  • alpha:0〜1の値を指定する。1ならLasso回帰、0ならRidge回帰になる。デフォルトは1。
  • nlambda:計算するλの個数。デフォルトは100。
  • standardize:モデルを当てはめる前にxを標準化するかどうか*4。デフォルトはTRUE。
  • lower.limits, upper.limits:βの下限・上限。デフォルトは-Inf〜Inf。
  • penalty.factor:変数毎に罰則の大きさを変えたい時に指定する。
fit <- glmnet(x,y)

結果fitをprint( )に渡すと、試されたλ毎の結果が表示される。

> print(fit)

Call:  glmnet(x = x, y = y) 

   Df  %Dev  Lambda
1   0  0.00 1.63100
2   2  5.53 1.48600
3   2 14.59 1.35400
4   2 22.11 1.23400
5   2 28.36 1.12400
~~~
  • Lambda:重み付けパラメータλ
  • DF:係数が0になっていない説明変数の個数
  • %Dev:モデルで説明されるデビアンスの割合

λが1.631より大きくなると全てのβが0になるのでモデルで説明されるdevianceが0になったということ。

結果fitをplot( )に渡すと、λを変化させたときの各係数の推移をプロットする。
指定する引数は、次のとおり。

  • xvar:横軸に何をとるか。
    • "norm"とするとL1正則(=罰則の大きさ)
    • "lambda"とするとλの対数
    • "dev"とするとモデルで説明されているdevianceの割合
  • label=TRUEとすると変数名も表示する

図2:対数λに対して係数βの推移をプロットした

λが小さいとき(グラフ左)は罰則が相対的に小さいので全ての変数が残っているが、λを大きくしていく(グラフ右)と影響の小さい変数の係数が0に落ちていく。

図3:L1正則に対して係数βの推移をプロットした

変数がたくさん残っている(グラフ右)うちはL1正則は大きいが、λを大きくしていくにつれて変数が脱落していき(グラフ左)L1正則は小さくなっていく。

cv.glmnet( )で重み付けパラメータλを変えながらモデルの交差検証を行う

交差検証(cross-validation, CV)はモデルの汎化性能(= 新しく測定されるデータをどれくらい誤差なく予測できるか)を評価する方法。 K分割交差検証(K-fold cross-validation)ではデータをK個に分割し、そのうちのK-1個のデータで作ったモデルを残りの1個のデータで評価する。

cv.glmnet( )で次の引数を指定する。

  • family:glmnetと同じ。
  • nfold:分けるグループ数。
  • foldid:グループの分け方を揃えたいときに指定する。
  • type.measure:交差検証に使う損失関数。指定できる種類・デフォルトは、familyで指定するモデルの種類によって決まる(下の表を参照。■はdefault)。
    • "default":モデルのfamilyに応じたデフォルトの損失関数を使用する。
    • "mse":平均2乗誤差(mean square error)
    • "deviance":デビアンス
    • "class":誤分類率(misclassification error)
    • "auc":ROC曲線の曲線下面積(area under the curve)
    • "mae":平均絶対誤差(mean absolute error)
    • "C":Harrel's C

図4:モデルの種類別利用可能な損失関数

結果の渡し方はglmnet( )と同じ。

> cvfit <- cv.glmnet(x, y)
> print(cvfit)

Call:  cv.glmnet(x = x, y = y) 

Measure: Mean-Squared Error 

     Lambda Index Measure     SE Nonzero
min 0.06897    35   1.129 0.1276      10
1se 0.14517    27   1.247 0.1676       8

minでは損失関数が最小となるとなるときのλなどが表示される。
1seで表示されるλは何かというと、

largest value of lambda such that error is within 1 standard error of the minimum.

とのこと。CV errorの最小値に、そのときのCV errorの1標準誤差分を足したCV errorを与えるλを使うことを推奨する「one standard error rule」なるものがあるらしい*5

任意のλのときの係数βを取り出したいときは、coef( )を使う。指定したいλは(なぜか)s=で指定する*6
CV errorを最小にするλを与えたいときは"lambda.min"、one standar error ruleに基づいたλを与えたいときは"lambda.1se"を指定する(デフォルトはこちら)。

> coef(cvfit, s="lambda.min")
21 x 1 sparse Matrix of class "dgCMatrix"
                      s1
(Intercept)  0.147927056
V1           1.337393911
V2           .          
V3           0.704086480
~~~~~  
V18          .          
V19          .          
V20         -1.061382443

ちなみにglmnet()オブジェクトを使って次のようにしても同じ。

fit <- glmnet(x, y)
cvfit <- cv.glmnet(x, y)
coef(fit, s=cvfit$lambda.min

最後に交差検証のプロットを描く。

plot(cvfit)

図5:対数λに対して交差検証の結果をプロットした

プロットで表示される縦線は、lambda.minlambda.1seに対応している。

おわりに

  • 目にする機会が増えたので勉強してみましたが、自分で使う機会があるかどうか...
  • 換毛期です。茶トラは明るい毛色なので大変です。

参考資料

*1:least absolute shrinkage and selection operator

*2:正則化項について分かりやすく解説されています。qiita.com

*3:convex combination:和が1となる非負係数の線型結合

*4:標準化の詳細はこちら:A deep dive into glmnet:&nbsp;standardizestatisticaloddsandends.wordpress.com

*5:1SE ruleに関する議論:stats.stackexchange.com

*6:理由は"In case we want to allow one to specify the model size in other ways in the future."だからとのこと

傾向スコア解析(3):傾向スコアマッチング(PSM)を使って交絡を調整する [R]

傾向スコア(propensity score, PS)を使った交絡調整方法には、

  1. マッチング
  2. 層別化
  3. PSを共変量とした回帰モデル
  4. 逆確率重み付け法

がある。

ここでは「1. マッチング(propensity score matching, PSM)」について説明する。

パッケージとデータの準備

PSAgraphicsパッケージのlindnerデータセットを使う。

lindnerデータは、Lindnerセンターで経皮的冠動脈インターベンション(percutaneous cardiac intervention, PCI)を受けた996例において、abciximabという抗血小板薬を追加したときの効果をみた研究のデータ。*1

下のコードでPCI後6か月時点の予後をdeathとして追加した。

library(tidyverse)
library(PSAgraphics)
data("lindner")
d <- lindner %>%
  mutate(death = if_else(lifepres==0,1,0))
> head(d)
  lifepres cardbill abcix stent height female diabetic acutemi ejecfrac ves1proc death
1      0.0    14301     1     0    163      1        1       0       56        1     1
2     11.6     3563     1     0    168      0        0       0       56        1     0
3     11.6     4694     1     0    188      0        0       0       50        1     0
4     11.6     7366     1     0    175      0        1       0       50        1     0
5     11.6     8247     1     0    168      1        0       0       55        1     0
6     11.6     8319     1     0    178      0        0       0       50        1     0

このうち、使用する変数は以下のとおり。

  • death:PCI後6か月時点の予後(0=生存, 1=死亡)
  • abcix:abciximab使用有無(0=PCI単独, 1=PCI+abciximab)
  • stent:冠動脈ステント留置(0=なし, 1=あり)
  • height:身長(cm)
  • female:性別(0=男性, 1=女性)
  • diabetic:糖尿病有無(0=なし, 1=あり)
  • acutemi:7日前以内の急性心筋梗塞有無(0=なし, 1=あり)
  • ejecfrac:左室収縮率(%)
  • ves1proc:病変血管数

マッチング前のデータで共変量のバランスを確認する

tableoneパッケージを使って要約する方法を紹介したが、ここではMatchItパッケージを使う。「治療群を示す変数 ~ 共変量」の要領で指定する。 ポイントはmethod=NULLを指定すること。本来はマッチングの方法を指定する引数だが、NULLを指定するとマッチングが行われないので、マッチング前のデータに関してバランス評価を行うことができる。

library(MatchIt)
unmatched <- matchit(abcix ~ stent + height + female + diabetic + acutemi + ejecfrac + ves1proc,
                   data=d, method=NULL)
> summary(unmatched)

Call:
matchit(formula = abcix ~ stent + height + female + diabetic + 
    acutemi + ejecfrac + ves1proc, data = d, method = NULL)

Summary of Balance for All Data:
         Means Treated Means Control Std. Mean Diff. Var. Ratio eCDF Mean eCDF Max
distance        0.7265        0.6406          0.6609     1.1161    0.1714   0.2760
stent           0.7049        0.5839          0.2652          .    0.1210   0.1210
height        171.4427      171.4463         -0.0003     1.0201    0.0079   0.0250
female          0.3309        0.3859         -0.1168          .    0.0550   0.0550
diabetic        0.2049        0.2685         -0.1575          .    0.0636   0.0636
acutemi         0.1791        0.0604          0.3095          .    0.1187   0.1187
ejecfrac       50.4026       52.2886         -0.1810     1.0238    0.0356   0.1138
ves1proc        1.4628        1.2047          0.3654     2.1614    0.0433   0.1884


Sample Sizes:
          Control Treated
All           298     698
Matched       298     698
Unmatched       0       0
Discarded       0       0

均衡化の指標については後ほど。

傾向スコアでマッチさせたコホートを作成する

MatchItパッケージのmatchit( )を使って治療群の各症例に対して対照群から近い症例を選んでペアを作る。近さの基準となる距離として傾向スコアが用いられることがほとんど。 マッチングの方法には色々あるが、Austin先生*2も「復元抽出なしの1:1最近傍マッチング」が一番いいんじゃない?って仰っているようなので、ひとまずこれを使う。

「治療群を示す変数 ~ 共変量」の他には、以下の引数を指定する。

  • method:マッチングの方法
    • nearest:最近傍マッチング(nearest matching別名)。別名:貪欲マッチング(greedy matching)
  • distance:前述のとおり、症例間の距離としてPSが用いられることがほとんどなので、この引数でPSの推定方法を指定することができる。
    • "glm":デフォルトはdistance="glm"でlink="logit"`が指定されているとロジスティック回帰モデルを使ってPSを推定する。
    • この他のPS推定法として、一般化加法モデル(generalized additive model, GAM), 一般化ブーストモデル(generalized boosted model, GBM), Lasso/ridge/elastic net, 決定木, ランダムフォレストなどが使える。
    • "mahalanobis":マハラノビス距離
  • estimand:効果を推定したい対象集団。デフォルトは"ATT"(Average Treatment effect on the Treated)。
  • replace:対照を復元抽出するかどうか。TRUEとすると、1つの対照症例が複数の治療群症例にマッチさせることが可能。
  • ratio:治療群患者1例に対してマッチさせる対照症例数
matched_1 <- matchit(abcix ~ stent + height + female + diabetic + acutemi + ejecfrac + ves1proc,
                   data=d,
                   method="nearest",
                   distance = "glm",
                   link = "logit",
                   estimand = "ATT",
                   replace=FALSE,
                   ratio=1,
                   seed=1234)

症例間の距離が遠すぎる人同士がペアにならないように、ある値以上近い場合にだけペアにすることがある。これは引数caliperで指定できる。例えばcaliper=0.2とすると、症例間の距離の標準偏差(SD)の0.2倍より小さい場合にだけペアにする。

論文ではときどき"Nearest matching with caliper width of 0.2 SD of the logit PS"のように、PSではなくPSのlogitをcaliperの基準にしているのを見かけるが、この場合は次のコードのようにdistanceを指定する必要がある(単にcaliper=0.2とするとPSを基準としたキャリパーになってしまうから)。

まずはglm( )でPS, logit PSを推定してデータセットに加えておく。

ps_model <- glm(abcix ~ stent + height + female + diabetic + acutemi + ejecfrac + ves1proc, 
                data = d, family = binomial(link="logit"))
d <- d %>%
  mutate(ps_logit = predict(ps_model, type = "link"),
         ps = predict(ps_model, type = "response"))

上記で作成したps_logitdistanceに指定する(推定したPS, logit PSを使用するのでコード実行必要)。 std.caliper=TRUEとした場合は「キャリパーをdistanceのSDの何倍にするか」をcaliperで指定する。 std.caliper=FALSEとした場合は、キャリパーを絶対値で指定する。

matched_2 <- matchit(abcix ~ stent + height + female + diabetic + acutemi + ejecfrac + ves1proc,
                   data=d,
                   method="nearest",
                   distance = d$ps_logit,
                   caliper=0.2,
                   std.caliper = TRUE,
                   ratio=1,
                   replace=F,
                   seed=1234)

マッチングの概要を確認すると、次のようにキャリパーが0.156になっている。

> matched_2
A matchit object
 - method: 1:1 nearest neighbor matching without replacement
 - distance: User-defined [caliper]
 - caliper: <distance> (0.156)
 - number of obs.: 996 (original), 584 (matched)
 - target estimand: ATT
 - covariates: stent, height, female, diabetic, acutemi, ejecfrac, ves1proc
> sd(d$ps_logit)*0.2
[1] 0.1556522

logit PSの0.2倍がキャリパーの設定値と一致していることが確認できた。

マッチさせたコホートで共変量のバランスを確認する

マッチングの様子はsummary( )で確認できる。

> summary(matched_1)

Call:
matchit(formula = abcix ~ stent + height + female + diabetic + 
    acutemi + ejecfrac + ves1proc, data = d, method = "nearest", 
    distance = "glm", link = "logit", estimand = "ATT", replace = F, 
    ratio = 1, seed = 1234)

Summary of Balance for All Data:
         Means Treated Means Control Std. Mean Diff. Var. Ratio eCDF Mean eCDF Max
distance        0.7265        0.6406          0.6609     1.1161    0.1714   0.2760
stent           0.7049        0.5839          0.2652          .    0.1210   0.1210
height        171.4427      171.4463         -0.0003     1.0201    0.0079   0.0250
female          0.3309        0.3859         -0.1168          .    0.0550   0.0550
diabetic        0.2049        0.2685         -0.1575          .    0.0636   0.0636
acutemi         0.1791        0.0604          0.3095          .    0.1187   0.1187
ejecfrac       50.4026       52.2886         -0.1810     1.0238    0.0356   0.1138
ves1proc        1.4628        1.2047          0.3654     2.1614    0.0433   0.1884


Summary of Balance for Matched Data:
         Means Treated Means Control Std. Mean Diff. Var. Ratio eCDF Mean eCDF Max Std. Pair Dist.
distance        0.8517        0.6406          1.6242     0.2455    0.4407   0.8289          1.6242
stent           0.7852        0.5839          0.4414          .    0.2013   0.2013          0.9123
height        171.4295      171.4463         -0.0016     1.1500    0.0109   0.0403          1.0803
female          0.2919        0.3859         -0.1997          .    0.0940   0.0940          0.8986
diabetic        0.1644        0.2685         -0.2577          .    0.1040   0.1040          0.7400
acutemi         0.4128        0.0604          0.9190          .    0.3523   0.3523          1.0065
ejecfrac       47.7953       52.2886         -0.4313     1.2340    0.0800   0.2315          1.0980
ves1proc        1.9228        1.2047          1.0170     2.7619    0.1197   0.5134          1.1405

Percent Balance Improvement:
         Std. Mean Diff. Var. Ratio eCDF Mean eCDF Max
distance          -145.7    -1179.0    -157.1   -200.3
stent              -66.4          .     -66.4    -66.4
height            -364.1     -604.0     -37.5    -61.3
female             -71.0          .     -71.0    -71.0
diabetic           -63.6          .     -63.6    -63.6
acutemi           -196.9          .    -196.9   -196.9
ejecfrac          -138.2     -795.3    -124.7   -103.4
ves1proc          -178.3      -31.8    -176.3   -172.5

Sample Sizes:
          Control Treated
All           298     698
Matched       298     298
Unmatched       0     400
Discarded       0       0

以下の4つのパートで構成されている。

  • Summary of Balance for All Data:マッチング前のデータのバランス
  • Summary of Balance for Matched Data:マッチング後のデータのバランス
  • Percent Balance Improvement:マッチング前後でバランスの各指標が何パーセント改善したか
  • Sample Sizes:matched cohortに採用された症例数

バランスの指標として表示されているものは以下のとおり。標準化平均差(standardized mean difference, SMD)が一番よく使われる。経験的累積分布関数(empirical cumulative distribution function, eCDF)に関する指標については明確な基準がない模様。

  • Means Treated:治療群の平均
  • Means Control:対照群の平均
  • Std. Mean Diff.:治療群間のSMD。<0.1〜0.15ならOK。
  • Var. Ratio:治療群間での分散比。連続変数に対してのみ計算される。上手くバランスがとれていれば、分散比は1に近くなるはず(0.5-2の間なら許容)。
  • eCDF Mean:群間でeCDFが平均してどれくらい乖離しているか
  • eCDF Max:群間でeCDFが最大でどれくらい乖離しているか
  • Std. Pair Dist.:各ペア内の距離

マッチングの様子を図示する

治療群・対照群のそれぞれで、マッチングサンプルに抽出された人、されなかった人の傾向スコアを図示する。

plot(matched, type="jitter")

図1:マッチングした集団でのPS分布を図示した

引数typeを指定して他のグラフを描ける。例えば、type="density"とすれば、各共変量の密度分布を比較する。

plot(matched_1, type="density")

図2:type="density"を指定して密度分布を比較した(一部省略)

QQプロットやeCDFは連続変数に対してのみ作図されるので、

plot(matched_1, type="qq", which.xs=c("height","ejecfrac","ves1proc"))

図3:type="qq"を指定してQQプロットを比較した

plot(matched_1, type="ecdf", which.xs=c("height","ejecfrac","ves1proc"))

図4:type="eCDF"を指定してeCDFを比較した

他にもcobaltパッケージのlove.plot( )を使ってSMDをプロットすることができる。

library(cobalt)
love.plot(matched_1, threshold = 0.15, abs = TRUE, stars="raw")

図4:cobaltパッケージのlove.plot( )を使ってSMDをプロットする

治療効果を推定する

作成されたマッチング後データは、match.data( )で抽出する。

m.out_1 <- match.data(matched_1)

中身を見てみると、

> head(m.out_1)
    lifepres cardbill abcix stent height female diabetic acutemi ejecfrac ves1proc death ps_logit        ps  distance weights subclass
367      0.0    14990     1     1    157      1        1       0       45        2     1 1.215812 0.7713257 0.7713257       1        1
369      0.0    96741     1     1    171      0        0       0       29        1     1 1.242675 0.7760293 0.7760293       1        2
370     11.6     7616     1     1    168      1        0       0       51        2     0 1.364860 0.7965484 0.7965484       1        3
371     11.6     7664     1     0    173      0        0       1       55        1     0 1.453961 0.8106073 0.8106073       1        4
374     11.6     8321     1     0    180      0        0       1       60        1     0 1.272454 0.7811625 0.7811625       1        5
375     11.6     8389     1     0    170      1        0       1       55        1     0 1.141000 0.7578631 0.7578631       1        6

distance, weights, subclassという変数が追加されている。 subclassはペアを示すID。

このデータを使ってglm( )を行うと、次のようになる(今回のマッチングではweightsは全て1なので、指定しなくても結果は同じ)。

fit <- glm(death ~ abcix, data=m.out_1,
           family = binomial(link = "logit"),
           weights = weights)
> summary(fit)$coef
              Estimate Std. Error    z value     Pr(>|z|)
(Intercept) -2.9373967  0.2649533 -11.086471 1.459321e-28
abcix       -0.7900164  0.4652834  -1.697925 8.952185e-02

マッチペアを考慮してロバスト分散推定を行うことが勧められているようなので、limtestパッケージおよびsandwichパッケージを使う。

llibrary(lmtest)
library(sandwich)
coeftest(fit, vcov. = vcovCL, cluster = ~subclass)
            Estimate Std. Error  z value Pr(>|z|)    
(Intercept) -2.93740    0.26540 -11.0679  < 2e-16 ***
abcix       -0.79002    0.45153  -1.7497  0.08018 .  

ロバスト分散推定を使わない場合と少しだけ推定値・標準誤差が異なっている。

他にはbootstrappingを用いる方法があるが、本記事ではここまで。

おわりに

  • matchit( )には色々なPS推定方法が指定できるので、最終的にマッチングを使わない場合でもmatchit( )でPS推定してしまうのもアリかもしれない。

参考資料

*1:Kereiakes DJ, Obenchain RL, Barber BL, Smith A, McDonald M, Broderick TM, Runyon JP, Shimshak TM, Schneider JF, Hattemer CR, Roth EM, Whang DD, Cocks D, Abbottsmith CW. Abciximab provides cost-effective survival advantage in high-volume interventional practice. Am Heart J. 2000 Oct;140(4):603-10.

*2:Peter C. Austin. A comparison of 12 algorithms for matching on the propensity score. Statist. Med. 2014, 33 1057–1069.