ふたつの分布が違うのをどう示したらいいですか

こんな記事を書いたらごく狭い範囲で反響があった。
mikuhatsune.hatenadiary.com

ガンマ分布の記事感動しました。そんなガンマ分布マスターであられる驚異のアニヲタさんに質問するもの失礼なほど低級な質問なんですが、、、
ある疾患があって、その疾患発症間隔(病院で観察された日)の分布が院外と院内で違う、ということを言いたいようです。ブログの例の混合分布ではなく、すでに違う二つの群のデータが取れているとして、その群に差がある、と言いたいと。
この場合は指数分布というかガンマ分布で回帰して係数がちがうと言えばいいでのでしょうか?

と聞かれたので考えた。

群が分かれている、ということなので、分布を近似してその近似した分布を決定しているパラメータを比較するか、分布を近似せずにそのまま扱うリサンプリングな方法で考えればいいのではないだろうか。
データはシミュレーション用に再度、ガンマ分布から適当に生成されたとする。

f:id:MikuHatsune:20200219225658p:plain
データを生成するガンマ分布
シミュレーションデータはこんな感じになる。30症例ずつくらいにしている。
f:id:MikuHatsune:20200219225800p:plain
シミュレーションデータ

リサンプリングでやってみる。
いま手元にあるデータから経験分布を作って、そのデータと同じ数だけ重複を許して、すなわちreplace=TRUE でいくつも偽データセットを作る。
その上で平均(分布が非対称ならmedian なり適当な要約量を使う)をとり、平均の差分が0なら一緒(みたいなもの)とみなし、差があれば違う、と考える。0をまたぐ偽セット数をp値っぽいものとみなす。
今回のシミュレーション結果としては

    2.5%      50%    97.5% 
4.842738 6.521429 8.466786 

となり、すべて平均の差が0以上なので、ふたつの分布は違うのだろう、と判断する。

stanでやってみる。
分布がなんらかのパラメータを持つ有名な分布で近似できるならば、そのパラメータを推定しにかかる。パラメータから平均など要約量が取れるので、それを比較する。
f:id:MikuHatsune:20200219231720p:plain
パラメータ推定値から計算した平均値の差は、リサンプリングでしたときとだいたい同じになる。これもふたつの分布は違うようだ、と判断する。

    2.5%      50%    97.5% 
4.704524 6.579990 8.594733 

f:id:MikuHatsune:20200219231704p:plain

