新型肺炎COVID-19が流行してから全身麻酔件数がどれほど減ったかをrstanで推定する

こんなツイーヨを観測した。

全身麻酔の導入において、気管挿管飛沫感染のリスクが高いので、不要不急の手術は控えるように麻酔科学会、各種外科系学会から提言が出ている。
とはいっても、不要不急の手術ってなんぞや、ということで、手術をしないと死ぬような緊急疾患は麻酔をするとして、癌手術なんかもいずれは手術できなくなりそうである。

上記では、ツイッターで適当に麻酔科から「手術室の運営方針として、手術をどれくらい減らしているか」を聞いていて、(1)制限なし(100%運用)、(2)やや制限(半分以上として70%運用)、(3)制限(半分以下として40%運用)、(4)予定手術はしない、緊急手術のみ対応(12%)、と勝手にした。というのも、制限具合をベータ分布からサンプリングする上でいい感じに設定する必要があるからである。
平均m、分散v=0.01 として、各設定についてベータ分布のパラメータ\alpha\beta は、平均\frac{\alpha}{\alpha + \beta}、分散\frac{\alpha\beta}{(\alpha+\beta)^2(\alpha+\beta+1)}となるから、
\alpha=\frac{m^2(1-m)}{v-m}\beta=(1-m)(\frac{m(1-m)}{v}-1)
ベータ分布を使って、平均的な手術室稼働率の低下具合はこんな感じとする。
f:id:MikuHatsune:20200426225806p:plain

上記のアンケート結果を使って、手術室稼働率の低下具合の週ごとの推移はこんな感じになる。通常通り運営していた施設はどんどん減って、少し制限(半分以上)しながら運営している施設が増えている。大きく制限(半分以下)している施設も遅れて増えてきているが、予定手術はしない、という施設はまだそこまで増加していない。しかし、手術しない、という施設が増えるのも時間の問題のようである。
f:id:MikuHatsune:20200426225909p:plain
推定の問題として、アンケート回答は4つなので、どの回答をするかというパラメータ\thetasimplex であるが、通常通り、という回答をする施設、というか割合は確実に減るはずなので、あるアンケート回答週tにおいて、通常通り、という回答が最初の項だとすると\theta_{t,1}<\theta_{t+1,1} というようにしてorderedでパラメータを作りたかったが、うまくいかなかった。
\theta_tについては時系列サンプリングにしている。あまりよくなかった。

さて、こういう事態になって、実際の全身麻酔件数はどれくらい減っているのかを推定する。全国での全身麻酔の件数は統計があるようなのでここからexcelデータを拾ってくる。
すべて | 統計データを探す | 政府統計の総合窓口

週ごとにアンケートを取ってくれているようなので、少なくとも週に1件以上は全身麻酔をしているデータを対象としようと思ったが、適当に年間100件以上している施設だけ解析に使った。1989件あった。
さて、各施設H_jが、ある週t においてアンケート回答結果がd=\{1,2,3,4\}だったとき、pを手術室運営%とすると
d=1 ならば通常通り100%の手術室運営なので、p \sim U(0.95, 1.05)
d=2 ならば、やや制限(半分以上として70%運用)として、p\sim \beta(0.7)
d=3 ならば、制限(半分以下として40%運用)として、p\sim \beta(0.4)
d=4 ならば、予定手術はしない、緊急手術のみ対応として、p\sim \beta(0.12)
とサンプリングして、前回実績数にp をかけて手術件数を減ずることにする。
結果、こんな感じになった。
最初の頃に比べて、手術件数は2/3になっているようである。
f:id:MikuHatsune:20200426231152p:plain
これの推定の問題として、最初に例えばやや制限(半分以上として70%運用)と答えたとして、次の週以降はやや制限、制限、予定はしない、のいずれかしか答えないだろうが、通常通り、も答えうる、というのがよろしくないので、もう少しサンプリングの制限を加えた推定というかコードにしたいが、とりあえずここまで。

qdate <- c("03-13", "03-20", "03-27", "04-06", "04-10", "04-17", "04-27")
dat <- rbind(
c(89.3, 5.4, 3.6, 1.8),
c(76.6, 17, 2.1, 4.3),
c(82.2, 6.8, 4.1, 6.8),
c(64.4, 23.1, 6.9, 5.6),
c(45.7, 38.5, 13.1, 2.7),
c(26.7, 37.5, 26.7, 9.1),
c(24.7, 37.8, 29.5, 8)
)/100
dat <- dat/rowSums(dat)
colnames(dat) <- c("通常通り", "少し制限(半分以上)", "大きく制限(半分以下)", "原則予定手術なし")

N <- c(113, 47, 73, 160, 221, 232, 288)

datN <- round(dat * N)

# GA のデータは、エクセルの全身麻酔、というところだけあればよい
# 列名をGAにしている
# 下から5行は DPC対象病院群ということになっているので削除
GA <- read.csv("generalanesth.csv")
GAN <- na.omit(head(GA$GA, -5))
GAN12 <- round(GAN[GAN > 100]/12)

m <- c(0.7, 0.4, 0.12)
v <- 0.01
b <- cbind(m^2*(1-m)/v-m, (1-m)*(m*(1-m)/v-1))
x <- seq(0, 1, length=300)
d <- mapply(function(z) dbeta(x, b[z, 1], b[z, 2]), seq(m))
cols <- c("green", "orange", "red")
par(mar=c(5, 5, 2, 2), las=1, cex.lab=1.5)
matplot(x*100, d, type="l", lwd=3, lty=1, xlab="平時と比べて手術件数の減少割合[%]", ylab="確率密度", col=cols)
legend("topright", legend=sprintf("平均%d%sの減少", m*100, "%"), col=cols, pch=15, cex=1.5)


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

code <- "
data {
  int<lower=1> T; // 時間の数
  matrix<lower=0, upper=1>[T, 4] M;
  int<lower=0> N_hospital; // 全身麻酔をしている病院の数
  int<lower=0> Ope[N_hospital]; // 各病院の全身麻酔件数
  int<lower=0> N[T, 4]; // アンケート回答数
  matrix<lower=0>[3, 2] b; // beta distribution
}
parameters {
  simplex[4] y[T];
  real<lower=0> s[4];
}
model {
  for(i in 3:T){
    for(j in 1:4){
      y[i][j] ~ normal(2*y[i-1][j] - y[i-2][j], s[j]);
    }
  }
  for(i in 1:T){
    N[i,] ~ multinomial(y[i]);
  }
}
generated quantities{
  matrix[T, N_hospital] R;
  int<lower=0> idx;
  for(i in 1:T){
    for(j in 1:N_hospital){
      idx = categorical_rng(y[i]);
      // multinomial_rng で整数をサンプリングしたいが、
      // matrix がrealしかもたないのでこうした
      if(idx == 1){
        R[i, j] = Ope[j] * uniform_rng(0.95, 1.05);
      } else if(idx == 2){
        R[i, j] = Ope[j] * beta_rng(b[1, 1], b[1, 2]);
      } else if(idx == 3){
        R[i, j] = Ope[j] * beta_rng(b[2, 1], b[2, 2]);
      } else {
        R[i, j] = Ope[j] * beta_rng(b[3, 1], b[3, 2]);
      }
    }
  }
}
"

m0 <- stan_model(model_code=code)
standata <- list(T=nrow(dat), M=dat, N_hospital=length(GAN12), Ope=GAN12, N=datN, b=b)
fit <- sampling(m0, standata, iter=2000, warmup=1000, 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(4), SIMPLIFY=FALSE), along=3)

# 透明にしながら実線も描けるようにする
cols256 <- list("green", "yellow", "orange", "red")
cols256 <- lapply(lapply(cols256, col2rgb), c)
cols <- mapply(function(z) rgb(z[1], z[2], z[3], maxColorValue=256), cols256)
colsa <- mapply(function(z) rgb(z[1], z[2], z[3], 20, maxColorValue=256), cols256)

yl <- c(0, 1)
par(mar=c(5, 5, 2, 2), cex.lab=1.5, las=1)
matplot(m[2,,], type="n", col=colsa, lty=1, lwd=3, ylim=yl, xaxt="n", xlab="週", ylab="アンケート回答割合")
axis(1, at=seq(qdate), labels=gsub("-", "/", qdate))
legend("topright", legend=colnames(dat), ncol=1, col=cols, pch=15, yjust=-0.1, xpd=TRUE, bty="n", cex=1.4)
for(i in seq(dim(m)[3])){
  lines(c(seq(N)), m[2,,i], col=cols[i], lwd=4)
  polygon(c(c(seq(N)), rev(c(seq(N)))), c(m[1,,i], rev(m[3,,i])), col=colsa[i], border=NA)
}


