ねこすたっと

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

分割時系列解析(Interrupted Time Series Analysis)による公衆衛生介入の評価 [R]

ITSAとは

例えば新しい政策が実施されるときなど、ある介入が集団に対して一斉に行われる場合には、介入を受けない対照群を設定することができません。

単純に介入の前後を比較するだけでは不十分です。 下図では介入に無関係な減少トレンドが見られますが、介入前後で平均を単純に比べてしまうと、あたかも介入に効果があったように見えてしまいます。

正しく評価するためには、背景にあるトレンドや周期性を考慮した解析が必要で、これが分割時系列解析(Interrupted Time Series Analysis, ITSA)です。

ITSデザインに適した状況は?

分析したい状況を、

  1. 介入
  2. アウトカム
  3. データ

の観点で考えます。

介入の導入時期は明確である必要があります。介入前と介入後を明確に区別できなければ、適切な分析ができません。

また、アウトカムは介入に速やかに反応するものが望ましいです。介入から反応までに遅延(lag)があっても問題ありませんが、効果発現の機序を踏まえ、lagが明確に設定できることが重要です。

最後に、介入前後でアウトカムが連続的に測定されていて、利用可能であることが必須です。 ルーチンで測定されているデータが適していますが、介入によってデータの測定や収集が影響されているとバイアスを生じてしまいます。

PenfoldとZhangによれば、介入前後のそれぞれにおいて最低でも8時点ずつのデータが必要とのことですが、周期性やデータの変動、効果の大きさなどの影響を受けるので単純ではなさそうです。

少なくとも、

  • トレンドが一定と考えられる範囲で時点数は多い方がいい
  • 介入前後で時点数が均等に近い方がいい

ということについては、デザインを考える段階で検討できそうです。

インパクトモデルを選ぶ

他の回帰分析と同様に、介入がアウトカムにもたらす影響について仮説(インパクトモデル)を設定することが必要です。これには主に次の3つのパターンがあります(図は Bernal JL et al. (2017) から)。

  • (a) 切片の変化(level change)
    介入が実施された直後に、アウトカムの水準(切片)に顕著な変動が生じるモデル。介入が即時に影響を与え、その影響が継続することを想定している。
  • (b) 傾きの変化(slope change)
    介入がアウトカムの増加または減少のペース(傾き)に変化をもたらすモデル。 介入の効果が徐々に現れ、時間とともに増強・減弱することを想定している。
  • (c) 切片と傾きの両方の変化

介入効果が出現するタイミングや持続性に関わる「遅延(lag)」を設定することもできます。

  • (d) 介入から遅れてslope changeが出現
  • (e) level changeが一過性
  • (f) slope changeが一過性

インパクトモデルは、既存の知見や想定している機序などを元にして事前に決めますが、lagの長さなどが異なった複数のモデルを使って感度分析を行うこともあります。

分析実行例

使用するデータ

ITSAには最低限、以下の3つの変数が必要です。

  • T:研究開始からの時間(月次、年次など)。サンプルデータのtimeがこれ。
  • X:介入前(=0)か介入後(=1)かを示すダミー変数
  • Y:時間  i におけるアウトカム

サンプルデータ(Bernal et al. (2017))を使って、実際に分析の流れを見ていきましょう。これはイタリアで2005年に施行された屋内での喫煙禁止法の効果を、2002-2006年の急性冠動脈イベント(ACE)による入院数を使って検証したデータです。

library(tidyverse)
data <- read_csv("sicily.csv")
> head(data)
# A tibble: 6 × 7
   year month  aces  time smokban     pop  stdpop
  <dbl> <dbl> <dbl> <dbl>   <dbl>   <dbl>   <dbl>
1  2002     1   728     1       0 364277. 379875.
2  2002     2   659     2       0 364277. 376496.
3  2002     3   791     3       0 364277. 377041.
4  2002     4   734     4       0 364277. 377116.
5  2002     5   757     5       0 364277. 377383.
6  2002     6   726     6       0 364277. 374113.

含まれている変数は以下のとおり:

  • year:カレンダー時間(年)
  • month:カレンダー時間(月)
  • aces:ACEによる入院数(←Y)
  • time:研究開始からの経過時間(←T)
  • smokeban:禁煙法施行前(=0)か後(=1)を示すダミー変数(←X)
  • pop:人口
  • stdpop:年齢標準化人口

ここではアウトカムとしてACE入院数をそのまま用いますが、これ以外にも標準化発生率(aces/stdpop)を使う、あるいはstdpopをオフセット項にする、などの方法もあると思います。

散布図を描く

まずは散布図を作成して、背景にあるトレンドや周期性、極端な外れ値がないかを見ましょう。

なんとなく周期性はあるように見えますが、このグラフだけで判断するのは難しそうですね...。 他に考慮すべき共変量がある場合は、その変数の時間依存性などを可視化して確認しておく方がいいかもしれません。

Level Change Model

介入の前後で異なった回帰直線を当てはめるだけであれば、それほど難しくないと思います。ただ、別々のデータセットで別々に推定された回帰係数を比較して、「切片や傾きの変化が統計的に有意か」などを見るのは難しいので工夫が必要です。

まず、切片の変化を想定したい場合は、以下の回帰モデルを使います。

 
\begin{aligned}
Y_i &= \beta_0 + \beta_1 T + \beta_2 X_i \\
\end{aligned}

この式では、時間Tに対する傾きはXによらず一定( \beta_1)で、X=1になると全体がY軸方向に  \beta_2 だけ平行移動するので、目的のlevel changeは  \beta_2 として推定されます。

Slope Change Model

次に、傾きの変化のみ想定したい場合は、以下の回帰モデルを追加います。

 
\begin{aligned}
Y_i &= \beta_0 + \beta_1 T + \beta_3 T_a \\
\end{aligned}

ここで、Taは介入後からの時間を表す変数で、

  • 介入前(X=0)のときTa=0
  • はじめてX=1となったときにTa=1となり、以後Tと同じように増えていく

という規則で作成します。

上記のTaに当たる変数time_afterを、下のコードで作成しておきます。

before_ban <- filter(data, smokban == 0) %>%
  slice(n()) %>%
  pull(time)
data <- data %>%
  mutate(time_after = (time - before_ban)*smokban)

介入が2005年1月(time=37)からなので、作成された変数は次のようになっています。

> data %>% slice(34:40)
# A tibble: 7 × 8
   year month  aces  time smokban     pop  stdpop time_after
  <dbl> <dbl> <dbl> <dbl>   <dbl>   <dbl>   <dbl>      <dbl>
1  2004    10   839    34       0 364700. 384052.          0
2  2004    11   887    35       0 364700. 384450.          0
3  2004    12   886    36       0 364700. 383428.          0
4  2005     1   831    37       1 364421. 388153.          1
5  2005     2   796    38       1 364421. 388373.          2
6  2005     3   833    39       1 364421. 386470.          3
7  2005     4   820    40       1 364421. 386033.          4

Level + Slope Change Model

切片と傾きの両方の変化を想定したい場合は、前述の項を両方含めます。

 
\begin{aligned}
Y_i &= \beta_0 + \beta_1 T +  \beta_2 X_i  + \beta_3 T_{a} \\
\end{aligned}

推定してみます。

fit <- lm(aces ~ time + smokban + time_after, data=data)
> summary(fit)

Call:
lm(formula = aces ~ time + smokban + time_after, data = data)

Residuals:
    Min      1Q  Median      3Q     Max 
-98.957 -38.575  -4.926  35.513 158.337 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) 728.4730    18.9129  38.517  < 2e-16 ***
time          4.4534     0.8914   4.996 6.28e-06 ***
smokban     -92.9697    30.0426  -3.095   0.0031 ** 
time_after    0.6879     1.9609   0.351   0.7271    
---
Signif. codes:  0***0.001**0.01*0.05 ‘.’ 0.1 ‘ ’ 1

Residual standard error: 55.56 on 55 degrees of freedom
Multiple R-squared:  0.4418, Adjusted R-squared:  0.4113 
F-statistic: 14.51 on 3 and 55 DF,  p-value: 4.441e-07