検定的に考えるならこんな感じだが、分布の形を平均値というひとつの統計量にして、しかもその平均値が分布をよく表すとも言えないのにこれでなんとかするのはどうなんだろうかと思う。
少し前に別の人に聞かれて、分布の形があるならカルバックライブラーとかJensen-Shannon とかどう?って返答したことがあるが、これを使った検定はあるのだろうか(適当

shape <- c(4, 6)
scale <- c(1.4, 2)
x <- 0:30
lab <- c("院内", "院外")
n <- c(30, 35)

d <- t(mapply(dgamma, x, list(shape), scale=list(scale)))
cols <- c("blue", "orange")
matplot(x, d, type="l", lty=1, col=cols, lwd=3, xlab="発症までの日数", ylab="確率密度")
legend("topright", legend=lab, lty=1, col=cols, lwd=3)

dat <- lapply(mapply(rgamma, n, shape, scale=scale, SIMPLIFY=FALSE), floor)
day_max <- max(unlist(dat))
b <- sapply(lapply(dat, factor, 0:day_max), table)

barplot(t(b), beside=TRUE, col=cols, xlab="発症までの日数", ylab="発症人数")
legend("topright", legend=lab, pch=15, col=cols, cex=2)

alpha <- 0.05
# リサンプリング
niter <- 1000
diff_mean <- replicate(niter, diff(sapply(mapply(sample, dat, replace=TRUE), mean)))
hist(diff_mean, xlab="発症までの平均日数差分 [院外 - 院内]", main="リサンプリング", nclass=30)
quantile(diff_mean, c(alpha/2, 0.5, 1-alpha/2))

# stan
library(rstan)
library(vioplot)
rstan_options(auto_write=TRUE)
options(mc.cores=parallel::detectCores())

code <- "
  data {
    int<lower=0> N1;
    real<lower=0> y1[N1];
    int<lower=0> N2;
    real<lower=0> y2[N2];
  }

  parameters {
  real<lower=0, upper=20> shape[2];
  real<lower=0, upper=10> scale[2];
  }

  model {
    y1 ~ gamma(shape[1], 1/scale[1]);
    y2 ~ gamma(shape[2], 1/scale[2]);
  }
"

m0 <- stan_model(model_code=code)
standata <- list(N1=n[1], y1=dat[[1]], N2=n[2], y2=dat[[2]])
fit <- sampling(m0, standata, iter=1000, warmup=300, chain=4)
ex <- extract(fit, pars=c("shape", "scale"))
mes <- ex$shape * ex$scale

vdat <- cbind.data.frame(estimate=c(mes), label=rep(lab, each=nrow(mes)))
vioplot(estimate ~ label, data=vdat, col=rev(cols), horizontal=TRUE, at=c(1, 2), side="right",
        xlab="発症までの平均日数", ylab="")
abline(h=c(1, 2), lty=3)
abline(v=shape*scale, lty=3)

apply(mes, 2, quantile, c(alpha/2, 0.5, 1-alpha/2))
diff_mes <- apply(mes, 1, diff)
quantile(diff_mes, c(alpha/2, 0.5, 1-alpha/2))
hist(diff_mes, xlab="発症までの平均日数差分 [院外 - 院内]", main="リサンプリング", nclass=30)


diff_true <- diff(shape*scale)
par(mfrow=c(1, 2))
hist(diff_mean, xlab="発症までの平均日数差分 [院外 - 院内]", main="リサンプリング", nclass=30)
abline(v=diff_true, lwd=3, col="red")
hist(diff_mes, xlab="発症までの平均日数差分 [院外 - 院内]", main="Rstan", nclass=30)
abline(v=diff_true, lwd=3, col="red")

混合分布:急性期と亜急性期の発症がある

こんな感じのデータを見かけた。
f:id:MikuHatsune:20200216160928p:plain
本人が言うには、8日あたりを境目にして、8日までに発症するパターンと、それ以降に発症するパターンに分かれそうだ、という。
データの意味合いと二峰性の具合から、おそらくふたつのガンマ分布が重なっているのでは、という気分になった。というのも、横軸が0以上の整数値となる分布はガンマ分布で、混合分布として解けばたぶんふたつのパターンがそもそもどれくらいの存在比で、かつ、ガンマ分布で近似されれば各々のパターンの発症平均日が推定できるので、たぶんうれしい。
実際のデータはこの2つの分布から生成された。
f:id:MikuHatsune:20200216161003p:plain

というわけで混合分布の推定で調べると、よく出てくるのはEMアルゴリズムベイズである。

グダグダやっていたのだが、Rのパッケージで簡単にガンマ分布(というか混合分布全般)をEMアルゴリズムで推定してくれるやつがあった。
kusanagi.hatenablog.jp
mixtools というパッケージ内でgammamixEM という関数がEMアルゴリズムをしているが、ベクトル化されていない部分があるので適当にパクって書き換える。

初期値の設定としては、混合比\lambda はディリクレ分布から適当に拾う。一様分布でも0.5 ずつでもよい。
ガンマ分布にはshape と呼ばれる\alpha と、scale もしくはrate (shape の逆数)と呼ばれる\beta のパラメータを設定する必要がある。mixtools では初期値の設定として、観測データx があるときにx をsort したあとに均等に分割して、分割されたセットx_i に対して(以下では表記の簡便化のためxx_i のつもりである)
E(x)=\bar{x}
E(x^2)=\bar{x^2}
\alpha \gets \frac{\bar{x}^2}{\bar{x^2}-\bar{x}^2}
\frac{1}{\beta} \gets \frac{\bar{x}^2}{\bar{x^2}-\bar{x}^2}\alpha の分母と表記を合わせるために逆数にしてrate 表記にしている)
と設定している。
パラメータの収束は以下のようになる。ふたつの分布があるという前提なので、初期値次第では入れ替わる。この場合は色が入れ替わっているのでそうなる。
f:id:MikuHatsune:20200216162211p:plain