library(vioplot)
Z <- as.data.frame(t(mapply(function(z) rowSums(ex$R[z,,]), seq(dim(ex$R)[1]))))
Z <- Z/4
par(mar=c(5, 5, 2, 2), cex.lab=1.5, las=0)
plot(c(0.5, length(qdate)+0.5), c(0, 1), type="n", xaxt="n", ylim=c(0, max(Z)), xlab="週", ylab="全施設での全身麻酔件数 [週あたり]")
axis(1, at=seq(qdate), labels=gsub("-", "/", qdate))
vioplot(Z, add=TRUE)
text(seq(qdate), apply(Z, 2, min), sprintf("%.1f%s", colMeans(Z)/colMeans(Z)[1]*100, "%"), pos=1)

新型肺炎COVID-19の無症状感染者の割合を慶応大学病院のPCR検査の結果からrstanで推定する

慶応大学病院で、入院中で(新型肺炎COVID-19について)無症状の患者67人をPCR検査すると、6%(=4人)の患者で陽性だった、という話。
感染症の先生は、めちゃめちゃ単純な推定を使って、一番楽観的な推定で47万人、一番悲観的な推定で430万人、東京に無症状の感染者がいるのではないか、と言っている。


慶応のPCR6%の意味 - 楽園はこちら側

この推定が雑なので、もう少し真面目に考えてみる。
PCR検査を受けたのが67人、PCR陽性だったのが4人、であることは確定したデータであるので、これを使って、無症状(67人は全員無症状である)なのに感染している人の割合をpPCR検査の感度をSn、特異度をSp とする。
N=67人の運命は

真に感染(p) 真に非感染(1-p) 合計
検査で陽性 NpS_n N(1-p)(1-S_p) 4=N\{pS_n+(1-p)(1-S_p)\}
検査で陰性 Np(1-S_n) N(1-p)S_p 63=N\{p(1-S_n)+(1-p)S_p\}
Np N(1-p) N=67

となる。
全体のN=67人から確率\theta=pS_n+(1-p)(1-S_p) で4人の陽性者が出る(真陽性と偽陽性も含む)とすればよい。ここで、PCRの特性として、感度は30-70%、特異度は99%以上、というのが有識者の認識のようなので、感度はS_n\sim U(0.3, 0.7)、特異度はS_p\sim U(0.99, 1)からサンプリングされるとする。これで区間付き推定がそれなりにできるはず。

結果はp=0.14 [0.04, 0.37] となった。およそ14%の人が無症状で感染しているようである。
f:id:MikuHatsune:20200424004911p:plain

 2.5%   50% 97.5%
 73 0.040 0.141 0.366

この結果を単純に東京都の人口1395万人に当てはめると、196万人が無症状で感染しているようである。ただし、慶応大学病院に入院している人が、東京都の人口のランダムサンプリングとはそう簡単には言えないので、もっと多いか、もっと少ないかすらもよくわからない。

   2.5%     50%   97.5% 
 560364 1965582 5102252
library(rstan)
rstan_options(auto_write=TRUE)
options(mc.cores=parallel::detectCores())
code <- "
data{
  int N_keio;
  int P_keio;
  int Pop; // 対象としている人口
  real<lower=0, upper=1> Sn[2];  // 感度の範囲
  real<lower=0, upper=1> Sp[2];  // 特異度の範囲
}
parameters{
  real<lower=0, upper=1> p; // prior probability
  real<lower=Sn[1], upper=Sn[2]> Sensitivity;
  real<lower=Sp[1], upper=Sp[2]> Specificity;
}
transformed parameters{
  real<lower=0, upper=1> theta;
  theta = p*Sensitivity + (1-p)*(1-Specificity);
}
model{
  P_keio ~ binomial(N_keio, theta);
}
generated quantities{
  int e;
  e = binomial_rng(Pop, p);
}
"
m0 <- stan_model(model_code=code)

N_keio <- 67
P_keio <- 4

standata <- list(N_keio=N_keio, P_keio=P_keio, Sn=c(0.3, 0.7), Sp=c(0.99, 1), Pop=13950000)
fit <- sampling(m0, standata, iter=1000, warmup=500, chain=4)
ex <- extract(fit, pars=head(fit@model_pars, -1))

alpha <- 0.05
cia <- c(alpha/2, 0.5, 1-alpha/2)
hist(ex$p, xlab="無症状性有病者の割合", ylab="頻度", freq=FALSE, las=1, main="", col="yellow")

round(quantile(ex$p, cia), 3) # 無症状感染者の割合
round(quantile(ex$e, cia))     # 東京都の人口で考えたときの無症状感染者の人数

新型肺炎COVID-19のSEIRS two stain model がrstanで出来たので満足した

諦めていた。
mikuhatsune.hatenadiary.com
しかし、なんか出来そうになった。
mikuhatsune.hatenadiary.com

\beta型の)コロナウイルスの流行具合から、相互に免疫があるものとして、新型肺炎の今後数年の流行がどうなっていくかをSEIRSモデルで、かつ2種類のウイルス(two strain)がいるモデルでシミュレーションした話。

最初の記事の16コンパートメントの推移は式が間違っていたようなのでがんばって修正した。
普通にRで\frac{dy}{dt}を足し合わせた場合の結果はこんな感じになる。
I_1 のコンパートメントはI_1=I_1S_2+I_1E_2+I_1I_2+I_1R_2
I_2 のコンパートメントはI_2=S_1I_2+E_1I_2+I_1I_2+R_1I_2
とすると、図になる。
f:id:MikuHatsune:20200423211552p:plain

さて、rstanを使ってパラメータの変動を考慮しつつSEIRS two strain モデルをやってみる。
一生懸命コードを書くだけであるが、16コンパートメントのsimplex を時系列T 分だけ定義する。時間依存パラメータ(ここでは\beta(t))も、時間依存でないパラメータたちも全部、ひとまず時間依存パラメータとして(つまりすべてのTで一定)として、パラメータの数と同じ長さをもつvectorをもつvector で定義する。
こんな感じでできた。
f:id:MikuHatsune:20200423212249p:plain

# SEIRS をシミュレーション
# mu beta nu x12 x21 gamma delta1 delta2
library(stringr)
labs <- c(outer(paste0(lab, 1), paste0(lab, 2), paste0))
times <- 0:200
y <- rep(0, 16)
y[c(1, 2, 5)] <- c(0.997, 0.002, 0.001)
Y <- y
p <- c(1/80, 2.2, 1/3, 0.78, 0.51, 1/5, 1/45, 1/45)
for(i in 2:length(times)){
  dydt <- rep(0, length(y))
  I1 <- (y[3]+y[7]+y[11]+y[15])
  I2 <- (y[9]+y[10]+y[11]+y[12])
  dydt[1 ] = -p[2]*(I1+I2)*y[1] + p[7]*y[4] + p[8]*y[13] + p[1] - y[1]*p[1];
  dydt[2 ] = -(p[1]+p[3]+(1-p[4])*p[2]*I2)*y[2]  + p[2]*I1*y[1] + p[8]*y[14];
  dydt[3 ] = -(p[1]+p[6]+(1-p[4])*p[2]*I2)*y[3]  + p[3]*y[2]    + p[8]*y[15];
  dydt[4 ] = -(p[1]+p[7]+(1-p[4])*p[2]*I2)*y[4]  + p[6]*y[3]    + p[8]*y[16];
  dydt[5 ] = -(p[1]+p[3]+(1-p[5])*p[2]*I1)*y[5]  + p[2]*I2*y[1] + p[7]*y[8];
  dydt[9 ] = -(p[1]+p[6]+(1-p[5])*p[2]*I1)*y[9]  + p[3]*y[5]    + p[7]*y[12];
  dydt[13] = -(p[1]+p[8]+(1-p[5])*p[2]*I1)*y[13] + p[6]*y[9]    + p[7]*y[16];

  dydt[6 ] = -(2*p[3]+p[1])*y[6] + p[2]*((1-p[4])*I2*y[2] + (1-p[5])*I1*y[5]);
  dydt[7 ] = -(p[1]+p[3]+p[6])*y[7] + (1-p[4])*p[2]*I2*y[3] + p[3]*y[6];
  dydt[8 ] = -(p[1]+p[3]+p[7])*y[8] + (1-p[4])*p[2]*I2*y[4] + p[6]*y[7];
  dydt[10] = -(p[1]+p[3]+p[6])*y[10] + (1-p[5])*p[2]*I1*y[9] + p[3]*y[6];
  dydt[14] = -(p[1]+p[3]+p[8])*y[14] + (1-p[5])*p[2]*I1*y[13] + p[6]*y[10];

  dydt[11] = -(2*p[6]+p[1])*y[11] + p[3]*(y[7]+y[10]);
  dydt[12] = -(p[1]+p[6]+p[7])*y[12] + p[3]*y[8] + p[6]*y[11];
  dydt[15] = -(p[1]+p[6]+p[8])*y[15] + p[3]*y[14] + p[6]*y[11];
  dydt[16] = -(p[1]+p[7]+p[8])*y[16] + p[6]*(y[12]+y[15]);
  Y <- rbind(Y, y+dydt)
  y <- y + dydt
}