係数の解釈は次のとおり:

  • (Intercept):観察開始時のACE入院件数の期待値(解釈不要)。
  • time:介入前のトレンド。平均して毎年4.5件ずつ増加傾向であった。
  • smokban:介入による切片の変化(level change)。法律の施行によりACE入院が約93件減り、統計的に有意な変化があった。
  • time_after:介入前後で傾きの変化(slop change)。法律の施行後、ACE入院の増加傾向が4.5から5.1に増えたが、統計的に有意な変化ではない。

自己相関を確認する

残差について、自己相関関数(autocorrelation function, ACF)を確認します。 自己相関は、時点tのデータと時点t-kのデータの相関係数をラグ k の関数として示したもの

residuals <- fit$residuals
acf(residuals, plot=TRUE)

Lag=6のときに負の相関、Lag=12で正の相関が強そうで、ACE入院件数には季節性がありそうですね。 ちなみにLag=0のときは、自分自身との相関係数なので常にACF=1です。

次に、偏自己相関関数(partial autocorrelation function, PACF)を確認します。 偏自己相関は、時点tと時点t-kの間にあるデータ(時点t-k+1 〜 t-1)を使って、時点t, t-kのデータを回帰した残差の相関です。2つの時点間のデータの影響を取り除いて、2時点の直接的な相関を見るもの。

pacf(residuals, plot=TRUE)

やはり季節性は考慮する必要がありそうですね。 ボックス・ジェンキンス法(Box-Jenkins method)でアプローチするか、標準誤差を調整して対応する方法があるそうです。

おわりに

  • 実装方法が難しいわけではないので、理論的なことや周辺領域(Box-Jenkins法)について深掘りしたかったんですが...。
  • 毛繕いを手伝ってあげるようにしてから、猫から発生する静電気がマシになってる気がします

参考文献

Joint Frailty-Copula Modelを使った生存時間解析 (2):実践編(joint.Coxパッケージ) [R]

前回、「Joint Frailty-Copula Modelを使った生存時間解析」の理論的なことについてまとめました。

necostat.hatenablog.jp

読んだ本:

今回は、上記の著者が作成したjoint.Coxパッケージを使って、Rで解析を実行する方法をまとめたいと思います。

ベースラインハザード関数の表し方

このパッケージでは、ベースラインハザード関数を表すために、

  • 3次M-スプライン(cubic M-spline)を用いたノンパラメトリックモデル
  • ワイブル分布(Weibull distribution)を用いたパラメトリックモデル

のどちらかを用いることができます。

M-スプラインは定義域で非負が保証されているスプラインで、自由度の高い曲線を当てはめることができる代わりに、パラメトリックモデルよりも計算負荷が高くなります。

ワイブル分布については、以前まとめた記事を参考にしてください。

necostat.hatenablog.jp

jointCox.reg( )でM-スプラインを用いる方法

指定する引数は以下のとおり。

  • event:「腫瘍増悪」のように、必ずしも観察終了とはならないイベント(non-terminal event)の発生有無(1=発生, 0=非発生)
  • t.event:non-terminal eventまでの時間
  • death:「死亡」のように、強制的に観察終了となってしまうイベント(terminal event)の発生有無(1=発生, 0=非発生)
  • t.death:terminal eventまでの時間
  • Z1:non-terminal eventに関連する共変量を行列形式で指定(Z1の列数=共変量の個数)
  • Z2Z1と同様に、terminal eventに関連する共変量
  • group:所属するクラスターを指定する変数
  • alpha:イベントごとにフレイリティ項を変更するかどうか(0=同じフレイリティ項を使用, 1=異なるフレイリティを使用)*1

スプラインを使う場合は「どの程度の滑らかさにするか」を示すパラメータ(smoothing parameter)を、以下のLikelihood cross-validation(LCV)を最大にするように決めます。

LCV = 対数尤度(当てはまりの良さ)- モデルの複雑さ(罰則)

2つのイベントを同時に考えてLCVを最大化するのは計算負荷が大きいそうで、それぞれのイベントについてLCV1, LCV2を最大にするようにsmoothing parameterを決めます。 このときsmoothing parameterを探索する範囲をkappa1, kappa2で指定できますが、正直どれくらいにしたらいいのか分からないのでデフォルトの設定を使うことにします。

では、joint.Coxパッケージに付属の卵巣腫瘍のデータ(dataOvarian)を使って試してみます。

library(tidyverse)
library(joint.Cox)
data(dataOvarian)

サンプルデータの中身:

> dataOvarian %>% glimpse
Rows: 1,003
Columns: 6
$ t.event <int> 1650, 30, 720, 450, 510, 1110, 210, 1380, 1800$ event   <dbl> 0, 1, 0, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0$ t.death <int> 1650, 30, 720, 780, 990, 1110, 600, 1410, 1800$ death   <dbl> 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0$ group   <dbl> 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4$ CXCL12  <dbl> 1.30594160, 1.28621637, -1.36903145, 1.6132696

これをjointCox.reg( )に渡します。計算の過程で "Error in integrate..." が出続けますが無視していいとのこと。

result <- jointCox.reg(
  t.event = dataOvarian$t.event,
  event = dataOvarian$event,
  t.death = dataOvarian$t.death,
  death = dataOvarian$death,
  Z1 = dataOvarian$CXCL12,
  Z2 = dataOvarian$CXCL12,
  group = dataOvarian$group,
  alpha = 0
)

出力結果

結果は以下の項目が一括で表示されます。少しずつ見ていきます。

まずは使用したデータのイベント数をまとめたもの。

> result
$count
   No.of samples No.of events No.of deaths No.of censors
4            110           76           46            64
8             58           48           36            22
11           278          185          113           165
14           557          266          290           267

次に、共変量(ここではCXCL12)に対する係数とその信頼区間。

$beta1
  estimate         SE      Lower      Upper 
0.19897418 0.03580646 0.12879353 0.26915484 

$beta2
  estimate         SE      Lower      Upper 
0.16540746 0.04234325 0.08241470 0.24840023 

η(イータ)はフレイルティ項の分散の推定値。

$eta
   estimate          SE       Lower       Upper 
0.033275908 0.029043661 0.006014176 0.184112694 

θはコピュラのパラメータです。どうやらクレイトンコピュラのみ使用可みたいです。 τはケンドールの順位相関係数で、クレイトンコピュラでは  \tau = \frac{\theta}{|theta + 2} で計算されます(この出力でもそのようになっていることを確認)。

$theta
 estimate        SE     Lower     Upper 
2.3632714 0.2354481 1.9440548 2.8728881 

$tau
  estimate     tau_se      Lower      Upper 
0.54162833 0.02473436 0.49290765 0.58956578 

LCVを最大にするκとそのときのLCV(だと思います)。

$LCV1
           K1          LCV1 
 3.103448e+16 -4.591487e+03 

$LCV2
           K2          LCV2 
 3.448276e+16 -4.159568e+03 

M-スプラインの係数です。geventのベースラインハザード関数、hdeathのそれを表すのに使います(詳細は後ほど)。

$g
[1]  9.136547e-01  1.686345e+00  1.855953e-97  4.447143e-02 8.208009e-155

$h
[1] 2.121898e-01 1.083565e+00 9.906754e-01 1.763836e-01 3.368575e-94

$g_var
              [,1]          [,2]         [,3]          [,4]          [,5]
[1,]  0.0044487471 -0.0027831825  0.00000e+00 -0.0003813422  0.000000e+00
[2,] -0.0027831825  0.0248178870  0.00000e+00  0.0008556739  0.000000e+00
[3,]  0.0000000000  0.0000000000 3.44456e-190  0.0000000000  0.000000e+00
[4,] -0.0003813422  0.0008556739  0.00000e+00  0.0524971946  0.000000e+00
[5,]  0.0000000000  0.0000000000  0.00000e+00  0.0000000000 6.737141e-305

$h_var
              [,1]          [,2]          [,3]          [,4]          [,5]
