zip になってるshape ファイルをRのみの操作でダウンロードして扱いたい

という相談を受けた。
昔は地図を扱うときにsp というパッケージを使っていたが、最近ではsf が流行りだそうだ。
notchained.hatenablog.com
インタラクティブにしたければleaflet があるが、普通にプレゼンとか論文の図にしたい、ということでsf を使いたいらしい。
そもそもsf を使うのにいろいろ準備しないといけなくて、ubuntu ではgdal が2.0.0 以上でないとだめなようで、apt-get するのにもppa を更新して最新のものがインストールされるようにしないといけない。
How To Install GDAL/OGR Packages on Ubuntu — mothergeo 0.0.1 documentation

質問ではダウンロード先がzipになっている。
統計GISデータダウンロード | 政府統計の総合窓口
Pythonならzipのままデータを扱ってなかのデータ(csv だったり)を扱うことができたはずだが、Rではやったことがない(あったかもしれないが記憶にない)ので、ローカルにダウンロードしたものを解凍してshpファイルだけ読み込んだらRからファイルを削除するコマンドを実行する、というなんとも言えない作業をすることにした。
1. Simple Features for R • sf

library(sf)
library(stringr)

# 命名規則がよくわからなかったので5ファイル分取ってみる
dlfiles <- sprintf("https://www.e-stat.go.jp/gis/statmap-search/data?dlserveyId=A002005212015&code=131%02d&coordSys=1&format=shape&downloadType=5", 1:5)

sh <- vector("list", length(dlfiles))
for(f in seq(dlfiles)){
 # Download .shp data
 u_shp <- dlfiles[f]
 fname <- sprintf("%02d.zip", f)
 download.file(u_shp, fname)
 unzip(fname)
 sh[[f]] <- st_read(list.files(pattern="shp"))
 file.remove(list.files(pattern=gsub(".shp", "", list.files(pattern="shp")))) # ほかを消す
}

f:id:MikuHatsune:20200607003039p:plain

こんなことをしていたら日本地図を扱う必要が出てきたので逆に聞いたら、jpndistrict というパッケージを教えてもらった。
jpndistrict package | R Documentation

新型肺炎COVID-19の日本の実効再生産数を推定したrstanのコードを解説してみる

西浦先生が日本の実効再生産数を推定した。
コードはrstanで下記から取れる。
https://nbviewer.jupyter.org/github/contactmodel/COVID19-Japan-Reff/tree/master/

解説動画を見逃したのでコードと関連論文からのお勉強になるが、肝としては、
・知りたいのは「感染した日」である。
・診断日もしくは報告日は、データを収集して統計を取っているのでわかる。
・診断されるには検査される必要があるから、だいたい症状か接触歴があって、発症日はそこそこデータがある。
・感染した瞬間、はもちろん発病(はほとんど)していないのでわからない。
という前提がある。PDFの「患者」の観測データについて、の項。
(誰からから感染させられる)ー感染日ー発症日ー診断日/報告日という一連の流れについて、まったく情報がないわけではなく、いままでの数理モデルの知見からは
・誰かから感染させられるー感染日:serial interval もしくはgeneration time と書いてある。厳密には違うようだが、使用しているパラメータはワイブル分布から同一のものとしている。PDFでは「世代時間<=明示的にまだ得られていない間、発病間隔と同じと想定」の項。
・感染日ー発症日:incubation period. 対数正規分布から平均5.6日くらいと適当に設定している(適当にというのはランダム、というわけではなく、過去の推定で当てはまりがよさそうな、という意味合いで、対数正規分布ではくワイブル分布やガンマ分布でもやれないことはない)。
・発症日ー報告日:Right-truncated reporting delay. 打ち切りを考慮したワイブル分布を考慮しているようで、尤度関数L
L(\theta|S_k,O_k,T)=\displaystyle\prod_{k=1}^K\frac{h(S_k-O_k|\theta)}{H(T-O_k|\theta)}
S_kkさんの報告日
O_kkさんの発症日
T:カレンダー上の直近の観測日
h:ワイブル分布の確率密度(PDF)
H:ワイブル分布の累積確率密度(CDF)
となる。


さて、たぶんこれが一番大事な話だと思うが、感染日ー発症日、そして発症日ー診断日/報告日というのをなんとか推定するのに、backprojection というものを利用している。報告日はそれなりにデータがあるので、発症日←診断日/報告日のbackprojection をして発症日を推定し、感染日←発症日を再度して感染日を推定している。rstan のコードではこの部分がsurveillance というパッケージからbackprojNPという関数を使って最尤推定法で求めた点推定値を流用して実効再生産数を求めている。backprojection についての説明は皆無なので、むしろこれを理解するのに時間がかかった。


A method of non-parametric back-projection and its application to AIDS data. - PubMed - NCBI
今回のCOVID-19のように、診断日ははっきりしているが感染日がわからない疾患として、HIVを題材にしている。ある日付t, t=\{1,2.\dots T\}HIVの診断数がY_t とすると、d の期間のincubation period はf_d の確率(PDF)に従うとする。incubation period を考慮した真の推定値\lambda_tポアソン分布に従うとして、尤度関数は
\displaystyle\prod_{t=1}^T(\displaystyle\sum_{i=1}^t \lambda_i f_{t-i})^{y_t} \exp(-\displaystyle\sum_{i=1}^t \lambda_i f_{t-i})
を最大化することで得られる、となっているらしい。\displaystyle\sum_{i=1}^t \lambda_i f_{t-i} の部分が一般的なポアソン分布のパラメータ\lambda 部分だがy_t! で割らなくていいのだろうか。
これはGLMで解くのが難しかったらしいので、EMアルゴリズムで解けるようにしたのが論文である。
\lambda_t というのが日付t=\{1,2,\dots, T\} における真の(平均)感染数とすると、EステップとMステップを合体させた式が
\lambda_t^{new}=\frac{\lambda_t^{old}}{F_{T-t}}\displaystyle\sum_{d=0}^{T-t}\frac{Y_{t+d} f_d}{\displaystyle\sum_{i=1}^{t+d} \lambda_t^{old} f_{t+d-i}}
となる。ここで、F_{T-t}=\displaystyle\sum_{d=0}^{T-t}f_d である。\lambda_t が更新されたら差分でもなんでもとって適当な閾値\epsilon を下回れば終了する。
ただし、このまま\lambda_t を使うと結構ギザギザなので、smoothing を行なっている。これがExpectation-Maximization-Smoothing (EMS) と書いてある所以のようで、上で求めたのを\phi_t^{new}\gets \lambda_t^{new} にしておく。やり方は\phi_t^{new} の前後での重み付け平均をしているだけで、重みw_i={k \choose i}/2^k とする。k=2 だと[0.25, 0.5, 0.25] となる。これを使って
\lambda_t^{new}=\displaystyle\sum_{i=0}^k w_i \phi_{t+i-\frac{k}{2}}^{new} とする。k は偶数とし、あるi を中心に前後の重み付け平均をとる。