i1 <- rep(1:4, 4)
i2 <- rep(1:4, each=4)
i12 <- list(i1, i2)

Y0 <- c(list(Y), mapply(function(z) t(apply(Y, 1, tapply, z, sum)), i12, SIMPLIFY=FALSE))

cols <- sample(na.omit(str_extract(colors(), "^(?!.*gr[ea]y).*$")), length(labs))
lwd <- 3
par(mfrow=c(1, 3), las=1, mar=c(4, 5, 2, 2), cex.lab=1.5)
matplot(Y0[[1]], type="l", lty=1, lwd=lwd, xlab="Time", ylab="Proportion", col=cols)
legend("topright", legend=labs, ncol=4, col=cols, pch=15, xpd=TRUE, bty="n", cex=1.5)
ci <- tapply(sample(seq(labs), 8), rep(1:2, each=4), c)
for(i in 2:3){
  matplot(Y0[[i]], type="l", lty=1, lwd=lwd, col=cols[ci[[i-1]]], xlab="Time", ylab="Proportion")
  legend("topright", legend=sprintf("%s%d", lab, i-1), ncol=4, col=cols[ci[[i-1]]], pch=15, xpd=TRUE, bty="n", cex=1.5)
}

# beta をb(t) にしたうえでSEIRS
library(rstan)
rstan_options(auto_write=TRUE)
options(mc.cores=parallel::detectCores())

code <- "
data {
   int<lower=1> T; // 時間の数
   int<lower=1> nC; // コンパートメントの数
   real TS[T]; // 各時刻
   real Y0[nC]; // Y の初期値 SEIR
   real m01[2]; // mu     の範囲
   real n01[2]; // nu     の範囲
   real x01[2]; // x12    の範囲
   real x02[2]; // x21    の範囲
   real g01[2]; // gamma  の範囲
   real d01[2]; // delta1 の範囲
   real d02[2]; // delta2 の範囲
   real p01[2]; // phi    の範囲
   real f01[2]; // f      の範囲
   real R01[2]; // R0     の範囲

}
parameters {
   real<lower=m01[1], upper=m01[2]> mu;
   real<lower=n01[1], upper=n01[2]> nu;
   real<lower=x01[1], upper=x01[2]> x12;
   real<lower=x02[1], upper=x02[2]> x21;
   real<lower=g01[1], upper=g01[2]> gamma;
   real<lower=d01[1], upper=d01[2]> delta1;
   real<lower=d02[1], upper=d02[2]> delta2;
   real<lower=p01[1], upper=p01[2]> phi;
   real<lower=f01[1], upper=f01[2]> f;
   real<lower=R01[1], upper=R01[2]> R0;
}
transformed parameters {
   simplex[16] y[T];
   vector<lower=0>[8] p[T];
   real<lower=0> I1[T];
   real<lower=0> I2[T];
   y[1] = to_vector(Y0); // Initialize
   for(i in 1:T){
     p[i][1] = mu;
     p[i][2] = gamma * R0 * (f/2*cos(2*pi()/52*(TS[i]-phi)) + (1-f/2)); // beta
     p[i][3] = nu;
     p[i][4] = x12;
     p[i][5] = x21;
     p[i][6] = gamma;
     p[i][7] = delta1;
     p[i][8] = delta2;
   }
   for(i in 1:(T-1)){
     // S1S2 E1S2 I1S2 R1S2 1-4
     // S1E2 E1E2 I1E2 R1E2 5-8
     // S1I2 E1I2 I1I2 R1I2 9-12
     // S1R2 E1R2 I1R2 R1R2 13-16
     I1[i] = (y[i][3]+y[i][7] +y[i][11]+y[i][15]);
     I2[i] = (y[i][9]+y[i][10]+y[i][11]+y[i][12]);
     y[i+1][1 ] = y[i][1 ] + -p[i][2]*(I1[i]+I2[i])*y[i][1] + p[i][7]*y[i][4] + p[i][8]*y[i][13] + p[i][1] - y[i][1]*p[i][1];
     y[i+1][2 ] = y[i][2 ] + -(p[i][1]+p[i][3]+(1-p[i][4])*p[i][2]*I2[i])*y[i][2]  + p[i][2]*I1[i]*y[i][1] + p[i][8]*y[i][14];
     y[i+1][3 ] = y[i][3 ] + -(p[i][1]+p[i][6]+(1-p[i][4])*p[i][2]*I2[i])*y[i][3]  + p[i][3]*y[i][2]    + p[i][8]*y[i][15];
     y[i+1][4 ] = y[i][4 ] + -(p[i][1]+p[i][7]+(1-p[i][4])*p[i][2]*I2[i])*y[i][4]  + p[i][6]*y[i][3]    + p[i][8]*y[i][16];
     y[i+1][5 ] = y[i][5 ] + -(p[i][1]+p[i][3]+(1-p[i][5])*p[i][2]*I1[i])*y[i][5]  + p[i][2]*I2[i]*y[i][1] + p[i][7]*y[i][8];
     y[i+1][9 ] = y[i][9 ] + -(p[i][1]+p[i][6]+(1-p[i][5])*p[i][2]*I1[i])*y[i][9]  + p[i][3]*y[i][5]    + p[i][7]*y[i][12];
     y[i+1][13] = y[i][13] + -(p[i][1]+p[i][8]+(1-p[i][5])*p[i][2]*I1[i])*y[i][13] + p[i][6]*y[i][9]    + p[i][7]*y[i][16];

     y[i+1][6 ] = y[i][6 ] + -(2*p[i][3]+p[i][1])*y[i][6] + p[i][2]*((1-p[i][4])*I2[i]*y[i][2] + (1-p[i][5])*I1[i]*y[i][5]);
     y[i+1][7 ] = y[i][7 ] + -(p[i][1]+p[i][3]+p[i][6])*y[i][7]  + (1-p[i][4])*p[i][2]*I2[i]*y[i][3]  + p[i][3]*y[i][6];
     y[i+1][8 ] = y[i][8 ] + -(p[i][1]+p[i][3]+p[i][7])*y[i][8]  + (1-p[i][4])*p[i][2]*I2[i]*y[i][4]  + p[i][6]*y[i][7];
     y[i+1][10] = y[i][10] + -(p[i][1]+p[i][3]+p[i][6])*y[i][10] + (1-p[i][5])*p[i][2]*I1[i]*y[i][9]  + p[i][3]*y[i][6];
     y[i+1][14] = y[i][14] + -(p[i][1]+p[i][3]+p[i][8])*y[i][14] + (1-p[i][5])*p[i][2]*I1[i]*y[i][13] + p[i][6]*y[i][10];

     y[i+1][11] = y[i][11] + -(2*p[i][6]+p[i][1])*y[i][11] + p[i][3]*(y[i][7]+y[i][10]);
     y[i+1][12] = y[i][12] + -(p[i][1]+p[i][6]+p[i][7])*y[i][12] + p[i][3]*y[i][8]  + p[i][6]*y[i][11];
     y[i+1][15] = y[i][15] + -(p[i][1]+p[i][6]+p[i][8])*y[i][15] + p[i][3]*y[i][14] + p[i][6]*y[i][11];
     y[i+1][16] = y[i][16] + -(p[i][1]+p[i][7]+p[i][8])*y[i][16] + p[i][6]*(y[i][12]+y[i][15]);
   }
   I1[T] = (y[T][3]+y[T][7] +y[T][11]+y[T][15]);
   I2[T] = (y[T][9]+y[T][10]+y[T][11]+y[T][12]);

}
model {
   mu     ~ uniform(m01[1], m01[2]);
   nu     ~ uniform(n01[1], n01[2]);
   x12    ~ uniform(x01[1], x01[2]);
   x21    ~ uniform(x02[1], x02[2]);
   gamma  ~ uniform(g01[1], g01[2]);
   delta1 ~ uniform(d01[1], d01[2]);
   delta2 ~ uniform(d02[1], d02[2]);
   phi    ~ uniform(p01[1], p01[2]);
   f      ~ uniform(f01[1], f01[2]);
   R0     ~ uniform(R01[1], R01[2]);
}

"

m0 <- stan_model(model_code=code)
times <- 0:200
y0 <- rep(0, 16)
y0[c(2, 5)] <- c(0.002, 0.001)
y0[1] <- 1-sum(y0)
standata <- list(T=length(times), TS=times, Y0=y0, nC=length(y0),
                 m01=1/c(81, 79), n01=1/c(14, 3), x01=c(0, 1), x02=c(0, 1),
                 g01=1/c(14, 3), d01=1/c(100, 25), d02=1/c(100, 25),
                 f01=c(0.18, 0.25), p01=c(1.5, 2.4), R01=c(2, 3))

standata <- list(T=length(times), TS=times, Y0=y0, nC=length(y0),
                 m01=1/c(81, 79), n01=1/c(14, 3), x01=c(0.5, 1), x02=c(0, 0.5),
                 g01=1/c(14, 3), d01=1/c(50, 20), d02=1/c(30, 25),
                 f01=c(0.18, 0.25), p01=c(1.5, 2.4), R01=c(3, 5))