[1,]  9.506484e-04 -0.0013733137 -3.285041e-06 -2.162385e-04  0.000000e+00
[2,] -1.373314e-03  0.0099792406 -1.177544e-02 -1.839687e-04  0.000000e+00
[3,] -3.285041e-06 -0.0117754352  5.668603e-02 -9.863048e-05  0.000000e+00
[4,] -2.162385e-04 -0.0001839687 -9.863048e-05  4.356045e-02  0.000000e+00
[5,]  0.000000e+00  0.0000000000  0.000000e+00  0.000000e+00 1.134729e-183

最後に収束状況全般について。

$convergence
                 MPL                   DF                  LCV                 code     No.of.iterations No.of.randomizations 
           -8604.369               20.000            -8618.603                1.000               91.000               10.000 

$convergence.parameters
NULL

ベースラインハザード関数を描く

推定されたスプラインの係数(g, h)から、スプライン曲線を描くことができます。

ここで使われているM-スプラインは、負にならないスプラインです。M-スプラインでは、隣接する区域の基底関数を使って再帰的に目的の次元(ここでは3次)の基底関数が作られます。

本を読むと、

  •  \varepsilon_1:最小のイベント発生時間
  •  \varepsilon_2 (\varepsilon_1 + \varepsilon_3)/2
  •  \varepsilon_3:最大のフォローアップ期間

の3つが内在するノットに設定されています。

内在ノット数3で3次のMスプラインを定義すると、基底関数は5つ必要になる*2そうで、この基底関数の係数がghとして与えられているようです。

joint.CoxパッケージにあるM.spline( )関数を使って、5つの基底関数を求めることができます。

M.spline(time, t_min, t_max)

以下の3つの引数を指定して使います。

  • time:対象区間の時点をベクトル形式で指定(例:time = c(1,2,3,4,5)など)。t_min, t_maxの範囲に収まっている必要あり。
  • t_min:時点の下限。non-terminal event発生までの時間の最小値を指定。
  • t_max:時点の上限。terminal event発生までの時間の最大値を指定*3

例にならって下のように指定します。

t_min = min(dataOvarian$t.event)
t_max = max(dataOvarian$t.death)

ベースラインハザード関数を書くときは、M.spline( )に実際の時点を与えるのではなく、関数式として定義してcurve( )に渡します。

# 平均線
r1_func = function(t){as.numeric(M.spline(t, t_min, t_max)%*%(result$g))}

# 95%信頼区間の下端
r1_low_func = function(t){
  M_vec = M.spline(t, t_min, t_max)
  r1_V = M_vec%*%(result$g_var)%*%t(M_vec)
  as.numeric(M_vec%*%(result$g) - 1.96*sqrt(diag(r1_V)))
}

# 95%信頼区間の上端
r1_upp_func = function(t){
  M_vec = M.spline(t, t_min, t_max)
  r1_V = M_vec%*%(result$g_var)%*%t(M_vec)
  as.numeric(M_vec%*%(result$g) + 1.96*sqrt(diag(r1_V)))
}

平均の線はM.spline( )で作成した5つの基底関数と、モデルの当てはめで推定したスプラインの係数(g)との積を計算した式を定義しています。

信頼区間の場合は、スプラインの係数の共分散行列(g_var)を使って標準誤差を求めて、mean±1.96*SEとして計算した式を定義しています。

これを下のコードで重ね描きすると、

curve(r1_func, from=t_min, to=t_max)
curve(r1_low_func, from=t_min, to=t_max, lty="dotted", add=TRUE)
curve(r1_upp_func, from=t_min, to=t_max, lty="dotted", add=TRUE)

となります。軸ラベルや範囲は適宜指定してください。

jointCox.Weibull.reg( )でワイブル分布を用いる方法

ベースラインハザード関数にワイブル分布を使う場合は、jointCox.Weibull.reg( )を使います。 指定する主な引数はjointCox.reg( )と同じです。

result <- jointCox.Weibull.reg(
  t.event = dataOvarian$t.event,
  event = dataOvarian$event,
  t.death = dataOvarian$t.death,
  death = dataOvarian$death,
  Z1 = dataOvarian$CXCL12,
  Z2 = dataOvarian$CXCL12,
  group = dataOvarian$group,
  alpha = 0
)

出力結果

出力結果を見ていきましょう(一部省略しています)。

Mスプラインを使ったときと、概ね同じようなβが推定されました。フライリティ項の分散は少し大きくなりました。

$beta1
  estimate         SE      Lower      Upper 
0.22058618 0.03912474 0.14390170 0.29727067 

$beta2
  estimate         SE      Lower      Upper 
0.17069401 0.04460975 0.08325891 0.25812912 

$eta
  Estimate         SE      Lower      Upper 
0.09406251 0.06685348 0.02335754 0.37879662 

クレイトンコピュラのパラメータθと、そこから求められるケンドールのτも値が少し変わりました。

$theta
 Estimate        SE     Lower     Upper 
1.7017831 0.1951057 1.3592960 2.1305630 

$tau
  Estimate         SE      Lower      Upper 
0.45971983 0.02847593 0.40463716 0.51580451 

イベント1, イベント2のそれぞれのベースラインハザードについて、ワイブル分布の尺度パラメータ(scale)と形状パラメータ(shape1)が推定されています。

$scale1
    Estimate           SE        Lower        Upper 
0.0006696365 0.0001630500 0.0004155048 0.0010792008 

$shape1
  Estimate         SE      Lower      Upper 
1.10866686 0.03383253 1.04429928 1.17700187 

$scale2
    Estimate           SE        Lower        Upper 
4.544557e-05 1.563874e-05 2.315104e-05 8.920982e-05 

$shape2
  Estimate         SE      Lower      Upper 
1.31537842 0.04662033 1.22710415 1.41000287 

ベースラインハザード関数を描く

推定されたワイブル分布の尺度パラメータと形状パラメータを取り出します。

scale = result$scale1[1]
shape = result$shape1[1]

密度関数にワイブル分布を使ったときのハザード関数は、

 
\begin{aligned}
h(t) &= \left( \frac{\alpha}{\sigma} \right)  \left( \frac{t}{\sigma} \right)^{\alpha - 1}
\end{aligned}

なので、下のコードのようにう関数を記述し、curve( )で描いてみます(dweibull( )とpweibull( )を使っても書けると思いますが、かえってコードが長くなりそうなので)。

r1_func_weibull <- function(t){(shape/scale)*(t/scale)^(shape-1)}
curve(r1_func_weibull, from=t_min, to=t_max)

出来たグラフは下のとおりなんですが...

M-スプラインで描いたときとあまりに違うので、どこか間違えているのかも...。

おわりに

  • 「Rで実行する方法をまとめてみたいと思います!」と意気込んだ割に、コードの意味を表面的になぞるだけになってしまいました。
  • GJRMというパッケージを使うともう少し自由度が高い joint model も扱えるっぽいです。
  • NHK「おれ、ねこ」の投稿用写真を選んでます。療養食なので「いつものご飯」と「スペシャルご飯」が同じになってしまう...。

参考資料

  • M-スプラインの話ではないですが、スプライン補間とは何かについて5回シリーズで詳しく解説してくださっています。私のような疫学分野の末端ユーザーでは、局面まで拡大する機会はないでしょうし、難しくて理解しきれなかったところもありますが、とても面白く読ませていただきました。

qiita.com

qiita.com

qiita.com

qiita.com

qiita.com

*1:書籍にはフレイリティのαの記載があったので、おそらくそのことだろう

*2:M-スプラインの基底関数の定義式をみると、当該区域とその隣区域の低次基底関数を使って、次の次数の基底関数を定義しているので、3次まで定義しようとすると5つよりも多くなる気がするんですが、私は理解できてません

*3:3番目の内在ノットは「最大のフォローアップ期間」と記載されているが、例では死亡発生までの時間の最大値を使用しているので、おそらくこれでよいのではないかと思う

ワイブル分布(Weibull distribution)を使った生存時間解析

前回、パラメトリック生存時間解析を勉強してみて、ワイブル分布について学び直す必要性を痛感したのでまとめました。

necostat.hatenablog.jp