パラメータ的にはずれているように見えるが、分布としてしまうと意外とずれていないようである。
f:id:MikuHatsune:20200216162502p:plain

このときのパラメータの推定値は

$shape
[1] 3.859840 9.942502

$rate
[1] 0.7749822 0.5455689

$pi
[1] 0.3431185 0.6568815

それぞれの分布の平均値は

[1]  4.980553 18.224100

同様のことをstanを使ってやっている。
stan のマニュアルにゼロ過剰モデルなどあるので、いろいろパクってgamma_lpdf でtarget すると書ける。
stan でやるとパラメータの点推定値のみが得られるが、サンプリング過程でたくさんのパラメータを得るので、信頼区間(というか信用区間だが)っぽいものも得られる。
バイオリンプロットのside パラメータで片側だけ描けるというのがわかった。かつての努力はいったいなんだったんだろうか。
mikuhatsune.hatenadiary.com
f:id:MikuHatsune:20200216163854p:plain

このときのパラメータの推定値は

$shape
[1] 3.896891 9.949112

$rate
[1] 0.7848028 0.5453568

$pi
[1] 0.3413815 0.6586185

それぞれの分布の平均値は

[1]  4.96544 18.24331

発症する平均日数の95%信用区間

            [,1]     [,2]
  2.5%  4.453284 17.54821
  97.5% 5.559489 18.79025

ただし、stanでやるときは複数のchain を使うと、分布が入れ替わる問題で\hat{R} が収束しないのでchain はひとつでやった。解決策を知りたい。

# シミュレーションデータの準備
x <- 0:100
n <- 1000
shape <- c(4, 9)
rate <- c(0.8, 0.5)
mix <- c(1, 2)
p <- mix/sum(mix)
d <- t(mapply(dgamma, x, list(shape), list(rate)) * p)
xl <- c(0, 50)
cols <- c("blue", "orange")
par(mar=c(4.5, 5, 2, 2))
matplot(x, d, type="l", xlim=xl, xlab="発症までの日数", ylab="確率密度", col=cols, lty=1, lwd=3)
# 実際に得たデータ
y <- sample(x, size=n, prob=rowSums(d), replace=TRUE)

# EM アルゴリズムを行う関数
EMgamma <- function(dat, k=2, eps=10e-8, maxit=300, seed=1234){
  set.seed(seed)
  idx <- sample(1:k, size=length(dat), replace=TRUE)
  x.bar <- tapply(dat, idx, mean)
  x2.bar <- tapply(dat^2, idx, mean)
  theta <- thetas <- c(x.bar^2/(x2.bar - x.bar^2), (x2.bar - x.bar^2)/x.bar)
  lambda <- lambdas <- c(MCMCpack::rdirichlet(1, rep(1, k)))
  dens1 <- t(mapply(dgamma, dat, shape=list(head(theta, k)), scale=list(tail(theta, k))) * lambda)
  ll <- old.obs.ll <- sum(log(rowSums(dens1)))
  gamma.ll <- function(theta, z, lambda, k) -sum(z * log(t(mapply(dgamma, dat, shape=list(head(theta, k)), scale=list(tail(theta, k))) * lambda)), na.rm=FALSE)
  diff <- 1 + eps
  iter <- 0
  while (diff > eps && iter < maxit) {
    # M step
    dens1 <- t(mapply(dgamma, dat, shape=list(head(theta, k)), scale=list(tail(theta, k))) * lambda)
    z <- dens1/rowSums(dens1)
    lambda.hat <- colMeans(z)
    out <- try(suppressWarnings(nlm(gamma.ll, p = theta, lambda = lambda.hat, k = k, z = z)), silent = TRUE)
    theta.hat <- out$estimate
    new.obs.ll <- sum(log(colSums(mapply(dgamma, dat, shape=list(head(theta.hat, k)), scale=list(tail(theta.hat, k))) * lambda.hat)))
    diff <- new.obs.ll - old.obs.ll
    old.obs.ll <- new.obs.ll
    ll <- c(ll, old.obs.ll)
    # E step
    lambda <- lambda.hat
    lambdas <- rbind(lambdas, lambda)
    theta <- theta.hat
    thetas <- rbind(thetas, theta)
    iter <- iter + 1
  }
  rownames(thetas) <- NULL
  rownames(lambdas) <- NULL
  return(list(theta=thetas, theta.hat=theta.hat, lambda=lambdas, lambda.hat=lambda.hat, ll=ll))
}

