新型肺炎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)

新型肺炎COVID-19の感染者数の推移をSEIRモデルを使ってrstanでシミュレーションする

読んだ。
A Simulation on Potential Secondary Spread of Novel Coronavirus in an Exported Country Using a Stochastic Epidemic SEIR Model. - PubMed - NCBI
COI:筆者はこの著者とは直接の関係はないので、純粋に統計解析のツッコミです。

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


やっていることとしては、SEIRモデルでもし日本がN=1000人の村で、そこに感染者E が一人やってきたら今後の感染拡大はどうなるか、という話。
SEIRモデルは、パラメータ\beta\delta\nu を用いて
\frac{dS}{dt}=-\frac{\beta SI}{N}
\frac{dE}{dt}=\frac{\beta SI}{N}-\delta E
\frac{dI}{dt}=\delta E-\nu I
\frac{dR}{dt}=\upsilon I
として、これを\beta\delta\nuをいろいろ振ったときに、最終的にSEIRがどう推移するか、をシミュレーションしている。

しかし、\frac{dR}{dt} の部分は定義されていない(というか不要)なパラメータ\upsilon がついていて、正しくは
\frac{dS}{dt}=-\frac{\beta SI}{N}
\frac{dE}{dt}=\frac{\beta SI}{N}-\delta E
\frac{dI}{dt}=\delta E-\nu I
\frac{dR}{dt}=\require{color}\textcolor{red}{\nu} I
である。
\betaは[0.01, 0.02], [0.04, 0.06], [0.1, 0.2], [0.4, 0.6], [0.8, 1]
\deltaは[1/4, 1/2, [1/10, 1/4], [1/14, 1/10]
\nuは[1/2, 1], [1/7, 1/2], [1/10, 1/7]
の5*3*3=45 パターンについて検討し、再生産数R_0=\frac{\beta}{\nu}についても推定する。
これ、dt のステップは0.2 刻みで、100日までシミュレーションした、と書いてあるが、離散的にやっているのにdt=0.2 とはどういうことなん…?よくわからん。

rstanでSEIRモデルをシミュレーションするには、ここらへんを参考にスクリプトをパクる。
観測したデータはないので、model ブロックでのサンプリングはない。代わりに、parametersブロックでbeta, delta, nu のそれぞれが(一様分布から)シミュレーション的にサンプリングされ、そのパラメータと、Y0に入力する初期値(S=999, E=1, I=0, R=0 のこと)に従って、rstan内ではintegrate_ode関数がなんやかんや微分方程式を解きながら数値解をy_hatに格納してくれる、ということになっている。
statmodeling.hatenablog.com
Contemporary statistical inference for infectious disease models using Stan. - PubMed - NCBI

functions {
   real[] sir(
      real t,
      real[] y,
      real[] theta,
      real[] x_r,
      int[] x_i
   ) {
      real dydt[4];
      dydt[1] = - theta[1]*y[1]*y[3];
      dydt[2] = theta[1]*y[1]*y[3] - theta[2]*y[2];
      dydt[3] = theta[2]*y[2] - theta[3]*y[3];
      dydt[4] = theta[3]*y[3];
      return dydt;
   }
}
data {
   int<lower=1> T; // 時間の数
   real T0; // time 0
   real TS[T]; // 各時刻
   real Y0[4]; // Y の初期値 SEIR
   real b01[2]; // beta の範囲
   real d01[2]; // delta の範囲
   real n01[2]; // nu の範囲
}
transformed data {
   real x_r[0];
   int x_i[0];
}
parameters {
   real<lower=b01[1], upper=b01[2]> beta;
   real<lower=d01[1], upper=d01[2]> delta;
   real<lower=n01[1], upper=n01[2]> nu;
}
transformed parameters {
   real y_hat[T,4];
   real theta[3];
   theta[1] = beta;
   theta[2] = delta;
   theta[3] = nu;
   y_hat = integrate_ode(sir, Y0, T0, TS, theta, x_r, x_i);
}
model {
  // 何もしない
}

さて、論文でいうところの最悪のシナリオである、\beta [0.8, 1], \delta [1/14, 1/10], \nu [1/10, 1/7] をやってみる。
論文ではFigure 1 のはずだが、なんか全然違う。
dt の幅と日付かと思ったが、それでもEI の数が全然違うので、なんかスクリプトがおかしいのだろう。ちなみにR_0=7.5 だった。

f:id:MikuHatsune:20200407210826p:plain
論文ではFigure 1 のはず。

\betaS\to I の多さを表すので、これが大きいと最悪のシナリオになるのは当然だと思うが、論文っぽさを出すなら\beta [0.001, 0.002] くらいじゃないかと思った。ただしこれだとR_0=0.013 で何もしなくても勝手に感染は収束する。
f:id:MikuHatsune:20200407211432p:plain

ベストなシナリオだと言っている\beta [0.01, 0.02], \delta [1/4, 1/2], \nu [1/2, 1] だが、これだとS はほとんど減らず、E も逆にまったく増えないので、感染は爆発しない。はずだが、自分がやるとまたおかしい。Figure 2 に相当するはずだった。どちらかというとfigure 4 に近い。
f:id:MikuHatsune:20200407211754p:plain

本当はスクリプトをもっと精査したかったが、ぶっちゃけて言えば生データを使っているわけでなくパラメータいじっただけなんだからスクリプトくらいsupple で出したら?って思った。
rstanでパラメータを与えた時の、モデルの推定範囲が出せるようになったのでよかった。

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

code <- "
functions {
   real[] sir(
      real t,
      real[] y,
      real[] theta,
      real[] x_r,
      int[] x_i
   ) {
      real dydt[4];
      dydt[1] = - theta[1]*y[1]*y[3];
      dydt[2] = theta[1]*y[1]*y[3] - theta[2]*y[2];
      dydt[3] = theta[2]*y[2] - theta[3]*y[3];
      dydt[4] = theta[3]*y[3];
      return dydt;
   }
}
data {
   int<lower=1> T; // 時間の数
   real T0; // time 0
   real TS[T]; // 各時刻
   real Y0[4]; // Y の初期値 SEIR
   real b01[2]; // beta の範囲
   real d01[2]; // delta の範囲
   real n01[2]; // nu の範囲
}
transformed data {
   real x_r[0];
   int x_i[0];
}
parameters {
   real<lower=b01[1], upper=b01[2]> beta;
   real<lower=d01[1], upper=d01[2]> delta;
   real<lower=n01[1], upper=n01[2]> nu;
}
transformed parameters {
   real y_hat[T,4];
   real theta[3];
   theta[1] = beta;
   theta[2] = delta;
   theta[3] = nu;
   y_hat = integrate_ode(sir, Y0, T0, TS, theta, x_r, x_i);
}
model {
  // 何もしない
}
"

# コンパイルしておく
m0 <- stan_model(model_code=code)
y0 <- c(S=999, E=1, I=0, R=0) # 初期値
N <- sum(y0)
times <- seq(0, 100, by=1)[-1] # 時刻 0.2 刻みはどういうこと?

# 各パラメータを振る範囲
lim <- list(b01=list(c(0.001, 0.002), c(0.04, 0.06), c(0.1, 0.2), c(0.4, 0.6), c(0.8, 1)),
            d01=list(c(1/4, 1/2), c(1/10, 1/4), c(1/14, 1/10)),
            n01=list(c(1/2, 1), c(1/7, 1/2), c(1/10, 1/7))
            )

# for で回したい時これをindex にする
b <- 1
d <- 3
n <- 3
standata <- list(T=length(times), T0=0, TS=times, Y0=y0, b01=lim$b01[[b]], d01=lim$d01[[d]], n01=lim$n01[[n]])
fit <- sampling(m0, standata, iter=200, warmup=0, chain=2, seed=1234)
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(cbind, 0, mapply(function(z) apply(ex$y_hat[,,z], 2, quantile, cia), seq(y0), SIMPLIFY=FALSE), SIMPLIFY=FALSE), along=3)
m <- abind::abind(mapply(function(z) cbind(c(0, y0[z], 0), apply(ex$y_hat[,,z], 2, quantile, cia)), seq(y0), SIMPLIFY=FALSE), along=3)