fit <- sampling(m0, standata, iter=1000, warmup=0, 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) cbind(c(0, y0[z], 0), apply(ex$y[,,z], 2, quantile, cia)), seq(y0), SIMPLIFY=FALSE), along=3)

lab <- c("S", "E", "I", "R")
labs <- c(outer(paste0(lab, 1), paste0(lab, 2), paste0))
names(y0) <- labs

# 透明にしながら実線も描けるようにする
cols256 <- list(S1S2='lightgreen', E1S2='yellow', I1S2='orange', R1S2='blueviolet',
                S1E2='yellow2', E1E2='yellow3', I1E2='orange2', R1E2='cadetblue',
                S1I2='orangered', E1I2='orangered2', I1I2='orangered3', R1I2='deepskyblue',
                S1R2='powderblue', E1R2='royalblue', I1R2='skyblue', R1R2='pink')
cols256 <- lapply(lapply(cols256, col2rgb), c)

cols <- mapply(function(z) rgb(z[1], z[2], z[3], maxColorValue=256), cols256)
colsa <- mapply(function(z) rgb(z[1], z[2], z[3], 20, maxColorValue=256), cols256)

par(mar=c(5, 5, 2, 2), cex.lab=1.5, las=1)
matplot(m[2,,], type="n", col=colsa, lty=1, lwd=3, xlab="Time (days)", ylab="Proportion of S/E/I/R patients")
#legend(0, 1, names(y0), ncol=length(y0), col=cols, pch=15, yjust=-0.1, xpd=TRUE, bty="n", cex=1.6)
legend("topright", legend=names(y0), ncol=4, col=cols, pch=15, yjust=-0.1, xpd=TRUE, bty="n", cex=1.4)
#legend(par()$usr[2], 1, names(y0), col=cols, pch=15, xpd=TRUE, bty="n", cex=1.6)
for(i in seq(dim(m)[3])){
  lines(c(0, times), m[2,,i], col=cols[i], lwd=4)
  polygon(c(c(0, times), rev(c(0, times))), c(m[1,,i], rev(m[3,,i])), col=colsa[i], border=NA)
}

###
library(googleVis)
library(stringr)
gmat <- cbind.data.frame(Time=times, head(m[2,,], -1))
colnames(gmat) <- c("Times", labs)
url <- "https://science.sciencemag.org/content/early/2020/04/14/science.abb5793"
webpage <- "Science. 2020 Apr 14. pii: eabb5793. doi: 10.1126/science.abb5793."
headertxt <- sprintf("<span><a href='%s' target='_blank'>%s</a></span> on %s", url, webpage, Sys.Date())
# googlevis 用のプロットオプション
ops <- list(
  fontSize=12, height=600, width=600, lineWidth=1,
  vAxis="{title:'Proportion', fontSize: 20, titleTextStyle: {fontSize: 20, bold: 'true', italic: 'false'}, viewWindow: {min: 0, max: 1}, gridlines: {color: '#00F', minSpacing: 100}, logScale: 'false'}",
  sizeAxis="{minValue: -80, maxValue: 75, maxSize: 15}",
  hAxis="{title:'Time', fontSize: 20, titleTextStyle: {fontSize: 20, bold: 'true', italic: 'false'}, viewWindow: {min: 0, max: 205}}",
  explorer="{actions: ['dragToZoom', 'rightClickToReset']}",
  focusTarget="category",
  selectionMode="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)




LineChartID105e2da79c06






Science. 2020 Apr 14. pii: eabb5793. doi: 10.1126/science.abb5793. on 2020-04-23

新型肺炎COVID-19の感染推移を時間依存パラメータを含むSEIRSモデルで推定しようと思って断念していたが出来るかもしれない

諦めていた。
mikuhatsune.hatenadiary.com
というのも、SEIRSモデルでS→Eになるパラメータ\beta が、時間依存的なパラメータとして
\beta(t)=\gamma R_0 \left[\frac{f}{2}\cos(\frac{2\pi}{52}(t-\phi))-(1-\frac{f}{2})\right]
で定義されるが、これはrstanではできなさそうである。というのが、integrate_ode関数が時間依存のパラメータをどう頑張っても取れないみたいだからである。
ODEs with time dependent parameters - Modeling - The Stan Forums

というわけでstanの神が助言してくれた。


S/E/I/R の各コンパートメントは、ある時刻t においてすべて足せば和は1になるので、simplexを必要な時刻数分だけリストか何かで持っておいて、transformed parametersブロック内でぶん回せばよさそうである。
まずは普通に\frac{dy}{dt}を地味に計算してSEIRモデルをシミュレーションした。
f:id:MikuHatsune:20200421183612p:plain

さて、rstan でtransformed parametersブロックにsimplexを各時刻に定義してリストとして持ち、上記式がf\sim U(0.18, 0.25), \phi\sim U(1.5, 2), R_0\sim U(2, 3) でサンプリングされて\beta(t)が決まる、としてコードを実行した。
推定区間がついたSEIRの推移が推定できた。
f:id:MikuHatsune:20200421183631p:plain

times <- 0:200
f <- 0.21
phi <- 2
R0 <- 2.2
Rt <- R0*(f/2*cos(2*pi/52*(times-phi)) + (1-f/2))
plot(times, Rt)

g <- 1.2    # gamma
d <- 0.3    # delta
b <- g * Rt # beta
e <- 0.4   # eta

y0 <- c(0.999, 0.001, 0, 0)
names(y0) <- c("S", "E", "I", "R")
res <- rbind.data.frame(y0, matrix(0, nr=length(times)-1, nc=4))
colnames(res) <- c("S", "E", "I", "R")
for(i in 2:length(times)){
  dS <- -b[i-1]*res$S[i-1]*res$I[i-1] + e*res$R[i-1]
  dE <-  b[i-1]*res$S[i-1]*res$I[i-1] - d*res$E[i-1]
  dI <-  d*res$E[i-1] - g*res$I[i-1]
  dR <-  g*res$I[i-1] - e*res$R[i-1]
  res[i, ] <- res[i-1,] + c(dS, dE, dI, dR)
}
cols <- c("black", "orange", "red", "lightgreen")
matplot(res, type="l", lty=1, lwd=3, col=cols, xlab="Time", ylab="Proportion")
legend("topright", legend=colnames(res), pch=15, col=cols)

# simplex でやってみる
code <- "
data {
   int<lower=1> T; // 時間の数
   real TS[T]; // 各時刻
   real Y0[4]; // Y の初期値 SEIR
   real b[T];
   real d;
   real g;
   real e;
}
parameters {
  real a; // dummy
}
transformed parameters {
   simplex[4] y_[T];
   y_[1] = to_vector(Y0);
   for(i in 2:T){
     y_[i][1] = y_[i-1][1] + -b[i-1]*y_[i-1][1]*y_[i-1][3] + e*y_[i-1][4];
     y_[i][2] = y_[i-1][2] +  b[i-1]*y_[i-1][1]*y_[i-1][3] - d*y_[i-1][2];
     y_[i][3] = y_[i-1][3] +  d*y_[i-1][2] - g*y_[i-1][3];
     y_[i][4] = y_[i-1][4] +  g*y_[i-1][3] - e*y_[i-1][4];
   }
}
model {
  a ~ uniform(0, 1); // 何もしない
}
"
m0 <- stan_model(model_code=code)
standata <- list(T=length(times), TS=times, Y0=y0, b=b, d=d, g=g, e=e)
fit <- sampling(m0, standata, iter=200, warmup=100, chain=2)
ex <- extract(fit, pars=head(fit@model_pars, -1))

# beta をb(t) にする
code <- "
data {
   int<lower=1> T; // 時間の数
   real TS[T]; // 各時刻
   real Y0[4]; // Y の初期値 SEIR
   // real b[T];
   real d;
   real g;
   real e;
   //real b01[2]; // beta の範囲
   real p01[2]; // phi  の範囲
   real f01[2]; // f の範囲
   real R01[2]; // R0 の範囲
}
parameters {
   real<lower=p01[1], upper=p01[2]> phi;
   real<lower=f01[1], upper=f01[2]> f;
   real<lower=R01[1], upper=R01[2]> R0;
}
transformed parameters {
   simplex[4] y_[T];
   real<lower=0> b[T];
   y_[1] = to_vector(Y0);
   for(i in 1:T){
     b[i] = g * R0 * (f/2*cos(2*pi()/52*(TS[i]-phi)) + (1-f/2));
   }
   // b = g * Rt;
   for(i in 2:T){
     y_[i][1] = y_[i-1][1] + -b[i-1]*y_[i-1][1]*y_[i-1][3] + e*y_[i-1][4];
     y_[i][2] = y_[i-1][2] +  b[i-1]*y_[i-1][1]*y_[i-1][3] - d*y_[i-1][2];
     y_[i][3] = y_[i-1][3] +  d*y_[i-1][2] - g*y_[i-1][3];
     y_[i][4] = y_[i-1][4] +  g*y_[i-1][3] - e*y_[i-1][4];
   }
}
model {
   f   ~ uniform(f01[1], f01[2]);
   phi ~ uniform(p01[1], p01[2]);
   R0  ~ uniform(R01[1], R01[2]);
}
"

m0 <- stan_model(model_code=code)
standata <- list(T=length(times), TS=times, Y0=c(0.999, 0.001, 0, 0), d=d, g=g, e=e,
                 f01=c(0.18, 0.25), p01=c(1.5, 2.4), R01=c(2, 3))
fit <- sampling(m0, standata, iter=1000, warmup=500, 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) cbind(c(0, y0[z], 0), apply(ex$y_[,,z], 2, quantile, cia)), seq(y0), SIMPLIFY=FALSE), along=3)

