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.

Case study: inferring epidemic parameters (SIR)

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 model

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 dims

The 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.

Fit an amortized posterior

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.

Condition on an observation

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)
Pairs plot of the SIR posterior with the true rates marked.

plot of chunk unnamed-chunk-4

The posterior concentrates around the true rates, and — importantly — reports its own uncertainty.

Is the posterior calibrated?

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 = calibrated
SBC rank histogram and expected-coverage plot for the SIR fit.

plot of chunk unnamed-chunk-5

plot_coverage(res)         # empirical vs nominal coverage: on the diagonal = good
SBC rank histogram and expected-coverage plot for the SIR fit.

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.

Posterior predictive check

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)
Posterior-predictive incidence curves with the observation overlaid.

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.

Spending simulations where they matter: sequential NPE

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)
Pairs plot of the sequential-NPE posterior with the true rates marked.

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.

Where to go next

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.