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

rstanで打ち切りデータがあるときのパラメータ推定をする

これに、打ち切りデータがあるときの平均値の推定問題がある。

StanとRでベイズ統計モデリング (Wonderful R)

StanとRでベイズ統計モデリング (Wonderful R)

データが正規分布に従うのだろうが、L=25を下回るデータは<25となっているので、このデータを無視して平均値を推定すると、本当にデータがあったときより過大評価するだろう、ということ。
打ち切りの場合に<25を無視するのではなく、<25となったときに(-\infty, L]区間までの(累積)確率分布を考慮すれば、少しはマシな推定になる、という話。
オリジナルのコードは

data {
  int N_obs;
  int N_cens;
  real Y_obs[N_obs];
  real L;
}
parameters{
  real<lower=0> m;
  real<lower=0> s_Y;
}
model{
  for(n in 1:N_obs){
    Y_obs[n] ~ normal(m, s_Y);
  }
  target += N_cens * normal_lcdf(L | m, s_Y);
}

となっているが、打ち切りのデータにインデックスをつけてforを回すパターンにした。また、サンプリングはY ~ normal() でもよいが、target 記法で合わせてみようと思って
サンプリングの部分はtarget += _lpdf(data | parameters)(そのデータをサンプリングする確率密度lpdf
打ち切りの部分はtarget += _lcdf(censor | parameters)L までの累積確率分布lcdf
となる。

rstan で推定した、正規分布のパラメータ\mu の事後分布である。この事後分布の最頻値は、シミュレーションで設定した真の値 50 より(たまたま)小さい。L は40 に設定したが、L を下回った値は<40 と記録されてしまうとして、このデータを除外して平均を推定した場合はOmit の縦線となり、Censor の縦線より大きくなっている。
f:id:MikuHatsune:20200405230844p:plain

さて、rstanで打ち切りデータを考慮した場合と、除外して平均を推定した場合とで、どれくらい推定値が変わるかをシミュレーションした。
除外した場合は、真の値よりほとんどの場合で大きい値を推定してしまう。
打ち切りを考慮した場合は、だいたい真の値を推定しているようだった。
f:id:MikuHatsune:20200405230827p:plain

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


# 偏差値っぽい感じで
m_true <- 50
s_true <- 10
n <- 30
y <- rnorm(n, m_true, s_true)

L <- 40
idx <- which(y < L)
y0 <- replace(y, idx, L)


code <- "
data {
  int N;
  real Y[N];
  int I[N];
  real L;
}
parameters{
  real<lower=0> m;
  real<lower=0> s;
}
model{
  for(i in 1:N){
    if(I[i] == 0){ // データがある場合
      target += normal_lpdf(Y[i] | m, s);
    } else {       // 打ち切りの場合
      target += normal_lcdf(L | m, s);
    }
  }
}
"

m0 <- stan_model(model_code=code)
standata <- list(N=length(y), Y=y, I=replace(rep(0, length(y)), y < L, 1), L=L)
fit <- sampling(m0, standata, iter=1000, warmup=300, chain=4)
ex <- extract(fit, pars=head(fit@model_pars, -1))


library(vioplot)
par(mar=c(3, 2, 3, 2))
plot(y, rep(0.99, n), pch=16, col=2, xlab="", ylab="", ylim=c(0.95, 1.5), yaxt="n")
vioplot(ex$m, horizontal=TRUE, side="right", ylim=range(y), add=TRUE)
pa <- par()$usr
abline(v=m_true); text(m_true, pa[4], "True", xpd=TRUE, srt=0, pos=3)
abline(v=median(ex$m)); text(median(ex$m), pa[4], "Censor", xpd=TRUE, srt=0, pos=3, offset=1.5)
abline(v=mean(y[y>L])); text(mean(y[y>L]), pa[4], "Omit", xpd=TRUE, srt=0, pos=3)
abline(v=L); text(L, pa[4], "L", xpd=TRUE, srt=0, pos=3)


# 除外した平均と、rstanで推定した平均の違いをシミュレーション
iter <- 1000
M <- matrix(0, iter, 2)
for(i in 1:iter){
  y <- rnorm(n, m_true, s_true)
  standata <- list(N=length(y), Y=y, I=replace(rep(0, length(y)), y < L, 1), L=L)
  fit <- sampling(m0, standata, iter=1000, warmup=300, chain=4)
  ex <- extract(fit, pars=head(fit@model_pars, -1))
  M[i, ] <- c(mean(y[y > L]), median(ex$m))
}


xylim <- range(M)
par(mar=c(5, 5, 3, 3), cex.lab=1.5, cex.axis=1.2)
plot(M, xlim=xylim, ylim=xylim, xlab="L 以下を除外した平均", ylab="累積確率を考慮した平均", pch=16)
abline(v=m_true, h=m_true, lty=3, col=4)
abline(0, 1, lty=3, col=4)
vioplot(M[,1], at=par()$usr[4], add=TRUE, side="right", xpd=TRUE, horizontal=TRUE, wex=3)
vioplot(M[,2], at=par()$usr[2], add=TRUE, side="right", xpd=TRUE, wex=3)

新型肺炎COVID-19の重症化率を推定する

読んだ。
Eurosurveillance | Estimating the infection and case fatality ratio for coronavirus disease (COVID-19) using age-adjusted data from the outbreak on the Diamond Princess cruise ship, February 2020
ダイヤモンド・プリンセス号のデータを使って、感染した場合の重症化率(というか死亡率)を推定する。結論から言うと全年齢層で1.3%、70歳以上だと6.4%になる。
しかしながら、各日に報告された感染者数と死亡者数から単純に計算した重症化率 naive case fatality ratio (nCFR) は、すべての感染者の転機がわからず、死亡が観測されるまでの時差によるので、この時差 delay を考慮した推定を行う。
この推定は、時刻t の(報告された)感染者数c_t、感染もしくは入院から死亡までt の時差がある割合f_t(訳に自信なし)(これは対数正規分布になっている)として、実際に得ている観測数に対しての過小評価分u_t
u_t=\frac{\displaystyle\sum_{i=0}^t\displaystyle\sum_{j=0}^\infty c_{i-j}f_t}{\displaystyle\sum_{i=0}^t c_j}
を計算することで出来る、とあるが、githubにあるスクリプトを見ても
u_t=\frac{\displaystyle\sum_{i=0}^t\displaystyle\sum_{j=0}^{\require{color}\textcolor{red}{i}} c_{i-j}f_t}{\displaystyle\sum_{i=0}^t c_{\require{color}\textcolor{red}{i}}}
な気がするのだが、よくわからない。
GitHub - thimotei/cCFRDiamondPrincess


ダイヤモンド・プリンセス号の死亡者は70歳以上のカテゴリにしかいないので、70歳以上のみにわけたときの重症化率も推定している。

やってみた結果、

Age group cIFR (95% CI) cCFR (95% CI)
All ages 1.299 (0.525, 2.657) 2.601 (1.049, 5.323)
70歳以上 6.445 (2.621, 12.785) 12.910 (5.259, 25.611)

となり、70歳以上と、cCFR (corrected CFR) はよいが、全年齢のcIFR (corrected infection fatality ratio) は信頼区間がずれた。

ちなみに、対数正規分布のパラメータは平均mと中央値Mが与えられていて、これを対数正規分布用のパラメータに変換するには\mu=\log(M)\sigma=\sqrt{2(\log(M)-\mu)} としているが、なんか違うっぽい。
RPubs - 対数正規分布から、平均値(中央値)と標準偏差を指定して乱数を生成させる
図の見た目はあっている。
f:id:MikuHatsune:20200331231832p:plain

正規分布のパラメータ
zMean <- 13
zMedian <- 9.1
propSymptomatic <- 619/309
propOver70Cases <- 124/619
propOver70Deaths <- 7/7
mu <- log(zMedian)
sigma <- sqrt(2*(log(zMean) - mu))

x <- seq(0, 40, length=300)
plot(x, dlnorm(x, mu, sigma), type="l", lwd=5,
     xlab="Days after hospitalisation", ylab="P(death on a given day|death)")


# 一部改変した
scale_cfr <- function(data_1_in, mu, sigma){
  case_incidence <- data_1_in$new_cases
  death_incidence <- data_1_in$new_deaths
  cumulative_known_t <- rep(0, nrow(data_1_in)) # cumulative cases with known outcome at time tt
  # calculating CDF between each of the days to determine the probability of death on each day

  for(ii in 1:nrow(data_1_in)){
    known_i <- 0 # number of cases with known outcome at time ii
    for(jj in 0:(ii - 1)){
      known_jj <- case_incidence[ii - jj]*(plnorm(jj, mu, sigma) - plnorm(jj - 1, mu, sigma))
      known_i <- known_i + known_jj
    }
    cumulative_known_t[ii] <- known_i # Tally cumulative known
  }
  # naive CFR value
  b_tt <- cumsum(death_incidence)/cumsum(case_incidence) 
  # corrected CFR estimator
  p_tt <- cumsum(death_incidence)/cumsum(cumulative_known_t)
  data.frame(nCFR = b_tt, cIFR = p_tt, total_deaths = cumsum(death_incidence), 
             cum_known_t = cumulative_known_t, total_cases = cumsum(case_incidence))
}

newData <- read.table(text="
         date new_cases new_deaths
1  2020-02-05        10          0
2  2020-02-06        10          0
3  2020-02-07        41          0
4  2020-02-08         3          0
5  2020-02-09         6          0
6  2020-02-10        65          0
7  2020-02-11         0          0
8  2020-02-12        39          0
9  2020-02-13        44          0
10 2020-02-14         0          0
11 2020-02-15        67          0
12 2020-02-16        70          0
13 2020-02-17        99          0
14 2020-02-18        88          0
15 2020-02-19        79          0
16 2020-02-20        13          2
17 2020-02-21         0          0
18 2020-02-22         0          0
19 2020-02-23         0          1
20 2020-02-24         0          0
21 2020-02-25         0          1
22 2020-02-26        71          0
23 2020-02-27         0          0
24 2020-02-28         0          2
25 2020-02-29         0          0
26 2020-03-01         0          1
27 2020-03-02         0          0
28 2020-03-03         0          0
29 2020-03-04         0          0
30 2020-03-05        -9          0
")

ageCorrectedDatNew <- cbind.data.frame(date=newData$date,
                                       new_cases=round(newData$new_cases * propOver70Cases),
                                       new_deaths=round(newData$new_deaths * propOver70Deaths))

res0 <- scale_cfr(newData, mu, sigma)
res1 <- scale_cfr(ageCorrectedDatNew, mu, sigma)
res <- list("all_age"=res0, "over_70"=res1)

ci <- mapply(function(z) binom.test(tail(z$total_deaths, 1), round(sum(z$cum_known_t)))$conf.int[1:2], res)
ci <- rbind(mapply(function(z) tail(z$cIFR, 1), res), ci)

# 無症候性陽性患者を考慮した CRF
ci * propSymptomatic

新型肺炎COVID-19 の感染陽性患者数の過小報告分をrstanで推定する

読んだ。
Ascertainment rate of novel coronavirus disease (COVID-19) in Japan | medRxiv
ascertainment rate という、感染者数(PCR陽性ベース)がどれくらいか、つまり、1だと実際の報告数が潜在的な患者数と同一で、>1だと過剰に報告されている、<1だと過小報告されている、と考えるならば、0.44(95%CI 0.37-0.50)で、軽症患者については実際の患者数は2倍くらい(実際の0.44分しか報告されていない)だろう、ということらしい。

2020年2月28日までの疫学データから、RT-PCRで陽性確定となった患者がほぼ毎日厚生労働省のHPで見れるので、それを都道府県、10歳きざみの年齢、そして重症度でカウントする。重症度の定義は、酸素療法を要して肺炎もしくは挿管されている患者か、ICUに入室した患者、となっている。

厚生労働省のページをいちいち見に行ってもよいが、広島県のHPが時系列で厚生労働省の発表をまとめているので、そこから毎日データを見に行って確認した。2020年2月28日までは症例198まで番号がついている。論文ではNは言及がないが、図を見ると足すと200人前後っぽいのでたぶんいいのかもしれない。
患者の発生状況等 - 広島市公式ホームページ

都道府県ベースで10歳きざみのデータが報告されている。そして各自治体レベルで、重症かそうかの報告がされている。ただし、「胸部X線およびCTで肺炎像」というのがはたしてすべて重症肺炎かというとそうでもない。抗生剤さえ開始すれば酸素療法はいらない肺炎は多数存在する。そもそもコロナウイルスは抗生剤が効くようなものではないので、細菌性肺炎が合併するならまだしも、抗生剤はいらない。
重症度については各自治体で報告様式が異なっている。例えば東京都は、「症例○は重篤である」と書いてあるし、北海道では「症例○は非侵襲性呼吸補助療法を要した」「挿管された」などと書いてある。
図からはsevere な症例について21症例がみてとれるが、今回自分でデータを漁ったところ、12症例しかわからなかった。ただし、ただの肺炎(画像診断で肺炎、となっている症例)は除外したのでこれが過小カウントになってしまったかもしれない。
都道府県ごとの人口は平成26年度版の5歳階級データを拝借した。
https://www.e-stat.go.jp/dbview?sid=0003104197

モデルとしては、各都道府県xの各年齢階級aの人口N_{x,a}について、非重症患者数D_{n}と重症患者数D_s (これらは各都道府県の各年齢階級のインデックスがつく)について、確率p_{x,a}ポアソン過程で患者が発生する、としている。
ここで、非重症患者は重症患者よりf_a の割合で患者報告数が多い(これは年齢階級のみのインデックス)、というパラメータをいれている。ここのパラメータは中国の初期段階での患者報告データから流用している。おそらくtable 1である。
Clinical Characteristics of Coronavirus Disease 2019 in China. - PubMed - NCBI

さて、肝心のascertainment rate はk として、対数尤度関数を
ll=\displaystyle\sum_x\displaystyle\sum_a\ln[\frac{(N_{x,a}kf_ap_{x,a})^{D_{n,x,a}}exp(-N_{x,a}kf_a p_{x,a})}{D_{n,x,a}!}\frac{(N_{x,a}p_{x,a})^{D_{s,x,a}}exp(-N_{x,a}p_{x,a})}{D_{s,x,a}!}]
と定義するが、これはポアソン分布の確率密度関数
P(X=k|\lambda)=\frac{\lambda^k exp(-\lambda)}{k!}
であり、非重症患者では\lambda \gets N_{x,a}kf_ap_{x,a}、重症患者では\lambda \gets N_{x,a}p_{x,a} とすればよい。

ここまで出来たのでrstanでやってみる。
結論から言うとascertainment rate k の推定はそれなりによかった。

     2.5%       50%     97.5% 
0.3640505 0.4345467 0.5170327 

しかし、肝心の推定患者数はグダグダだった。非重症患者はもとより、重症患者が異常に多く推定されてしまった。
f:id:MikuHatsune:20200329220208p:plain

グダグダだった理由として、2020年2月28日までの都道府県別年齢階級別のデータがほんとうに論文で解析したデータとあっているのか不明だし、f_a の値も不明だし、重症患者の定義が

severe dyspnea that required oxygen support plus pneumonia or intubation

と書いてあるが、肺炎であることが必ずしも重症ではないし、重症患者のカウントの仕方が不明だった。

こんな短い論文ですら再現出来ないのだから自分の技量は(ここで記事が途絶えている

# 都道府県別の年齢階級別の人口
pop <- read.table(text="
         10歳未満 10代 20代 30代 40代 50代 60代 70代 80代
北海道        397  462  511  643  738  693  853  641  461
青森県         95  123  109  147  172  182  210  163  120
岩手県         98  119  108  144  160  172  196  160  128
宮城県        191  217  256  305  316  296  329  239  180
秋田県         70   88   73  110  123  144  172  139  118
山形県         88  105   92  127  136  151  174  135  123
福島県        149  190  171  221  243  265  293  218  185
茨城県        239  279  281  363  411  366  444  324  213
栃木県        165  186  189  253  280  254  301  207  145
群馬県        163  194  179  239  283  242  296  221  159
埼玉県        603  673  778  953 1137  858 1031  808  398
千葉県        505  560  636  798  953  738  907  715  385
東京都       1020 1041 1662 2072 2220 1583 1611 1339  841
神奈川県      760  824  999 1246 1497 1080 1186  957  547
新潟県        179  213  201  268  300  295  356  272  228
富山県         84   99   88  125  148  128  167  130  101
石川県         97  112  115  139  161  138  171  127   97
福井県         68   78   70   92  104  100  116   89   75
山梨県         66   83   79   94  118  108  122   95   76
長野県        176  204  171  245  289  258  306  250  211
岐阜県        174  203  190  240  283  246  303  238  165
静岡県        316  349  331  452  528  460  548  431  292
愛知県        682  727  835 1012 1147  858  976  773  446
三重県        154  177  170  219  257  224  263  211  149
滋賀県        135  145  154  185  202  167  191  139   97
京都府        208  238  301  321  371  294  374  303  200
大阪府        723  824  950 1131 1366  999 1231 1047  566
兵庫県        472  532  542  674  815  664  798  634  411
奈良県        110  134  133  156  193  166  210  168  105
和歌山県       75   91   81  102  130  123  150  125   94
鳥取県         48   55   49   67   71   74   88   65   60
島根県         57   63   54   76   82   87  110   85   82
岡山県        165  184  194  230  255  223  279  220  173
広島県        247  265  276  346  396  332  417  319  234
山口県        111  128  118  155  178  167  229  182  143
徳島県         58   68   65   87   96   97  122   92   78
香川県         82   92   84  116  130  118  154  114   93
愛媛県        112  128  117  159  180  175  219  167  138
高知県         54   66   57   81   92   92  119   92   84
福岡県        455  472  557  655  688  609  736  536  381
佐賀県         77   86   76   97  102  107  124   90   77
長崎県        117  132  117  150  171  185  216  163  136
熊本県        160  171  166  207  217  230  265  202  177
大分県         98  106  105  135  144  145  183  140  116
宮崎県        100  108   91  127  133  146  173  129  107
鹿児島県      148  157  143  188  195  224  250  190  172
沖縄県        166  162  157  188  195  182  168  116   85
", check.name=FALSE
) * 1000

# 非重症患者
infec <- read.table(text="
         10歳未満 10代 20代 30代 40代 50代 60代 70代 80代
北海道          4    2    5    5    8    9   13   10    7
青森県          0    0    0    0    0    0    0    0    0
岩手県          0    0    0    0    0    0    0    0    0
宮城県          0    0    0    0    0    0    0    0    0
秋田県          0    0    0    0    0    0    0    0    0
山形県          0    0    0    0    0    0    0    0    0
福島県          0    0    0    0    0    0    0    0    0
茨城県          0    0    0    0    0    0    0    0    0
栃木県          0    0    0    0    0    0    1    0    0
群馬県          0    0    0    0    0    0    0    0    0
埼玉県          0    0    0    1    0    0    0    0    0
千葉県          0    0    2    0    1    2    4    2    0
東京都          0    0    1    2    4    7    2    8    2
神奈川県        0    0    3    2    2    6    2    1    4
新潟県          0    0    0    0    0    0    1    0    0
富山県          0    0    0    0    0    0    0    0    0
石川県          0    0    0    0    0    2    1    0    0
福井県          0    0    0    0    0    0    0    0    0
山梨県          0    0    0    0    0    0    0    0    0
長野県          0    0    0    0    0    0    1    0    0
岐阜県          0    0    0    0    0    1    0    0    0
静岡県          0    0    0    0    0    0    1    0    0
愛知県          0    0    1    0    2    2   14    8    1
三重県          0    0    0    0    0    1    0    0    0
滋賀県          0    0    0    0    0    1    0    0    0
京都府          0    0    2    0    0    0    0    0    0
大阪府          0    0    0    0    5    1    0    0    0
兵庫県          0    0    0    0    0    0    0    0    0
奈良県          0    0    0    0    0    0    1    0    0
和歌山県        0    0    0    1    1    5    2    1    1
鳥取県          0    0    0    0    0    0    0    0    0
島根県          0    0    0    0    0    0    0    0    0
岡山県          0    0    0    0    0    0    0    0    0
広島県          0    0    0    0    0    0    0    0    0
山口県          0    0    0    0    0    0    0    0    0
徳島県          0    0    0    0    0    0    0    0    0
香川県          0    0    0    0    0    0    0    0    0
愛媛県          0    0    0    0    0    0    0    0    0
高知県          0    0    0    1    0    0    0    0    0
福岡県          0    0    0    0    0    0    2    0    0
佐賀県          0    0    0    0    0    0    0    0    0
長崎県          0    0    0    0    0    0    0    0    0
熊本県          0    0    1    0    0    2    2    0    0
大分県          0    0    0    0    0    0    0    0    0
宮崎県          0    0    0    0    0    0    0    0    0
鹿児島県        0    0    0    0    0    0    0    0    0
沖縄県          0    0    0    0    0    0    2    0    1
", check.name=FALSE
)

# 重症患者
infec_severe <- read.table(text="
         10歳未満 10代 20代 30代 40代 50代 60代 70代 80代
北海道          0    0    1    0    0    1    0    0    2
青森県          0    0    0    0    0    0    0    0    0
岩手県          0    0    0    0    0    0    0    0    0
宮城県          0    0    0    0    0    0    0    0    0
秋田県          0    0    0    0    0    0    0    0    0
山形県          0    0    0    0    0    0    0    0    0
福島県          0    0    0    0    0    0    0    0    0
茨城県          0    0    0    0    0    0    0    0    0
栃木県          0    0    0    0    0    0    0    0    0
群馬県          0    0    0    0    0    0    0    0    0
埼玉県          0    0    0    0    0    0    0    0    0
千葉県          0    0    0    0    0    0    0    0    0
東京都          0    0    0    0    0    2    1    1    1
神奈川県        0    0    0    0    0    0    0    0    1
新潟県          0    0    0    0    0    0    0    0    0
富山県          0    0    0    0    0    0    0    0    0
石川県          0    0    0    0    0    0    0    0    0
福井県          0    0    0    0    0    0    0    0    0
山梨県          0    0    0    0    0    0    0    0    0
長野県          0    0    0    0    0    0    0    0    0
岐阜県          0    0    0    0    0    0    0    0    0
静岡県          0    0    0    0    0    0    0    0    0
愛知県          0    0    0    0    0    0    0    0    0
三重県          0    0    0    0    0    0    0    0    0
滋賀県          0    0    0    0    0    0    0    0    0
京都府          0    0    0    0    0    0    0    0    0
大阪府          0    0    0    0    0    0    0    0    0
兵庫県          0    0    0    0    0    0    0    0    0
奈良県          0    0    0    0    0    0    0    0    0
和歌山県        0    0    0    0    0    0    0    1    0
鳥取県          0    0    0    0    0    0    0    0    0
島根県          0    0    0    0    0    0    0    0    0
岡山県          0    0    0    0    0    0    0    0    0
広島県          0    0    0    0    0    0    0    0    0
山口県          0    0    0    0    0    0    0    0    0
徳島県          0    0    0    0    0    0    0    0    0
香川県          0    0    0    0    0    0    0    0    0
愛媛県          0    0    0    0    0    0    0    0    0
高知県          0    0    0    0    0    0    0    0    0
福岡県          0    0    0    0    0    0    0    0    0
佐賀県          0    0    0    0    0    0    0    0    0
長崎県          0    0    0    0    0    0    0    0    0
熊本県          0    0    0    0    0    0    0    0    0
大分県          0    0    0    0    0    0    0    0    0
宮崎県          0    0    0    0    0    0    0    0    0
鹿児島県        0    0    0    0    0    0    0    0    0
沖縄県          0    0    0    0    0    0    1    0    0
", check.name=FALSE
)

# nejm のfa
f1 <- rep(c(8, 490, 241, 109), c(2, 3, 2, 2))/848
f2 <- rep(c(1, 67, 51, 44), c(2, 3, 2, 2))/163
fa <- f1/f2

# http://www.ourphn.org.au/wp-content/uploads/20200225-Article-COVID-19.pdf
# の論文にある fa
# これを使ってもいい再現にはならない
f1 <- c(0.01,1,7,18,38,130,309,213,208) # death
f2 <- c(416,549,3619,7600,8571,10008,8583,3918,1408) # confirmed case
fa <- f2/f1

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

code <- "
  data{
    int<lower=0> A;          // age group
    int<lower=0> X;          // prefecture
    matrix<lower=0>[X, A] N; // population
    int<lower=0> Dn[X, A];   // non-severe
    int<lower=0> Ds[X, A];   // severe
    matrix<lower=0>[X, A] f; // ratio of non-severe to severe
  }
  parameters{
    matrix<lower=0, upper=0.3>[X, A] p;
    real<lower=0> k;
  }
  transformed parameters{
    matrix<lower=0>[X, A] lambda1;
    matrix<lower=0>[X, A] lambda2;
    lambda1 = (k * N .* p) .* f;
    lambda2 = (N .* p);
  }
  model{
    for(a in 1:A){
      for(x in 1:X){
        Dn[x, a] ~ poisson(lambda1[x, a]);
        Ds[x, a] ~ poisson(lambda2[x, a]);
      }
    }
  }
"

m0 <- stan_model(model_code=code)
standata <- list(X=nrow(pop), A=ncol(pop), N=pop, Dn=infec, Ds=infec_severe, f=t(replicate(nrow(pop), fa)))
fit <- sampling(m0, standata, iter=10000, warmup=3000, chain=4)
ex <- extract(fit, pars=head(fit@model_pars, -1))
alpha <- 0.05
cia <- c(alpha/2, 0.5, 1-alpha/2)
quantile(ex$k, cia) # k の推定区間

est <- list("non-severe"=t(mapply(function(z) colSums(ex$lambda1[z,,]), 1:dim(ex$lambda1)[1])),
            severe=t(mapply(function(z) colSums(ex$lambda2[z,,]), 1:dim(ex$lambda2)[1])))
Ninfec <- lapply(list(infec, infec_severe), colSums)
xl <- c(0.5, ncol(pop)+0.5)
yl <- c(0, max(unlist(est), unlist(Ninfec)))
par(mfrow=c(2, 1), las=1)
for(i in seq(Ninfec)){
  plot(Ninfec[[i]], type="n", col="red", xaxt="n", xlim=xl, ylim=yl, xlab="Age group", ylab="count")
  vioplot(as.data.frame(est[[i]]), ylim=yl, add=TRUE)
  points(Ninfec[[i]], pch=15, col="red")
  axis(1, at=seq(ncol(pop)), labels=colnames(pop))
  legend("topleft", legend=c("Estimate", "Data"), pch=15, col=c(grey(0.3), "red"))
  title(names(est)[i])
}