新型肺炎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の感染者数の推移を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 の感染陽性患者数の過小報告分を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])
}

新型肺炎COVID-19 の潜伏期間をrstanで推定する

読んだ。
Incubation period of 2019 novel coronavirus (2019-nCoV) infections among travellers from Wuhan, China, 20-28 January 2020. - PubMed - NCBI

最初に武漢で肺炎が発生したときに、88症例について感染履歴を聴取して、ワイブル分布で潜伏期間を推定すると平均6.4日(95% credible interval (CI): 5.6–7.7)、潜伏期間の幅は2.1から11.1日(2.5th to 97.5th percentile)だった、という。
論文ではワイブル分布のほかに、ガンマ分布、対数正規分布で推定して、looicでもっともよかったのがワイブル分布だった、と言っている。
supplemental にスクリプトがあったのでぱくってやってみる。

結果としてはだいぶずれたような気がする。

weibull gamma lognormal
2.5% 4.629226 5.531133 5.174367
50% 6.828260 6.353485 6.070993
97.5% 9.616947 7.583359 7.347166
looIC 480.119496 524.649064 586.254024

前処理にtidyverseがうざすぎるのでこちらで前処理したデータを置いておく。

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

# tidyverse がうざいので加工済みデータを置いておく
input <- list(N=88, 
tStartExposure=c(-2,-18,-18,10,3,8,12,13,-18,-18,8,10,-11,-18,-18,-18,12,-18,-18,-18,-18,-18,-18,11,11,-18,-18,-18,-18,-18,12,12,15,15,-18,-18,-18,-18,19,-18,-18,-18,-18,-18,-18,-18,18,-18,-18,6,-18,-18,-18,9,-18,11,-18,-18,-18,-18,-18,-18,-18,-18,-18,-18,19,-18,-18,-18,-18,-18,-18,-18,13,13,-18,-18,-18,-18,-18,-18,13,-18,-18,-18,-18,-18),
tEndExposure=c(3,12,3,11,4,16,16,16,15,15,16,11,9,2,12,17,15,17,18,13,16,11,18,14,18,20,12,10,17,15,15,14,17,20,18,13,23,23,22,20,21,21,21,15,21,17,20,18,20,7,20,20,20,20,18,22,22,18,18,18,9,20,18,22,23,19,19,22,22,13,22,22,23,23,17,17,22,18,18,22,18,20,15,20,19,23,22,22),
tSymptomOnset=c(3,15,4,14,9,16,16,16,16,16,16,14,10,10,14,20,19,19,20,20,17,13,19,19,20,21,18,18,18,16,20,16,20,20,19,17,24,24,23,21,23,23,23,21,22,24,24,19,21,14,23,23,21,20,21,22,23,19,23,21,13,22,24,25,25,25,25,24,24,21,23,23,24,25,18,18,23,22,22,24,24,25,22,25,25,24,25,25)
)