指数分布(exponential distribution)

まずは生存時間(あるいはイベントの発生間隔)のパラメトリックモデルの基本になる指数分布から。 確率密度関数(probability density function, PDF)は次の式で表されます。

 
\begin{aligned}
f(t) &= \lambda e^{- \lambda t} \\
\end{aligned}

ここで、 \lambda率パラメータ(rate parameter)と呼ばれ、大きいほどイベントの発生が速くなり、イベント発生までの平均時間  \frac{1}{\lambda} は短くなります。パラメータとして \lambda の逆数を使った書き方をする場合もありますが、Rで使う関数(rexp( )など)ではrateパラメータを指定するので、それに合わせました。

様々なλの指数分布

パラメータλの大きさによって、発生するまでの平均時間が変わりますが、PDFはいずれも単調減少です。

生存関数(時点tよりも長く生存する確率)は、PDFをtから∞まで積分して、

 
\begin{aligned}
S(t) &= \int_t^{\infty} f(s) ds \\
&= \left[ - e^{- \lambda s} \right]_t^{\infty} \\
&= e^{- \lambda t}
\end{aligned}

となります。また、ハザード関数は、

 
\begin{aligned}
h(t) &= \frac{f(t)}{S(t)} \\
&= \frac{\lambda e^{- \lambda t}}{e^{- \lambda t}} \\
&= \lambda
\end{aligned}

となるため、生存時間によらずハザードが一定であることがわかります。

ワイブル分布(Weibull distribution)

形状パラメータ

ワイブル分布の基本形のPDFは次のようになります。

 
\begin{aligned}
f(t) &= \alpha t^{\alpha - 1} e^{-t^{\alpha}}
\end{aligned}

α(>0)はワイブル分布の形状を決めるパラメータで、形状パラメータ(shape parameter)と呼ばれます。

このPDFから生存関数は、

 
\begin{aligned}
S(t) &= \int_t^{\infty} f(s) ds \\
&= \left[ - e^{- s^\alpha} \right]_t^{\infty} \\
&= e^{- t^\alpha}
\end{aligned}

となるので、ハザード関数は、

 
\begin{aligned}
h(t) &= \frac{f(t)}{S(t)} \\
&= \frac{ \alpha t^{\alpha - 1} e^{-t^{\alpha}}}{e^{- t^\alpha}} \\
&=  \alpha t^{\alpha - 1} 
\end{aligned}

となります。

ハザード関数を見ると、αの値によって分布が以下のような特徴を持つことが分かります。

  • 0<α<1のとき:
    時間が経過するにつれてハザード率が低下する。術後合併症など、早期に発生するイベントをモデル化するときに適している。
  • α=1のとき:
    指数分布と同じく、ハザード率が時間に依存しない。ランダムな要因によるイベントをモデル化するときに適している。
  • α>1のとき:
    時間が経過するにつれてハザード率が増加する。老化が原因のイベントなど、時間が経つほど発生しやすいイベントをモデル化するときに適している。

様々な形状パラメータのワイブル分布(σ=1)

尺度パラメータ

ワイブル分布の基本形で t を \frac{t}{\sigma} に置き換えると、分布の形状はそのままで、t軸方向にσ倍に拡大することができます。最初の係数   \frac{\alpha}{\sigma} は、曲線下面積が1になるようにするために付け加えられています。

 
\begin{aligned}
f(t) &= \left( \frac{\alpha}{\sigma} \right)  \left( \frac{t}{\sigma} \right)^{\alpha - 1} e^{-\left( \frac{t}{\sigma} \right)^\alpha}
\end{aligned}

このパラメータσ(>0)は尺度パラメータ(scale parameter)と呼ばれ、分布の平均や分散に影響します。例えば、発生時間の期待値は、

 
\begin{aligned}
E[T] &= \sigma \Gamma \left( 1 + \frac{1}{\alpha} \right)
\end{aligned}

となり、αとσの両方で決定されます(ここで  \Gamma はガンマ関数であり、階乗を実数に拡張したもの)。

尺度パラメータを追加したワイブル分布の生存関数は、

 
\begin{aligned}
S(t) &= e^{- \left( \frac{t}{\sigma} \right)^\alpha}
\end{aligned}

ハザード関数は、

 
\begin{aligned}
h(t) &= \frac{f(t)}{S(t)} \\
&= \frac{\left( \frac{\alpha}{\sigma} \right)  \left( \frac{t}{\sigma} \right)^{\alpha - 1} e^{-\left( \frac{t}{\sigma} \right)^\alpha}}{e^{- \left( \frac{t}{\sigma} \right)^\alpha}} \\
&= \left( \frac{\alpha}{\sigma} \right)  \left( \frac{t}{\sigma} \right)^{\alpha - 1}
\end{aligned}

となります。

rweibull( )などRで使う関数では、αを引数shapeで、σを引数scaleで指定します。

様々な尺度パラメータのワイブル分布(α=2)

おわりに

  • ワイブル分布の特殊形としての指数分布は、最初に紹介した指数分布の書き方とパラメータが逆数( \lambda = \frac{1}{\sigma})になっているので、少し混乱しやすいかもしれません。ポアソン分布との関係を考えると、指数分布のパラメータは逆数でない方がいいですね。
  • ワイブル分布は一般的に指数分布が持つような「無記憶性(memorylessness)」を持ちません。
  • 最強寒波のため猫トイレの掃除は1週間延期です。

参考資料

  • 今回を含め、よくこちらを参考にさせていただいてます。

manabitimes.jp

Joint Frailty-Copula Modelを使った生存時間解析 (1):理論編

前回、競合リスクデータの解析方針の1つとして、生存時間を対象としてモデル化することをあげたので、読んだ本の内容を整理してみました。

前回の記事:

necostat.hatenablog.jp

読んだ本:

パラメトリック生存関数

生存時間は連続変数ですが、その確率分布は右に長い裾を引いていることが多く、正規分布ではうまく当てはまりません。

使えそうな分布として指数分布がパッと思いつきますが、指数分布ではハザード関数が一定になってしまうという不自由さから、ワイブル分布(Weibull distribution)を用いる方が一般的です。

ワイブル分布は2つの正のパラメータα, σがあって、shapeパラメータαが「時間経過とイベント発生率の関係」を決めているので指数分布よりも表現の自由度が高いです。

  • 0<α<1のときは、観察開始初期ほどイベント発生が多く、時間経過とともに減っていく
  • α=1のときは、時間経過に寄らず一定(= 指数分布)
  • α>1のときは、初期は少ないが時間経過とともに増えていく

フレイルティ(frailty)でクラスター内の類似性をモデル化する

同一施設の患者は類似した傾向を持っている可能性があります。同一のクラスターに属する患者が共有する共通性を、フレイルティ(frailty)*1としてモデルに含めることができます。これは一般化線形モデルでクラスター効果を考慮するときに導入される変量効果(random-effect)と同じようなものです。

比例ハザードモデルでは、個々の患者のハザード関数  \lambda_{j} は下のように、 ベースラインハザード  \lambda_0(t) と患者の特性によって決まる係数  \exp(Z_j \beta) の積として表されます。 「患者の特性によって、その患者のハザードがベースライン(= 全ての共変量が0のときのハザード)の何倍になるか」を決めているということになります。

 
\begin{aligned}
\lambda_{j} (t | Z_j) &= \lambda_0(t) \exp(Z_j \beta) 
\end{aligned}

ここで、患者が属するクラスターによって決まるフレイルティ項  b_i b_i > 0)を導入します。

 
\begin{aligned}
\lambda_{ij} (t | Z_{ij}) &= b_i \lambda_0(t) \exp(Z_{ij} \beta) 
\end{aligned}

ハザードが高いクラスターでは  b_i > 1、低いクラスターでは 0 <   b_i < 1となります。同じクラスターに属する人は、同じフレイルティを共有するので、類似した傾向を持つことになるわけです。

また、混合効果モデルと同様、各クラスターの  b_i は直接推定されるのではなく、設定した確率分布に従う潜在変数として扱われます*2。混合効果モデルではクラスター効果は正規分布に従うと仮定されることが多いですが、 フレイルティ b_i ではガンマ分布が用いられることが多いとのこと。