surveillanceではsurveillance:::backprojNP.fitおよびsurveillance:::em.step.beckerでEMSを行なっている。普通にやると遅いので、RccpC++を呼び出して行なっている。eq3aというmethod 名が論文での式3a(eq3a)である。
こんな感じになる。感染日と報告日は10-14日くらいずれていそうな感じがある。
f:id:MikuHatsune:20200513203350p:plain

入力するデータがそれなりに整ったので、後はrstanで実効再生産数R_t を推定する。平均的な時間t での感染者数は(domestic とimported でデータは分かれているが、簡便のため略)
E(C)=R_t\displaystyle\sum_{\tau =0}^{t-1}C_{total}(t-\tau)g(\tau)\frac{F(T-t)}{F(T-t+\tau)}
ここで
C_{total}(t):感染者数の総計
g:generation interval の確率密度(PDF)
F:感染日から報告日までの遅れの累積確率(CDF)。これはincubation period のPDFと発症日ー報告日の遅れのPDFとの畳み込みになる(感染日→発症日→報告日)。
である。後はポアソン分布を仮定する。

ここまでくるとrstanのコードがだいぶわかるようになってくる。
冒頭に使用するCPUコア数をオプションで指定すれば、利用可能な最大CPUで並列してくれる。コードでは2chain しか使ってないが、収束が非常に速いので困りはしない。

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

functionsブロックでは、好きな関数を定義してコード内で使えるようにする。rstanは離散確率分布は別に定義しないと柔軟に扱えないことが多く、今回のようにある日(離散)での確率密度はPMFになるので、累積確率分布の差分を取って柔軟にrstan内で扱えるようにする。
generation interval にはワイブル分布、incubation period には対数正規分布なので、それぞれ定義する。

