The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]metanet.ch.
This vignette works an applied, likelihood-free problem end to end: recovering the transmission and recovery rates of an epidemic from a noisy incidence curve. There is no tractable likelihood — the data come from a stochastic compartmental simulator — which is precisely the setting simulation-based inference was developed for.
The classic SIR model splits a population of size
N into Susceptible, Infected, and Recovered compartments.
Two rates govern the dynamics: the contact rate \(\beta\) (how fast the infection spreads)
and the recovery rate \(\gamma\) (how
fast infected individuals recover). The package includes this model as a
built-in task.
library(neuralsbi)
task <- task_sir() # prior + simulator for the SIR model
task
#> <nsbi_task> sir: 2 parameters -> 10 data dimsThe task bundles a prior (log-normal on \(\beta, \gamma\)) and a
simulator that solves the SIR dynamics and returns the
observed infected fraction at 10 time points.
We simulate from the prior and train a neural posterior estimator. A Mixture Density Network is a good default here; the posterior is smooth and unimodal.
fit <- npe(task$prior, task$simulator,
n_simulations = 3000,
density_estimator = "mdn",
max_epochs = 250, seed = 1)Training is amortized: this one fit can be conditioned on any incidence curve without re-simulating.
Suppose we observe an outbreak generated by \(\beta = 0.4,\ \gamma = 0.125\) (basic reproduction number \(R_0 = \beta/\gamma = 3.2\)).
theta_true <- c(beta = 0.4, gamma = 0.125)
x_obs <- task$simulator(matrix(theta_true, nrow = 1))
post <- posterior(fit, x_obs = x_obs)
summary(post, n = 5000)
#> parameter mean sd q2.5 q25 q50 q75
#> 1 theta1 0.4007750 0.006536198 0.3897076 0.3967231 0.4005885 0.4044068
#> 2 theta2 0.1300866 0.007765928 0.1168075 0.1254525 0.1300085 0.1347168
#> q97.5
#> 1 0.4124505
#> 2 0.1436619
draws <- sample(post, 10000)
pairplot(draws, truth = theta_true)plot of chunk unnamed-chunk-4
The posterior concentrates around the true rates, and — importantly — reports its own uncertainty.
A posterior is only trustworthy if it is calibrated. We check with Simulation-Based Calibration and an expected-coverage plot, neither of which needs a reference posterior.
res <- sbc(fit, task$simulator, n_sbc = 80, n_posterior_samples = 300,
seed = 2)
#> Warning in stats::chisq.test(tab): Chi-squared approximation may be incorrect
#> Warning in stats::chisq.test(tab): Chi-squared approximation may be incorrect
res # per-parameter uniformity p-values (large = good)
#> <nsbi_sbc> 80 trials, 300 posterior samples each
#> per-parameter uniformity p-values (large = calibrated):
#> 0.522 0.940
plot_sbc(res, param = 1) # rank histogram: flat = calibratedplot of chunk unnamed-chunk-5
plot of chunk unnamed-chunk-5
If the rank histograms are flat and the coverage curve hugs the diagonal, the posterior’s credible intervals mean what they say: a 90% interval contains the truth about 90% of the time.
Finally, push posterior draws back through the simulator and compare the predicted incidence curves to the observation.
pp <- posterior_predictive(post, task$simulator, n = 200)
matplot(t(pp), type = "l", col = adjustcolor("grey", 0.3),
xlab = "observation time", ylab = "infected fraction")
lines(as.numeric(x_obs), col = "firebrick", lwd = 2)plot of chunk unnamed-chunk-6
The observed curve should sit comfortably within the cloud of predictive draws. A systematic mismatch would flag model misspecification — a signal no point estimate can give you.
An amortized fit spreads its simulation budget over the whole prior,
but when a single outbreak is of interest, most of those simulations
describe epidemics nothing like the observed one. Sequential NPE
(npe_sequential(), using truncated proposals) alternates
simulation and training, restricting each new round of simulations to
the parameter region the current posterior considers plausible.
fit_seq <- npe_sequential(task$prior, task$simulator, x_obs = x_obs,
n_rounds = 2, n_simulations = 1500,
density_estimator = "mdn", max_epochs = 200, seed = 3)
fit_seq # per-round budgets and acceptance rates
#> <nsbi_snpe> Sequential NPE fit (TSNPE, truncated-prior proposals)
#> density estimator : mdn
#> rounds : 2
#> simulations : 3000
#> acceptance/round : 1.00, 0.32
#> targeted x_obs : 0, 0, 0.004, 0.241, 0.139, 0.025, 0.004, 0.002, 0, 0
#> NOT amortized: only valid at (or near) the targeted x_obs.
#> -> build a posterior with posterior(fit, x_obs = ...)
post_seq <- posterior(fit_seq, x_obs = x_obs)
draws_seq <- sample(post_seq, 10000)
pairplot(draws_seq, truth = theta_true)plot of chunk unnamed-chunk-7
With a comparable total simulation budget, the sequential fit
typically yields a tighter posterior around this particular outbreak.
The trade-off: the result is specific to x_obs, so
conditioning on a different incidence curve means refitting.
This case study covered the whole workflow: prior, simulator,
amortized training, conditioning, calibration checks, predictive checks,
and a sequential refinement. The earlier vignettes treat each stage in
more depth — vignette("neuralsbi") for the core functions,
vignette("density-estimators") for when to use
"maf" or "nsf" instead of the MDN, and
vignette("diagnostics") for the complete set of checks.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.