# 3つの確率分布を一気に作る
dists <- c("weibull", "gamma", "lognormal")
code <- sprintf("
  data{
    int<lower=1> N;
    vector[N] tStartExposure;
    vector[N] tEndExposure;
    vector[N] tSymptomOnset;
  }
  parameters{
    real<lower=0> par[2];
    vector<lower=0, upper=1>[N] uE;	// Uniform value for sampling between start and end exposure
  }
  transformed parameters{
    vector[N] tE; 	// infection moment
    tE = tStartExposure + uE .* (tEndExposure - tStartExposure);
  }
  model{
    // Contribution to likelihood of incubation period
    target += %s_lpdf(tSymptomOnset -  tE  | par[1], par[2]);
  }
  generated quantities {
    // likelihood for calculation of looIC
    vector[N] log_lik;
    for (i in 1:N) {
      log_lik[i] = %s_lpdf(tSymptomOnset[i] -  tE[i]  | par[1], par[2]);
    }
  }
", dists, dists)
names(code) <- dists

m0 <- mapply(stan_model, model_code=code)
fit <- mapply(sampling, m0, list(input), iter=10000, warmup=3000, chain=4)
ps <- mapply(function(z) extract(z)$par, fit, SIMPLIFY=FALSE)

# 確率分布のパラメータから得られる解析的な平均値
means <- cbind(ps$weibull[,1]*factorial(1+1/ps$weibull[,2]+1),
               ps$gamma[,1] / ps$gamma[,2],
               exp(ps$lognormal[,1]))
alpha <- c(0.025, 0.5, 0.975)
res <- apply(means, 2, quantile, alpha)
ll <- mapply(function(z) loo(extract_log_lik(z))$looic, fit)
rbind(res, looIC=ll)

rstanで自分で定義した確率分布からサンプリングする:Johnson's SU 分布

本当はこの通りにしたかったが、自作関数のサンプリングが遅すぎたので先に正規分布normalからのサンプリングがvectorかひとつひとつかtargetかで変わるのかを検証していた。
結論から言うと組み込みのvector型サンプリングは速いが、自作関数はひとつひとつサンプリングすることになるので、圧倒的遅さ。
mikuhatsune.hatenadiary.com

これをやっていたが、実際のデータはやはり単純にガンマ分布ではあてはまりが悪かった。
mikuhatsune.hatenadiary.com
というわけで原点から最頻値が遠くて、かつ、左右非対称な分布をゴリ押ししようと思っていたら、Gumbel 分布gumbel)でできるようだが、もっと歪ませたいと思っていたら、Johnson's SU 分布とうものがあるらしい。これは、適当な変換を挟んで正規分布に従うようにして、確率密度関数
\frac{\delta}{\sqrt{2 \pi \lambda^2}}\frac{1}{\sqrt{1+(\frac{x-\xi}{\lambda})^2}}e^{-\frac{1}{2}*(\gamma+\delta\sinh^{-1} (\frac{x-\xi}{\lambda}))^2}
となる。これをrstanで定義すると
delta/(2*pi()*lambda^2)^0.5 * 1/(1+((x-xi)/lambda)^2)^0.5 * exp(-0.5*(gamma+delta*asinh((x-xi)/lambda))^2)
となる。

Johnson 関数群自体はSuppDistsパッケージで使える。適当に正の範囲(といっても定義域は負もある)で最頻値が50程度で左右非対称な分布を作る。パラメータを与えると、sJohnson関数で平均や分散など得られる。

sJohnson(parms)
$title
[1] "Johnson Distribution"

$gamma
[1] -5.5

$delta
[1] 2

$xi
[1] 7.5

$lambda
[1] 5

$type
SU 
 3 

$Mean
[1] 51.63246

$Median
[1] 46.44676

$Mode
[1] 37.23034

$Variance
[1] 561.298

$SD
[1] 23.69173

$ThirdCentralMoment
[1] -23119.93

$FourthCentralMoment
[1] 2784825

$PearsonsSkewness...mean.minus.mode.div.SD
[1] 0.6078967

$Skewness...sqrtB1
[1] -1.738587

$Kurtosis...B2.minus.3
[1] 5.83916


推定した結果はそこそこよい。しかし時間がクッソかかった。
f:id:MikuHatsune:20200314214536p:plain

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

library(SuppDists)
parms <- list(gamma=-5.5, delta=2, xi=7.5, lambda=5, type="SU")
n <- 3000
hoge <- rJohnson(n, parms)

code <- "
  functions{ //手作り正規分布を定義
    real johnsonSU_lpdf(real x, real gamma, real delta, real xi, real lambda){
      real tmp;
      tmp = delta/(2*pi()*lambda^2)^0.5 * 1/(1+((x-xi)/lambda)^2)^0.5 * exp(-0.5*(gamma+delta*asinh((x-xi)/lambda))^2);
      return log(tmp);
    }
  }
  data {
    int<lower=0> N;
    real<lower=0> y[N];
  }
  parameters {
  real gamma;
  real<lower=0> delta;
  real xi;
  real<lower=0> lambda;
  }
  model {
    for(i in 1:N){
    y[i] ~ johnsonSU(gamma, delta, xi, lambda);
    }
  }
"

m0 <- stan_model(model_code=code)
standata <- list(N=length(hoge), y=hoge)
fit <- sampling(m0, standata, iter=1000, warmup=500, chain=4)
ex <- extract(fit, pars=head(fit@model_pars, -1))
pa <- c(lapply(ex, median), type="SU")


x <- seq(0, 200, length=300)
d0 <- dJohnson(x, parms)
d1 <- dJohnson(x, pa)
hist(hoge, nclass=100, freq=F, main="Johnson SU", col="yellow")
lines(x, d0, col=2, lwd=4)
lines(x, d1, col=4, lwd=4)

rstanでの確率分布からのサンプリングの速さを比較する