out <- EMgamma(y, k=2, seed=123)

lwd <- 3
par(mfcol=c(3, 1), mar=c(1, 5, 1.2, 1), cex.lab=2, cex.axis=1.5, las=1)
# 混合比
matplot(out$lambda, type="l", lty=1, col=cols, xlab="iteration", ylab=expression(lambda), ylim=c(0, 1), lwd=lwd)
abline(h=p, lty=3, col=cols, lwd=lwd)
# shape parameter
par(mar=c(1, 5, 1.2, 1))
matplot(out$theta[, 1:k], type="l", lty=1, ylab="shape parameter", col=cols, lwd=lwd)
abline(h=shape, lty=3, col=cols, lwd=lwd)
# scale (rate)  parameter
par(mar=c(4.5, 5, 1.2, 1))
matplot(out$theta[, (2*k-1):(2*k)], type="l", xlab="iteration", ylab="scale parameter", lty=1, col=cols, lwd=lwd)
abline(h=1/rate, lty=3, col=cols, lwd=lwd)

# EM でのプロット
pars <- list(shape=head(out$theta.hat, 2), rate=1/tail(out$theta.hat, 2), pi=out$lambda.hat)
pars$rate <- switch(which.min(pars$shape), pars$rate, rev(pars$rate))
pars$pi <- switch(which.min(pars$shape), pars$pi, rev(pars$pi))
pars$shape <- switch(which.min(pars$shape), pars$shape, rev(pars$shape))
est <- t(mapply(dgamma, x, list(pars$shape), list(pars$rate)) * pars$pi)

alpha <- 0.05
me <- pars$shape / pars$rate

library(vioplot)
b <- table(factor(y, x))
par(mar=c(4.5, 5, 4, 2), las=1, cex.lab=2)
plot(b, xlim=xl, xlab="発症までの日数", ylab="発症人数",)
for(i in  seq(ncol(est))){
  lines(x, est[, i]*n, lwd=3, col=cols[i])
  lines(x, d[, i]*n, lwd=3, col=cols[i], lty=3)
}
axis(3, at=me, labels=sprintf("%.1f days", me), padj=0)
text(par()$usr[1], par()$usr[4], "発症平均日数", xpd=TRUE, pos=3, cex=1.2)
legend("topright", legend=c("真の値", "推定値"), lty=c(1, 3), lwd=3, cex=1.5)

# stan を使った推定
library(rstan)
rstan_options(auto_write=TRUE)
options(mc.cores=parallel::detectCores())

code <- "
  data {
    int<lower=0> N;
    int<lower=0> y[N];
  }

  parameters {
  real<lower=0, upper=20> shape[2];
  real<lower=0, upper=10> rate[2];
  simplex[2] pi;
  }

  model {
    real ps[2];
    for(n in 1:N){
      ps[1] = log(pi[1]) + gamma_lpdf(y[n] | shape[1], rate[1]);
      ps[2] = log(pi[2]) + gamma_lpdf(y[n] | shape[2], rate[2]);
      target += log_sum_exp(ps);
    }
  }
"

m0 <- stan_model(model_code=code)
standata <- list(N=length(y), y=y)
fit <- sampling(m0, standata, iter=1000, warmup=300, chain=1)
ex <- extract(fit, pars=c("shape", "rate", "pi"))
mes <- ex$shape/ex$rate
pars <- lapply(ex, apply, 2, median)
pars$rate <- switch(which.min(pars$shape), pars$rate, rev(pars$rate))
pars$pi <- switch(which.min(pars$shape), pars$pi, rev(pars$pi))
mes <- switch(which.min(pars$shape), mes, mes[,rev(seq(ncol(mes)))])
pars$shape <- switch(which.min(pars$shape), pars$shape, rev(pars$shape))
est <- t(mapply(dgamma, x, list(pars$shape), list(pars$rate)) * pars$pi)