parms <- apply(ex$b, 2, quantile, cia)

lab <- c("S", "E", "I", "R")
# 透明にしながら実線も描けるようにする
cols256 <- list(S=c(0, 255, 0), E=c(255, 202, 5), I=c(255, 0, 0), R=c(0, 255, 255))
cols <- mapply(function(z) rgb(z[1], z[2], z[3], maxColorValue=256), cols256)
colsa <- mapply(function(z) rgb(z[1], z[2], z[3], 20, maxColorValue=256), cols256)

par(mar=c(5, 5, 3, 5), cex.lab=1.5, las=1)
matplot(m[2,,], type="n", col=colsa, lty=1, lwd=3, xlab="Time (days)", ylab="Proportion of S/E/I/R patients")
#legend(0, N, names(y0), ncol=length(y0), col=cols, pch=15, yjust=-0.1, xpd=TRUE, bty="n", cex=2)
legend(par()$usr[2], 1, names(y0), col=cols, pch=15, xpd=TRUE, bty="n", cex=2)
for(i in seq(dim(m)[3])){
  lines(c(0, times), m[2,,i], col=cols[i], lwd=4)
  polygon(c(c(0, times), rev(c(0, times))), c(m[1,,i], rev(m[3,,i])), col=colsa[i], border=NA)

}

新型肺炎COVID-19が今後2022年まで流行が続くというのをrstanで再現しようとしたが断念した

読んだ。
Projecting the transmission dynamics of SARS-CoV-2 through the postpandemic period. - PubMed - NCBI
COVID-19がこのままだと2022年も続いているのではないか、といろいろ話題になった論文。
githubがおいてあるが、ここにあるデータは
GitHub - c2-d2/CoV-seasonality: Code for regression model from Kissler, Tedijanto et al. 2020.

コロナウイルスの流行具合をCDCのNREVSSというデータベースから2015-2019年の間までデータを使った、というが、肝心のデータは2018年4月以降しかもう置いてないっぽい。
NREVSS | Coronavirus National Trends | CDC
https://www.cdc.gov/surveillance/nrevss/images/coronavirus/Corona4PP_Nat.htm
こちらの過去の報告の図からデータを引っこ抜ければ、2014-2017年はデータが取れるが、めんどうなのでやめた。そもそも再現できないじゃん…
Human coronavirus circulation in the United States 2014-2017. - PubMed - NCBI

コロナウイルスの検査数と、インフルエンザ様症状(ILI)の受診割合でかけることで、週ごとの(\beta型)コロナウイルスの頻度、としている。日ごとのR_u

R_u=\displaystyle\sum_{t=u}^{u+i_{max}}\frac{b(t)g(t-u)}{\displaystyle\sum_{a=0}^{i_{max}}b(t-a)g(a)}
b(t):時刻tの株(strain)ごとの発生率
g(a):時刻a のgeneration interval。論文では平均8.4日、標準偏差3.8日のweibull 分布を使っているが、githubでは他の報告の対数正規分布やガンマ分布も使っている。
i_{max}:generation interval が累積確率分布で99%になる日。論文では19日としている。
github ではfunc.WTという関数で定義されているが、各株のデータのはいっているdata.frameを入力するようなので、vectorを入力するような感じに書き換えた。