rstanで自作関数、というかrstanに実装されていない確率分布からサンプリングをしたくてコードを書いていたが、その前にコードの書き方でサンプリングの効率というか速さが違うので速くなる書き方をしよう、という検証。
結論から言うと、実装されている関数ならば、vector 型でサンプリングするのがよく、自作関数を作るとひとつのrealもしくはintでサンプリングするので、遅い。

5つのパターンを比較する。どれも正規分布N(1, 1)からデータを作り、正規分布normal(m, s)で推定する。
code0は、vector型でy ~ normal(m, s)とサンプリングする。
code1は、ひとつひとつy[i] ~ normal(m, s)とサンプリングする。
code2は、target記法を用いてtarget += normal(y[i] | m, s)とサンプリングする。
code3は、自作関数tmp = delta/(2*pi()*lambda^2)^0.5 * 1/(1+((x-xi)/lambda)^2)^0.5 * exp(-0.5*(gamma+delta*asinh((x-xi)/lambda))^2); を定義し、code1のようにひとつひとつサンプリングする。
code4は、code3の自作関数をtarget記法でサンプリングする。
ちなみにrstanで自作関数を利用するとき、func_lpdfで関数を定義し、返り値はlog(return)とする。また、target記法で利用するときは、func_lpdfで記述する。

結果としてはcode0vector型でのサンプリングが一番早かった。rstanに定義されている関数を利用する場合は、ひとつひとつサンプリングするcode1のほうがtarget記法より速いようだった。
自作関数を利用する場合は、target記法のほうが速そうな気がする。
f:id:MikuHatsune:20200312225316p:plain

hoge <- rnorm(300, 1, 1)
code0 <- "
    data {
    int<lower=0> N;
    real y[N];
  }
  parameters {
    real m;
    real<lower=0> s;
  }
  model {
    y ~ normal(m, s);
  }
"
code1 <- "
    data {
    int<lower=0> N;
    real y[N];
  }
  parameters {
    real m;
    real<lower=0> s;
  }
  model {
    for(i in 1:N){
      y[i] ~ normal(m, s);
    }
  }
"
code2 <- "
    data {
    int<lower=0> N;
    real y[N];
  }
  parameters {
    real m;
    real<lower=0> s;
  }
  model {
    for(i in 1:N){
      target += normal_lpdf(y[i] | m, s);
    }
  }
"
code3 <- "
  functions{ //手作り正規分布を定義
    real my_lpdf(real x, real m, real s){
      real tmp;
      tmp = 1/(2*pi()*s^2)^0.5 * exp(-0.5*((x-m)/s)^2);
      return log(tmp);
    }
  }
  data {
    int<lower=0> N;
    real y[N];
  }
  parameters {
    real m;
    real<lower=0> s;
  }
  model {
    for(i in 1:N){
      y[i] ~ my(m, s);
    }
  }
"
code4 <- "
  functions{ //手作り正規分布を定義
    real my_lpdf(real x, real m, real s){
      real tmp;
      tmp = 1/(2*pi()*s^2)^0.5 * exp(-0.5*((x-m)/s)^2);
      return log(tmp);
    }
  }
  data {
    int<lower=0> N;
    real y[N];
  }
  parameters {
    real m;
    real<lower=0> s;
  }
  model {
    for(i in 1:N){
      target += my_lpdf(y[i] | m, s);
    }
  }
"
codes <- list(code0, code1, code2, code3, code4)
names(codes) <- c("vector", "elementwise", "element_target", "self_function", "self_target")

# 一括してコンパイルするが
# コンピュータのメモリが貧弱だと並列にすると死ぬかもしれない
library(rstan)
rstan_options(auto_write=TRUE)
# options(mc.cores=parallel::detectCores())


ms <- mapply(stan_model, model_code=codes)
standata <- list(N=length(hoge), y=hoge)
fits <- mapply(function(z) sampling(z, standata, iter=2000, warmup=1000, chain=100), ms)

elp <- mapply(function(w) colSums(mapply(function(z) attributes(z)$elapsed_time, w@sim$samples)), fits)


library(vioplot)
vioplot(elp, horizontal=TRUE, side="right", yaxt="n", xlab="elapsed time [sec]")
abline(h=seq(ncol(elp)), lty=3)
axis(1)
text(par()$usr[2], seq(ncol(elp))+0.3, colnames(elp), xpd=TRUE, pos=2)