functions {
    /* discretesized version of lognormal distribution */
    vector plognormal(real mu, real sigma, int K) {
        vector[K] res; 
        for (k in 1:K)
            res[k] = exp(lognormal_lcdf(k | mu, sigma));

        return append_row(res[1], res[2:K]-res[1:(K-1)]);        
    }

    /* discretesized version of Weibull distribution */
    vector pweibull(real kappa, real theta, int K) {
        vector[K] res; 
        for (k in 1:K)
            res[k] = -expm1(-pow(1.0*k/theta, kappa));

        return append_row(res[1], res[2:K]-res[1:(K-1)]);  
    }

確率分布の畳み込みを行うのも関数を定義する。rstanにはreverse関数がないようなのでYrevというのを後のtransformed dataブロックで作っているが、X[1:k]Y[k:1]でやれば同じである。ただし、rstanは普通のRみたいにY[k:1]とやるのは受け付けてくれなかった。

/* calculating the convolutions */
// X: first function, Yrev: reversed version of the second function
// K: length of X and Yrev
// the result is the vector of length K-1, because the first element equal zero is omitted
vector convolution(vector X, vector Yrev, int K) {
    vector[K-1] res;
    res[1] = X[1]*Yrev[K];
    for (k in 2:K-1) 
        res[k] = dot_product(head(X, k), tail(Yrev, k));

    return res;        
}

// Special form of the convolution with adjusting to the delay
// F: cumulative distribution function of the reporting delay
vector convolution_with_delay(vector X, vector Yrev, vector F, int K) {
    vector[K-1] res;
    vector[K] Z = X ./ F;

    res[1] = F[2]*Z[1]*Yrev[K];
    for (k in 3:K) 
        res[k-1] = F[k]*dot_product(head(Z, k-1), tail(Yrev, k-1));

    return res;        

modelブロック、R_t が2.4 くらいにいるだろうという事前分布はいいとして、scaleパラメータが1のガンマ分布からサンプリングしたのは単にRt .* conv が期待値になるようにしたかったからなんでしょうか。

model {
    Rt ~ normal(2.4, 2.0);
    Rt_adj ~ normal(2.4, 2.0);
    
    target += gamma_lpdf(tail(domestic_backproj, K-1) | Rt .* conv + 1e-13, 1.0)
            + gamma_lpdf(tail(domestic_backproj, K-1) | Rt_adj .* conv_delay_adj + 1e-13, 1.0);
}


backprojection はEMSでなくてもベイズ的に推定できるはずなので、これもrstan に組み込めばよさそうだが、まだこれができていないので保留。

新型肺炎COVID-19の山梨大学医学部附属病院でのPCR検査の結果をrstanで解析する

山梨大学医学部附属病院で、慶応大学病院と同じように入院中の患者にPCR検査を行うと、370人中全員が陰性だったらしい。
山梨大病院、すべての入院患者らにPCR検査 全員陰性 [新型コロナウイルス]:朝日新聞デジタル

山梨大の島田真路学長は「陽性者がいる場合も考え、(入院患者への検査を)先駆的に取り組んだ。全員が陰性となり、ほっとしている。入院患者へのPCR検査をやらないと安心できない。本来はすべての病院で積極的にやるべきだ」と語った。

慶応大学病院のPCR検査の結果を解析したように、まったく同じことをやってみる。
mikuhatsune.hatenadiary.com

PCRの感度を30%〜70%、特異度を>99% とすると、感染者割合は0.405%(95%信用区間 0.015%〜2.35%)、370人に検査したので1人(95%信用区間 0〜10人)は本当は陽性の患者がいると思われる。
PCR検査で安心できるね()
f:id:MikuHatsune:20200509174534p:plain

新型肺炎COVID-19の集中治療を要する患者の推移をSIRモデルを使ってrstanで推定する

こんなことをした。
mikuhatsune.hatenadiary.com
集中治療学会が、人工呼吸器を要している患者、ECMOをしている患者、ECMOで死亡した患者、ECMOから回復した患者、など日ベースで公開してる。
これに、毎日の感染者や死亡者のデータをくっつけて、SIRを使って人工呼吸器を要する患者やECMOを要する患者を推定する。
Japan Coronavirus: 9,231 Cases and 190 Deaths - Worldometer

SIRモデルとしてはこんなのを想定する。
SIRは普通にSIRモデルだが、Iから人工呼吸器管理になる人V、VからECMO管理になる人E、ECMOから死亡する人DEとして
Iから死亡する人はDIとすると、推移にパラメータpは9つ必要で
\frac{dS}{dt}=-\frac{p_1SI}{N}
\frac{dI}{dt}=\frac{p_1SI}{N}-(p_2I+p_6)I
\frac{dR}{dt}=p_2I+p_8V+p_9E
\frac{dV}{dt}=p_3I - (p_7+p_8)V
\frac{dE}{dt}=p_4V-p_9E
\frac{dD_I}{dt}=p_6I+p_7V
\frac{dD_E}{dt}=p_5E
となる。ただしN=S+I+R とする。
最初の人口は1.2億人で始める。
f:id:MikuHatsune:20200506171908p:plain

5月5日までのデータで既にかなりずれているようにも思えるが、SIRモデルの簡潔さからいうと仕方ないように思う。
f:id:MikuHatsune:20200506172907p:plain

pはこんな感じになった。例えばp_4 は人工呼吸器からECMOにいくパラメータで、p_5 はECMOから死亡にいくパラメータだが、これは1日あたりなので、適当に積分したらいいのだろうか。
f:id:MikuHatsune:20200506172957p:plain

365日後まで推定すると、感染者(軽症も含む)のピークは9月2日にきて、人工呼吸器を要する患者のピークは10月6日、ECMOを行なっている患者のピークは11月22日にくる。集中治療としては10-11月中が最大の山場、となるのかもしれない。
f:id:MikuHatsune:20200506173109p:plain

mat <- read.csv("japan.csv", stringsAsFactors=FALSE)       # worldmeter のデータ
ecmo <- merge(l222[,c("date", "全国")], merge(l555[,c("date", "全国")], l333, by.x="date", by.y="date"), by.x="date", by.y="date")
colnames(ecmo) <- c("date", "ECMOall", "ventilation", "ECMOrecovery", "ECMOdeath", "ECMOdoing")

mat$Date <- gsub("Apr ", "04-", gsub("Mar ", "03-", sprintf("2020-%s", gsub("Feb ", "02-", mat$Date))))
m <- as.data.frame(merge(ecmo, mat, by.x="date", by.y="Date"))

Pop <- 120000000

DE <- m$ECMOdeath
DI <- m$Total.Deaths - DE
E <- m$ECMOdoing
I <- m$Total.Cases
V <- m$ventilation - E
R <- I - m$Active.Cases
S <- Pop - (I + R + E + V + DI + DE)
N <- cbind.data.frame(S=S, I=I, R=R, V=V, E=E, DI=DI, DE=DE)


library(igraph)
G <- graph_from_edgelist(rbind(c(1,2),c(2,3),c(2,4),c(2,6),c(4,5),c(4,3),c(4,6),c(5,7),c(5,3)))
G <- graph_from_edgelist(rbind(c(1,2),c(2,3),c(2,4),c(4,5),c(5,7),c(2,6),c(4,6),c(4,3),c(5,3)))
lmat <- layout.norm(rbind(c(-1,0),c(0,0),c(1,0),c(0,-1),c(0,-2),c(1,-1),c(0,-3)))

pmat <- mapply(function(z) colMeans(lmat[get.edgelist(G)[z,],]), seq(nrow(get.edgelist(G))))
ppos <- c(3, 3, 2, 2, 2, 3, 3, 1, 1)


V(G)$label <- c("S", "I", "R", "V", "E", "DI", "DE")
V(G)$label.cex <- 2
V(G)$label.font <- 2
V(G)$label.color <- "black"
V(G)$size <- 20
V(G)$color <- cols
V(G)$color[6] <- "grey"
E(G)$color <- "black"
E(G)$width <- 3
# svg("fig02.svg", 48/9, 12/9)
par(mar=c(0, 0, 0, 0))
plot(G, layout=lmat)
legend("bottomleft", legend=sprintf("%s:\t%s", c("S", "I", "R", "V", "E", "DI", "DE"), names(cols)), pch=15, col=cols, bty="n", cex=1.5)
for(i in 1:9){
  #txt <- as.expression(substitute(italic(g[x%->%~y]), list(x=i, y=i+1)))
  txt <- as.expression(substitute(italic(p[x]), list(x=i)))
  text(pmat[1, i], pmat[2, i], txt, xpd=TRUE, pos=ppos[i], cex=1.5)
}


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

code <- "
data{
  int<lower=1> T;
  int<lower=1> Tf;
  int<lower=1> N; // population
  int<lower=0> S0[7]; //S, I, R, V, E, DI, DE
  int<lower=0> S[T, 7]; //S, I, R, V, E, DI, DE
}
parameters{
  vector<lower=0, upper=0.5>[9] p;
}
transformed parameters{
  vector<lower=0>[7] y[T+Tf];
  y[1] = to_vector(S0);
  for(i in 1:(T+Tf-1)){
    y[i+1][1] = y[i][1] - p[1]*y[i][1]*y[i][2]/sum(y[i]);
    y[i+1][2] = y[i][2] + p[1]*y[i][1]*y[i][2]/sum(y[i]) - (p[2]+p[3]+p[6])*y[i][2];
    y[i+1][3] = y[i][3] + p[2]*y[i][2]+p[8]*y[i][4]+p[9]*y[i][5];
    y[i+1][4] = y[i][4] + p[3]*y[i][2] - (p[4]+p[7]+p[8])*y[i][4];
    y[i+1][5] = y[i][5] + p[4]*y[i][4] - (p[5]+p[9])*y[i][5];
    y[i+1][6] = y[i][6] + p[6]*y[i][2] + p[7]*y[i][4];
    y[i+1][7] = y[i][7] + p[5]*y[i][5];
  }
}
model{
  for(i in 1:T){
    S[i,] ~ multinomial(y[i]/sum(y[i]));
  }
}
"

m0 <- stan_model(model_code=code)
standata <- list(T=nrow(N), S=N, S0=unlist(N[1,]), N=Pop, Tf=365)
fit <- sampling(m0, standata, iter=7000, warmup=5000, chain=4)
ex <- extract(fit, pars=head(fit@model_pars, -1))

alpha <- 0.05
cia <- c(alpha/2, 0.5, 1-alpha/2)
m <- abind::abind(mapply(function(z) apply(ex$y[,,z], 2, quantile, cia), seq(dim(ex$y)[3]), SIMPLIFY=FALSE), along=3)

p <- apply(ex$p, 2, quantile, cia)

cols <- c("Susceptible"="blue", "Infected"="yellow", "Recovery"="skyblue", Ventilation="darkgreen", ECMO="orange", "Death infection"="black", "Death ECMO"="red")

library(vioplot)
par(mar=c(4.5, 5, 2, 2), cex.lab=1.5)
vioplot(as.data.frame(ex$p), ylab=as.expression(substitute(italic(g))), xlab="Parameter", horizontal=TRUE, side="right", colMed=NA, las=1)
text(p[2,], seq(ncol(p)), sprintf("%.3f", p[2,]), pos=1)
abline(h=seq(ncol(p)), lty=3)

xl <- c(0, 82)
yl <- c(0, 18000)
par(mar=c(4.5, 5, 4, 3))
matplot(m[2,,-1], type="l", col=cols[-1], xlim=xl, ylim=yl, lwd=4, lty=1, xlab="Date", ylab="人数")
for(i in 2:ncol(N)){
  points(N[,i], pch=16, col=cols[i])
}
legend("topleft", legend=names(cols), col=cols, pch=15, xpd=TRUE, ncol=1, cex=1.3)

xl <- c(0, dim(m)[2])
yl <- c(0, max(m[2,,-1]))
matplot(m[2,,-1], type="l", col=cols[-1], xlim=xl, ylim=yl, lwd=3, lty=1, xlab="", ylab="人数")
for(i in 2:ncol(N)){
  points(N[,i], pch=16, col=cols[i])
}

Mday <- apply(m[2,,], 2, which.max)
xl <- c(0, dim(m)[2])
yl <- c(0, 1)
xd <- 10
yd <- 0.03 + 0.05*(0:2)
d <- seq(as.Date("2020-02-15"), by="day", length.out=dim(m)[2])
r <- rle(format(d, "%h"))
par(mar=c(4.5, 5, 4, 3))
matplot(m[2,,]/Pop, type="l", col=cols, xlim=xl, ylim=yl, lwd=3, lty=1, xlab="Date", ylab="Proportion", xaxt="n", las=1, cex.lab=1.5)
axis(1, at=cumsum(r$lengths), labels=r$values, cex.axis=0.75)
legend("left", legend=names(cols), col=cols, pch=15, xpd=TRUE, ncol=1, cex=1.3)
j <- 0
pa <- par()$usr
for(i in c(5, 4, 2)){
  j <- j + 1
  # txt <- sprintf("Max %s on %d (%.2f)", names(cols)[i], Mday[i], m[2, Mday[i], i]/Pop)
  txt <- sprintf("Max %s on %s (%.2f)", names(cols)[i], format(d[Mday[i]], "%m/%d"), m[2, Mday[i], i]/Pop)
  segments(Mday[i], pa[4], y1=pa[4]+yd[j], xpd=TRUE, col=cols[i], lwd=3)
  segments(Mday[i], pa[4]+yd[j], x1=Mday[i]+xd, xpd=TRUE, col=cols[i], lwd=3)
  text(Mday[i]+xd, pa[4]+yd[j], txt, xpd=TRUE, pos=4, cex=1.2)
  abline(v=Mday[i], lty=3)
}
# worldmeter のデータ取り
library(stringr)
country <- "japan"
html <- sprintf("https://www.worldometers.info/coronavirus/country/%s", country)
h <- paste(readLines(html), collapse="")

s0 <- str_extract_all(h, '<script type="text/javascript">.*?</script>')[[1]]
s1 <- mapply(function(z) str_extract_all(z, " categories: \\[.*?\\]"), s0)
s2 <- lapply(mapply(function(z) str_replace(str_replace_all(z, "categories|[\":\\[\\]]", ""), "^ +", ""), s1), unique)
s3 <- unname(sapply(s2, strsplit, ","))


d1 <- mapply(function(z) str_extract_all(z, "data: \\[.*?\\]"), s0)
d2 <- lapply(mapply(function(z) str_replace(str_replace_all(z, "data|[\":\\[\\]]", ""), "^ +", ""), d1), unique)
d3 <- unname(lapply(lapply(d2, strsplit, ","), lapply, as.numeric))

n1 <- mapply(str_extract_all, s0, " title: \\{\\s+text: '.+?'")
n2 <- unname(rapply(n1, function(z) str_remove_all(str_extract(z, "text: '.+?'"), "text: |'"), how="replace"))

mat <- cbind.data.frame(s3[[1]], d3[[2]][[1]], d3[[3]][[1]], d3[[4]][[1]], d3[[5]][[1]], d3[[6]][[1]], d3[[7]][[1]], d3[[8]][[1]])
colnames(mat) <- c("Date", n2[[2]][[1]], n2[[3]][1], n2[[4]][1], n2[[5]][1], n2[[6]][1], n2[[7]][1], "Recoveries")

write.table(mat, sprintf("%s.csv", country), sep=",", row.names=FALSE)

新型肺炎COVID-19 の集中治療学会のデータを引っこ抜く

こんなデータがある。
covid19.jsicm.org
COVID-19による全国の人工呼吸器患者数とECMO稼働数(と離脱者数、死亡者数、現在も治療されている最中の数)がjsでグリグリできる。
グリグリできるのはいいが、47都道府県+地方別とデータを取るのが面倒なので、引っこ抜いた。

ページが更新されるたびにhtmlをローカルに保存して、COVID-19 重症患者状況_files のなかのJSONが記述されているjs(5月5日は 0a923574fbb3da6cc56.js だった)を読み込んでゴリ押しする。

人工呼吸器の都道府県および地方別のgooglevis も置いておく。



無駄な努力、ご苦労様(ヒソカ

library(stringr)
dlevel <- function(z) as.numeric(as.character(z))
l <- readLines("COVID-19 重症患者状況_files/40a923574fbb3da6cc56.js")
l0 <- str_extract_all(gsub('\"', "", l), "JSON.parse\\(.+?\\)")[[1]]

# 地方区分
dist <- cbind.data.frame(a=str_extract_all(l0[1], "a\\d")[[1]],
dist=c(str_remove_all(str_extract_all(l0[1], "a\\d:.+?,")[[1]], "[a|\\d|,|:]"), "九州"))
dist <- apply(dist, 2, as.character)

# ECMO 転帰
l3 <- gsub("\\}", ",", str_extract_all(l0[3], "date.+?\\}")[[1]])
l33 <- mapply(function(z) do.call(rbind.data.frame, strsplit(strsplit(z, ",")[[1]], ":")), l3, SIMPLIFY=FALSE)
l333 <- as.data.frame(t(mapply(function(z) z[,2], l33, USE.NAMES=FALSE)))
l333[,-1] <- apply(l333[,-1], 2, dlevel)
l333 <- data.frame(date=l333[,1], "実施中"=l333[,2], "死亡"=l333[,3], "離脱"=as.numeric(as.character(l333$V6))+as.numeric(as.character(l333$V7)))

# 都道府県
l4 <- gsub("data:\\{", "", gsub("\\}", ",", str_extract_all(l0[4], "data.+?\\}")[[1]]))
l44 <- mapply(function(z) do.call(rbind.data.frame, strsplit(strsplit(z, ",")[[1]], ":")), l4, SIMPLIFY=FALSE, USE.NAMES=FALSE)[[1]]
l444 <- matrix(as.character(unlist(l44)), nc=2)

# ECMO 装着数
l2 <- gsub("\\}", ",", str_extract_all(l0[2], "date.+?\\}")[[1]])
l22 <- mapply(function(z) do.call(rbind.data.frame, strsplit(strsplit(z, ",")[[1]], ":")), l2, SIMPLIFY=FALSE)
l222 <- as.data.frame(t(mapply(function(z) z[,2], l22, USE.NAMES=FALSE)))
colnames(l222) <- l22[[1]][,1]
colnames(l222)[match(c(l444[,1], dist[,1]), colnames(l222))] <- c(l444[,2], dist[,2])
l222[,-1] <- apply(l222[,-1], 2, dlevel)

# 人工呼吸器都道府県別
l5 <- gsub("\\}", ",", str_extract_all(l0[5], "date.+?\\}")[[1]])
l55 <- mapply(function(z) do.call(rbind.data.frame, strsplit(strsplit(z, ",")[[1]], ":")), l5, SIMPLIFY=FALSE)
l555 <- as.data.frame(t(mapply(function(z) z[,2], l55, USE.NAMES=FALSE)))
colnames(l555) <- l55[[1]][,1]
l555[,-1] <- apply(l555[,-1], 2, dlevel)
colnames(l555)[match(c(l444[,1], dist[,1]), colnames(l555))] <- c(l444[,2], dist[,2])
library(googleVis)
library(stringr)
gmat <- l555[,1:48]
# gmat$date <- seq(gmat$date)
# gmat$date <- as.Date(as.character(gmat$date))
url <- "https://covid19.jsicm.org/"
webpage <- "COVID-19 重症患者状況 日本COVID-19対策ECMOnet集計"
headertxt <- sprintf("<span><a href='%s' target='_blank'>%s</a></span> on %s", url, webpage, Sys.Date())
# googlevis 用のプロットオプション
ops <- list(
  fontSize=12, height=400, width=400, lineWidth=1,
  vAxis="{title:'人数', fontSize: 20, titleTextStyle: {fontSize: 20, bold: 'true', italic: 'false'}, viewWindow: {min: 0, max: 100}, gridlines: {color: '#00F', minSpacing: 100}, logScale: 'false'}",
  sizeAxis="{minValue: -80, maxValue: 75, maxSize: 15}",
  hAxis="{title:'Date', fontSize: 20, titleTextStyle: {fontSize: 20, bold: 'true', italic: 'false'}, viewWindow: {min: 0, max: 81}}",
  explorer="{actions: ['dragToZoom', 'rightClickToReset']}",
  focusTarget="datum", # datum category
  selectionMode="single", # multiple
  legend="{position: 'right', textStyle: {color: 'black', fontSize: 10}, pageIndex: 1}"
)
gvis <- gvisLineChart(gmat, options=ops)
gvis$html$caption <- NULL
gvis$html$footer <- str_replace(gvis$html$footer,
                                "<span> \n  .+\n  .+\n</span>",
                                headertxt)
plot(gvis)


gmat <- l555[,c(1, 49:ncol(l555))]
# gmat$date <- seq(gmat$date)
# gmat$date <- as.Date(as.character(gmat$date))
url <- "https://covid19.jsicm.org/"
webpage <- "COVID-19 重症患者状況 日本COVID-19対策ECMOnet集計"
headertxt <- sprintf("<span><a href='%s' target='_blank'>%s</a></span> on %s", url, webpage, Sys.Date())
# googlevis 用のプロットオプション
ops <- list(
  fontSize=12, height=400, width=400, lineWidth=1,
  vAxis="{title:'人数', fontSize: 20, titleTextStyle: {fontSize: 20, bold: 'true', italic: 'false'}, viewWindow: {min: 0, max: 320}, gridlines: {color: '#00F', minSpacing: 100}, logScale: 'false'}",
  sizeAxis="{minValue: -80, maxValue: 75, maxSize: 15}",
  hAxis="{title:'Date', fontSize: 20, titleTextStyle: {fontSize: 20, bold: 'true', italic: 'false'}, viewWindow: {min: 0, max: 81}}",
  explorer="{actions: ['dragToZoom', 'rightClickToReset']}",
  focusTarget="category", # datum category
  selectionMode="single", # multiple
  legend="{position: 'right', textStyle: {color: 'black', fontSize: 10}, pageIndex: 1}"
)
gvis <- gvisLineChart(gmat, options=ops)
gvis$html$caption <- NULL
gvis$html$footer <- str_replace(gvis$html$footer,
                                "<span> \n  .+\n  .+\n</span>",
                                headertxt)
plot(gvis)

新型肺炎COVID-19の神戸における真のIgG抗体陽性患者数をrstanで推定する

読んだ。
Estimation of seroprevalence of novel coronavirus disease (COVID-19) using preserved serum at an outpatient setting in Kobe, Japan: A cross-sectional study. | medRxiv
神戸市の入院中の患者で、入院中の検査を適当(ランダムサンプリングのつもりで)にIgG抗体検査をすると、1000人中33人が抗体陽性だったということで、seropositive の頻度は3.3%(95%CI 2.3%〜4.6%)、と言っている。これは二項検定で計算できて

round(binom.test(33, 1000)$conf.int, 3)
[1] 0.023 0.046
attr(,"conf.level")
[1] 0.95

となるが、これは1000人から陽性患者を検出する確率pがたしかにpであるときに、最尤推定値が\frac{33}{1000}で、二項分布から信頼区間を計算していいのである。すなわち、抗体検査の感度と特異度がともに100(のとき、accuracy が1)のときに、「1000人中、抗体検査陽性になる患者がpだけ存在する」=「1000人中、抗体検査陽性になった患者がpだけ存在する」と言っていいのであって、(抗体検査陽性)=(本当に陽性で、抗体検査も陽性)+(本当は陰性で、抗体検査は陽性)である状況が考慮されていない。
最近やった。
mikuhatsune.hatenadiary.com
mikuhatsune.hatenadiary.com

KURABOの抗体検査は、論文によると(SARS-CoV-2 Antibody Detection Kit (IgM/IgG))、IgG検査は521人に行われ、感度 0.7638、特異度 1、精度 0.9424だった、という。これを一生懸命分割表で逆算すると

真に陽性 真に陰性 合計
検査陽性 感度 97 0 97
検査陰性性 30 特異度 394 424
127 394 N=521

となり、この検査精度を推定したときの対象集団の事前確率rは、精度(A_c:accuracy)とすれば
A_c=\frac{rNS_n+(1-r)NS_p}{N}=A_c
r=\frac{A_c-S_p}{S_n-S_p}
で、24.4%のようである。

Sn <- 0.7638
Sp <- 1
Ac <- 0.9424
N_KURABO <- 521

r <- (Ac - Sp)/(Sn - Sp)

M0 <- matrix(c(r*Sn, (1-r)*(1-Sp), r*Sn+(1-r)*(1-Sp), r*(1-Sn),
               (1-r)*Sp, r*(1-Sn)+(1-r)*Sp,
               r, (1-r), 1), 3, byrow=TRUE)
M <- round(M0*N_KURABO)

M[1,1]/M[3,1]
M[2,2]/M[3,2]
(M[1,1]+M[2,2])/M[3,3]

年齢構成を考えずに、とりあえずバルクの集団として、「真の抗体陽性の人の割合」を推定する。抗体検査の感度はほぼ100%なので、陽性になってしまえばその人はほぼ確実に真に陽性の患者(SPNINという)であるが、感度は75%程度なので、真に陽性である人が、検査陰性になってしまい、取りこぼしている可能性がある。というわけで、ただ単純に\frac{33}{1000}で二項分布を考えると、過小評価するおそれがある。
というわけでやった。
4.1%(2.6%〜5.9%)が真に抗体陽性の確率、となった。3.3%(2.3%〜4.6%)よりか多くなっており、論文では感度が75%であることを考慮していない分、過小評価となっている。
f:id:MikuHatsune:20200502225559p:plain
f:id:MikuHatsune:20200502230829p:plain

library(rstan)
rstan_options(auto_write=TRUE)
options(mc.cores=parallel::detectCores())
 
code <- "
data{
  int N_IgG;           // 神戸集団でのIgG検査数 1000
  int N_IgG_positive;  // 神戸集団でのIgG検査陽性数 33
  int N_KURABO_P;      // KURABO検査での陽性結果
  int N_KURABO_TP;     // KURABO検査での真の陽性結果
  int N_KURABO_N;      // KURABO検査での陰性結果
  int N_KURABO_TN;     // KURABO検査での真の陰性結果
}
parameters{
  real<lower=0, upper=1> p;  // IgG陽性の有病率
  real<lower=0, upper=1> Sn; // KURABOの検査の感度
  real<lower=0, upper=1> Sp; // KURABOの検査の特異度
}
transformed parameters{
  real<lower=0, upper=1> theta = p*Sn + (1-p)*(1-Sp);  // KURABO検査で陽性になる確率
}
model{
  N_KURABO_P ~ binomial(N_KURABO_TP, Sn);  // KURABO検査の感度
  N_KURABO_N ~ binomial(N_KURABO_TN, Sp);  // KURABO検査の特異度
  N_IgG_positive ~ binomial(N_IgG, theta); // 神戸集団でのIgG陽性
}
"

m0 <- stan_model(model_code=code)
standata <- list(N_IgG=1000, N_IgG_positive=33,
                 N_KURABO_P=M[1,1], N_KURABO_TP=M[3,1], N_KURABO_N=M[2,2], N_KURABO_TN=M[3,2])

fit <- sampling(m0, standata, iter=30000, warmup=10000, chain=4)
ex <- extract(fit, pars=head(fit@model_pars, -1))

alpha <- 0.05
cia <- c(alpha/2, 0.5, 1-alpha/2)

labs <- c("IgG positive prevalenve in Kobe", "KURABO Sensitivity", "KURABO Specificity", "KURABO Positive result probability")
cols <- c("lightgreen", "violet", "violet", "violet")
xl <- c(0, 1)
par(mfcol=c(4, 1), mar=c(4.5, 4.5, 3.5, 2), cex.axis=1.5)
for(i in seq(labs)){
  CrI <- quantile(ex[[i]], cia)
  # sprintf("%.2f [%.2f, %.2f]", CrI[2], CrI[1], CrI[3])
  hist(ex[[i]], main=sprintf("%s\n%.3f [%.3f, %.3f]", labs[i], CrI[2], CrI[1], CrI[3]), freq=FALSE, xlab="Probability", nclass=50, border=NA, col=cols[i], cex.main=1.5)
}

cols <- c(rgb(144,238,144,200,maxColorValue=256), rgb(238,130,238,120,maxColorValue=256))
hist(ex$p, xlab="Prevalence", nclass=100, border=NA, col=cols[1], cex.main=1.5, main="")
hist(ex$theta, xlab="Probability", nclass=100, border=NA, col=cols[2], cex.main=1.5, add=TRUE)
legend("topright", legend=c("真の推定", "素の推定"), col=cols, pch=15, cex=2, bty="n")


次に、年齢構成がデータとしてあるので、グループ別にやった。ただ分けてfor loopを回すだけである。
10歳未満や90歳以上は標本が少なく、信用区間の幅が非常に広くなってしまったが、それ以外の階級では概ね5%〜10%くらいが真に抗体陽性となった。男女別でみても6.5%〜6.7%とほとんど差はなく、男女合わせて年齢構成を考慮した場合、6.7%の抗体陽性となった。
f:id:MikuHatsune:20200502225419p:plain

Age_M <- c(7, 17, 17, 41, 64, 84, 87, 85, 81, 6)
Age_F <- c(1, 10, 19, 49, 91, 80, 84, 81, 83, 13)
Positive_M <- c(0, 0, 0, 1, 2, 4, 2, 6, 1, 0)
Positive_F <- c(0, 0, 0, 2, 3, 2, 3, 3, 4, 0)
names(Age_M) <- names(Age_F) <- c("10<", apply(matrix(c(1, head(rep(2:9, each=2), -1)), nr=2)*10, 2, paste, collapse="-"), ">90")

code <- "
data{
  int Cat;
  int Age_M[Cat];           // 年齢区分の数
  int Age_F[Cat];           // 年齢区分の数
  int Positive_M[Cat];
  int Positive_F[Cat];
  int N_KURABO_P;      // KURABO検査での陽性結果
  int N_KURABO_TP;     // KURABO検査での真の陽性結果
  int N_KURABO_N;      // KURABO検査での陰性結果
  int N_KURABO_TN;     // KURABO検査での真の陰性結果
}
parameters{
  vector<lower=0, upper=1>[Cat] pM;  // IgG陽性の有病率
  vector<lower=0, upper=1>[Cat] pF;  // IgG陽性の有病率
  real<lower=0, upper=1> Sn; // KURABOの検査の感度
  real<lower=0, upper=1> Sp; // KURABOの検査の特異度
}
transformed parameters{
  vector<lower=0, upper=1>[Cat] thetaM = pM*Sn + (1-pM)*(1-Sp);  // KURABO検査で陽性になる確率
  vector<lower=0, upper=1>[Cat] thetaF = pF*Sn + (1-pF)*(1-Sp);  // KURABO検査で陽性になる確率
}
model{
  N_KURABO_P ~ binomial(N_KURABO_TP, Sn);  // KURABO検査の感度
  N_KURABO_N ~ binomial(N_KURABO_TN, Sp);  // KURABO検査の特異度
  Positive_M ~ binomial(Age_M, thetaM); // 神戸集団でのIgG陽性
  Positive_F ~ binomial(Age_F, thetaF); // 神戸集団でのIgG陽性
}
generated quantities{
  int RM[sum(Age_M)];
  int RF[sum(Age_F)];
  for(i in 1:Cat){
    for(j in ((i==1)?1:(sum(Age_M[1:(i-1)])+1)):sum(Age_M[1:i])){
      RM[j] = bernoulli_rng(pM[i]);
    }
    for(j in ((i==1)?1:(sum(Age_F[1:(i-1)])+1)):sum(Age_F[1:i])){
      RF[j] = bernoulli_rng(pF[i]);
    }
  }
}
"


m1 <- stan_model(model_code=code)
standata <- list(Cat=length(Age_M), Age_M=Age_M, Age_F=Age_F,
                 Positive_M=Positive_M, Positive_F=Positive_F,
                 N_KURABO_P=M[1,1], N_KURABO_TP=M[3,1], N_KURABO_N=M[2,2], N_KURABO_TN=M[3,2])

fit <- sampling(m1, standata, iter=10000, warmup=5000, chain=4)
ex <- extract(fit, pars=head(fit@model_pars, -1))

mapply(function(j) (ifelse(j==1, 1, sum(Age_M[1:(j-1)])+1)):sum(Age_M[1:j]), 1:10)

library(vioplot)

v <- lapply(list(ex$pM, ex$pF), as.data.frame)
v <- mapply(function(z1, z2) cbind.data.frame(z1, rowMeans(z2)), list(ex$pM, ex$pF), list(ex$RM, ex$RF), SIMPLIFY=FALSE)
cols <- c("skyblue", "pink")
Mcols <- c("blue", "red")
xl <- c(0, 1.1)
xd <- 0.23
P <- lapply(v, apply, 2, quantile, cia)

labs <- c(names(Age_M), "All")
par(mar=c(4.5, 4.5, 3.5, 9), cex.axis=1.5, cex=1.25)
plot(rev(seq(labs)), type="n", xlim=xl, ylim=c(0.5, length(labs)+0.5), yaxt="n", xlab="Prevalence", ylab="")
axis(2, at=rev(seq(labs)), labels=labs, las=1)
title("Age adjusted seroprevalence in Kobe")
pa <- par()$usr
for(i in seq(v)){
  vioplot(v[[i]], at=rev(seq(labs)), horizontal=TRUE, side=c("right", "left")[i], add=TRUE, rectCol=Mcols[i], col=cols[i], colMed=Mcols[i], lineCol=NA)
  for(j in seq(labs)){
    txt <- sprintf("%.3f [%.3f, %.3f]", P[[i]][2, j], P[[i]][1, j], P[[i]][3, j])
    text(pa[2], length(labs)+1-j+xd*c(1, -1)[i], txt, xpd=TRUE, pos=4)
    text(pa[2], length(labs)+1-j+xd*c(1, -1)[i], c("M", "F")[i], xpd=TRUE, pos=2, font=2)
  }
}
PAll <- quantile(rowMeans(cbind(ex$RM, ex$RF)), cia)
txt <- sprintf("%.3f [%.3f, %.3f]", PAll[2], PAll[1], PAll[3])
text(pa[2], pa[3]+xd, txt, xpd=TRUE, pos=4)
text(pa[2], pa[3]+xd, "All", xpd=TRUE, pos=2, font=2)
abline(h=rev(seq(labs)), lty=3)


草生える()

新型肺炎COVID-19の抗体検査から集団の有病率をrstanで推定する

結論から言うと、抗体検査を受けた202人の集団において、9.4%(95% 信用区間で5.4%〜13.9%)が感染者である。ただし、この抗体検査を受けた202人は、東京都(とか他の都道府県一般)のランダムサンプリングとは言えないので、単純に東京都人口の9.4%が感染している、とは言えないことに注意。

こんな記事を観測した。
https://www.tokyo-np.co.jp/s/article/2020043090070748.htmlwww.tokyo-np.co.jp
解析に必要な数値の事実ベースで抽出すると

一般市民の4.8%、医療従事者の9.1%が陽性(抗体あり)で、過去に感染していたことが分かった。
一般市民の147人の4.8%%にあたる7人が陽性、医療従事者55人のうち9.1%の5人が陽性だった。
ホームページで希望者を募り、20~80歳の男性123人、女性79人を検査した。
★このうち一カ月以内に発熱のあった人は52人
★同居者でコロナウイルス感染者がいる人は2人
PCR検査を受診したことがある人は9人
PCR検査で陽性反応だった1人も含む
★市民・医療従事者を合計した202人全体では5.9%の12人(男女とも6人)が陽性だった。
以前のPCR検査で陰性とされたが、抗体検査で陽性だった人もいた。

このうち、★は今回の解析で使用したデータである。

興味があるのは、「202人中、抗体検査で陽性だったのは12人(5.9%)」という結果から、「202人中、感染しているのはX人」を推定することである。検査しているのだから感染者(抗体検査なので既感染を含むが、面倒なので「今、感染している」こととする)検査陽性の人のことだろう、と思うかもしれないが、それは臨床検査学的にはよくある間違い(というかベイズ統計屋的に間違い)で、検査陽性というのは、
(本当に感染していて、検査で正しく陽性)+(本当は感染していないのに、検査で間違って陽性)
というパターンの足し算の結果、「検査陽性」となっているので、(本当は感染していない人)を加算していることに注意しなければならない。
が、検査の特性として、(本当は感染していないのに、検査で間違って陽性)としてしまう確率などがわかれば、ある程度正しく推定できる。これらの性能は感度と特異度と言われ、検査として売り出すなら絶対に評価している。

ここでは、PCR検査は感度 0.1-0.5、特異度0.99程度、とした。一方で、抗体検査は感度0.3-0.7、特異度0.8-0.99程度とした。というのも、インフルエンザの抗体検査がこれくらいだからである。気になる人は適当にパラメータをいじって追試してほしい。

さて、推定したいのは「感染している人の割合」であるが、202人は属性がバラバラである。一ヶ月以内に発熱のあった人、同居者でコロナウイルス感染者がいる人、PCR検査を受信したことがある人(は、つまり陰性だったのだろうと推測)、PCR検査で陽性だった人、を除外すると、202人中138人は、一体どういう人なのかよくわからないが、ホームページで募った希望者、ということから推測するに、無症状の人とする。つまり、
p_1:無症状の138人のうち本当に感染している人の割合
p_2:このうち一カ月以内に発熱のあった52人のうち本当に感染している人の割合
p_3:同居者でコロナウイルス感染者がいる2人のうち本当に感染している人の割合
p_4PCR検査を受診したことがある9人(で、PCR検査陰性だった)のうち本当に感染している人の割合
p_5PCR検査で陽性反応だった1人のうち本当に感染している人の割合
をそれぞれ推定することになる。ただし、202人が5つのカテゴリに属し、総計で12人検査陽性、という制限がある。
ここで、変数は5つあるが、PCR検査は「怪しそうな人にやる」のが原則である。「一カ月以内に発熱があった」というのは、いま感染しているかどうかにはあまり寄与しない(※抗体検査で既感染を疑う設定なら意味があるが、この解析では現時点の感染を意味しているので注意)。しかし、無症状よりかは感染していそうな感じはするので、p_1< p_2 くらいの緩い制限を持ちつつ、PCR検査は、事前確率p_3 の集団に行われる、とする。
すると、PCR検査陰性(p_4)とPCR検査陽性(p_5)の運命がふたつともある。ここで、事前確率p_3のときの陰性事後確率と陽性事後確率は、実はそれぞれ計算できて
p_4=\frac{p_3 (1-S_n)}{p_3 (1-S_n)+(1-p_3)(1-S_p)}PCR検査の陰性事後確率)
p_4=\frac{p_3 (1-S_n)}{p_3 (1-S_n)+(1-p_3)S_p} こっちです)
p_5=\frac{p_3S_n}{p_3S_n + (1-p_3)(1-S_p)}PCR検査の陽性事後確率)
となる。ただしS_nは感度、S_pは特異度である。
pは各カテゴリの感染者割合(確率)なので、実際に(抗体検査で)陽性になる確率q
q=pS_n + (1-p)(1-S_p)
である。ここでS_nS_pは抗体検査の感度と特異度である。
こちらでも似たようなことをやった。
mikuhatsune.hatenadiary.com