parms <- apply(ex$theta, 2, quantile, cia)
R0s <- ex$beta / ex$nu

# 透明にしながら実線も描けるようにする
cols256 <- list(S=c(244, 0, 255), E=c(255, 102, 5), I=c(0, 255, 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)
matplot(m[2,,], type="n", col=colsa, lty=1, lwd=3, xlab="Time (days)", ylab="Number 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], N, 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)

}

# SEIR のパラメータ
txt <- lapply(list(b1=lim$b01[[b]][1], b2=lim$b01[[b]][2], d1=lim$d01[[d]][1], d2=lim$d01[[d]][2], n1=lim$n01[[n]][1], n2=lim$n01[[n]][2]), round, 3)
sub <- as.expression(substitute(beta~"["*b1*","~b2*"],"~delta~"["*d1*","~d2*"],"~nu~"["*n1*","~n2*"]", txt))
text(mean(times), par()$usr[4], sub, xpd=TRUE, pos=3, cex=1.4)

# R0
R0ci <- round(quantile(R0s, cia), 3)
subR0 <- as.expression(substitute(R[0]==r1~"["*b1*","~b2*"]", list(r1=R0ci[2], b1=R0ci[1], b2=R0ci[3])))
text(par()$usr[2], par()$usr[3], subR0, xpd=TRUE, adj=c(0.7, 4), xpd=TRUE, cex=1.2)