コピュラ(copula)で2つの生存関数の相関をモデル化する

医学研究では1つの対象者について複数のイベントを測定することが珍しくありません。このようなデータを解析するとき、コピュラを用いて生存関数の依存関係(= 相関)をモデル化することで競合リスクを考慮することができます

コピュラ(copula),  C_{\theta}とは、2つの生存関数を結びつけ、1つの結合分布として扱うために用いられる関数です。添字にあるパラメータθは2つの変数間の依存度の強さを表すために用いられます。

よく用いられるコピュラはアルキメデス型コピュラと呼ばれるもので、[0, 1] の範囲を[0, ∞] に変換する生成関数(generator function)  \Phi_{\theta} とその逆関数  \Phi_{\theta}^{-1} を用いて、2つの生存関数

 
\begin{aligned}
u &= Pr(X>x|Z)\\
v &= Pr(Y>y|Z)
\end{aligned}

を次のように結合させます。

 
\begin{aligned}
Pr(X>x, Y>y |Z) &= C_{\theta} (u, v) \\
&=  \Phi_{\theta}^{-1} \left( \Phi_{\theta}(u) + \Phi_{\theta}(v)\right)
\end{aligned}

生存時間解析でよく使われるのがクレイトンコピュラ(Clayton copula)で、その生成関数および逆生成関数は、

 
\begin{aligned}
\Phi_{\theta} (t) &= \frac{1}{\theta} (t^{-\theta} - 1) \\
\Phi_{\theta}^{-1} (s) &= (1 + \theta s)^{-\frac{1}{\theta}}
\end{aligned}

となります(0<t<1, s>0, θ>0)。

ちなみに、それぞれの定義域・値域を確認すると以下のとおり:

  • t→0のとき \Phi_{\theta} (t) → ∞
  • t→1のとき  \Phi_{\theta} (t) → 0
  • s→0のとき \Phi_{\theta}^{-1} (s)→ 1
  • s→∞のとき  \Phi_{\theta}^{-1} (s)→ 0

前述の生成関数と逆生成関数から、クレイトンコピュラの式は下のようになることが分かります。

 
\begin{aligned}
C_{\theta} (u, v) &= \Phi_{\theta}^{-1} \left( \frac{1}{\theta} (u^{-\theta} - 1) + \frac{1}{\theta} (v^{-\theta} - 1)\right) \\
&= \Phi_{\theta}^{-1} \left( \frac{1}{\theta} (u^{-\theta} + v^{-\theta} - 2) \right) \\
&= \left( 1 + \theta  \frac{1}{\theta} (u^{-\theta} + v^{-\theta} - 2) \right)^{-\frac{1}{\theta}} \\
 &= \left(u^{-\theta} + v^{-\theta} - 1 \right)^{-\frac{1}{\theta}} \\
\end{aligned}

依存度の指標1:ケンドールの順位相関係数