5つのカテゴリにq_{*}=(q_1, q_2,q_3, q_4, q_5) と抗体検査で陽性となる確率がついていて、それぞれ138, 52, 2, 9, 1 人がいて、検査陽性となる人が合計12人なので、ひたすらfor loop で回してbinomial_lpmf関数でtarget += する、としてみる。多項分布とかもっといい方法があれば教えてほしい。
最終的にはgenerated quantities ブロックで、202人5カテゴリの人たちが感染者かそうでないかをpでランダムサンプリング(bernoulli_rng でできる)することでシミュレーションし、最終的に「202人のうち感染者の割合」を推定する。

9.4%(95% CI 5.4%〜13.9%)が感染者と推定された。
f:id:MikuHatsune:20200501163624p:plain

p_1:無症状の138人のうち本当に感染している人の割合は1.67%
p_2:このうち一カ月以内に発熱のあった52人のうち本当に感染している人の割合は10.7%
p_3:同居者でコロナウイルス感染者がいる2人のうち本当に感染している人の割合は85.1%
p_4PCR検査を受診したことがある9人(で、PCR検査陰性だった)のうち本当に感染している人の割合は80.1%
p_5PCR検査で陽性反応だった1人のうち本当に感染している人の割合は99.8%
だった。
f:id:MikuHatsune:20200501163649p:plain
本当は「医療従事者であるか」とか「男性か女性か」の属性も加味したかったが断念した。

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

