--- title: "EpiNova: Getting Started" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{EpiNova: Getting Started} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set( echo = TRUE, fig.width = 8, fig.height = 4, message = FALSE, warning = FALSE ) library(EpiNova) library(ggplot2) ``` # Why EpiNova? | Feature | eSIR | EpiNova | |---|---|---| | Compartmental models | SIR only | SIR, SEIR, SEIRD, SVEIR, SVEIRD, age-SEIR | | pi(t) shape | Step / exponential | Step, exponential, spline, GP, composite | | Inference | JAGS (external binary) | MLE, SMC (pure R); HMC via Stan (optional) | | Rt estimation | No | Built-in; EpiEstim wrapper (optional) | | Spatial structure | None | Multi-patch + gravity mobility | | Scenario comparison | No | plot_scenarios() | | Forecast scoring | No | CRPS, coverage, MAE | --- ## Data We use the Hubei Province COVID-19 data from January to February 2020. ```{r data} NI_complete <- c(41, 41, 41, 45, 62, 131, 200, 270, 375, 444, 549, 729, 1052, 1423, 2714, 3554, 4903, 5806, 7153, 9074, 11177, 13522, 16678, 19665, 22112, 24953, 27100, 29631, 31728, 33366) RI_complete <- c(1, 1, 7, 10, 14, 20, 25, 31, 34, 45, 55, 71, 94, 121, 152, 213, 252, 345, 417, 561, 650, 811, 1017, 1261, 1485, 1917, 2260, 2725, 3284, 3754) N <- 58.5e6 Y <- NI_complete / N - RI_complete / N R <- RI_complete / N ``` --- ## 1. SEIRD model with a smooth spline intervention ```{r seird_spline} pi_spline <- build_pi_spline( knot_times = c(0, 10, 22, 30, 60, 120), knot_values = c(1, 0.95, 0.60, 0.35, 0.25, 0.25) ) params <- list(beta = 0.35, gamma = 0.07, sigma = 0.20, delta = 0.005, I0 = Y[1], E0 = Y[1] * 2) init <- c(S = 1 - params$E0 - params$I0, E = params$E0, I = params$I0, R = R[1], D = 0) times <- 0:200 traj <- solve_model(params, init, times, model = "SEIRD", pi_fn = pi_spline) plot_trajectory(traj, obs_Y = Y, obs_R = R, T_obs_end = length(Y) - 1, title = "SEIRD + Spline pi(t) - Hubei Province") ``` --- ## 2. Scenario comparison ```{r scenarios} pi_none <- function(t) 1 pi_mild <- build_pi_step(c(10), c(1, 0.6)) pi_strict <- build_pi_spline(c(0, 10, 22, 60), c(1, 0.9, 0.25, 0.35)) scenario_pis <- list( "No intervention" = pi_none, "Mild lockdown" = pi_mild, "Strict lockdown" = pi_strict ) sc_df <- do.call(rbind, lapply(names(scenario_pis), function(nm) { tr <- solve_model(params, init, times, model = "SEIRD", pi_fn = scenario_pis[[nm]]) data.frame( time = tr$time, I_median = tr$I, I_lower = tr$I * 0.75, I_upper = tr$I * 1.25, scenario = nm ) })) plot_scenarios(sc_df, obs_Y = Y) ``` --- ## 3. Built-in Rt estimation (no extra packages needed) ```{r rt} new_cases <- pmax(0L, diff(NI_complete)) Rt_df <- estimate_Rt_simple(new_cases, mean_si = 5.2, sd_si = 2.8, window = 7L) plot_Rt(Rt_df, change_times = c(10, 22)) ``` --- ## 4. Composite NPI functions ```{r composite} lockdown <- build_pi_step(c(10, 60), c(1.0, 0.4, 0.65)) masks <- build_pi_spline(c(0, 15, 30, 90), c(1, 0.92, 0.80, 0.80)) combined <- compose_pi(lockdown, masks) traj_combined <- solve_model(params, init, times, model = "SEIRD", pi_fn = combined) plot_trajectory(traj_combined, obs_Y = Y, obs_R = R, T_obs_end = length(Y) - 1, title = "Composite NPI: lockdown x mask mandate") ``` --- ## 5. Two-patch spatial model ```{r multipatch, fig.height = 5} M <- gravity_mobility( N_vec = c(58.5e6, 1.4e9 - 58.5e6), dist_mat = matrix(c(0, 1000, 1000, 0), nrow = 2), kappa = 1e-7, max_travel = 0.02 ) ode_fn <- build_multipatch_SEIR( n_patches = 2, M = M, beta_vec = c(0.35, 0.25), gamma_vec = c(0.07, 0.07), sigma_vec = c(0.20, 0.20), pi_fn_list = list(pi_spline, build_pi_step(c(15), c(1, 0.5))) ) init_mat <- matrix( c(1 - 1e-4, 1 - 1e-5, 1e-5, 5e-6, 1e-4, 1e-5, 0, 0), nrow = 2 ) mp_df <- solve_multipatch(ode_fn, init_mat, times = 0:150, n_patches = 2) plot_multipatch_snapshot(mp_df, t_snapshot = 30, patch_names = c("Hubei", "Rest of China")) ``` --- ## 6. Forecast scoring ```{r scoring} holdout <- Y[20:30] fc_df <- data.frame( time = 19:29, I_median = traj$I[20:30], I_lower = traj$I[20:30] * 0.60, I_upper = traj$I[20:30] * 1.40 ) score_forecast(fc_df, holdout) ``` --- ## Package philosophy EpiNova is built on three principles absent from eSIR: 1. **Composability** - interventions, models, and inference backends are independent building blocks. 2. **Modularity** - no mandatory external binaries; optional packages unlock extra capabilities. 3. **Calibration accountability** - every forecast can be scored with `score_forecast()`.