func.SI_pull <- function(value, serial_int){
  if(serial_int == "SARS"){
    return(dweibull(value, shape=SARS_shape, scale=SARS_scale))
  } else if(serial_int == "nCoV_Li"){
    return(dgamma(value, shape=nCoV_Li_shape, scale=nCoV_Li_scale))
  } else if(serial_int == "nCoV_Nishiura"){
    return(dweibull(value, shape=nCoV_Nishiura_shape, scale=nCoV_Nishiura_scale))
  }
}
func.WT <- function(week_list, daily_inc, proxy_name, org_list, serial_int){
  df.Reff_results <- data.frame(Week_start=week_list) # Initialize dataframe to hold results
  stop <- get(paste0(serial_int, "_stop")) 
  # Estimate daily R effective
  for(v in org_list){ #For each strain
    RDaily <- numeric()
    for(u in 1:(length(week_list)*7)){ #Estimate for each day
      sumt <- 0
      for(t in u:(u+stop)){ #Look ahead starting at day u through (u+max SI)
        suma <- 0 
        for(a in 0:(stop)){ #Calc denominator, from day t back through (t-max SI)
          suma = daily_inc[t-a,v]*func.SI_pull(a, serial_int) + suma
        } 
        sumt = (daily_inc[t,v]*func.SI_pull(t-u, serial_int))/suma + sumt
      }
      RDaily[u] = sumt
    }   
    # Take geometric mean over daily values for current week, and one week before + after (for smoothing), to get weekly estimates
    RWeekly <- numeric()
    for(i in 1:length(week_list)) RWeekly[i] <- geoMean(as.numeric(RDaily[max(1,7*(i-2)+1):min(length(RDaily),7*(i+1))]), na.rm=TRUE)
    RWeekly[1:3] <- NA #Set results for first 3 weeks to NA (unreliable using WT)
    RWeekly[(length(RWeekly)-2):length(RWeekly)] <- NA #Set results for last 3 weeks to NA (unreliable using WT)
    df.Reff_results[,paste0(v,"_R_",proxy_name)] <- RWeekly
  } 
  return(df.Reff_results)
}
Ru <- function(vec, pdf="weibull", pars=c(2.35, 9.48), alpha=0.99){
  Imax <- eval(parse(text=sprintf("ceiling(q%s(%f, %f, %f))", pdf, alpha, pars[1], pars[2])))
  g <- function(x) eval(parse(text=sprintf("d%s(%f, %f, %f)", pdf, x, pars[1], pars[2])))
  res <- mapply(function(u)  sum(
         mapply(function(t) (vec[t]*g(t-u))/sum(mapply(function(a) vec[t-a]*g(a), 0:Imax)), u:(u+Imax))
         ),  (Imax+1):(length(RDaily)-Imax))
  NAs <- rep(NA, Imax)
  RDaily <- c(NAs, res, NAs)
  RWeekly <- mapply(function(i) geoMean(RDaily[max(1,7*(i-2)+1):min(length(RDaily),7*(i+1))], na.rm=TRUE), 1:(ceiling(length(RDaily)/7))
  RWeekly[1:3] <- RWeekly[(length(RWeekly)-2):length(RWeekly)] <- NA
  retuen(list(RDaily, RWeekly))
}

感染の流行はtwo-strain SEIRS モデルというものを使っている。要は図のSEIRS x two strain の16コンポーネント微分方程式になるのだが、解けるわけがないので前にやったとおりrstanで推定してみる。
mikuhatsune.hatenadiary.com
16コンポーネントはこんな感じになる。
f:id:MikuHatsune:20200419224151p:plain

さて、このSEIRSモデルに\mu, \beta(t), \nu, \chi_{12}, \chi_{21}, \gamma, \delta_1, \delta_2 の8つのパラメータが設定してある。ここで、\beta(t) だけが時間依存のパラメータで、再生産係数R_0 に対して
\beta(t)=\gamma R_0(t)
R_0=\max(R_0)\left[\frac{f}{2}\cos(\frac{2\pi}{52}(t-\phi))+(1-\frac{f}{2})\right]
と、R_0(t)に依存することになる。
これをrstanで頑張ってみようと思ったが、rstanで微分方程式を作ったときに、パラメータは各時刻tですべて一定でしか(自分の力量では)出来ないので、今回の再現では\betaは一様分布からサンプリングされる決め打ちのシミュレーションになってしまった。誰か時間依存\beta(t) のままやれる方法を教えてほしい。f:id:MikuHatsune:20200419224943p:plain
16コンパートメントもあるとゴチャゴチャするので、googlevisも作ってみた。

(htmlが破壊されてレイアウトが変になったので削除しました)


うまく再現できなかったがこれも引っ張り過ぎると他のことが進まなくなるのでここらへんで終了。

# SEIRS two-strain 16 compornents
lab <- c("S", "E", "I", "R")
labs <- c(outer(paste0(lab, 1), paste0(lab, 2), paste0))
library(igraph)
mat <- as.matrix(read.table(text="
0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0
0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0
1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0
0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0
0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0
0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0
0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0
0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0
0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0
0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0
0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1
1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0
0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0
0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1
0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0
", header=FALSE))
rownames(mat) <- colnames(mat) <- labs
g <- graph_from_adjacency_matrix(mat)

rot <- -pi/4
lay <- cbind(rep(c(-3,-1,1,3), each=4), rep(c(3,1,-1,-3), 4))
lay <- t(matrix(c(cos(rot), sin(rot), -sin(rot), cos(rot)), 2) %*% t(lay))


par(mar=rep(0.5, 4))
V(g)$label.cex <- 1.5
V(g)$label.font <- 2
V(g)$size <- 25
E(g)$curved <- 0 #0.05
E(g)$curved[c(7, 25, 31:32)] <- c(1, -1) #0.05
E(g)$curved[c(15, 27, 23, 29)] <- c(1, -1)*0.7 #0.05
plot(g, layout=lay)

# SEIRS two strain
library(rstan)
rstan_options(auto_write=TRUE)
options(mc.cores=parallel::detectCores())

code <- "
functions {
   real[] sir(
      real t,
      real[] y,
      real[] p, // mu beta nu x12 x21 gamma delta1 delta2
      real[] x_r,
      int[] x_i
   ) {
     // S1S2 E1S2 I1S2 R1S2 1-4
     // S1E2 E1E2 I1E2 R1E2 5-8
     // S1I2 E1I2 I1I2 R1I2 9-12
     // S1R2 E1R2 I1R2 R1R2 13-16
      real dydt[16];
      dydt[1] = -p[1]*y[1]*y[3]-p[2]*y[1]*(y[3]+y[7]+y[11]+y[15])-p[2]*y[1]*(y[9]+y[10]+y[11]+y[12])+p[7]*y[4]+p[8]*y[13];
      dydt[2] = p[1]*y[1]*y[3]-p[1]*y[2]-p[3]*y[2]-(1-p[4])*p[2]*y[2]*(y[9]+y[10]+y[11]+y[12])+p[8]*y[14];
      dydt[3] = p[3]*y[2]-p[6]*y[3]-p[1]*y[3]-(1-p[4])*y[3]*(y[9]+y[10]+y[11]+y[12])+p[8]*y[15];
      dydt[4] = -(p[1]+p[7]+(1-p[4])*(y[9]+y[10]+y[11]+y[12]))*y[4]+p[6]*y[3]+p[8]*y[16];
      dydt[5] = -(p[1]+p[3]+(1-p[5])*p[2]*(y[3]+y[7]+y[11]+y[15]))*y[5]+p[2]*(y[9]+y[10]+y[11]+y[12])*y[1]+p[7]*y[8];
      dydt[6] = -(p[1]+2*p[3])*y[6]+p[2]*((1-p[4])*(y[9]+y[10]+y[11]+y[12])*y[2]+(1-p[5])*(y[3]+y[7]+y[11]+y[15])*y[5]);
      dydt[7] = -(p[1]+p[3]+p[6])*y[7]+p[3]*y[6]+(1-p[4])*p[2]*(y[9]+y[10]+y[11]+y[12])*y[3];
      dydt[8] = -(p[1]+p[3]+p[7])*y[8]+p[6]*y[7]+(1-p[5])*p[2]*(y[9]+y[10]+y[11]+y[12])*y[4];
      dydt[9] = -(p[1]+p[6]+(1-p[6])*p[2]*(y[3]+y[7]+y[11]+y[15]))*y[9]+p[7]*y[12]+p[3]*y[5];
      dydt[10]= -(p[1]+p[3]+p[6])*y[7]+p[3]*y[6]+(1-p[5])*p[2]*(y[3]+y[7]+y[11]+y[15])*y[9];
      dydt[11]= -(p[1]+2*p[6])*y[11]+p[3]*(y[7]+y[10]);
      dydt[12]= -(p[1]+p[6]+p[7])*y[12]+p[3]*y[8]+p[6]*y[11];
      dydt[13]= -(p[1]+p[8]+(1-p[6])*p[2]*(y[3]+y[7]+y[11]+y[15]))*y[13]+p[6]*y[9]+p[7]*y[16];
      dydt[14]= -(p[1]+p[3]+p[8])*y[14]+p[6]*y[10]+(1-p[6])*p[2]*(y[3]+y[7]+y[11]+y[15])*y[13];
      dydt[15]= -(p[1]+p[6]+p[8])*y[15]+p[3]*y[14]+p[6]*y[11];
      dydt[16]= -(p[1]+p[7]+p[8])*y[16]+p[6]*(y[12]+y[15]);
      return dydt;
   }
}
data {
   int<lower=1> T; // 時間の数
   real T0; // time 0
   real TS[T]; // 各時刻
   real Y0[16]; // Y の初期値 SEIR
   real m01[2]; // mu   の範囲
   real b01[2]; // beta の範囲
   real n01[2]; // nu   の範囲
   real x12[2]; // x12  の範囲
   real x21[2]; // x21  の範囲
   real g01[2]; // gamma  の範囲
   real d1[2]; // delta1 の範囲
   real d2[2]; // delta2 の範囲
}
transformed data {
   real x_r[0];
   int x_i[0];
}
parameters {
   real<lower=m01[1], upper=m01[2]> mu;
   real<lower=b01[1], upper=b01[2]> beta;
   real<lower=n01[1], upper=n01[2]> nu;
   real<lower=x12[1], upper=x12[2]> X12;
   real<lower=x21[1], upper=x21[2]> X21;
   real<lower=g01[1], upper=g01[2]> gamma;
   real<lower=d1[1], upper=d1[2]> delta1;
   real<lower=d2[1], upper=d2[2]> delta2;
}
transformed parameters {
   real y_hat[T,16];
   real theta[8];
   theta[1] = mu;
   theta[2] = beta;
   theta[3] = nu;
   theta[4] = X12;
   theta[5] = X21;
   theta[6] = gamma;
   theta[7] = delta1;
   theta[8] = delta2;
   y_hat = integrate_ode(sir, Y0, T0, TS, theta, x_r, x_i);
}
model {
  // 何もしない
}
"

m0 <- stan_model(model_code=code)
y0 <-  replace(rep(0, 16), 1, 0) # 初期値
y0[c(3, 7, 11, 15, 9, 10, 12)] <- 0.0005
y0 <- replace(y0, 1, 1-sum(y0))
times <- seq(0, 100, by=1)[-1] # 時刻 0.2 刻みはどういうこと?

# 各パラメータを振る範囲
lim <- list(m01=1/c(81, 79), b01=1/c(14, 1), n01=1/c(14, 3), x12=c(0, 1), x21=c(0, 1), g01=1/c(14, 3), d1=1/c(100, 25), d2=1/c(100, 25))

standata <- c(list(T=length(times), T0=0, TS=times, Y0=y0), lim)
fit <- sampling(m0, standata, iter=200, warmup=100, chain=2)
ex <- extract(fit, pars=head(fit@model_pars, -1))

cols <- matlab::jet.colors(length(labs))
i <- 23 # たまたまこのシミュレーションが見栄えがよかったので
matplot(ex$y_hat[i,,], type="l", lty=1, col=cols, lwd=2, xlab="time", ylab="proportion")
legend("topright", legend=labs, ncol=4, col=cols, pch=15, bg="white")

# googlevis で可視化
library(googleVis)
library(stringr)
gmat <- cbind.data.frame(Time=times, ex$y_hat[i,,])
colnames(gmat) <- c("Times", labs)
url <- "https://science.sciencemag.org/content/early/2020/04/14/science.abb5793"
webpage <- "Science. 2020 Apr 14. pii: eabb5793. doi: 10.1126/science.abb5793."
headertxt <- sprintf("<span><a href='%s' target='_blank'>%s</a></span> on %s", url, webpage, Sys.Date())
# googlevis 用のプロットオプション
ops <- list(
  fontSize=12, height=600, width=600, lineWidth=1,
  vAxis="{title:'Proportion', fontSize: 20, titleTextStyle: {fontSize: 20, bold: 'true', italic: 'false'}, viewWindow: {min: 0, max: 1}, gridlines: {color: '#00F', minSpacing: 100}, logScale: 'false'}",
  sizeAxis="{minValue: -80, maxValue: 75, maxSize: 15}",
  hAxis="{title:'Time', fontSize: 20, titleTextStyle: {fontSize: 20, bold: 'true', italic: 'false'}, viewWindow: {min: 0, max: 105}}",
  explorer="{actions: ['dragToZoom', 'rightClickToReset']}",
  focusTarget="category",
  selectionMode="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の無症状感染者の割合をrstanで推定しようとしたが断念した

読んだ。
Estimating the asymptomatic proportion of coronavirus disease 2019 (COVID-19) cases on board the Diamond Princess cruise ship, Yokohama, Japan, 2020. - PubMed - NCBI
COI:なし

ダイヤモンド・プリンセス号のPCR検査と陽性数および症状のある・なしのデータから、無症状でPCR陽性となる患者の割合を推定しようという試み。

The asymptomatic proportion was defined as the proportion of asymptomatically infected individuals among the total number of infected individuals.

とあるように、全感染者数に対して無症状な人の割合をasymptomatic proportion とし、論文では17.9% (95% credible interval (CrI): 15.5–20.2%) だった、と言っている。

データはダイヤモンド・プリンセス号の各日の報告者数で、tableになっている。ただし、2/14日まではNAで、無症状だった人は累計で35人いたので、感度分析では2/5から2/13までこの35人をランダムに振った、と言っている。
適当に補完したsymptomatic/asymptomatic な患者はこんな感じである。supplemental の図っぽくしている。
f:id:MikuHatsune:20200413214835p:plain

The reported asymptomatic cases consists of both true asymptomatic infections and cases who had not yet developed symptoms at the time of data collection but became symptomatic later, i.e. the data are right censored. Each datum consists of an interval of time during which the individual may have been infected and a binary variable indicating whether they were symptomatic as at 18 February.

と言っているように、無症状の患者というのは、(1)本当に無症状のまま経過する人と、(2)PCR検査が陽性となった時点で無症状(だが、いつかは症状が出たかもしれない)、つまりみぎ打ち切りデータというのと、2パターンある。
各患者は、感染しうる期間と、2/18時点で症状があったかなかったか、というデータを持っている。しかし、「感染しうる期間」は以降で[a_i, b_i]で定義されているがデータを見てもそんなのない(?)し、2/18時点で症状があったかなかったか、というのもない(?)。そもそもtable は2/20まである(?)。

モデルとしては、ある患者i についてある感染しうる期間[a_i, b_i]に、X_i のタイミングで感染したとする。症状が出る打ち切り時刻はc だが、index がついてないので全患者に共通なのだろうか。いや多分違うと思う(c_i では?)。
感染から症状が出るまでの時間D_i=c-X_i は、適当な累積確率分布F_D に従うと仮定し、これは平均6.4日、標準偏差2.3日のワイブル分布としている。

The asymptomatic proportion, p, is the probability an individual will never develop symptoms.

というように、p を推定したい。

ある患者がcに無症状である確率g(x, p)
{\displaystyle 
\begin{eqnarray}
  g(x, p)=\left\{
    \begin{array}{ll}
      p + (1-p)(1-F_D(c-x)) & asymptomatic\\
     F_D(c-x) & symptomatic
    \end{array}
  \right.
\end{eqnarray}
}
とする。g(x, p)は全患者で独立なので掛けあわせてよい、つまりは対数では足しあわせてよい。

ということでやってみようと思ったが、感染しうる期間[a_i, b_i]がないし打ち切り時刻cPCR陽性のときの症状がある/なしの患者数とその日付、ということにしてなんとかデータを作ってみる。

結果では

The posterior median estimate of the true proportion of asymptomatic individuals among the reported asymptomatic cases is 0.35 (95% credible interval (CrI): 0.30–0.39), with the estimated total number of the true asymptomatic cases at 113.3 (95%CrI: 98.2–128.3) and the estimated asymptomatic proportion (among all infected cases) at 17.9% (95%CrI: 15.5–20.2%).

と言っていて、asymptomatic と言っていた人のうち本当に今後も無症状のままなtrue asymptomatic は0.35で、全(PCR陽性)患者に占めるasymtomatic は17.9%と言っている。

モデルとして怪しい(絶対間違っている)今回のコードでのp の推定結果は、0.65 だった。
f:id:MikuHatsune:20200413204659p:plain
p はtrue asymptomatic (?) だったのか?よくわからない。いや上のモデル式でもthe probability an individual will never develop symptoms と言っているしこれが推定したいp ?
このp を用いて、generated quantities ブロック内で、PCR陽性時には無症状だった人についてbinomial で今後も無症状か有症状になるかサンプリングした結果は、0.32 だった。
これが自分のなかではtrue asymptomatic と思っているのだが、全然違う。
f:id:MikuHatsune:20200413205004p:plain

このネタであんまり粘っても正解が得られそうにないのでいいところで諦めた。

txt <- "
Date	Number of passengers and crew members on board	Number of disembarked passengers and crew members (cumulative)	Number of tests	Number of tests (cumulative)	Number of individuals testing positive	Number of individuals testing positive (cumulative)	Number of symptomatic cases	Number of asymptomatic cases	Number of asymptomatic cases (cumulative)
2020-02-05	3711	NA	31	31	10	10	NA	NA	NA
2020-02-06	NA	NA	71	102	10	20	NA	NA	NA
2020-02-07	NA	NA	171	273	41	61	NA	NA	NA
2020-02-08	NA	NA	6	279	3	64	NA	NA	NA
2020-02-09	NA	NA	57	336	6	70	NA	NA	NA
2020-02-10	NA	NA	103	439	65	135	NA	NA	NA
2020-02-11	NA	NA	NA	NA	NA	NA	NA	NA	NA
2020-02-12	NA	NA	53	492	39	174	NA	NA	NA
2020-02-13	NA	NA	221	713	44	218	NA	NA	NA
2020-02-14	3451	260	NA	NA	NA	NA	NA	NA	NA
2020-02-15	NA	NA	217	930	67	285	29	38	73
2020-02-16	NA	NA	289	1219	70	355	32	38	111
2020-02-17	3183	528	504	1723	99	454	29	70	181
2020-02-18	NA	NA	681	2404	88	542	23	65	246
2020-02-19	NA	NA	607	3011	79	621	11	68	314
2020-02-20	NA	NA	52	3063	13	634	7	6	320
"

library(rstan)
rstan_options(auto_write=TRUE)
options(mc.cores=parallel::detectCores())
dat <- read.delim(text=txt, stringsAsFactors=FALSE, check.names=TRUE)
N0 <- dat$Number.of.individuals.testing.positive..cumulative.
n0 <- dat$Number.of.individuals.testing.positive
n1 <- dat$Number.of.symptomatic.cases
n2 <- dat$Number.of.asymptomatic.cases

s <- mapply(function(z) sample(1:n0[z], 1), c(8, 11))
nn0 <- replace(n0, c(7:8, 10:11), c(s[1], n0[8]-s[1], s[2], n0[11]-s[2])) # 各日の検査陽性を補完
N1 <- cumsum(nn0)

na.ind <- 10
nn2 <- replace(n2, 1:na.ind, c(rmultinom(1, size=35, prob=nn0[1:na.ind]))) # asymptomatic の各日人数
nn1 <- replace(n1, 1:na.ind, nn0[1:na.ind] - nn2[1:na.ind]) # symptomatic の各日人数
nn1[na.ind + 1] <- nn1[na.ind + 1] - nn1[na.ind]
nn2[na.ind + 1] <- nn2[na.ind + 1] - nn2[na.ind]
nn1;nn2;nn0
nn12 <- c(nn1, nn2)
sym <- unlist(mapply(rep, 0:1, each=sapply(list(nn1, nn2), sum)))


mat1 <- t(do.call(cbind, mapply(function(i) replicate(nn1[i], replace(rep(0, nrow(dat)), i, 1)), seq(nrow(dat)))))
mat2 <- t(do.call(cbind, mapply(function(i) replicate(nn2[i], replace(rep(0, nrow(dat)), i, 2)), seq(nrow(dat)))))
mat2 <- matrix(unlist(mat2), nc=nrow(dat), byrow=FALSE)
mat <- rbind(mat1, mat2)
colnames(mat) <- dat$Date
Xi <- apply(mat>1, 1, which) # 陽性になった日

Nsym <- apply(mat>0, 2, tapply, sym, sum)
rownames(Nsym) <- c("symptomatic", "asymptomatic")
#Nsym <- Nsym[2:1,]

cols <- c("white", "red", "green")
mat0 <- t(mat)
par(mar=c(4, 3, 3, 2))
image(1:nrow(mat0), 1:ncol(mat0), mat0, col=cols, xaxt="n", yaxt="n", xlab="", ylab="")
axis(1, at=1:nrow(mat0), labels=gsub("-", "/", gsub("2020-", "", rownames(mat0))), las=2)
pa <- par()$usr
text(1:nrow(mat0), cumsum(Nsym[1,]), Nsym[1,], pos=3, col=cols[2])
text(pa[1]-0.5, tapply(seq(sym), sym, mean), rownames(Nsym), srt=90, pos=3, xpd=TRUE, col=cols[2:3], cex=2)
text(1:nrow(mat0), c(Nsym[2,1], head(cumsum(Nsym[2,]), -1))+sum(Nsym[1,]), Nsym[2,], pos=1, col=cols[3], font=2)
text(1:nrow(mat0), rep(pa[4], ncol(Nsym)), colSums(Nsym), xpd=TRUE, pos=3)
text(pa[1]-0.8, pa[4], "Total", xpd=TRUE, pos=3, cex=1)
abline(v=1:nrow(mat0)+0.5, lty=3)

code <- "
data{
  int<lower=1> N;
  int<lower=0> Day;
  real<lower=0> par[2];
  int<lower=0, upper=1> sympt[N];
  int<lower=0> All[Day];
  int<lower=0> Asympt[Day];
  real<lower=0> Up;

}
parameters{
  real<lower=0, upper=Up> infect_interval[N]; // 感染日までの期間
  real<lower=0, upper=1> p;
}
model{
  for(i in 1:N){
    if(sympt[i] == 0){
      target += weibull_lcdf(infect_interval[i]| par[1], par[2]);
    } else {
      target += log(p + (1-p)*(1-weibull_cdf(infect_interval[i], par[1], par[2])));
    }
  }
  Asympt ~ binomial(All, p);
}
generated quantities{
  vector[N] r;
  for(i in 1:N){
    if(sympt[i] == 0){
      r[i] = 0;
    } else {
      r[i] = bernoulli_rng(p);
    }
  }
}
"

pars <- c(3.02, 7.25)
m0 <- stan_model(model_code=code)
standata <- list(N=nrow(mat), Day=ncol(mat), par=pars, sympt=sym, All=nn0, Asympt=nn2, Up=100)
fit <- sampling(m0, standata, iter=1000, warmup=500, chain=4, seed=2223)
ex <- extract(fit, pars=head(fit@model_pars, -1))
hist(ex$p, col="pink", main="", xlab="The asymptomatic proportion")
hist(rowMeans(ex$r), col="yellow", main="",
     xlab="The estimated asymptomatic proportion (among all infected cases)")

新型肺炎COVID-19の感染者数をgooglevis を使って表示する

こんな記事を観測した。
www.ft.com
新型肺炎の感染者数および死亡者数を、ECDCという機関のデータから取得してプロットしている。
Homepage | European Centre for Disease Prevention and Control
この機関、いいところがcsv データを整備してくれていて、かつ、Rでデータを使えるようにデータ取得のスクリプトまで書いてくれているという優しさ。

library(utils)
#read the Dataset sheet into “R”. The dataset will be called "data".
data <- read.csv("https://opendata.ecdc.europa.eu/covid19/casedistribution/csv", na.strings = "", fileEncoding = "UTF-8-BOM")

前述のサイトでは7日間の移動平均を取っている。Rでやるなら

filter(d, rep(1, 7))/7

でできる。そのままパクるのは芸がないので時系列でもやろうと思ったが200カ国近くやるのは時間がかかるのでspline で適当に補完した。
前述のサイトにならって最初に30人以上の感染者が報告された日付を0にそろえてプロットしている。
googlevisならインタラクティブに、見たい国のデータを触れば見れるのに、と思ってやった。
googlevis のgvisLineChart関数のオプションはここを見ながら頑張る。
Line Chart  |  Charts  |  Google Developers





LineChartID1192a1e30d4






Data from European Centre for Disease Prevention and Control (ECDC) on 2020-04-12

最初の感染報告から今日までを見たいと思ったのでベタにやってみた。見にくい。



LineChartID11922e2c0b29






Data from European Centre for Disease Prevention and Control (ECDC) on 2020-04-12

library(utils)
#read the Dataset sheet into “R”. The dataset will be called "data".
data <- read.csv("https://opendata.ecdc.europa.eu/covid19/casedistribution/csv", na.strings = "", fileEncoding = "UTF-8-BOM")

# spline
library(stringr)
library(googleVis)

daily_case <- 30
p <- vector("list", length(s))
for(i in seq(s)){
  y <- rev(s[[i]]$case)
  if(length(y) > 8){
    sp <- smooth.spline(y, spar=0.5)
    y1 <- round(replace(sp$y, sp$y < 0, 0), 2)
    p[[i]] <- cbind(x=sp$x - which(y > daily_case)[1] + 1, y=y1)
  }
}
names(p) <- names(s)

lm <- apply(do.call(rbind, p), 2, range, na.rm=TRUE)
X <- seq(lm[1,1], lm[2,1])
Y0 <- mapply(function(z) replace(rep(NA, length(X)), na.omit(match(z[,1], X)), z[,2]), p)
#Y0 <- log10(Y0)
#Y0 <- replace(Y0, Y0 == -Inf, NA)
Y <- cbind.data.frame(X, Y0)
Y <- Y[-c(1:min(unlist(sapply(apply(!is.na(Y[,-1]), 2, which), head, 1)))), ]
Y <- subset(Y, X >= 0)

url <- "https://opendata.ecdc.europa.eu/covid19/casedistribution/csv"
webpage <- "European Centre for Disease Prevention and Control (ECDC)"
headertxt <- sprintf("Data from <span><a href='%s' target='_blank'>%s</a></span> on %s", url, webpage, Sys.Date())
# googlevis 用のプロットオプション
ops <- list(
  fontSize=12, height=800, width=800, lineWidth=1,
  vAxis="{title:'Number [log10]', fontSize: 20, titleTextStyle: {fontSize: 20, bold: 'true', italic: 'false'}, viewWindow: {min: 0, max: 40000}, gridlines: {color: '#00F', minSpacing: 100}, logScale: 'true'}",
  hAxis="{title:'Days since average daily cases passed 30', fontSize: 5, titleTextStyle: {fontSize: 10, bold: 'true', italic: 'false'}, viewWindow: {min: 0, max: 85}}",
  explorer="{actions: ['dragToZoom', 'rightClickToReset']}",
  focusTarget="datum",
  selectionMode="multiple",
  legend="{position: 'right', textStyle: {color: 'black', fontSize: 10}, pageIndex: 3}"
)
gmap <- gvisLineChart(Y, options=ops)

gmap$html$caption <- NULL
#gmap$html$footer <- NULL
gmap$html$footer <- str_replace(gmap$html$footer,
                                "<span> \n  .+\n  .+\n</span>",
                                headertxt)
plot(gmap)


# 日付すべてプロットする
daily_case <- 30
XX <- as.Date(sprintf("%d-%02d-%02d", data$year, data$month, data$day))
X <- seq.Date(min(XX), max(XX), by=1)
p <- vector("list", length(s))
for(i in seq(s)){
  y <- rev(s[[i]]$case)
  if(length(y) > 8){
    x <- as.Date(sprintf("%d-%02d-%02d", s[[i]]$year, s[[i]]$month, s[[i]]$day))
    idx <- match(rev(x), X)
    sp <- smooth.spline(idx, y, spar=0.5)
    px <- min(idx):max(idx)
    py <- predict(sp, px)
    y1 <- round(replace(py$y, py$y < 0, 0), 2)
    p[[i]] <- cbind.data.frame(x=seq.Date(min(x), max(x), by=1), y=y1)
  }
}
names(p) <- names(s)
lm <- do.call(rbind, p)
Y0 <- mapply(function(z) replace(rep(NA, length(X)), na.omit(match(z[,1], X)), z[,2]), p)
#Y0 <- log10(Y0)
#Y0 <- replace(Y0, Y0 == -Inf, NA)
Y <- cbind.data.frame(X, Y0)
Y <- Y[-c(1:min(unlist(sapply(apply(!is.na(Y[,-1]), 2, which), head, 1)))), ]
colnames(Y)[1] <- "Date"

Y$Date <- as.character(Y$Date)

url <- "https://opendata.ecdc.europa.eu/covid19/casedistribution/csv"
webpage <- "European Centre for Disease Prevention and Control (ECDC)"
headertxt <- sprintf("Data from <span><a href='%s' target='_blank'>%s</a></span> on %s", url, webpage, Sys.Date())
# googlevis 用のプロットオプション
ops <- list(
  fontSize=12, height=800, width=800, lineWidth=1,
  vAxis="{title:'Number [log10]', fontSize: 20, titleTextStyle: {fontSize: 20, bold: 'true', italic: 'false'}, viewWindow: {min: 0, max: 40000}, gridlines: {color: '#00F', minSpacing: 100}, logScale: 'true'}",
  sizeAxis="{minValue: -80, maxValue: 75, maxSize: 15}",
  hAxis="{title:'Date', fontSize: 5, titleTextStyle: {fontSize: 10, bold: 'true', italic: 'false'}, viewWindow: {min: 0, max: 105}}",
  explorer="{actions: ['dragToZoom', 'rightClickToReset']}",
  focusTarget="datum",
  selectionMode="multiple",
  legend="{position: 'right', textStyle: {color: 'black', fontSize: 10}, pageIndex: 3}"
)
gmap <- gvisLineChart(Y, options=ops)

gmap$html$caption <- NULL
#gmap$html$footer <- NULL
gmap$html$footer <- str_replace(gmap$html$footer,
                                "<span> \n  .+\n  .+\n</span>",
                                headertxt)
plot(gmap)