alpha <- 0.05
me <- pars$shape / pars$rate
mes.ci <- apply(mes, 2, quantile, c(alpha/2, 1-alpha/2))  

# バイオリンプロットも使って発症する平均日数の分布もプロットする
library(vioplot)
b <- table(factor(y, x))
par(mar=c(4.5, 5, 4, 2), las=1, cex.lab=2)
plot(b, xlim=xl, xlab="発症までの日数", ylab="発症人数",)
for(i in  seq(ncol(est))){
  lines(x, est[, i]*n, lwd=3, col=cols[i])
  lines(x, d[, i]*n, lwd=3, col=cols[i], lty=3)
  vioplot(mes[,i], at=par()$usr[4], add=TRUE, horizontal=TRUE, wex=10,
          rectCol=NA, colMed=NA, lineCol=NA,
          side="right", xpd=TRUE, col=cols[i])
}
# axis(3, at=me, labels=sprintf("%.1f days", me), padj=-1)
axis(3, at=me, labels=sprintf("%.1f days\n[%.1f, %.1f]", me, mes.ci[1,], mes.ci[2,]), padj=-0.5)
text(par()$usr[1], par()$usr[4], "発症平均日数", xpd=TRUE, pos=3, cex=1.2)
legend("topright", legend=c("真の値", "推定値"), lty=c(1, 3), lwd=3, cex=1.5)

モニター

読んだ。

INTENSIVIST Vol.3 No.2 2011(特集:モニター)

INTENSIVIST Vol.3 No.2 2011(特集:モニター)

  • 作者:
  • 出版社/メーカー: メディカルサイエンスインターナショナル
  • 発売日: 2011/04/14
  • メディア: 単行本
COI:昔安かったのを買って積んでた。

右心カテーテルと動脈血圧モニターの話がそれなりに分かった。
結局PACは心臓手術だとルーチンで入れてしまっているが、PA圧が30超えるかくらいしか興味がないし、せっかくCIやCVPが出るならちゃんとSVRを計算して血管抵抗も考えようと思った。
安かったら買いだと思う。

ICUルーチン

読んだ。

INTENSIVIST Vol.6 No.2 2014 (特集:ICUルーチン)

INTENSIVIST Vol.6 No.2 2014 (特集:ICUルーチン)

  • 作者:
  • 出版社/メーカー: メディカルサイエンスインターナショナル
  • 発売日: 2014/04/14
  • メディア: 単行本(ソフトカバー)
COI:安くなってて買ったのを積んでた。

胸部X線を毎朝撮るのとか、PPIをとりあえずいれておくのがどんだけ意味があるのかと思って読んだ。あまり意味がなさそう。
テーマがペラいのでページ数もペラかった。静脈ルート問題とか、バーンアウト問題とかそういうのも載っていたので少し参考になった。
読むべきかと聞かれたら必ずしもそうでもないと思う。

不整脈

読んだ。

INTENSIVIST VOL.1NO.4 2009 (特集:不整脈)

INTENSIVIST VOL.1NO.4 2009 (特集:不整脈)

  • 作者:藤谷茂樹
  • 出版社/メーカー: メディカルサイエンスインターナショナル
  • 発売日: 2009/11/30
  • メディア: 単行本(ソフトカバー)
COI:古くて安かったので買って積んでたのを時間ができたので読んだ。

不整脈治療はCAST試験の前と後で変わる、というまえがきで始まる。
www.ncbi.nlm.nih.gov
不整脈治療を抗不整脈薬ですると、逆に予後が悪い、という話で、それならば不整脈治療はいったいどうすればいいんだ、という話になる。

本書は心房細動と、心室不整脈に大別して2009年頃のエビデンスをあげている。
心房細動はrate control、というのが(いまでも)定説だが、そのエビデンスとなった研究の経緯がそこそこわかった。

心室不整脈はリドカイン一択なのだが、Naチャネル阻害は心機能低下時にはダメ、ということで、でもリドカインしか使ったことないしなあ…と思いながら、アミオダロンの話が多く書かれていた。
現代ではアミオダロン一択だと思うが、10年も前ならリドカインか、ニフェカラントだったのだろう。ニフェカラントはマジで使ったことない。