code <- "
data{
  int Ncat;        //カテゴリの数
  int Ns[Ncat]; //カテゴリごとの人数
  int N_pos;     //陽性患者 12人
  real<lower=0, upper=1> Sn_PCR[2];  // PCR検査の感度の範囲
  real<lower=0, upper=1> Sp_PCR[2];  // PCR検査の特異度の範囲
  real<lower=0, upper=1> Sn_Anti[2];   // 抗体検査の感度の範囲
  real<lower=0, upper=1> Sp_Anti[2];   // 抗体検査の特異度の範囲
  real<lower=0, upper=1> b1[2];           // 無症状の人の感染者割合の範囲
  real<lower=0, upper=1> b2[2];           // 一ヶ月以内に発熱した人の感染者割合の範囲
  real<lower=0, upper=1> b3[2];           // 同居している人がコロナウイルス感染者の人の感染者割合の範囲
  real<lower=0, upper=1> b4[2];           // PCR陰性者の感染者割合の範囲
  real<lower=0, upper=1> b5[2];           // PCR陽性者の感染者割合の範囲
}
parameters{
  real<lower=Sn_PCR[1], upper=Sn_PCR[2]> Sensitivity; // PCR検査の感度
  real<lower=Sp_PCR[1], upper=Sp_PCR[2]> Specificity; // PCR検査の特異度
  real<lower=Sn_Anti[1], upper=Sn_Anti[2]> Sn;                // 抗体検査の感度
  real<lower=Sp_Anti[1], upper=Sp_Anti[2]> Sp;                // 抗体検査の特異度
  real<lower=b1[1], upper=b1[2]> p1;
  real<lower=b2[1], upper=b2[2]> p2;
  real<lower=b3[1], upper=b3[2]> p3;
}
transformed parameters{
  real<lower=b4[1], upper=b4[2]> p4 = p3*(1-Sensitivity)/(p3*(1-Sensitivity) + (1-p3)*Specificity);
  real<lower=b5[1], upper=b5[2]> p5 = p3*Sensitivity/(p3*Sensitivity + (1-p3)*(1-Specificity));
  vector<lower=0, upper=1>[Ncat] theta;
  theta[1] = p1*Sn + (1-p1)*(1-Sp);
  theta[2] = p2*Sn + (1-p2)*(1-Sp);
  theta[3] = p3*Sn + (1-p3)*(1-Sp);
  theta[4] = p4*Sn + (1-p4)*(1-Sp);
  theta[5] = p5*Sn + (1-p5)*(1-Sp);
}
model{ // 合計12人が陽性になるようにひたすら回す
  for(i in 0:Ns[5]){
    target += binomial_lpmf(i | Ns[5], theta[5]);
    for(j in 0:Ns[3]){
      target += binomial_lpmf(j | Ns[3], theta[3]);
      for(k in 0:(Ns[4]-i-j)){
        target += binomial_lpmf(k | Ns[4], theta[4]);
        for(l in 0:(N_pos-i-j-k)){
          target += binomial_lpmf(l | Ns[2], theta[2]);
          target += binomial_lpmf(N_pos-l-k-j-i | Ns[1], theta[1]);
        }
      }
    }
  }
}
generated quantities{
  int e[sum(Ns)];
  for(i in 1:Ns[1]) e[i] = bernoulli_rng(p1);
  for(i in sum(Ns[1:(2-1)]):sum(Ns[1:2])) e[i] = bernoulli_rng(p2);
  for(i in sum(Ns[1:(3-1)]):sum(Ns[1:3])) e[i] = bernoulli_rng(p3);
  for(i in sum(Ns[1:(4-1)]):sum(Ns[1:4])) e[i] = bernoulli_rng(p4);
  for(i in sum(Ns[1:(5-1)]):sum(Ns[1:5])) e[i] = bernoulli_rng(p5);
}
"
m0 <- stan_model(model_code=code)

