noroEEriFit <- update(
noroEEFit,
end = list(f = addSeason2formula(f = ~ - 1 + ri(type = "iid", corr = "none")))
)3 Incorporating Random Effects
Endemic-epidemic models can also be extended to handle unobserved heterogeneity across regions by introducing random effects into the linear predictors, as established by Paul and Held (2011).
3.1 Mathematical Formulation
Since only the formulation of the linear predictors change, we just look at them. To capture regional variations not explained by covariates, we can add district-specific random intercepts (\(b_i\)) to the log-linear predictors of each component. For simplicity we just show it for the endemic component, which we also use as our example later. \[\log(\nu_{it}) = \beta_0^{(\nu)} + b_i^{(\nu)}\]
Where:
\(\beta_0\) is the global fixed intercept,
\(b_i^{(\nu)}\) is the independent and identically distributed (IID) random intercept for district \(i\).
We assume these random intercepts are normally distributed around zero and uncorrelated across components.
More information about random intercepts for hhh4 models (CAR, etc) can be found in Paul and Held (2011) and the hhh4 vignettes.
3.2 Using surveillance
We use the same endemic-epidemic model from before and just add random intercepts to the endemic component, using the ri() function in the formula. This can be done easily with the update() function in surveillance. We don’t need to specify a new control list.
summary(noroEEriFit)
Call:
hhh4(stsObj = object$stsObj, control = control)
Random effects:
Var Corr
end.ri(iid) 0.1916
Fixed effects:
Estimate Std. Error
ar.1 -1.15194 0.06739
ne.1 -1.67581 0.16718
end.sin(2 * pi * t/52) -0.07215 0.04031
end.cos(2 * pi * t/52) -0.99464 0.03861
end.ri(iid) 0.64276 0.14679
neweights.logd -1.49530 2.33178
overdisp 0.19164 0.01214
Penalized log-likelihood: -5778.44
Marginal log-likelihood: -39.24
Number of units: 12
Number of time points: 207
The global intercept end.ri is shown in the summary as well as the variance. To see the individual deviations for each district of the global intercept, we use the ranef() function.
ranef(noroEEriFit)end.ri(iid).chwi end.ri(iid).frkr end.ri(iid).lich end.ri(iid).mahe
-0.06225989 -0.78545221 -0.06068782 -0.21805723
end.ri(iid).mitt end.ri(iid).neuk end.ri(iid).pank end.ri(iid).rein
-0.11733941 -0.17377112 0.63640874 0.02752094
end.ri(iid).span end.ri(iid).zehl end.ri(iid).scho end.ri(iid).trko
-0.39323483 0.77340037 0.33785807 0.03561439
In surveillance we can easily plot a map of the random intercepts:
plot(noroEEriFit, type = "ri", component = "end", exp = TRUE)
3.3 Using RTMB
If we want to add random effects in RTMB, we just need to add a vector of random intercepts of the length of districts \(i\) in addition to the global intercept, as well as a parameter for the standard deviation of the distribution of the random intercepts.
First, we create a helper function for the mean \(\mu_{it}\):
meanEEri <- function(parms, data){
## ENDEMIC
getAll(parms, data)
ri_mat <- matrix(end.ri.vec, nrow = n_time, ncol = n_unit, byrow = TRUE)
s_vec <- end.sin * Ssin + end.cos * Scos
end.exppred <- exp(end.ri + ri_mat + s_vec)
end <- end.exppred[-1, ]
## EPIDEMIC
Y_lag <- Y[1:(n_time - 1), ]
ar.exppred <- exp(ar.1)
ar <- ar.exppred * Y_lag
W <- W_n^(-exp(neweights.logd))
diag(W) <- 0
W_norm <- W / rowSums(W)
ne.exppred <- exp(ne.1)
ne <- ne.exppred * (Y_lag %*% W_norm)
return(list(mu = end + ar + ne,
end = end,
epi = ar + ne,
epi.ar = ar, epi.ne = ne,
end.exppred = end.exppred,
ar.exppred = ar.exppred,
ne.exppred = ne.exppred,
neW = W))
}We build an objective function using meanEEri(). Here, we need to add the penalization in the log-likelihood, which is the sum of log-density of the random intercepts.
objectiveEEri <- function(parms, data) {
getAll(parms, data)
Y <- OBS(Y)
fit <- meanEEri(parms, data)
## Initialize negative log likelihood
nll <- 0
## penalization random intercepts
nll <- nll - sum(dnorm(end.ri.vec, mean = 0, sd = exp(end.ri.sd), log=TRUE))
REPORT(fit$mu)
## negative log likelihood
nll <- nll - sum(dnbinom2(Y[-1, ], mu = fit$mu, size = exp(log_overdispersion), log = TRUE))
return(nll)
}We add the random intercept vector and standard deviation to the parameter list:
parms <- list(
ar.1 = 0,
ne.1 = 0,
end.sin = 0,
end.cos = 0,
end.ri = 0, # global intercept
neweights.logd = log(2),
log_overdispersion = 0,
end.ri.vec = diag(data$W_n), # to get a named vector of 0s
end.ri.sd = 0
)
## diagonal of neighbourhood matrix is named vector of 0s
diag(data$W_n)chwi frkr lich mahe mitt neuk pank rein span zehl scho trko
0 0 0 0 0 0 0 0 0 0 0 0
Finally we can process the objective function, where we need to add which our random intercept is, using the random argument:
objEEri <- MakeADFun(cmb(objectiveEEri, data), parms, random = "end.ri.vec")We fit the model and calculate uncertainties as before.
ptm <- proc.time() # track runtime
## fit the model
optEEri <- nlminb( objEEri$par, objEEri$fn, objEEri$gr)
## calculate uncertainties
sdrEEri <- sdreport(objEEri)
runtime_EEri <- proc.time() - ptmWe use summary() to get the estimates of the RTMB model. Here we get everything in the order of the given parms list.
summary(sdrEEri) Estimate Std. Error
ar.1 -1.14316597 0.06728866
ne.1 -1.56959201 0.15900895
end.sin -0.06086412 0.04262330
end.cos -0.99501939 0.04058070
end.ri 0.58293730 0.15162042
neweights.logd -1.70215772 2.54499183
log_overdispersion 1.64341142 0.06327162
end.ri.sd -0.82785463 0.22994131
end.ri.vec -0.05327835 0.15806521
end.ri.vec -0.81108640 0.21486455
end.ri.vec -0.05373716 0.15593932
end.ri.vec -0.21668062 0.17571221
end.ri.vec -0.11177964 0.16062392
end.ri.vec -0.17402361 0.15971358
end.ri.vec 0.66594199 0.15190447
end.ri.vec 0.03744401 0.15562227
end.ri.vec -0.41090156 0.17463112
end.ri.vec 0.80437956 0.15323840
end.ri.vec 0.36049131 0.15293007
end.ri.vec 0.04467813 0.15691482
If we just print the “sdr” object, we get the parameters without random intercepts similar to the hhh4 summary.
sdrEErisdreport(.) result
Estimate Std. Error
ar.1 -1.14316597 0.06728866
ne.1 -1.56959201 0.15900895
end.sin -0.06086412 0.04262330
end.cos -0.99501939 0.04058070
end.ri 0.58293730 0.15162042
neweights.logd -1.70215772 2.54499183
log_overdispersion 1.64341142 0.06327162
end.ri.sd -0.82785463 0.22994131
Maximum gradient component: 0.0002147566
We can get the random intercepts from the “sdr” object like this:
sdrEEri$par.random end.ri.vec end.ri.vec end.ri.vec end.ri.vec end.ri.vec end.ri.vec
-0.05327835 -0.81108640 -0.05373716 -0.21668062 -0.11177964 -0.17402361
end.ri.vec end.ri.vec end.ri.vec end.ri.vec end.ri.vec end.ri.vec
0.66594199 0.03744401 -0.41090156 0.80437956 0.36049131 0.04467813
Unfortunenately, the names of end.ri.vec specified in parms are lost during the processing.
We could specify the random effects by individual names directly in the parameter list:
end.ri.vec <- rep(0, data$n_unit)
names(end.ri.vec) <- paste0("end.ri.", colnames(data$Y))
parms_i <- c(
list(
ar.1 = 0,
ne.1 = 0,
end.sin = 0,
end.cos = 0,
end.ri = 0, # global intercept
neweights.logd = log(2),
log_overdispersion = 0),
as.list(end.ri.vec),
list(end.risd = 0)
)Since unlist() is not overloaded for the “advector” class, we use a hacky trick, calling c() with all random intercepts as objects to be concatenated. Otherwise, we would not track the random intercepts specified in parms_i as those in the AD graph (tapeing).
## unlist() is not working for class "advector"
## but "advector" is what is tracked on the tape
group_par <- function(parms, component, type, which = colnames(data$Y), threeparts = TRUE) {
sepr <- ifelse(threeparts, ".", "")
parnames <- paste0(component, ".", type, sepr, which)
return(do.call(c, parms[parnames]))
}
meanEEri_i <- function(parms, data){
## ENDEMIC
end.ri.vec <- group_par(parms, "end", type = "ri")
print(class(end.ri.vec))
ri_mat <- matrix(end.ri.vec, nrow = data$n_time - 1, ncol = data$n_unit, byrow = TRUE)
s_vec <- parms$end.sin * data$Ssin[-1] + parms$end.cos * data$Scos[-1]
end <- exp(parms$end.ri + ri_mat + s_vec)
## EPIDEMIC
Y_lag <- data$Y[1:(data$n_time - 1), ]
ar <- exp(parms$ar.1) * Y_lag
W <- data$W_n^(-exp(parms$neweights.logd))
diag(W) <- 0
W_norm <- W / rowSums(W)
ne <- exp(parms$ne.1) * (Y_lag %*% W_norm)
return(list(mu = end + ar + ne,
end = end,
epi = ar + ne,
epi.ar = ar,
epi.ne = ne))
}
objectiveEEri_i <- function(parms, data) {
data$Y <- OBS(data$Y)
## penalization random intercepts
end.ri.vec <- group_par(parms, "end", type = "ri")
print(class(end.ri.vec))
nll <- - sum(dnorm(end.ri.vec, mean = 0, sd = exp(parms$end.risd), log=TRUE))
## helper function for mean calculation
fit <- meanEEri_i(parms, data)
REPORT(fit$mu)
## negative log likelihood
nll <- nll - sum(dnbinom2(data$Y[-1, ], mu = fit$mu, size = exp(parms$log_overdispersion), log = TRUE))
return(nll)
}objEEri_i <- MakeADFun(cmb(objectiveEEri_i, data), parms_i, random = "end\\.ri\\.", regexp = TRUE)
ptm <- proc.time() # track runtime
## fit the model
optEEri_i <- nlminb(objEEri_i$par, objEEri_i$fn, objEEri_i$gr)
## calculate uncertainties
sdrEEri_i <- sdreport(objEEri_i)
runtime_EEri_i <- proc.time() - ptmsummary(sdrEEri_i) Estimate Std. Error
ar.1 -1.14316597 0.06728866
ne.1 -1.56959201 0.15900895
end.sin -0.06086412 0.04262330
end.cos -0.99501939 0.04058070
end.ri 0.58293730 0.15162042
neweights.logd -1.70215772 2.54499183
log_overdispersion 1.64341142 0.06327162
end.risd -0.82785463 0.22994131
end.ri.chwi -0.05327835 0.15806521
end.ri.frkr -0.81108640 0.21486455
end.ri.lich -0.05373716 0.15593932
end.ri.mahe -0.21668062 0.17571221
end.ri.mitt -0.11177964 0.16062392
end.ri.neuk -0.17402361 0.15971358
end.ri.pank 0.66594199 0.15190447
end.ri.rein 0.03744401 0.15562227
end.ri.span -0.41090156 0.17463112
end.ri.zehl 0.80437956 0.15323840
end.ri.scho 0.36049131 0.15293007
end.ri.trko 0.04467813 0.15691482
all.equal(unname(sdrEEri_i$par.fixed), unname(sdrEEri$par.fixed))[1] TRUE
all.equal(unname(sdrEEri_i$par.random), unname(sdrEEri$par.random))[1] TRUE
Now we have random intercepts named after each district, similar to hhh4. Also we confirm that the results of the two different mean functions are the same.
3.4 Comparing Random Effects
Now, comparing the random intercepts, we see that they are slightly different:
ranef(noroEEriFit) - sdrEEri$par.randomend.ri(iid).chwi end.ri(iid).frkr end.ri(iid).lich end.ri(iid).mahe
-0.0089815350 0.0256341861 -0.0069506612 -0.0013766093
end.ri(iid).mitt end.ri(iid).neuk end.ri(iid).pank end.ri(iid).rein
-0.0055597774 0.0002524912 -0.0295332525 -0.0099230732
end.ri(iid).span end.ri(iid).zehl end.ri(iid).scho end.ri(iid).trko
0.0176667314 -0.0309791984 -0.0226332423 -0.0090637418
3.5 Comparison Table
| hhh4 | RTMB | diff. | |
|---|---|---|---|
| Parameters | |||
| ar.1 | -1.151944754 (0.067391921) | -1.14316597 (0.067288661) | -0.00878 (0.000103) |
| ne.1 | -1.675806569 (0.167178089) | -1.569592015 (0.159008953) | -0.106 (0.00817) |
| end.sin(2 * pi * t/52) | -0.072145386 (0.04031047) | -0.06086412 (0.042623297) | -0.0113 (-0.00231) |
| end.cos(2 * pi * t/52) | -0.994644769 (0.038609056) | -0.995019389 (0.040580695) | 0.000375 (-0.00197) |
| end.ri(iid) | 0.642763485 (0.146789015) | 0.582937302 (0.151620418) | 0.0598 (-0.00483) |
| neweights.logd | -1.495297213 (2.331782994) | -1.702157718 (2.544991834) | 0.207 (-0.213) |
| -log(overdisp) | 1.652151463 (0.063353182) | 1.643411422 (0.063271618) | 0.00874 (8.16e-05) |
| end.ri(iid).chwi | -0.062259886 (0.155637505) | -0.053278351 (0.15806521) | -0.00898 (-0.00243) |
| end.ri(iid).frkr | -0.785452212 (0.199339283) | -0.811086399 (0.214864553) | 0.0256 (-0.0155) |
| end.ri(iid).lich | -0.06068782 (0.153407802) | -0.053737159 (0.15593932) | -0.00695 (-0.00253) |
| end.ri(iid).mahe | -0.218057227 (0.1719338) | -0.216680618 (0.175712206) | -0.00138 (-0.00378) |
| end.ri(iid).mitt | -0.117339415 (0.157736552) | -0.111779637 (0.160623917) | -0.00556 (-0.00289) |
| end.ri(iid).neuk | -0.173771119 (0.156816424) | -0.17402361 (0.15971358) | 0.000252 (-0.0029) |
| end.ri(iid).pank | 0.636408739 (0.148154051) | 0.665941991 (0.151904468) | -0.0295 (-0.00375) |
| end.ri(iid).rein | 0.027520941 (0.153101183) | 0.037444015 (0.155622268) | -0.00992 (-0.00252) |
| end.ri(iid).span | -0.393234829 (0.168681881) | -0.41090156 (0.174631122) | 0.0177 (-0.00595) |
| end.ri(iid).zehl | 0.773400366 (0.14881506) | 0.804379565 (0.153238404) | -0.031 (-0.00442) |
| end.ri(iid).scho | 0.33785807 (0.150281072) | 0.360491312 (0.152930075) | -0.0226 (-0.00265) |
| end.ri(iid).trko | 0.035614392 (0.154153859) | 0.044678133 (0.156914824) | -0.00906 (-0.00276) |
| Random Effects Variance | 0.191570297 | 0.190956568 | 0.000613729 |
| Metrics | |||
| Log-Likelihood | -5778.442432354 | -5797.142934267 | 18.700501913 |
| Runtime (seconds) | 0.457 | 1.09 | -0.633 |
The difference in the negative log-likelihood is large, with 18.7, although the parameter estimates are quite similar.
We can use the surveillance:::penLogLik() function with the parameters of our RTMB model. We have to use the terms from the noroEEriFit model.
end.ri_sd <- sdrEEri$par.fixed[length(sdrEEri$par.fixed)]
coefs <- summary(sdrEEri)[, "Estimate"]
(coefs <- coefs[-which(names(coefs) == "end.ri.sd")]) ar.1 ne.1 end.sin end.cos
-1.14316597 -1.56959201 -0.06086412 -0.99501939
end.ri neweights.logd log_overdispersion end.ri.vec
0.58293730 -1.70215772 1.64341142 -0.05327835
end.ri.vec end.ri.vec end.ri.vec end.ri.vec
-0.81108640 -0.05373716 -0.21668062 -0.11177964
end.ri.vec end.ri.vec end.ri.vec end.ri.vec
-0.17402361 0.66594199 0.03744401 -0.41090156
end.ri.vec end.ri.vec end.ri.vec
0.80437956 0.36049131 0.04467813
penLogLikRTMB <- surveillance:::penLogLik(
theta = coefs,
sd.corr = end.ri_sd,
model = terms(noroEEriFit)
)
print(penLogLikRTMB, digits = 12)[1] -5778.7583852
noroEEriFit$loglikelihood - penLogLikRTMB[1] 0.3159528
So if we use the same penalized log-likelihood function, we get much closer likelihoods.
Both implemenations use the penalized log-likelihood to obtain parameter estimates in presence of random effects: \[ \ell_{pen}(\beta,b;\Sigma) = \ell(\beta,b) + \log p(b|\Sigma), \] where \(\log(p(b|\Sigma))\) is the penalty term corresponding to the log distribution of the random effect vector \(b.\) Both use Laplace approximation for maximizing (minimizing) the marginal likelihood \[ L_{marg}(\Sigma) = \int exp(\ell_{pen}(\beta,b;\Sigma))d\beta db. \]
But, in hhh4 the “[…] variance components are treated as fixed when estimating the fixed and random effects. The variance components itself are estimated through maximizing the marginal likelihood after integration with respect to the fixed and random effects” (Paul and Held (2011)). So an alternating algorithm is used, further described in the inference part of Paul and Held (2011). Allowing hhh4 to drop terms that are constant in the corresponding iterative step. “First update regression coefficients given the current variance parameters, then update variance components given current regression coefficients via Newton steps. Iterate these two steps until convergence is reached, i.e. parameter estimates no longer change.” (Paul and Held (2011)).
In RTMB, we specify the negative joint log-likelihood as a function of parameters (\(\beta\), in TMB paper \(\theta\)) and random effects (\(b\), in TMB paper \(u\)). The random effects are then integrated out of the marginal likelihood via Laplace approximation: \[ L(\beta) = \int exp(-f(b, \beta))db .\]
The optimization of random effects is performed automatically by the fn (likelihood function) and gr (gradient function). For each evaluation of the marginal likelihood, inner optimization is performed to find the optimal random effects conditional on the current values of the other parameters, including the variance components (Kristensen et al. (2016)).
While the approaches are very similar, the difference is in the procedure. Wherehhh4 uses an alternating algorithm, RTMB performs the optimization of random effect within each evaluation of the marginal likelihood. This can lead to differences in the negative log-likelihood.
Since the parameter estimates are slightly different, we look at the difference in the estimated mean:
est.fixed <- split(sdrEEri$par.fixed, names(sdrEEri$par.fixed))
est.random <- split(sdrEEri$par.random, names(sdrEEri$par.random))
fitted.values <- meanEEri(c(est.fixed, est.random), data)
head(fitted.values$mu - fitted(noroEEriFit)) chwi frkr lich mahe mitt
[1,] 0.006068942 0.011236171 0.0067880957 0.009772715 7.634405e-03
[2,] 0.029355445 0.034569513 0.0319267153 0.034427356 2.953338e-02
[3,] 0.018616752 0.023767663 0.0168256677 0.019766583 1.382965e-02
[4,] -0.004102662 0.002337225 -0.0008278179 0.001388850 -3.449048e-05
[5,] 0.005747826 0.011455386 0.0014588205 0.005490621 3.508954e-03
[6,] -0.004870384 -0.001837448 -0.0077968763 -0.003636782 -6.666599e-03
neuk pank rein span zehl
[1,] 4.283202e-03 0.001657139 0.004215129 0.003638195 -0.0007812496
[2,] 3.122603e-02 0.028168088 0.027341247 0.026535763 0.0298485970
[3,] 1.389371e-02 0.013032806 0.013686380 0.015835973 0.0105324858
[4,] -3.547448e-03 -0.003080424 -0.004174383 -0.006396883 -0.0019488920
[5,] 5.583251e-05 0.000123934 0.001528364 0.001401550 -0.0024099659
[6,] -8.641756e-03 -0.006584102 -0.008458921 -0.007374049 -0.0116441224
scho trko
[1,] 0.005976764 0.005553169
[2,] 0.026080562 0.028741690
[3,] 0.014545971 0.017939961
[4,] -0.002914309 -0.003179647
[5,] 0.002143818 0.003446114
[6,] -0.005273626 -0.005431302
The maximal absolute difference is 0.2210901, which is not negligible.
The plots of the total fitted values look the same.
plot(noroEEriFit, type = "fitted", total = TRUE)
plot.RTMBfit(fitted.values, data)