vignettes/composableIDModelR.Rmd
composableIDModelR.RmdcomposableIDModelR builds infectious disease models from interchangeable parts and runs them with ComposableTuringIDModels.jl in Julia. A model has two parts:
Every part has an R constructor with the same name and arguments as its Julia counterpart, so the ComposableTuringIDModels.jl documentation applies directly.
library(composableIDModelR)
#>
#> Attaching package: 'composableIDModelR'
#> The following object is masked from 'package:stats':
#>
#> GammacomposableIDModelR needs Julia (>= 1.12), the version its pinned dependencies were resolved with, ideally installed with juliaup. The Julia packages are installed and loaded the first time a model is simulated or fitted, which can take several minutes. To follow progress, run the setup yourself:
To run MCMC chains in parallel, set JULIA_NUM_THREADS
before Julia starts:
Sys.setenv(JULIA_NUM_THREADS = "4")Distributions come from distspec, so they can be given by their natural parameters or by a mean and standard deviation.
latent <- RandomWalk(
init = Normal(0, 0.25),
epsilon_t = HierarchicalNormal(std = HalfNormal(0.05))
)
infection <- Renewal(
generation_time = Gamma(shape = 6.5, scale = 0.62),
rt = latent,
initialisation = Normal(log(10), 0.5)
)
observation <- NegativeBinomialError(cluster_factor = HalfNormal(0.1))
model <- IDModel(infection, observation)
model
#> <julia model spec>
#> IDModel(
#> Renewal(;
#> generation_time = Gamma(6.5, 0.62),
#> rt = RandomWalk(;
#> init = Normal(0.0, 0.25),
#> ϵ_t = HierarchicalNormal(; std = HalfNormal(0.05))
#> ),
#> initialisation = Normal(2.3025850929940459, 0.5)
#> ),
#> NegativeBinomialError(; cluster_factor = HalfNormal(0.1))
#> )Building a model does not start Julia. The printout is the Julia code
the model runs, which you can also get as a string with
as_julia(). Arguments left out use the Julia defaults.
simulate() draws parameters from their priors and
simulates the latent process, infections and observations. Each quantity
is returned as a matrix with one row per simulation.
sims <- simulate(model, nsim = 20, n = 50, seed = 1)
#> Starting Julia ...
names(sims)
#> [1] "generated_y_t" "expected_y_t" "I_t" "Z_t"
#> [5] "Rt"
matplot(t(sims$Rt), type = "l", lty = 1, col = "grey50", log = "y",
xlab = "Day", ylab = "Rt")
Prior simulations are a quick check that the priors produce plausible epidemics before fitting.
We fit the model to one of the simulated series. nuts()
sets up the sampler; short chains keep this example fast.
y <- sims$generated_y_t[1, ]
fitted <- fit(
model, y,
method = nuts(draws = 250, warmup = 250, chains = 2),
seed = 2
)
fitted
#> <composableIDModelR fit>
#> Model:
#> IDModel(
#> Renewal(;
#> generation_time = Gamma(6.5, 0.62),
#> rt = RandomWalk(;
#> init = Normal(0.0, 0.25),
#> ϵ_t = HierarchicalNormal(; std = HalfNormal(0.05))
#> ),
#> initialisation = Normal(2.3025850929940459, 0.5)
#> ),
#> NegativeBinomialError(; cluster_factor = HalfNormal(0.1))
#> )
#>
#> Data: 50 time points
#> Sampling: 2 chains of 250 draws after 250 warmup
#>
#> Convergence:
#> Max Rhat: 1.03
#> Min bulk ESS: 254
#> Divergent transitions: 1
#>
#> Use summary() for parameter estimates and plot() to visualise.summary() gives posterior summaries of the parameters,
and the draws work with the posterior package and anything
built on it, such as bayesplot.
summary(fitted)
#> # A tibble: 53 × 10
#> variable mean median sd mad q5 q95 rhat ess_bulk
#> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 rw_init -0.0353 -0.0315 0.130 0.128 -0.257 0.177 0.999 273.
#> 2 std 0.0592 0.0555 0.0215 0.0217 0.0295 0.0986 1.01 289.
#> 3 ϵ_t[1] -0.0530 -0.0584 0.971 0.905 -1.65 1.54 1.01 565.
#> 4 ϵ_t[2] -0.00416 -0.0275 0.993 0.985 -1.60 1.68 1.00 357.
#> 5 ϵ_t[3] -0.101 -0.0899 0.937 0.871 -1.73 1.40 1.01 725.
#> 6 ϵ_t[4] -0.0787 -0.0409 0.985 0.998 -1.65 1.60 0.999 667.
#> 7 ϵ_t[5] 0.182 0.151 0.976 1.00 -1.28 1.79 1.01 545.
#> 8 ϵ_t[6] -0.0388 -0.0377 0.961 1.03 -1.59 1.53 1.00 491.
#> 9 ϵ_t[7] -0.0212 -0.0730 1.01 0.970 -1.66 1.78 1.01 407.
#> 10 ϵ_t[8] 0.143 0.172 0.949 0.933 -1.37 1.72 1.01 532.
#> # ℹ 43 more rows
#> # ℹ 1 more variable: ess_tail <dbl>Generated quantities (the latent process Z_t, infections
I_t, expected and predicted observations, and
Rt for renewal models) are matrices with one row per
posterior draw:
str(fitted$generated)
#> List of 6
#> $ generated_y_t: num [1:500, 1:50] 17 17 17 17 17 17 17 17 17 17 ...
#> $ expected_y_t : num [1:500, 1:50] 11 11.2 11.1 15 13.9 ...
#> $ I_t : num [1:500, 1:50] 11 11.2 11.1 15 13.9 ...
#> $ Z_t : num [1:500, 1:50] 0.1297 0.2278 0.1527 0.1978 0.0429 ...
#> $ predicted_y_t: num [1:500, 1:50] 9 5 8 14 10 14 12 14 11 17 ...
#> $ Rt : num [1:500, 1:50] 1.14 1.26 1.16 1.22 1.04 ...
plot(fitted, type = "Rt")
type = "cases" compares posterior predictions with the
data, and horizon adds a forecast, which continues the
latent process beyond the data.
plot(fitted, type = "cases", horizon = 14)
The forecast draws themselves come from predict():
Changing an assumption means replacing one part. Here observations follow a delay from infection and are Poisson distributed, and the reproduction number follows an AR(1) process:
IDModel(
Renewal(
generation_time = Gamma(shape = 6.5, scale = 0.62),
rt = AR(damp = truncated(Normal(0.9, 0.05), 0, 1)),
initialisation = Normal(log(10), 0.5)
),
LatentDelay(PoissonError(), delay = LogNormal(1.6, 0.42))
)
#> <julia model spec>
#> IDModel(
#> Renewal(;
#> generation_time = Gamma(6.5, 0.62),
#> rt = AR(; damp = truncated(Normal(0.9, 0.05), 0.0, 1.0)),
#> initialisation = Normal(2.3025850929940459, 0.5)
#> ),
#> LatentDelay(PoissonError(), LogNormal(1.6, 0.42))
#> )julia_spec() constructs any Julia component by name, and
julia() inserts Julia code verbatim, for example a function
argument. Positional R arguments become positional Julia arguments and
named ones become keyword arguments. .role says what the
component is, so that it is only accepted where it belongs:
"distribution", "latent",
"infection", "observation" or
"model".
IDModel(
DirectInfections(
Z = julia_spec("HilbertSpaceGP", .role = "latent"),
initialisation = Normal(log(100), 1)
),
LatentDelay(PoissonError(), delay = julia_spec("Frechet", 3, 2,
.role = "distribution"))
)
#> <julia model spec>
#> IDModel(
#> DirectInfections(;
#> Z = HilbertSpaceGP(),
#> initialisation = Normal(4.6051701859880918, 1.0)
#> ),
#> LatentDelay(PoissonError(), Frechet(3.0, 2.0))
#> )The Mishra et al. (2020) case study applies these steps to COVID-19 data from South Korea.