Ns <- c(138, 52, 2, 9, 1)
standata <- list(Ncat=length(Ns), Ns=Ns, N_pos=12, 
                 Sn_PCR=c(0.1, 0.5), Sp_PCR=c(0.99, 1),
                 Sn_Anti=c(0.3, 0.7), Sp_Anti=c(0.8, 0.99),
                 b1=c(0.0001, 0.3), b2=c(0.05, 0.8), b3=c(0.5, 0.99), b4=c(0, 1), b5=c(0.3, 1)
                 )

fit <- sampling(m0, standata, iter=5000, warmup=1000, chain=4)
ex <- extract(fit, pars=head(fit@model_pars, -1))

labs <- c("Sensitivity of PCR", "Specificity of PCR", "Sensitivity of immunoassay", "Specificity of immunoassay", "Prevalence of asymptomatic", "Prevalence of fever within a month", "Prevalence of housemate positive", "Prevalence of PCR negative", "Prevalence of PCR positive")
alpha <- 0.05
cia <- c(alpha/2, 0.5, 1-alpha/2)

xl <- c(0, 1)
par(mfcol=c(2, 5), mar=c(4.5, 4.5, 2.5, 2))
for(i in seq(labs)){
  CrI <- quantile(ex[[i]], cia)
  # sprintf("%.2f [%.2f, %.2f]", CrI[2], CrI[1], CrI[3])
  hist(ex[[i]], main=sprintf("%s\n%.2f [%.2f, %.2f]", labs[i], CrI[2], CrI[1], CrI[3]), freq=FALSE, xlab="Probability", nclass=50, border=NA, col="violet", xlim=xl)
}

CrI <- quantile(rowMeans(ex$e), cia)
hist(rowMeans(ex$e), main=sprintf("%s\n%.2f [%.2f, %.2f]", "Prevalence", CrI[2], CrI[1], CrI[3]), freq=FALSE, xlab="Probability", nclass=50, border=NA, col="lightgreen", xlim=xl)