+
+
+
+
+
+
+
+Getting stated with EpiAware
This tutorial introduces the basic functionality of EpiAware
. EpiAware
is a package for making inferences on epidemiological case/determined infection data using a model-based approach.
EpiAware
models
The models we consider are discrete-time $t = 1,\dots, T$ with a latent random process, $Z_t$ generating stochasticity in the number of new infections $I_t$ at each time step. Observations are treated as downstream random variables determined by the actual infections and a model of infection to observation delay.
Mathematical definition
$$\begin{align}
+Z_\cdot &\sim \mathcal{P}(\mathbb{R}^T) | \theta_Z, \\
+I_0 &\sim f_0(\theta_I), \\
+I_t &\sim g_I(\{I_s, Z_s\}_{s < t}, \theta_{I}), \\
+y_t &\sim f_O(\{I_s\}_{s \leq t}, \theta_{O}).
+\end{align}$$
Where, $\mathcal{P}(\mathbb{R}^T) | \theta_Z$ is a parametric process on $\mathbb{R}^T$. $f_0$ and $f_O$ are parametric distributions on, respectively, the initial number of infections and the observed case data conditional on underlying infections. $g_I$ is distribution of new infections conditional on infections and latent process in the past. Note that we assume that new infections are conditional on the strict past, whereas new observations can depend on infections on the same time step.
Code structure outline
An EpiAware
model in code is created from three modular components:
A LatentModel
: This defines the model for $Z_\cdot$. This chooses $\mathcal{P}(\mathbb{R}^T) | \theta_Z$.
An EpiModel
: This defines a generative process for infections conditional on the latent process. This chooses $f_0(\theta_I)$, and $g_I(\{I_s, Z_s\}_{s < t}, \theta_{I})$.
An ObservationModel
: This defines the observation model. This chooses $f_O({I_s}_{s \leq t}, \theta_{O})$
Reproductive number
EpiAware
models do not need to specify a time-varying reproductive number $\mathcal{R}_t$ to generate $I_\cdot$, however, this is often a quantity of interest. When not directly used we will typically back-calculate $\mathcal{R}_t$ from the generated infections:
$$\mathcal{R}_t = {I_t \over \sum_{s \geq 1} g_s I_{t-s} }.$$
Where $g_s$ is a discrete generation interval. For this reason, even when not using a reproductive number approach directly, we ask for a generation interval.
+
+begin
+ using EpiAware
+ using Turing
+ using Distributions
+ using StatsPlots
+ using Random
+ using DynamicPPL
+ using Statistics
+ using DataFramesMeta
+ using LinearAlgebra
+end
+
+
+
+Random walk LatentModel
As an example, we choose the latent process as a random walk with parameters $\theta_Z$:
Conditional on the parameters the random walk is then generated by white noise:
$$\begin{align}
+Z_t &= Z_0 + \sigma_{RW} \sum_{t= 1}^T \epsilon_t, \\
+\epsilon_t &\sim \mathcal{N}(0,1).
+\end{align}$$
In EpiAware
we provide a constructor for random walk latent models with priors for $\theta_Z$. We choose priors,
$$\begin{align}
+Z_0 &\sim \mathcal{N}(0,1),\\
+\sigma^2_Z &\sim \text{HalfNormal}(0.01).
+\end{align}$$
+
+rwp = EpiAware.RandomWalk(Normal(),
+ truncated(Normal(0.0, 0.02), 0.0, Inf))
+RandomWalk{Normal{Float64}, Truncated{Normal{Float64}, Continuous, Float64, Float64, Float64}}(Distributions.Normal{Float64}(μ=0.0, σ=1.0), Truncated(Distributions.Normal{Float64}(μ=0.0, σ=0.02); lower=0.0, upper=Inf))
+
+
+Direct infection EpiModel
This is a simple model where the unobserved log-infections are directly generated by the latent process $Z$.
$$\log I_t = \log I_0 + Z_t.$$
As discussed above, we still ask for a defined generation interval, which can be used to calculate $\mathcal{R}_t$.
+
+truth_GI = Gamma(2, 5)
+Distributions.Gamma{Float64}(α=2.0, θ=5.0)
+
+
+The EpiData
constructor performs double interval censoring to convert our continuous estimate of the generation interval into a discretized version. We also implement right truncation using the keyword D_gen
.
+
+model_data = EpiData(truth_GI, D_gen = 10.0)
+EpiData{Float64, typeof(exp)}([0.056722565637507084, 0.0944813112842689, 0.11668723746973605, 0.12773814268679387, 0.130948497124998, 0.1287976856448402, 0.12312384970427602, 0.11527489380975135, 0.10622581663782862], 9, exp)
+
+
+We can supply a prior for the initial log_infections.
+
+log_I0_prior = Normal(log(100.0), 1.0)
+Distributions.Normal{Float64}(μ=4.605170185988092, σ=1.0)
+
+
+And construct the EpiModel
.
+
+epi_model = DirectInfections(model_data, log_I0_prior)
+DirectInfections{Normal{Float64}}(EpiData{Float64, typeof(exp)}([0.056722565637507084, 0.0944813112842689, 0.11668723746973605, 0.12773814268679387, 0.130948497124998, 0.1287976856448402, 0.12312384970427602, 0.11527489380975135, 0.10622581663782862], 9, exp), Distributions.Normal{Float64}(μ=4.605170185988092, σ=1.0))
+
+
+Delayed Observations ObservationModel
The observation model is a negative binomial distribution with mean μ
and cluster factor 1 / r
. Delays are implemented as the action of a sparse kernel on the infections $I(t)$.
$$\begin{align}
+y_t &\sim \text{NegBinomial}(\mu = \sum_{s\geq 0} K[t, t-s] I(s), r), \\
+1 / r &\sim \text{Gamma}(3, 0.05/3).
+\end{align}$$
+
+
+We also set up the inference to occur over 100 days.
+
+time_horizon = 100
+100
+
+
+We choose a simple observation model where infections are observed 0, 1, 2, 3 days later with equal probability.
+
+obs_model = EpiAware.DelayObservations(
+ fill(0.25, 4),
+ time_horizon,
+ truncated(Gamma(5, 0.05 / 5), 1e-3, 1.0)
+)
+DelayObservations{Float64, Truncated{Gamma{Float64}, Continuous, Float64, Float64, Float64}}(sparse(Integer[1, 2, 3, 4, 2, 3, 4, 5, 3, 4 … 97, 98, 99, 100, 98, 99, 100, 99, 100, 100], Integer[1, 1, 1, 1, 2, 2, 2, 2, 3, 3 … 97, 97, 97, 97, 98, 98, 98, 99, 99, 100], [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25 … 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25], 100, 100), Truncated(Distributions.Gamma{Float64}(α=5.0, θ=0.01); lower=0.001, upper=1.0))
+
+
+Generate cases from the EpiAware
model
Having chosen an EpiModel
, LatentModel
and ObservationModel
, we can implement the model as a Turing
model using make_epi_aware
.
By giving missing
to the first argument, we indicate that case data will be generated from the model rather than treated as fixed.
+
+full_epi_aware_mdl = make_epi_aware(missing, time_horizon;
+ epi_model = epi_model,
+ latent_model = rwp,
+ observation_model = obs_model)
+Model{typeof(make_epi_aware), (:y_t, :time_steps), (:epi_model, :latent_model, :observation_model, :pos_shift), (:y_t,), Tuple{Missing, Int64}, Tuple{DirectInfections{Normal{Float64}}, RandomWalk{Normal{Float64}, Truncated{Normal{Float64}, Continuous, Float64, Float64, Float64}}, DelayObservations{Float64, Truncated{Gamma{Float64}, Continuous, Float64, Float64, Float64}}, Float64}, DefaultContext}(EpiAware.make_epi_aware, (y_t = missing, time_steps = 100), (epi_model = DirectInfections{Normal{Float64}}(EpiData{Float64, typeof(exp)}([0.056722565637507084, 0.0944813112842689, 0.11668723746973605, 0.12773814268679387, 0.130948497124998, 0.1287976856448402, 0.12312384970427602, 0.11527489380975135, 0.10622581663782862], 9, exp), Distributions.Normal{Float64}(μ=4.605170185988092, σ=1.0)), latent_model = RandomWalk{Normal{Float64}, Truncated{Normal{Float64}, Continuous, Float64, Float64, Float64}}(Distributions.Normal{Float64}(μ=0.0, σ=1.0), Truncated(Distributions.Normal{Float64}(μ=0.0, σ=0.02); lower=0.0, upper=Inf)), observation_model = DelayObservations{Float64, Truncated{Gamma{Float64}, Continuous, Float64, Float64, Float64}}(sparse(Integer[1, 2, 3, 4, 2, 3, 4, 5, 3, 4 … 97, 98, 99, 100, 98, 99, 100, 99, 100, 100], Integer[1, 1, 1, 1, 2, 2, 2, 2, 3, 3 … 97, 97, 97, 97, 98, 98, 98, 99, 99, 100], [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25 … 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25], 100, 100), Truncated(Distributions.Gamma{Float64}(α=5.0, θ=0.01); lower=0.001, upper=1.0)), pos_shift = 1.0e-6), DefaultContext())
+
+
+We choose some fixed parameters:
Initial incidence is 100.
In the direct infection model, the initial incidence and in the initial value of the random walk form a non-identifiable pair. Therefore, we fix $Z_0 = 0$.
+
+fixed_parameters = (rw_init = 0.0, init_incidence = log(100.0))
+(rw_init = 0.0, init_incidence = 4.605170185988092)
+
+
+We fix these parameters using fix
, and generate a random epidemic.
+
+cond_generative_model = fix(full_epi_aware_mdl, fixed_parameters)
+Model{typeof(make_epi_aware), (:y_t, :time_steps), (:epi_model, :latent_model, :observation_model, :pos_shift), (:y_t,), Tuple{Missing, Int64}, Tuple{DirectInfections{Normal{Float64}}, RandomWalk{Normal{Float64}, Truncated{Normal{Float64}, Continuous, Float64, Float64, Float64}}, DelayObservations{Float64, Truncated{Gamma{Float64}, Continuous, Float64, Float64, Float64}}, Float64}, DynamicPPL.FixedContext{@NamedTuple{rw_init::Float64, init_incidence::Float64}, DefaultContext}}(EpiAware.make_epi_aware, (y_t = missing, time_steps = 100), (epi_model = DirectInfections{Normal{Float64}}(EpiData{Float64, typeof(exp)}([0.056722565637507084, 0.0944813112842689, 0.11668723746973605, 0.12773814268679387, 0.130948497124998, 0.1287976856448402, 0.12312384970427602, 0.11527489380975135, 0.10622581663782862], 9, exp), Distributions.Normal{Float64}(μ=4.605170185988092, σ=1.0)), latent_model = RandomWalk{Normal{Float64}, Truncated{Normal{Float64}, Continuous, Float64, Float64, Float64}}(Distributions.Normal{Float64}(μ=0.0, σ=1.0), Truncated(Distributions.Normal{Float64}(μ=0.0, σ=0.02); lower=0.0, upper=Inf)), observation_model = DelayObservations{Float64, Truncated{Gamma{Float64}, Continuous, Float64, Float64, Float64}}(sparse(Integer[1, 2, 3, 4, 2, 3, 4, 5, 3, 4 … 97, 98, 99, 100, 98, 99, 100, 99, 100, 100], Integer[1, 1, 1, 1, 2, 2, 2, 2, 3, 3 … 97, 97, 97, 97, 98, 98, 98, 99, 99, 100], [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25 … 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25], 100, 100), Truncated(Distributions.Gamma{Float64}(α=5.0, θ=0.01); lower=0.001, upper=1.0)), pos_shift = 1.0e-6), FixedContext((rw_init = 0.0, init_incidence = 4.605170185988092), DynamicPPL.DefaultContext()))
+
+random_epidemic = rand(cond_generative_model)
+(ϵ_t = [-0.3245105752051425, 0.5841792768429854, 0.837698738466377, 0.04991144411048826, 0.43709682639657876, 1.5113816873648402, 0.5183464762750044, -1.2951234902508262, -0.8475356353822158, 0.5731492794677614 … -0.9718005452114998, 0.2986685041305902, 0.8778707060932857, 0.2779274792378409, -1.3849265413496943, 0.9094660271125533, 1.2532018893752603, 1.5863018030422251, -0.04686725620120323, 0.4638612794817786], σ²_RW = 0.026203541122590525, neg_bin_cluster_factor = 0.0472919247750861, var"y_t[1]" = 24, var"y_t[2]" = 53, var"y_t[3]" = 106, var"y_t[4]" = 105, var"y_t[5]" = 121, var"y_t[6]" = 115, var"y_t[7]" = 149, var"y_t[8]" = 185, var"y_t[9]" = 121, var"y_t[10]" = 134, var"y_t[11]" = 124, var"y_t[12]" = 75, var"y_t[13]" = 105, var"y_t[14]" = 95, var"y_t[15]" = 118, var"y_t[16]" = 126, var"y_t[17]" = 125, var"y_t[18]" = 94, var"y_t[19]" = 124, var"y_t[20]" = 64, var"y_t[21]" = 59, var"y_t[22]" = 137, var"y_t[23]" = 75, var"y_t[24]" = 83, var"y_t[25]" = 157, var"y_t[26]" = 136, var"y_t[27]" = 104, var"y_t[28]" = 131, var"y_t[29]" = 228, var"y_t[30]" = 173, var"y_t[31]" = 156, var"y_t[32]" = 165, var"y_t[33]" = 183, var"y_t[34]" = 182, var"y_t[35]" = 112, var"y_t[36]" = 145, var"y_t[37]" = 161, var"y_t[38]" = 117, var"y_t[39]" = 169, var"y_t[40]" = 173, var"y_t[41]" = 108, var"y_t[42]" = 123, var"y_t[43]" = 69, var"y_t[44]" = 52, var"y_t[45]" = 105, var"y_t[46]" = 132, var"y_t[47]" = 117, var"y_t[48]" = 99, var"y_t[49]" = 119, var"y_t[50]" = 93, var"y_t[51]" = 80, var"y_t[52]" = 96, var"y_t[53]" = 77, var"y_t[54]" = 89, var"y_t[55]" = 166, var"y_t[56]" = 104, var"y_t[57]" = 74, var"y_t[58]" = 115, var"y_t[59]" = 126, var"y_t[60]" = 102, var"y_t[61]" = 146, var"y_t[62]" = 102, var"y_t[63]" = 157, var"y_t[64]" = 96, var"y_t[65]" = 156, var"y_t[66]" = 159, var"y_t[67]" = 92, var"y_t[68]" = 143, var"y_t[69]" = 83, var"y_t[70]" = 130, var"y_t[71]" = 121, var"y_t[72]" = 133, var"y_t[73]" = 208, var"y_t[74]" = 171, var"y_t[75]" = 186, var"y_t[76]" = 227, var"y_t[77]" = 218, var"y_t[78]" = 205, var"y_t[79]" = 189, var"y_t[80]" = 111, var"y_t[81]" = 182, var"y_t[82]" = 301, var"y_t[83]" = 199, var"y_t[84]" = 234, var"y_t[85]" = 194, var"y_t[86]" = 208, var"y_t[87]" = 393, var"y_t[88]" = 381, var"y_t[89]" = 406, var"y_t[90]" = 429, var"y_t[91]" = 169, var"y_t[92]" = 405, var"y_t[93]" = 256, var"y_t[94]" = 300, var"y_t[95]" = 330, var"y_t[96]" = 240, var"y_t[97]" = 245, var"y_t[98]" = 287, var"y_t[99]" = 439, var"y_t[100]" = 610)
+
+true_infections = generated_quantities(cond_generative_model, random_epidemic).I_t
+100-element Vector{Float64}:
+ 94.88256898856683
+ 104.29298246375241
+ 119.43910104412493
+ 120.4080084136958
+ 129.23612130314984
+ 165.0578731221061
+ 179.50508482958563
+ ⋮
+ 304.0060267388828
+ 352.2240260411064
+ 431.4404807480081
+ 557.7508150421348
+ 553.5353707088188
+ 596.6992603591709
+
+generated_obs = generated_quantities(cond_generative_model, random_epidemic).generated_y_t
+100-element Vector{Int64}:
+ 24
+ 53
+ 106
+ 105
+ 121
+ 115
+ 149
+ ⋮
+ 330
+ 240
+ 245
+ 287
+ 439
+ 610
+
+
+
+
+
+
Fixing $Z_0 = 0$ for the random walk was based on inference principles; in this model $Z_0$ and $\log I_0$ are non-identifiable.
However, we now treat the generated data as truth_data
and make inference without fixing any other parameters.
We do the inference by MCMC/NUTS using the Turing
NUTS sampler with default warm-up steps.
+
+truth_data = generated_obs
+100-element Vector{Int64}:
+ 24
+ 53
+ 106
+ 105
+ 121
+ 115
+ 149
+ ⋮
+ 330
+ 240
+ 245
+ 287
+ 439
+ 610
+
+inference_mdl = fix(
+ make_epi_aware(truth_data, time_horizon; epi_model = epi_model,
+ latent_model = rwp, observation_model = obs_model),
+ (rw_init = 0.0,)
+)
+Model{typeof(make_epi_aware), (:y_t, :time_steps), (:epi_model, :latent_model, :observation_model, :pos_shift), (), Tuple{Vector{Int64}, Int64}, Tuple{DirectInfections{Normal{Float64}}, RandomWalk{Normal{Float64}, Truncated{Normal{Float64}, Continuous, Float64, Float64, Float64}}, DelayObservations{Float64, Truncated{Gamma{Float64}, Continuous, Float64, Float64, Float64}}, Float64}, DynamicPPL.FixedContext{@NamedTuple{rw_init::Float64}, DefaultContext}}(EpiAware.make_epi_aware, (y_t = [24, 53, 106, 105, 121, 115, 149, 185, 121, 134 … 169, 405, 256, 300, 330, 240, 245, 287, 439, 610], time_steps = 100), (epi_model = DirectInfections{Normal{Float64}}(EpiData{Float64, typeof(exp)}([0.056722565637507084, 0.0944813112842689, 0.11668723746973605, 0.12773814268679387, 0.130948497124998, 0.1287976856448402, 0.12312384970427602, 0.11527489380975135, 0.10622581663782862], 9, exp), Distributions.Normal{Float64}(μ=4.605170185988092, σ=1.0)), latent_model = RandomWalk{Normal{Float64}, Truncated{Normal{Float64}, Continuous, Float64, Float64, Float64}}(Distributions.Normal{Float64}(μ=0.0, σ=1.0), Truncated(Distributions.Normal{Float64}(μ=0.0, σ=0.02); lower=0.0, upper=Inf)), observation_model = DelayObservations{Float64, Truncated{Gamma{Float64}, Continuous, Float64, Float64, Float64}}(sparse(Integer[1, 2, 3, 4, 2, 3, 4, 5, 3, 4 … 97, 98, 99, 100, 98, 99, 100, 99, 100, 100], Integer[1, 1, 1, 1, 2, 2, 2, 2, 3, 3 … 97, 97, 97, 97, 98, 98, 98, 99, 99, 100], [0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25 … 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25, 0.25], 100, 100), Truncated(Distributions.Gamma{Float64}(α=5.0, θ=0.01); lower=0.001, upper=1.0)), pos_shift = 1.0e-6), FixedContext((rw_init = 0.0,), DynamicPPL.DefaultContext()))
+
+chn = sample(inference_mdl,
+ NUTS(; adtype = AutoReverseDiff(true)),
+ MCMCThreads(),
+ 250,
+ 4;
+ drop_warmup = true)
+ | iteration | chain | ϵ_t[1] | ϵ_t[2] | ϵ_t[3] | ϵ_t[4] | ϵ_t[5] | ϵ_t[6] | ... |
---|
1 | 126 | 1 | 1.15885 | 1.65953 | 0.922999 | -1.32209 | 0.326622 | 0.821712 | |
2 | 127 | 1 | -0.694295 | -0.88852 | -0.331172 | 1.85546 | 0.626332 | -0.37274 | |
3 | 128 | 1 | -0.0574015 | -0.702891 | 0.62663 | 0.451822 | -0.675678 | -0.462883 | |
4 | 129 | 1 | 0.0387053 | -0.703266 | 2.08068 | 0.446057 | -1.19198 | -0.867791 | |
5 | 130 | 1 | 0.615644 | -0.686403 | 1.55179 | -1.01553 | -0.0941861 | 1.28523 | |
6 | 131 | 1 | 2.55703 | -1.00934 | 1.21827 | -0.150463 | 1.61262 | -0.0782016 | |
7 | 132 | 1 | 0.0888739 | 2.16494 | 0.0407923 | 0.580672 | 0.355992 | 1.49668 | |
8 | 133 | 1 | -0.410145 | 2.1424 | -0.591201 | 1.61573 | 0.546974 | 1.2129 | |
9 | 134 | 1 | -0.41357 | -1.26248 | 0.538455 | -0.889234 | -1.05674 | 1.4544 | |
10 | 135 | 1 | -0.745 | 1.21835 | 0.834888 | -0.0217767 | -0.320541 | 1.40438 | |
... |
+
+
+Predictive plotting
We can spaghetti plot generated case data from the version of the model which hasn't conditioned on case data using posterior parameters inferred from the version conditioned on observed data. This is known as posterior predictive checking, and is a useful diagnostic tool for Bayesian inference (see here).
Because we are using synthetic data we can also plot the model predictions for the unobserved infections and check that (at least in this example) we were able to capture some unobserved/latent variables in the process accurate.
+
+let
+ post_check_mdl = fix(full_epi_aware_mdl, (rw_init = 0.0,))
+ post_check_y_t = mapreduce(hcat, generated_quantities(post_check_mdl, chn)) do gen
+ gen.generated_y_t
+ end
+
+ predicted_I_t = mapreduce(hcat, generated_quantities(inference_mdl, chn)) do gen
+ gen.I_t
+ end
+
+ p1 = plot(post_check_y_t, c = :grey, alpha = 0.05, lab = "")
+ scatter!(p1, truth_data,
+ lab = "Observed cases",
+ xlabel = "Time",
+ ylabel = "Cases",
+ title = "Post. predictive checking: cases",
+ ylims = (-0.5, maximum(truth_data) * 1.5),
+ c = :green)
+
+ p2 = plot(predicted_I_t, c = :grey, alpha = 0.05, lab = "")
+ scatter!(p2, true_infections,
+ lab = "Actual infections",
+ xlabel = "Time",
+ ylabel = "Unobserved Infections",
+ title = "Post. predictions: infections",
+ ylims = (-0.5, maximum(true_infections) * 1.5),
+ c = :red)
+
+ plot(p1, p2,
+ layout = (1, 2),
+ size = (700, 400))
+end
+
+
+
+As well as checking the posterior predictions for latent infections, we can also check how well inference recovered unknown parameters, such as the random walk variance or the cluster factor of the negative binomial observations.
+
+let
+ parameters_to_plot = (:σ²_RW, :neg_bin_cluster_factor)
+
+ plts = map(parameters_to_plot) do name
+ var_samples = chn[name] |> vec
+ histogram(var_samples,
+ bins = 50,
+ norm = :pdf,
+ lw = 0,
+ fillalpha = 0.5,
+ lab = "MCMC")
+ vline!([getfield(random_epidemic, name)], lab = "True value")
+ title!(string(name))
+ end
+ plot(plts..., layout = (2, 1))
+end
+
+
+
+
As mentioned at the top, we don't directly use the concept of reproductive numbers in this note. However, we can back-calculate the implied $\mathcal{R}(t)$ values, conditional on the specified generation interval being correct.
Here we spaghetti plot posterior sampled time-varying reproductive numbers against the actual.
+
+let
+ n = epi_model.data.len_gen_int
+ Rt_denom = [dot(reverse(epi_model.data.gen_int), true_infections[(t - n):(t - 1)])
+ for t in (n + 1):length(true_infections)]
+ true_Rt = true_infections[(n + 1):end] ./ Rt_denom
+
+ predicted_Rt = mapreduce(hcat, generated_quantities(inference_mdl, chn)) do gen
+ _It = gen.I_t
+ _Rt_denom = [dot(reverse(epi_model.data.gen_int), _It[(t - n):(t - 1)])
+ for t in (n + 1):length(_It)]
+ Rt = _It[(n + 1):end] ./ _Rt_denom
+ end
+
+ plt = plot((n + 1):time_horizon, predicted_Rt, c = :grey, alpha = 0.05, lab = "")
+ plot!(plt, (n + 1):time_horizon, true_Rt,
+ lab = "true Rt",
+ xlabel = "Time",
+ ylabel = "Rt",
+ title = "Post. predictions: reproductive number",
+ c = :red,
+ lw = 2)
+end
+
+
+