ワンコイン以下で買えたので、最新のエビデンスはどうでもいいけど病態生理くらいは知りたい、という場合にはよい。ただし不整脈は何度勉強してもよくわからない。

線虫でがん検査、約85%の確率で特定

という記事を観測した。
詳細はいつもの通り不明だが、記事によると、

がん患者1400人に実施した検査では的中率は約85%に上り、特にステージ0~1の患者は87%で判定できた。一般的ながん検査「腫瘍マーカー」よりかなり高確率という。
反応するのは胃、大腸、肺、乳、膵(すい)臓、肝臓、子宮、前立腺など15種のがん。現時点では検査でがんの部位までは判明しないが、今後は特定も目指す。

ということらしく、あるか、ないかはわかる。そして、がん患者という陽性例に対して検査性能をはかっているので、感度は85%のようである。

こんな話をやっていた。
mikuhatsune.hatenadiary.com
この手の話の場合、たいてい事前確率が低いと事後確率も低い、という話になる。
ここから感度と特異度と事前確率のスクリプトをパクる。

事前確率は日本のがん統計から取ってくる。
ganjoho.jp
2018年度版のがん統計の、incidence(罹患率)はここからの引用らしい。罹患率が事前確率でいいかはこの際置いておく。
www.ncbi.nlm.nih.gov

55歳以下だとせいぜい2%、80歳くらいの高齢者でも6%くらい。
f:id:MikuHatsune:20200117193233p:plain

post <- function(pre, sn, sp){
  res <- array(0, sapply(list(pre, sn, sp), length))
 for(pre1 in seq(pre)){
   for(sn1 in seq(sn)){
     for(sp1 in seq(sp)){
        res[pre1, sn1, sp1] <- (pre[pre1] * sn[sn1]) / (pre[pre1] * sn[sn1] + (1-pre[pre1])*(1-sp[sp1]))
      }
    }
  }
  return(res)
}

incidence <- c(0.2, 0.35, 0.9, 2, 5, 10, 25, 70, 100, 200, 300, 600, 800, 900, 1100, 1200, 1100, 1000)
names(incidence) <- c(sprintf("< %d", seq(5, 85, 5)), "85+")
log_incidence <- log10(incidence)

par(mfrow=c(2, 1), mar=c(4, 5, 2, 2), las=1, cex.lab=1.2)
plot(log_incidence, type="o", pch=15, xaxt="n", yaxt="n", xlab="Age group", ylab="罹患率 10万人あたり", lwd=3)
axis(1, at=seq(incidence), labels=names(incidence))
axis(2, at=0:3, labels=10^(0:3))
abline(h=0:3, lty=3)
title("各年齢層におけるがん罹患率")

txt <- "Cancer Epidemiol. 2014 Oct;38(5):490-5. \nがんの統計’18 "
text(par()$usr[2], par()$usr[3], txt, xpd=TRUE, adj=c(1, -0.5))

p <- post(pre=incidence/100000, sn=0.85, sp=0.85)[,,1]
par(mar=c(5, 5, 2, 2), las=1)
plot(p, type="o", pch=15, xaxt="n", xlab="Age group", ylab="検査結果陽性で本当に癌の確率", lwd=3)
axis(1, at=seq(p), labels=names(incidence))
title("感度 0.85,特異度 0.85 の検査による各年齢層のがん事後確率")

急性心不全

読んだ。

INTENSIVIST VOL.2 NO.4 2010 (特集:急性心不全)

INTENSIVIST VOL.2 NO.4 2010 (特集:急性心不全)

  • 作者:
  • 出版社/メーカー: メディカルサイエンスインターナショナル
  • 発売日: 2010/11/01
  • メディア: 単行本
COI:古すぎて激安だったのを買って積んでたけど最近心不全が多いのでまた急いで読んだ。

意識低い系なのでCS1-5が何となくわかっていて、でも診るのはCS1がほとんどでニトロとNIPPVで終わり、みたいなことが多いので勉強した。
例によって序盤は心臓生理、心不全とは、みたいな話で、圧容量曲線の解説が詳しかったので満足した。
JBPOT取得者なので心エコーの話も良かったと思う。