This is a follow-up again to my previous posts. I have since discovered that effects come in from the linear regression that make the use of cumulative cases even less valid.
Again, I am playing with simulated epidemics of 10 days free growth, followed by 10 days with \(NPI_1\) then 10 days with \(NPI_1\) and \(NPI_2\) What is key is that \(NPI_1\) lasts from day 11 to day 30 and \(NPI_2\) lasts from day 21 to day 30. This is exactly what was done in the original paper. The result is that the regression over-weights the \(NPI_1\) and so has to under-weight \(NPI_2\).
I could try to explain it in intuitive terms but I’m not sure that I’m right. Fundamentally it’s a result of apply linear regression to something that is just not linear.
Here are some examples of this going badly wrong. The highlight is where the NPI that causes growth to increase comes out better than the NPI that drops growth to 0!
library(knitr)
# This class represents and Non-pharmacological intervention.
# The NPI is in place from day |start| to day |end| inclusive.
setClass("npi",
slots = list(
start = "numeric",
end = "numeric",
factor = "numeric"
))
# Convenient constructor for NPIs.
NPI <-
function(start, end, factor)
new("npi",
start = start,
end = end,
factor = factor)
# Returns true if a day is impacted by |npi|
in_npi <- function(npi, i) {
i >= npi@start && i <= npi@end
}
# Returns the last day impacted by any NPI in |npis|
npis_end <- function(npis) {
max(sapply(npis, function(x) {
x@end
}))
}
# A vector of 0/1 for each day simulated. A day gets a 1 if the NPI is active on that day, 0 otherwise.
# Used as a predictor in the linear regression.
npi_pred <- function(npis, i) {
last <- npis_end(npis)
npi = npis[[i]]
pred <- numeric()
value = 0
for (d in 1:last) {
pred <- append(pred, (if (in_npi(npi, d))
1
else
0))
}
pred
}
# Scales a growth rate so that it becomes |rate| every |period| days.
scale <- function(rate, period) {
exp(log(rate) / period)
}
# Returns a vector of growth rates by combining |npis| with a base growth rate.
# All growth is scaled over |period| days.
createGrowthRates <- function(r0, npis, period) {
last <- npis_end(npis)
rates <- numeric()
for (i in 1:last) {
rate <- r0
for (npi in npis) {
if (in_npi(npi, i)) {
rate <- rate * npi@factor
}
}
rates <- append(rates, scale(rate, period))
}
rates
}
# Create a timeseries of cases based on the supplied vector of daily growth rates.
createCases <- function(rates) {
cases <- c(1)
for (rate in rates) {
cases <- append(cases, tail(cases, 1) * rate)
}
cases
}
# Returns the coefficients for the impact of |npis| by applying linear regression to diff(log(|cases|)).
regress <- function(npis, cases) {
d_l = diff(log(cases))
npi1 <- npi_pred(npis, 1)
npi2 <- npi_pred(npis, 2)
d = data.frame(y = d_l,
npi1 = npi1,
npi2 = npi2)
r.model <- lm(y ~ npi1 + npi2, data = d)
r.model$coefficients
}
# Dump out some info on the coefficients.
renderRates <- function(c, period, title) {
rate_c = exp(c * period)
d = data.frame(c, rate_c)
print(kable(d, col.names=c("log(r)", paste("r ", period, " days")),
caption=paste("Regression using ", title), valign='t'))
}
# Applying regression and print the results and comparisons.
regressAndRender <- function(npis, cases, period, title) {
c <- regress(npis, cases)
renderRates(c, period, title)
c
}
# Apply the methodology to |npis| with a base growth rate of |r0|.
# Scale all rates to be per |period|.
# Returns a list of coefficient sets from the regressions it ran.
# It will skip daily-case regression if any NPI has a factor of 0.
doOne <- function(npis, r0, period) {
rates <- createGrowthRates(r0, npis, period)
daily = createCases(rates)
cumulative = cumsum(daily)
plot(daily, ylab="daily cases")
plot(cumulative, ylab="cumulative cases")
d_l_daily <- diff(log(daily))
d_l_cumulative <- diff(log(cumulative))
plot(d_l_daily,ylab="delta(log(daily cases))")
plot(d_l_cumulative, ylab="delta(log(cumulative cases))")
cs = list()
if (min(sapply(npis, function(npi) {npi@factor})) > 0) {
c1 = regressAndRender(npis, daily, period, "daily")
cs = append(cs, c1)
} else {
print("Skipping daily due to 0-factor")
}
c2 = regressAndRender(npis, cumulative, period, "cumulative")
cs = append(cs, c2)
}
Two equally effective NPIs
Consider an epidemic with growth rate of 2.5 per 7-days. Apply two NPIs, each of which cut the 7-day growth rate by 50%, one running from days 11-30 and the other from days 21-30. The final 10 days have both NPIs in place for a cut of 75%.
Using daily cases, linear regression assigns the expected coefficients from which we can recover the 7-day growth rate.
Using cumulative cases, linear regression assigns a much lower coefficient to the first NPI.
npis <- c(NPI(11, 30, 0.5), NPI(21, 30, 0.5))
cs = doOne(npis, 2.5, 7)
Registered S3 methods overwritten by 'htmltools':
method from
print.html tools:rstudio
print.shiny.tag tools:rstudio
print.shiny.tag.list tools:rstudio
Regression using daily
(Intercept) |
0.1308987 |
2.5 |
npi1 |
-0.0990210 |
0.5 |
npi2 |
-0.0990210 |
0.5 |
Regression using cumulative
(Intercept) |
0.3136616 |
8.9856700 |
npi1 |
-0.2063621 |
0.2358559 |
npi2 |
-0.0646044 |
0.6362075 |
Two very unequal NPIs but the same outcome
Another epidemic where the first NPI reduces growth by 1% and the second NPI reduces it by 99% but still appears weaker in the cumulative regression
npis <- c(NPI(11, 30, .99), NPI(21, 30, .01))
cs = doOne(npis, 2.5, 7)
Regression using daily
(Intercept) |
0.1308987 |
2.50 |
npi1 |
-0.0014358 |
0.99 |
npi2 |
-0.6578815 |
0.01 |
Regression using cumulative
(Intercept) |
0.3136616 |
8.9856700 |
npi1 |
-0.1630651 |
0.3193539 |
npi2 |
-0.1335377 |
0.3926784 |
An NPI that makes things worse beats the NPI that ends the epidemic!
Finally the absurdity, where the first NPI increases growth by 5% and the second NPI stops growth entirely but still appears weaker from the regression!
npis <- c(NPI(11, 30, 1.05), NPI(21, 30, 0))
cs = doOne(npis, 2.5, 7)
[1] "Skipping daily due to 0-factor"
Regression using cumulative
(Intercept) |
0.3136616 |
8.9856700 |
npi1 |
-0.1587407 |
0.3291686 |
npi2 |
-0.1549208 |
0.3380891 |
LS0tCnRpdGxlOiAiUHJvYmxlbXMgd2l0aCBkZWx0YShsb2coY3VtdWxhdGl2ZSkpIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgpUaGlzIGlzIGEgZm9sbG93LXVwIGFnYWluIHRvIG15IHByZXZpb3VzIHBvc3RzLgpJIGhhdmUgc2luY2UgZGlzY292ZXJlZCB0aGF0IGVmZmVjdHMKY29tZSBpbiBmcm9tIHRoZSBsaW5lYXIgcmVncmVzc2lvbgp0aGF0IG1ha2UgdGhlIHVzZSBvZiBjdW11bGF0aXZlIGNhc2VzCmV2ZW4gbGVzcyB2YWxpZC4KCkFnYWluLCBJIGFtIHBsYXlpbmcgd2l0aCBzaW11bGF0ZWQgZXBpZGVtaWNzCm9mIDEwIGRheXMgZnJlZSBncm93dGgsCmZvbGxvd2VkIGJ5IDEwIGRheXMgd2l0aCAkTlBJXzEkCnRoZW4gMTAgZGF5cyB3aXRoICROUElfMSQgYW5kICROUElfMiQKV2hhdCBpcyBrZXkgaXMgdGhhdCAkTlBJXzEkIGxhc3RzIGZyb20gZGF5IDExIHRvIGRheSAzMAphbmQgJE5QSV8yJCBsYXN0cyBmcm9tIGRheSAyMSB0byBkYXkgMzAuClRoaXMgaXMgZXhhY3RseSB3aGF0IHdhcyBkb25lIGluIHRoZSBvcmlnaW5hbCBwYXBlci4KVGhlIHJlc3VsdCBpcyB0aGF0CnRoZSByZWdyZXNzaW9uIG92ZXItd2VpZ2h0cyB0aGUgJE5QSV8xJAphbmQgc28gaGFzIHRvIHVuZGVyLXdlaWdodCAkTlBJXzIkLgoKSSBjb3VsZCB0cnkgdG8gZXhwbGFpbiBpdCBpbiBpbnR1aXRpdmUgdGVybXMKYnV0IEknbSBub3Qgc3VyZSB0aGF0IEknbSByaWdodC4KRnVuZGFtZW50YWxseSBpdCdzIGEgcmVzdWx0IG9mIGFwcGx5IGxpbmVhciByZWdyZXNzaW9uCnRvIHNvbWV0aGluZyB0aGF0IGlzIGp1c3Qgbm90IGxpbmVhci4KCkhlcmUgYXJlIHNvbWUgZXhhbXBsZXMgb2YgdGhpcyBnb2luZyBiYWRseSB3cm9uZy4KVGhlIGhpZ2hsaWdodCBpcyB3aGVyZSB0aGUgTlBJIHRoYXQgY2F1c2VzIGdyb3d0aCB0byBpbmNyZWFzZQpjb21lcyBvdXQgYmV0dGVyIHRoYW4KdGhlIE5QSSB0aGF0IGRyb3BzIGdyb3d0aCB0byAwIQoKYGBge3J9CmxpYnJhcnkoa25pdHIpCiMgVGhpcyBjbGFzcyByZXByZXNlbnRzIGFuZCBOb24tcGhhcm1hY29sb2dpY2FsIGludGVydmVudGlvbi4KIyBUaGUgTlBJIGlzIGluIHBsYWNlIGZyb20gZGF5IHxzdGFydHwgdG8gZGF5IHxlbmR8IGluY2x1c2l2ZS4Kc2V0Q2xhc3MoIm5waSIsCiAgICAgICAgIHNsb3RzID0gbGlzdCgKICAgICAgICAgICBzdGFydCA9ICJudW1lcmljIiwKICAgICAgICAgICBlbmQgPSAibnVtZXJpYyIsCiAgICAgICAgICAgZmFjdG9yID0gIm51bWVyaWMiCiAgICAgICAgICkpCiMgQ29udmVuaWVudCBjb25zdHJ1Y3RvciBmb3IgTlBJcy4KTlBJIDwtCiAgZnVuY3Rpb24oc3RhcnQsIGVuZCwgZmFjdG9yKQogICAgbmV3KCJucGkiLAogICAgICAgIHN0YXJ0ID0gc3RhcnQsCiAgICAgICAgZW5kID0gZW5kLAogICAgICAgIGZhY3RvciA9IGZhY3RvcikKCiMgUmV0dXJucyB0cnVlIGlmIGEgZGF5IGlzIGltcGFjdGVkIGJ5IHxucGl8CmluX25waSA8LSBmdW5jdGlvbihucGksIGkpIHsKICBpID49IG5waUBzdGFydCAmJiBpIDw9IG5waUBlbmQKfQoKIyBSZXR1cm5zIHRoZSBsYXN0IGRheSBpbXBhY3RlZCBieSBhbnkgTlBJIGluIHxucGlzfApucGlzX2VuZCA8LSBmdW5jdGlvbihucGlzKSB7CiAgbWF4KHNhcHBseShucGlzLCBmdW5jdGlvbih4KSB7CiAgICB4QGVuZAogIH0pKQp9CgojIEEgdmVjdG9yIG9mIDAvMSBmb3IgZWFjaCBkYXkgc2ltdWxhdGVkLiBBIGRheSBnZXRzIGEgMSBpZiB0aGUgTlBJIGlzIGFjdGl2ZSBvbiB0aGF0IGRheSwgMCBvdGhlcndpc2UuCiMgVXNlZCBhcyBhIHByZWRpY3RvciBpbiB0aGUgbGluZWFyIHJlZ3Jlc3Npb24uCm5waV9wcmVkIDwtIGZ1bmN0aW9uKG5waXMsIGkpIHsKICBsYXN0IDwtIG5waXNfZW5kKG5waXMpCiAgbnBpID0gbnBpc1tbaV1dCiAgcHJlZCA8LSBudW1lcmljKCkKICB2YWx1ZSA9IDAKICBmb3IgKGQgaW4gMTpsYXN0KSB7CiAgICBwcmVkIDwtIGFwcGVuZChwcmVkLCAoaWYgKGluX25waShucGksIGQpKQogICAgICAxCiAgICAgIGVsc2UKICAgICAgICAwKSkKICB9CiAgcHJlZAp9CgojIFNjYWxlcyBhIGdyb3d0aCByYXRlIHNvIHRoYXQgaXQgYmVjb21lcyB8cmF0ZXwgZXZlcnkgfHBlcmlvZHwgZGF5cy4Kc2NhbGUgPC0gZnVuY3Rpb24ocmF0ZSwgcGVyaW9kKSB7CiAgZXhwKGxvZyhyYXRlKSAvIHBlcmlvZCkKfQoKIyBSZXR1cm5zIGEgdmVjdG9yIG9mIGdyb3d0aCByYXRlcyBieSBjb21iaW5pbmcgfG5waXN8IHdpdGggYSBiYXNlIGdyb3d0aCByYXRlLgojIEFsbCBncm93dGggaXMgc2NhbGVkIG92ZXIgfHBlcmlvZHwgZGF5cy4KY3JlYXRlR3Jvd3RoUmF0ZXMgPC0gZnVuY3Rpb24ocjAsIG5waXMsIHBlcmlvZCkgewogIGxhc3QgPC0gbnBpc19lbmQobnBpcykKICByYXRlcyA8LSBudW1lcmljKCkKICBmb3IgKGkgaW4gMTpsYXN0KSB7CiAgICByYXRlIDwtIHIwCiAgICBmb3IgKG5waSBpbiBucGlzKSB7CiAgICAgIGlmIChpbl9ucGkobnBpLCBpKSkgewogICAgICAgIHJhdGUgPC0gcmF0ZSAqIG5waUBmYWN0b3IKICAgICAgfQogICAgfQogICAgcmF0ZXMgPC0gYXBwZW5kKHJhdGVzLCBzY2FsZShyYXRlLCBwZXJpb2QpKQogIH0KICByYXRlcwp9CgojIENyZWF0ZSBhIHRpbWVzZXJpZXMgb2YgY2FzZXMgYmFzZWQgb24gdGhlIHN1cHBsaWVkIHZlY3RvciBvZiBkYWlseSBncm93dGggcmF0ZXMuCmNyZWF0ZUNhc2VzIDwtIGZ1bmN0aW9uKHJhdGVzKSB7CiAgY2FzZXMgPC0gYygxKQogIGZvciAocmF0ZSBpbiByYXRlcykgewogICAgY2FzZXMgPC0gYXBwZW5kKGNhc2VzLCB0YWlsKGNhc2VzLCAxKSAqIHJhdGUpCiAgfQogIGNhc2VzCn0KCiMgUmV0dXJucyB0aGUgY29lZmZpY2llbnRzIGZvciB0aGUgaW1wYWN0IG9mIHxucGlzfCBieSBhcHBseWluZyBsaW5lYXIgcmVncmVzc2lvbiB0byBkaWZmKGxvZyh8Y2FzZXN8KSkuCnJlZ3Jlc3MgPC0gZnVuY3Rpb24obnBpcywgY2FzZXMpIHsKICBkX2wgPSBkaWZmKGxvZyhjYXNlcykpCiAgbnBpMSA8LSBucGlfcHJlZChucGlzLCAxKQogIG5waTIgPC0gbnBpX3ByZWQobnBpcywgMikKICBkID0gZGF0YS5mcmFtZSh5ID0gZF9sLAogICAgICAgICAgICAgICAgIG5waTEgPSBucGkxLAogICAgICAgICAgICAgICAgIG5waTIgPSBucGkyKQogIHIubW9kZWwgPC0gbG0oeSB+IG5waTEgKyBucGkyLCBkYXRhID0gZCkKICByLm1vZGVsJGNvZWZmaWNpZW50cwp9CgojIER1bXAgb3V0IHNvbWUgaW5mbyBvbiB0aGUgY29lZmZpY2llbnRzLgpyZW5kZXJSYXRlcyA8LSBmdW5jdGlvbihjLCBwZXJpb2QsIHRpdGxlKSB7CiAgcmF0ZV9jID0gZXhwKGMgKiBwZXJpb2QpCiAgZCA9IGRhdGEuZnJhbWUoYywgcmF0ZV9jKQogIHByaW50KGthYmxlKGQsIGNvbC5uYW1lcz1jKCJsb2cocikiLCBwYXN0ZSgiciAiLCBwZXJpb2QsICIgZGF5cyIpKSwKICAgICAgICAgICAgICBjYXB0aW9uPXBhc3RlKCJSZWdyZXNzaW9uIHVzaW5nICIsIHRpdGxlKSwgdmFsaWduPSd0JykpCn0KCiMgQXBwbHlpbmcgcmVncmVzc2lvbiBhbmQgcHJpbnQgdGhlIHJlc3VsdHMgYW5kIGNvbXBhcmlzb25zLgpyZWdyZXNzQW5kUmVuZGVyIDwtIGZ1bmN0aW9uKG5waXMsIGNhc2VzLCBwZXJpb2QsIHRpdGxlKSB7CiAgYyA8LSByZWdyZXNzKG5waXMsIGNhc2VzKQogIHJlbmRlclJhdGVzKGMsIHBlcmlvZCwgdGl0bGUpCiAgYwp9CgojIEFwcGx5IHRoZSBtZXRob2RvbG9neSB0byB8bnBpc3wgd2l0aCBhIGJhc2UgZ3Jvd3RoIHJhdGUgb2YgfHIwfC4KIyBTY2FsZSBhbGwgcmF0ZXMgdG8gYmUgcGVyIHxwZXJpb2R8LgojIFJldHVybnMgYSBsaXN0IG9mIGNvZWZmaWNpZW50IHNldHMgZnJvbSB0aGUgcmVncmVzc2lvbnMgaXQgcmFuLgojIEl0IHdpbGwgc2tpcCBkYWlseS1jYXNlIHJlZ3Jlc3Npb24gaWYgYW55IE5QSSBoYXMgYSBmYWN0b3Igb2YgMC4KZG9PbmUgPC0gZnVuY3Rpb24obnBpcywgcjAsIHBlcmlvZCkgewogIHJhdGVzIDwtIGNyZWF0ZUdyb3d0aFJhdGVzKHIwLCBucGlzLCBwZXJpb2QpCiAgZGFpbHkgPSBjcmVhdGVDYXNlcyhyYXRlcykKICBjdW11bGF0aXZlID0gY3Vtc3VtKGRhaWx5KQoKICBwbG90KGRhaWx5LCB5bGFiPSJkYWlseSBjYXNlcyIpCiAgcGxvdChjdW11bGF0aXZlLCB5bGFiPSJjdW11bGF0aXZlIGNhc2VzIikKCiAgZF9sX2RhaWx5IDwtIGRpZmYobG9nKGRhaWx5KSkKICBkX2xfY3VtdWxhdGl2ZSA8LSBkaWZmKGxvZyhjdW11bGF0aXZlKSkKCiAgcGxvdChkX2xfZGFpbHkseWxhYj0iZGVsdGEobG9nKGRhaWx5IGNhc2VzKSkiKQogIHBsb3QoZF9sX2N1bXVsYXRpdmUsIHlsYWI9ImRlbHRhKGxvZyhjdW11bGF0aXZlIGNhc2VzKSkiKQoKICBjcyA9IGxpc3QoKQogIGlmIChtaW4oc2FwcGx5KG5waXMsIGZ1bmN0aW9uKG5waSkge25waUBmYWN0b3J9KSkgPiAwKSB7CiAgICBjMSA9IHJlZ3Jlc3NBbmRSZW5kZXIobnBpcywgZGFpbHksIHBlcmlvZCwgImRhaWx5IikKICAgIGNzID0gYXBwZW5kKGNzLCBjMSkKICB9IGVsc2UgewogICAgcHJpbnQoIlNraXBwaW5nIGRhaWx5IGR1ZSB0byAwLWZhY3RvciIpCiAgfQogIGMyID0gcmVncmVzc0FuZFJlbmRlcihucGlzLCBjdW11bGF0aXZlLCBwZXJpb2QsICJjdW11bGF0aXZlIikKICBjcyA9IGFwcGVuZChjcywgYzIpCn0KYGBgCgojIFR3byBlcXVhbGx5IGVmZmVjdGl2ZSBOUElzCgpDb25zaWRlciBhbiBlcGlkZW1pYyB3aXRoIGdyb3d0aCByYXRlIG9mIDIuNSBwZXIgNy1kYXlzLgpBcHBseSB0d28gTlBJcywKZWFjaCBvZiB3aGljaCBjdXQgdGhlIDctZGF5IGdyb3d0aCByYXRlIGJ5IDUwJSwKb25lIHJ1bm5pbmcgZnJvbSBkYXlzIDExLTMwCmFuZCB0aGUgb3RoZXIgZnJvbSBkYXlzIDIxLTMwLgpUaGUgZmluYWwgMTAgZGF5cyBoYXZlIGJvdGggTlBJcyBpbiBwbGFjZSBmb3IgYSBjdXQgb2YgNzUlLgoKVXNpbmcgZGFpbHkgY2FzZXMsCmxpbmVhciByZWdyZXNzaW9uIGFzc2lnbnMgdGhlIGV4cGVjdGVkIGNvZWZmaWNpZW50cwpmcm9tIHdoaWNoIHdlIGNhbiByZWNvdmVyIHRoZSA3LWRheSBncm93dGggcmF0ZS4KClVzaW5nIGN1bXVsYXRpdmUgY2FzZXMsCmxpbmVhciByZWdyZXNzaW9uIGFzc2lnbnMgYSBtdWNoIGxvd2VyIGNvZWZmaWNpZW50CnRvIHRoZSBmaXJzdCBOUEkuCgpgYGB7cn0KbnBpcyA8LSBjKE5QSSgxMSwgMzAsIDAuNSksIE5QSSgyMSwgMzAsIDAuNSkpCmNzID0gZG9PbmUobnBpcywgMi41LCA3KQpgYGAKIyBUd28gdmVyeSB1bmVxdWFsIE5QSXMgYnV0IHRoZSBzYW1lIG91dGNvbWUKCkFub3RoZXIgZXBpZGVtaWMgd2hlcmUgdGhlIGZpcnN0IE5QSQpyZWR1Y2VzIGdyb3d0aCBieSAxJQphbmQgdGhlIHNlY29uZCBOUEkgcmVkdWNlcyBpdCBieSA5OSUKYnV0IHN0aWxsIGFwcGVhcnMgd2Vha2VyIGluIHRoZSBjdW11bGF0aXZlIHJlZ3Jlc3Npb24KCmBgYHtyfQpucGlzIDwtIGMoTlBJKDExLCAzMCwgLjk5KSwgTlBJKDIxLCAzMCwgLjAxKSkKY3MgPSBkb09uZShucGlzLCAyLjUsIDcpCmBgYAojIEFuIE5QSSB0aGF0IG1ha2VzIHRoaW5ncyB3b3JzZSBiZWF0cyB0aGUgTlBJIHRoYXQgZW5kcyB0aGUgZXBpZGVtaWMhCgpGaW5hbGx5IHRoZSBhYnN1cmRpdHksCndoZXJlIHRoZSBmaXJzdCBOUEkKaW5jcmVhc2VzIGdyb3d0aCBieSA1JQphbmQgdGhlIHNlY29uZCBOUEkgc3RvcHMgZ3Jvd3RoIGVudGlyZWx5CmJ1dCBzdGlsbCBhcHBlYXJzIHdlYWtlciBmcm9tIHRoZSByZWdyZXNzaW9uIQoKYGBge3J9Cm5waXMgPC0gYyhOUEkoMTEsIDMwLCAxLjA1KSwgTlBJKDIxLCAzMCwgMCkpCmNzID0gZG9PbmUobnBpcywgMi41LCA3KQpgYGAK