生存関数間の依存度を表す1つ目の指標は、ケンドールの順位相関係数(Kendall's  \tauです。

 
\begin{aligned}
\tau &= Pr \{(X_1 - X_2)(Y_1 - Y_2)>0\} - Pr \{(X_1 - X_2)(Y_1 - Y_2)<0\}
\end{aligned}

τは2つの生存関数からランダムに抽出した生存時間(X1, X2とY1, Y2)の順位が一致する確率から、一致しない確率を引いたもので、 順位が完全に一致、つまり完全に正の相関があればτ=1となり、順位が完全に逆であればτ=-1となります。

τはθによって決まり、クレイトンコピュラでは

 
\begin{aligned}
\tau &= \frac{\theta}{\theta + 2}
\end{aligned}

の関係が成り立ちます(θ=0に近いほど依存度が低く、θが大きくなるほど依存度が高くなる)。

依存度の指標2:クロスレシオ関数

2つ目の指標はクロスレシオ関数*3(cross-ratio function, CRF)です。こちらもパラメータθの関数になるので、θを添えて  R_{\theta} と書くみたいです。

CRFは下の式のように、コピュラ関数とその2階微分、偏微分で定義されます。

 
\begin{aligned}
R_\theta (u, v) &= \frac{\frac{\partial^2 C_\theta(u,v)}{\partial u \partial v}}{\frac{\partial C_\theta(u,v)}{\partial u} \frac{\partial C_\theta(u,v)}{\partial v}}C_\theta(u,v)
\end{aligned}

2階微分は同時分布の密度、偏微分はそれぞれの生存確率の周辺分布の密度を表しています(多分)。2つの変数が独立なら、同時分布は周辺分布の積になるので、「周辺分布に比べて、同時分布が大きいなら相関がある」という解釈になるのかと…。コピュラ関数をかけているのは標準化のためでしょうか。

一般的にCRFは生存確率の関数なので、「ある時点での局所依存度」を表しますが、 クレイトンコピュラでは  R_{\theta} = 1 + \theta であり、生存時間に寄らずCRFが一定になっています。

解釈は以下のとおり:

  •  R_{\theta} > 1のとき、局所における正の相関
  • 0 <  R_{\theta} < 1のとき、局所における負の相関
  •  R_{\theta} = 1のとき、局所における独立

もう少し実践的には、「ある時点xでイベント1を発生した人は、その時点まで発生しなかった人に比べて、イベント2ののハザードが何倍か」と解釈できます。条件付きのハザード比・オッズ比として解釈することの詳細は教科書などを読んでみてください。

Joint Frailty-Copula Model

フライリティとコピュラを組み込んだ結合分布モデルを数式で書いておきます。

まず、2つのイベントのハザードは、

 
\begin{aligned}
\lambda_{xij} (t | b_i) &= b_i \lambda_{x0}(t) \exp(Z_1 \beta_1) \\ 
\lambda_{yij} (t | b_i) &= b_i^{\alpha}  \lambda_{y0}(t) \exp(Z_1 \beta_1) \\ 
\end{aligned}

と定義されます。ここで、 \lambda_0 はぞれぞれのイベントのベースラインハザード関数です。 イベント間のフライリティの違いは、パラメータαで考慮されます*4

それぞれのハザード関数から生存関数が  S_{xij} (x|b_i, Z_{ij}),  S_{yij} (y|b_i, Z_{ij}) が1対1対応で定義され、 この2つをコピュラ関数で結びつけます。

 
\begin{aligned}
Pr(X_{ij} > x, Y_{ij} > y | b_i, Z_{ij}) = C_{\theta} \left( S_{xij} (x|u_i, Z_{ij}), S_{yij}(y|u_i, Z_{ij})  \right)
\end{aligned}

ベースラインハザード関数はM-スプライン(定義域で非負が保証されるスプライン)やワイブル分布を使ってモデル化して推定するみたいですが、どんなふうな出力になるかは実践編で試してみたいと思います。

おわりに

  • 実際にどのように使えるのかに興味がありますが、実践編の前にワイブル分布の復習が必要かもしれません。
  • 猫にご飯をあげるとき、「待て」を教えてます。全く待てないです。

参考資料

  • 武冨 奈菜美. 生存時間解析・信頼性解析のための統計モデル.日本統計学会誌 第 52 巻, 第 2 号, 2023 年 3 月 69 頁 ∼ 112 頁

*1:係数?

*2:つまり、効果を推定する対象ではない。

*3:交差比関数?適切な日本語訳がわかりません

*4:全く別々に設定して推定することもできるのかもしれませんが、そうするとイベント間の依存性が推定できなく( or しにくく)なるかもしれません。

競合リスクデータに対する解析

以前、競合リスクが存在する生存時間を解析する方法について勉強したのですが、すっかり忘れている(というかあまり深く理解できていなかった)ので、頭の引き出しを整理したいと思います。

ちなみに以前書いた記事はこちら:

necostat.hatenablog.jp

necostat.hatenablog.jp

まずは登場する関数と表記方法について。

  • 生存関数(survival function), S(t):ある時点tまでにイベントを発生しなかった人の割合。
  • 累積発生関数(cumulative incidence function, CIF), F(t):時点tまでにイベントを発生した人の割合。
  • ハザード関数(hazard function), h(t):リスクセットに含まれる対象が、次の瞬間にイベントを発生する条件付き確率密度(瞬間発生率)。
  • 累積ハザード関数(cumulative hazard function), H(t):ある時点tまで累積したハザード。ハザード関数を区間0〜tまで積分したもの。

特定のある時点を示したいときは、 S(i) のようにtに代わってiを使います。また、複数あるイベントのうち特定のイベントを示したいときは、 h_j(t) のように添字を用いることにします。

率と割合の1対1対応関係

競合リスクがない場合、ハザード関数の定義

 
\begin{aligned}
\frac{dF(t)}{dt} &= S(t)h(t) \\
h(t) &=  \frac{dF(t)/df}{S(t)}\\
\end{aligned}

から、以下の関係式が成り立ちます。

 
\begin{aligned}
F(t) &=  \int_0^t S(s) h(s)ds\\
\end{aligned}

これを(不正確を許容して)日本語で噛み砕くと、

  • h(s)ds:微小時間 ds において対象集団がイベントを発生する確率
  • S(s):時点sまでイベントを発生せず、リスクセットに含まれる確率

なので、その積は微小時間dsの中でイベントが発生する確率になります。 この確率を、観察開始(s=0)から時点tまで連続的に足し合わせていけば累積発生割合になる、というわけです。

ここから以下の関係式が導けますし、

 
\begin{aligned}
S(t) &= e^{-H(t)}\\
\end{aligned}

S(t) = 1 -F(t)を使って書き直すと、

 
\begin{aligned}
F(t) &= 1 - e^{-H(t)}\\
\end{aligned}

となります。累積ハザード関数はハザード関数を積分して得られますので、 発生率(rate)を表すハザード関数と、発生割合(risk)を表す累積発生関数が1対1で対応していることが分かります。

Coxハザードモデルでは、共変量の係数からハザード比(hazard ratio)が推定されますが、 ハザード(= 率)と発生割合は1対1対応しているので、ハザードに対する共変量の効果と、発生割合に対する共変量の効果の方向は一致します*1。 そのため著者・読者は「率なのか割合なのか」を厳密に区別せず、「危険度が増加・減少する」などのようなやや曖昧な表現を使うことが許容されてい(ると思い)ます。

生存時間解析にまつわる関数同士の関係性はこちらで復習してください。

necostat.hatenablog.jp

競合リスクがあると困ること

2つ以上のイベントを観察していて、あるタイプのイベントが観察されると他のタイプのイベントが観察できなくなるとき、イベントのタイプ同士が発生リスクを競合することになります。例えば、疾患の再発というイベントが、死亡という別のイベントによって観察を妨げられてしまう、といった状況です。

このとき、興味あるイベントの観察を妨げるイベントのことを、競合リスク(competing risk)あるいは競合イベント(competing event)と呼びます。

生存時間解析では「観察打ち切り(censor)」が不可避です。 疾患と関係ない理由での脱落や、事前に設定された観察期間終了が理由であれば、打ち切りとイベント観察は独立で無情報であると考えられます(noninformative censoring)。 しかし、競合リスクが理由の観察打ち切りであれば無情報とは言えず、「無情報打ち切りの仮定」は成り立ちません

率と割合の1対1対応関係が崩れる

いま、競合するイベントが2種類あるとしましょう。イベント1の原因別ハザード関数 h_1(t) とするとき、イベント1の累積発生割合は、

 
\begin{aligned}
F_1(t) &=  \int_0^t S(s) h_1(s)ds\\
\end{aligned}

となります。

このときのリスクセット(= 興味あるイベントが発生しうるとして観察する集団)を「どちらのイベントも発生していない人」とすると、 生存関数は、

 
\begin{aligned}
S(t) &= e^{-H_1(t)-H_2(t)}\\
\end{aligned}

となります。

ハザード関数はイベント1のことだけに注目しているのに、発生割合(あるいは生存割合)に変換しようとするとイベント2の発生状況も関係してくることがわかります。つまり、ハザード関数(= rate)と累積発生関数(=risk)が1対1対応ではありません。ハザードを対象としたモデルで関連がみられた共変量が、発生割合に対しても同じ方向の効果を持つとは限らない、ということになります。

CIFと1-KMの違い

競合イベントがない場合(例:全死亡)では、カプランマイヤー法で推定した生存時間を1から引いたもの(1-KM)と累積発生関数(CIF)は一致します。

競合イベントがある場合に、興味あるイベント以外のイベントを打ち切り扱いにしていると、1-KMは実際のCIFよりも大きくなります。既に他のイベントを発生してリスクセットから除くべき人を打ち切り扱いにするため、1-KMを用いてCIFを推定すると過大評価することになります。 このため、複数のイベントの累積発生割合を1-KMで推定すると、

 
\begin{aligned}
\Sigma CIF_{KM} \geq 1 - KM_{all}
\end{aligned}

となり、その合計が1を超えてしまう状況が生じる可能性があります。

そこで、前述の原因別ハザード関数を使ってCIFを推定すれば、

 
\begin{aligned}
\Sigma CIF_{CS} = 1 - KM_{all}
\end{aligned}

となり、整合性が取れます。

競合リスクデータに対する解析方針

競合リスクを含んだデータを解析するとき、モデル化する対象は主に3つ考えられます。

  1. イベント発生のハザード(rate)
  2. イベント発生のリスク(risk)
  3. イベント発生までの時間(time-to-event)

1. ハザードを対象としたモデル

先程説明した「原因別ハザード関数(cause-specific hazard function)」を使ったモデルで解析します。モデルから推定された共変量の係数を指数変換したもの  e^{\beta} を、ハザード比(hazard ratio, HR)として解釈する点はCox比例ハザードモデルと同じです。

このモデルでのリスクセットは「まだどのイベントも発生していない人」なので、共変量の係数を「ある時点までどのイベントも発生していなかった人において、瞬間発生率がどれくらい増加・減少するか」と解釈できます。 (あとで説明する部分分布ハザードモデルと比べると)この状況を想定することは難しくなく、共変量とイベント1の因果関係として疫学的に解釈することがが容易にできます*2

しかし、前述のとおりハザードと累積発生割合は1対1対応していません。ハザードを増加(あるいは減少)させる共変量が、発生割合に対しても同様の効果を持つかどうかはわかりません。このため「率(rate)を対象としたモデル(rate-regression model)」と表現してみました*3

2. リスクを対象としたモデル

部分分布ハザード関数(sub-distribution hazard function)」を使ったモデルで、提唱者の名前をとってFine-Gray regression modelとも呼ばれます。 このモデルでは、次の式で表される部分分布ハザードを対象にモデル化します。

 
\begin{aligned}
h_1(t) &=  \frac{dF_1(t)/df}{1-F_1(t)}\\
\end{aligned}

先程の原因別ハザードでは、分母が  S(t) = e^{-H_1(t)-H_2(t)} でしたが、このモデルでは  1-F(t) = 1 - e^{-H_1(t)} を用います。つまり、リスクセットを 「他のイベントの発生の有無は問わず、興味あるイベントを発生していない人」と考えます。 原因別ハザードでは、興味あるイベント以外のイベントを発生した人をリスクセットから除いたのに対し、部分分布ハザードでは興味の対象のイベントを発生していない限り、これらの人もリスクセットに含める点が異なっています。

前述の部分分布ハザードの定義の両辺を積分して計算すると、

 
\begin{aligned}
 \int \frac{dF_1(t)}{1-F_1(t)} &=  \int h_1(s) ds \\
\ln(1 - F_1(t)) &= - \int h_1(s) ds \\
F_1(t) &= 1 - exp \left(-\int h_1(s) ds \right)
\end{aligned}

となり、イベント1についてハザードと累積発生割合が1対1で対応することになります。 このモデルで推定された係数βは、累積発生割合との関連をモデル化していると考えられるため、リスク回帰モデル(risk-regression model)と呼ばれることがあります*4「ハザード比」として記載されていても、実際はリスクとの関連を評価しているので、部分分布ハザード比(sub-distribution hazard ratio, SDHR)と区別する方がいいでしょう。

このモデルでは、実際には興味のイベントを発生しえない人(例:死亡が競合リスクの場合)もリスクセットに含むため、具体的な状況を想定することが難しく、因果推論などの疫学的な解釈には向いていません。 代わりに、累積発生割合との関連を直接モデル化するので、発生割合をターゲットにした予測をしたいときに有用です。

3. イベント発生までの時間を対象としたモデル

「対象者が、それぞれのイベント発生までの時間を潜在的に持っているが、実際観察されるのは一部のみ」という考え方でイベント発生までの時間をモデル化する方法です。 イベント間での相関関係をコピュラ(copula)という関数で表現します。

Joint frailty-copula modelについては、また別の機会に...(多分)

おわりに

  • 半年ぶりくらいに書きました。競合リスクについて、あまり分かってなかったことが分かりました。
  • 猫の恫喝にも負けず、ちゅ〜るは1週間に2回までにしています。

参考資料

pubmed.ncbi.nlm.nih.gov

  • 共変量の係数βの大小比較して、どちらの共変量が発生に対して影響が大きいかは言えるが、大きさの違いなど定量的なことは言えない
  • 異なったイベントに対して推定されたモデルで係数βを比較して、その共変量がどちらのイベントに対して影響が大きいかということは言えない
  • 割合が小さい(<0.2)ときは、complementary log-log linkとlogit linkの差が小さいので、累積発生割合が低いなら  e^{\beta} をオッズ比と解釈できる

などがナルホドでした。

*1:定量的にも同じ効果と言えるわけではない(例:ハザード比=2だからといって、リスク比=2と言えるわけではない)

*2:因果関係と解釈する他の要件を満たしているかどうかは、とりあえず置いておいて...

*3:文献に記載があったわけではなく、risk-regression modelに対応させた表現として記載しただけです。

*4:CIFのcomplementary log-log modelの回帰係数と解釈できる

カッパ係数でカテゴリー変数の一致度をみる [R]

カッパ係数(Cohen's kappa statistic)とは

測定値の真の値が一定であると考えられる対象者に対して、異なる時点、あるいは異なる測定者(評価者)によって測定が行われた場合に、どれほど近い測定値が得られるか」を測定の信頼性(reliability)と呼びます。 測定データの変動は、真の個人差による変動と測定誤差による変動から成りますが、前者が占める割合(=測定誤差を除いた部分の割合)を信頼性の指標としています。

データがカテゴリー変数の場合、信頼性の指標としてカッパ係数(Cohen's kappa statistic)が用いられます。

定義

例として、2名の評価者が50人の対象者についてAかBかを独立に判定して、下の表のような結果になったとします。

両者の評価が一致している割合、つまり、(28+11)/50=0.78を指標としたいところですが、偶然一致する割合を差し引く必要があります。

例えば、評価者1が50人からランダムに選んだ35人に判定Aをつけ、評価者2も同様に50人中32人に判定Aをつけると、約22人(←(35/50)×(32/50)×50=22.4)は両評価者から判定Aをもらうことになります。判定Bについても同様で、適当に評価をつけても約5人(←(15/50)×(18/50)×50=5.4)は両者から判定Bをもらいます。 実際に評価して39人(=28+11)で一致したという結果は、前述の偶然の一致による分(=27.8人)を差し引いて評価しなければいけません。

観察された一致割合を  p_o、偶然の一致割合*1 p_c とすると、カッパ係数  \kappa は、

 
\begin{aligned}
\kappa &= \frac{p_o - p_c}{1 - p_c}
\end{aligned}

と定義されます。

ここで偶然の一致割合  p_c は、周辺確率の積から計算されます。例えば「判定Aで偶然一致する確率」は、評価者1がAと判定する確率と評価者2がAと判定する確率をかけたもの(←(35/50)×(32/50)=0.448)で、「判定Bで偶然一致する確率」についても同様です((15/50)×(18/50)=0.108)。この2つを足したもの(0.448+0.108=0.556)が  p_c になります。

先程の例ではカッパ係数は、

 
\begin{aligned}
\kappa &= \frac{0.78 - 0.556}{1 - 0.556} \\
&= \frac{0.224}{0.444} \\
&= 0.504
\end{aligned}

となります。

後で説明する重み付けカッパ係数の理解のため、次のように変形しておきます(*)。

 
\begin{aligned}
\kappa &= \frac{p_o - p_c}{1 - p_c} \\
&= \frac{1-p_c -(1 - p_o)}{1 - p_c} \\
&= 1 - \frac{1 - p_o}{1 - p_c} \\
\end{aligned}

解釈

カッパ係数は-1〜1の範囲の値を取りますが、<0の値は「偶然一致するよりも一致が悪い*2」ということなので、実際に意味がある範囲は0〜1です。数式からは分かりにくいですが、下の図を見れば、カッパ係数は「観察された偶然に依らない一致が、最大値に対してどれくらいなのか」を示していると考えられます。

「カッパ係数がどれくらいだったら一致度が高いと言えるのか」は多少恣意的ですが、以下のような目安が使われることが多いようです。

Landis and Kochの基準:

  • 〜 ≦0:poor(不一致)
  • 0 〜 ≦0.2:slight(わずかに一致)
  • 0.2 〜 ≦0.4:fair(少しは一致)
  • 0.4 〜 ≦0.6:moderate(まあまあ一致)
  • 0.6 〜 ≦0.8:substantial(かなり一致)
  • 0.8 〜 ≦1:almost perfect(ほぼ完全に一致)

重み付けカッパ係数(weighted kappa statistic)

評価のカテゴリーが3つ以上で、カテゴリーに順序がある場合を考えます。
例として、2名の評価者が50人の対象者について、「良い・普通・悪い」の3段階で評価して次のような結果になったとしましょう。

このとき、「良い-悪い」という不一致に比べて「良い-普通」という不一致はまだマシですよね。後者を「軽い不一致」、前者を「重い不一致」として区別したくなりますが、前述のカッパ係数では両者を同じ程度の不一致として扱うことになってしまいます。

そこで、不一致の距離に応じて重みづけをした重み付けカッパ係数(weighted kappa statistic)の出番です。

定義

集計表の各セルに与えられる重みを  w_{ij} として、以下のように定義されます。

 
\begin{aligned}
\kappa_w &= 1 - \frac{1 - \sum w_{ij} p_{o,ij}}{1 - \sum w_{ij} p_{c,ij}}
\end{aligned}

前述の(*)と比べてみると、対応関係がわかりやすいですね。

代表的な重みの付け方は、下に示すカテゴリーの距離の2乗に応じた重み付け(Fleiss-Cohen法)です。

 
\begin{aligned}
w_{ij} &= 1 - \left( \frac{i - j}{k-1} \right)^2
\end{aligned}

ここでkはカテゴリー数で、「良い・普通・悪い」の3段階ならk=3になります。(i-j)は2人の評定の差なので、一致していれば0, 最も離れている場合でk-1になります。つまり、評定が一致しているところはw=1(完全一致)、評定が最も離れているところはw=0(完全不一致)となるような重みです。

2次ではなく1次の重み付けの方法もあります。一致しているところはw=1、最も離れているところはw=0となる点は同じです。

 
\begin{aligned}
w_{ij} &= 1 -  \frac{| i - j |}{k-1} 
\end{aligned}

Rで計算してみる

サンプルデータを作成する

前述の2つの例をサンプルデータにします。

1つ目:カテゴリーが2つ(判定A, B)

rater1 <- c(rep("A",35),rep("B",15))
rater2 <- c(rep("A",28),rep("B",7),rep("A",4),rep("B",11))
data1 <- data.frame(rater1,rater2)
table_data1 <- table(data1)

2つ目:カテゴリーが3つで順序あり(1=良い, 2=普通, 3=悪い)

rater1 <- c(rep(1,10),rep(2,28),rep(3,12))
rater2 <- c(rep(1,8),rep(2,1),rep(3,1),
            rep(1,7),rep(2,16),rep(3,5),
            rep(1,0),rep(2,3),rep(3,9))
data2 <- data.frame(rater1,rater2)
table_data2 <- table(data2)

それぞれのデータについて、以下の2種類の形式を用意しておきます。

  • それぞれの患者に対する評価を1行に収めたデータ:data1,data2
> head(data1)
  rater1 rater2
1      A      A
2      A      A
3      A      A
4      A      A
5      A      A
6      A      A
  • 上記を表に集計したデータ:table_data1, table_data2
> table_data1
      rater2
rater1  A  B
     A 28  7
     B  4 11

方法1:irrパッケージを用いる

irrパッケージのkappas( )関数を使ってみます。 渡すデータは1患者1行タイプを使います(集計表タイプはダメ)。

通常のカッパ係数は、weight = "unweighted"とします(デフォルトなので指定不要)。

library(irr)
kappa2(data1, weight = "unweighted")
 Cohen's Kappa for 2 Raters (Weights: unweighted)

 Subjects = 50 
   Raters = 2 
    Kappa = 0.505 

        z = 3.6 
  p-value = 0.000318 

2次重み付けカッパ係数は、weight = "squared"と指定します(ちなみに1次重み付けはweight = "equal"です。)。

kappa2(data2, weight = "squared")
 Cohen's Kappa for 2 Raters (Weights: squared)

 Subjects = 50 
   Raters = 2 
    Kappa = 0.615 

        z = 4.41 
  p-value = 1.02e-05 

p-valueは帰無仮説: \kappa = 0 に対してのP値ですが、そもそもカッパ係数=0は臨床的に意味がないので、このP値は気にしなくていいと思います。

方法2:vcdパッケージを用いる

次にvcdパッケージのKappa( )関数を使ってみます。 こちらは集計表タイプのみ受け付けてくれるようです。

library(vcd)
Kappa(table_data1)
            value    ASE     z Pr(>|z|)
Unweighted 0.5045 0.1285 3.925 8.68e-05
Weighted   0.5045 0.1285 3.925 8.68e-05

何も指定しなくても重み付けカッパ係数も表示されるみたいですが、ここはカテゴリー数が2なので通常のカッパ係数と同じになります。

重み付けの方法を指定したい場合は、引数weightsで指定します。

  • "Equal-Spacing":1次
  • "Fleiss-Cohen":2次

結果を保存しておくと使用したweightを表示したり、

K_result <- Kappa(table_data2, weights = "Fleiss-Cohen")
> summary(K_result)
            value    ASE     z  Pr(>|z|)
Unweighted 0.4720 0.1029 4.590 4.443e-06
Weighted   0.6154 0.0993 6.198 5.736e-10

Weights:
     [,1] [,2] [,3]
[1,] 1.00 0.75 0.00
[2,] 0.75 1.00 0.75
[3,] 0.00 0.75 1.00

信頼区間を表示したりできます。

> confint(K_result, level=0.95)
            
Kappa              lwr       upr
  Unweighted 0.2704601 0.6736393
  Weighted   0.4207690 0.8100002

信頼区間が計算できるので、 \kappa=0 以外の帰無仮説に対して有意差があるかどうかも判断できます。

方法3:psychパッケージを用いる

最後にpsychパッケージのcohen.kappa( )関数を使ってみます。 データ形式は1対象者1行タイプでも集計表タイプでもOKです。

library(psych)
> cohen.kappa(data1)
Call: cohen.kappa1(x = x, w = w, n.obs = n.obs, alpha = alpha, levels = levels)

Cohen Kappa and Weighted Kappa correlation coefficients and confidence boundaries 
                 lower estimate upper
unweighted kappa  0.25      0.5  0.76
weighted kappa    0.25      0.5  0.76

 Number of subjects = 50 

重みはweightsに行列を与えて指定することができるみたいですが、デフォルトでは2次重み付けが使われているようです。

> cohen.kappa(data2)
Call: cohen.kappa1(x = x, w = w, n.obs = n.obs, alpha = alpha, levels = levels)

Cohen Kappa and Weighted Kappa correlation coefficients and confidence boundaries 
                 lower estimate upper
unweighted kappa  0.27     0.47  0.67
weighted kappa    0.38     0.62  0.85

 Number of subjects = 50 

信頼区間水準はalphaで指定します。デフォルトは0.05で、95%信頼区間が表示されます。

おわりに

  • Rで実行するならvcdパッケージが便利そうです。
  • カッパ係数は有病割合(例:両評価者ともAの割合と、両方ともBの割合の差)の影響を受けます。
  • カッパ係数は不一致の非対称(例:評価者1がAで評価者2がBの割合と、評価者1がBで評価者2がAの割合の差)
  • 毎日窓の外を眺めている猫を見てると、抱っこ紐で連れ出したくなります。

参考資料

*1:偶然でも一致が期待される割合

*2:例えば、一方の評価者がひねくれているとか、評価の仕方が逆になっているなど

*3:重み付けカッパ係数について記載したAppendixで分子のΣが抜けている誤植があるように思います。

空間データを可視化する(2)(sfパッケージ) [R]

はじめに

Rのsfパッケージを使って、神戸市の校区境界と学校所在地をプロットしようとしています。前回はデータのダウンロードから加工(部分抽出・変数追加)までやってみました。

necostat.hatenablog.jp

今回はプロットしてみます。

GISデータをプロットしてみる

例1:plot( )を使う

まずは中学校区を標準のplot( )を使ってみます。

plot(MS_poly_kobe)

各属性データにもとづいて塗り分けられた図が作成されました。

3番目の図だけ欲しかったら、

plot(MS_poly_kobe[3])

とします(出力は割愛)。

例2:geom_sf( )を使う

ggplot2での作図に慣れている人は、geom_sf( )を使うと便利です。

ggplot() +
  geom_sf(data = ES_poly_kobe) +
  theme_minimal()

例3:離散変数に従って塗り分ける

3つのカテゴリーをランダムに割り当てた離散変数に従って、小学校区を塗り分けてみます。 面を塗る色を指定する引数はfill、境界線の色を指定する引数はcolourです。

ggplot() +
  geom_sf(data = ES_poly_kobe, aes(fill = rnd_cat), colour = "white") +
  theme_minimal() 

例4:連続変数に従って塗り分ける

次はランダムに作成した連続変数に従って塗り分けてみます。

ggplot() +
  geom_sf(data = ES_poly_kobe, aes(fill = rnd_cont)) +
  scale_fill_gradient(low = "#FFFFFF", high = "#336699") +
  theme_minimal()

例5:複数の図を重ねる

geom_sf( )を使えば、複数の図を重ねることも簡単です。 中学校区境界の白地図に、中学校所在地を重ねてみましょう。 所在地のプロットの色は連続カラースケールにしてみます。

ggplot() +
  geom_sf(data = MS_poly_kobe, fill = "white") +
  geom_sf(data = MS_point_kobe, aes(colour = rnd_cont)) +
  scale_colour_gradient(low = "#FF6B6B", high = "#336699") +
  theme_minimal()

レイヤーの順序を入れ替えるときは注意が必要です。面データを後から重ねるときは、下のように透過度100%(alpha=0)にしておかないと、先にプロットした所在地が見えなくなってしまいます。

ggplot() +
  geom_sf(data = MS_point_kobe, aes(colour = rnd_cont)) +
  geom_sf(data = MS_poly_kobe, alpha = 0) +
  scale_colour_gradient(low = "#FF6B6B", high = "#336699") +
  theme_minimal()

出力は省略します。

おわりに

  • 元々の属性にある「学校名」などに従って塗り分けるときは、凡例を表示しないようにしておく方がいいです。
ggplot() +
  geom_sf(data = ES_poly_kobe, aes(fill = A27_004), alpha = 0.5) +
  theme_minimal() + 
  theme(legend.position = "none")
  • 久しぶりの投稿ですが、猫2匹は元気にしています。

参考資料

  • GISデータに関する基礎的な解説がわかりやすいです。

www.esrij.com