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
log(r) r 7 days
(Intercept) 0.1308987 2.5
npi1 -0.0990210 0.5
npi2 -0.0990210 0.5
Regression using cumulative
log(r) r 7 days
(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
log(r) r 7 days
(Intercept) 0.1308987 2.50
npi1 -0.0014358 0.99
npi2 -0.6578815 0.01
Regression using cumulative
log(r) r 7 days
(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
log(r) r 7 days
(Intercept) 0.3136616 8.9856700
npi1 -0.1587407 0.3291686
npi2 -0.1549208 0.3380891

LS0tCnRpdGxlOiAiUHJvYmxlbXMgd2l0aCBkZWx0YShsb2coY3VtdWxhdGl2ZSkpIgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCgpUaGlzIGlzIGEgZm9sbG93LXVwIGFnYWluIHRvIG15IHByZXZpb3VzIHBvc3RzLgpJIGhhdmUgc2luY2UgZGlzY292ZXJlZCB0aGF0IGVmZmVjdHMKY29tZSBpbiBmcm9tIHRoZSBsaW5lYXIgcmVncmVzc2lvbgp0aGF0IG1ha2UgdGhlIHVzZSBvZiBjdW11bGF0aXZlIGNhc2VzCmV2ZW4gbGVzcyB2YWxpZC4KCkFnYWluLCBJIGFtIHBsYXlpbmcgd2l0aCBzaW11bGF0ZWQgZXBpZGVtaWNzCm9mIDEwIGRheXMgZnJlZSBncm93dGgsCmZvbGxvd2VkIGJ5IDEwIGRheXMgd2l0aCAkTlBJXzEkCnRoZW4gMTAgZGF5cyB3aXRoICROUElfMSQgYW5kICROUElfMiQKV2hhdCBpcyBrZXkgaXMgdGhhdCAkTlBJXzEkIGxhc3RzIGZyb20gZGF5IDExIHRvIGRheSAzMAphbmQgJE5QSV8yJCBsYXN0cyBmcm9tIGRheSAyMSB0byBkYXkgMzAuClRoaXMgaXMgZXhhY3RseSB3aGF0IHdhcyBkb25lIGluIHRoZSBvcmlnaW5hbCBwYXBlci4KVGhlIHJlc3VsdCBpcyB0aGF0CnRoZSByZWdyZXNzaW9uIG92ZXItd2VpZ2h0cyB0aGUgJE5QSV8xJAphbmQgc28gaGFzIHRvIHVuZGVyLXdlaWdodCAkTlBJXzIkLgoKSSBjb3VsZCB0cnkgdG8gZXhwbGFpbiBpdCBpbiBpbnR1aXRpdmUgdGVybXMKYnV0IEknbSBub3Qgc3VyZSB0aGF0IEknbSByaWdodC4KRnVuZGFtZW50YWxseSBpdCdzIGEgcmVzdWx0IG9mIGFwcGx5IGxpbmVhciByZWdyZXNzaW9uCnRvIHNvbWV0aGluZyB0aGF0IGlzIGp1c3Qgbm90IGxpbmVhci4KCkhlcmUgYXJlIHNvbWUgZXhhbXBsZXMgb2YgdGhpcyBnb2luZyBiYWRseSB3cm9uZy4KVGhlIGhpZ2hsaWdodCBpcyB3aGVyZSB0aGUgTlBJIHRoYXQgY2F1c2VzIGdyb3d0aCB0byBpbmNyZWFzZQpjb21lcyBvdXQgYmV0dGVyIHRoYW4KdGhlIE5QSSB0aGF0IGRyb3BzIGdyb3d0aCB0byAwIQoKYGBge3J9CmxpYnJhcnkoa25pdHIpCiMgVGhpcyBjbGFzcyByZXByZXNlbnRzIGFuZCBOb24tcGhhcm1hY29sb2dpY2FsIGludGVydmVudGlvbi4KIyBUaGUgTlBJIGlzIGluIHBsYWNlIGZyb20gZGF5IHxzdGFydHwgdG8gZGF5IHxlbmR8IGluY2x1c2l2ZS4Kc2V0Q2xhc3MoIm5waSIsCiAgICAgICAgIHNsb3RzID0gbGlzdCgKICAgICAgICAgICBzdGFydCA9ICJudW1lcmljIiwKICAgICAgICAgICBlbmQgPSAibnVtZXJpYyIsCiAgICAgICAgICAgZmFjdG9yID0gIm51bWVyaWMiCiAgICAgICAgICkpCiMgQ29udmVuaWVudCBjb25zdHJ1Y3RvciBmb3IgTlBJcy4KTlBJIDwtCiAgZnVuY3Rpb24oc3RhcnQsIGVuZCwgZmFjdG9yKQogICAgbmV3KCJucGkiLAogICAgICAgIHN0YXJ0ID0gc3RhcnQsCiAgICAgICAgZW5kID0gZW5kLAogICAgICAgIGZhY3RvciA9IGZhY3RvcikKCiMgUmV0dXJucyB0cnVlIGlmIGEgZGF5IGlzIGltcGFjdGVkIGJ5IHxucGl8CmluX25waSA8LSBmdW5jdGlvbihucGksIGkpIHsKICBpID49IG5waUBzdGFydCAmJiBpIDw9IG5waUBlbmQKfQoKIyBSZXR1cm5zIHRoZSBsYXN0IGRheSBpbXBhY3RlZCBieSBhbnkgTlBJIGluIHxucGlzfApucGlzX2VuZCA8LSBmdW5jdGlvbihucGlzKSB7CiAgbWF4KHNhcHBseShucGlzLCBmdW5jdGlvbih4KSB7CiAgICB4QGVuZAogIH0pKQp9CgojIEEgdmVjdG9yIG9mIDAvMSBmb3IgZWFjaCBkYXkgc2ltdWxhdGVkLiBBIGRheSBnZXRzIGEgMSBpZiB0aGUgTlBJIGlzIGFjdGl2ZSBvbiB0aGF0IGRheSwgMCBvdGhlcndpc2UuCiMgVXNlZCBhcyBhIHByZWRpY3RvciBpbiB0aGUgbGluZWFyIHJlZ3Jlc3Npb24uCm5waV9wcmVkIDwtIGZ1bmN0aW9uKG5waXMsIGkpIHsKICBsYXN0IDwtIG5waXNfZW5kKG5waXMpCiAgbnBpID0gbnBpc1tbaV1dCiAgcHJlZCA8LSBudW1lcmljKCkKICB2YWx1ZSA9IDAKICBmb3IgKGQgaW4gMTpsYXN0KSB7CiAgICBwcmVkIDwtIGFwcGVuZChwcmVkLCAoaWYgKGluX25waShucGksIGQpKQogICAgICAxCiAgICAgIGVsc2UKICAgICAgICAwKSkKICB9CiAgcHJlZAp9CgojIFNjYWxlcyBhIGdyb3d0aCByYXRlIHNvIHRoYXQgaXQgYmVjb21lcyB8cmF0ZXwgZXZlcnkgfHBlcmlvZHwgZGF5cy4Kc2NhbGUgPC0gZnVuY3Rpb24ocmF0ZSwgcGVyaW9kKSB7CiAgZXhwKGxvZyhyYXRlKSAvIHBlcmlvZCkKfQoKIyBSZXR1cm5zIGEgdmVjdG9yIG9mIGdyb3d0aCByYXRlcyBieSBjb21iaW5pbmcgfG5waXN8IHdpdGggYSBiYXNlIGdyb3d0aCByYXRlLgojIEFsbCBncm93dGggaXMgc2NhbGVkIG92ZXIgfHBlcmlvZHwgZGF5cy4KY3JlYXRlR3Jvd3RoUmF0ZXMgPC0gZnVuY3Rpb24ocjAsIG5waXMsIHBlcmlvZCkgewogIGxhc3QgPC0gbnBpc19lbmQobnBpcykKICByYXRlcyA8LSBudW1lcmljKCkKICBmb3IgKGkgaW4gMTpsYXN0KSB7CiAgICByYXRlIDwtIHIwCiAgICBmb3IgKG5waSBpbiBucGlzKSB7CiAgICAgIGlmIChpbl9ucGkobnBpLCBpKSkgewogICAgICAgIHJhdGUgPC0gcmF0ZSAqIG5waUBmYWN0b3IKICAgICAgfQogICAgfQogICAgcmF0ZXMgPC0gYXBwZW5kKHJhdGVzLCBzY2FsZShyYXRlLCBwZXJpb2QpKQogIH0KICByYXRlcwp9CgojIENyZWF0ZSBhIHRpbWVzZXJpZXMgb2YgY2FzZXMgYmFzZWQgb24gdGhlIHN1cHBsaWVkIHZlY3RvciBvZiBkYWlseSBncm93dGggcmF0ZXMuCmNyZWF0ZUNhc2VzIDwtIGZ1bmN0aW9uKHJhdGVzKSB7CiAgY2FzZXMgPC0gYygxKQogIGZvciAocmF0ZSBpbiByYXRlcykgewogICAgY2FzZXMgPC0gYXBwZW5kKGNhc2VzLCB0YWlsKGNhc2VzLCAxKSAqIHJhdGUpCiAgfQogIGNhc2VzCn0KCiMgUmV0dXJucyB0aGUgY29lZmZpY2llbnRzIGZvciB0aGUgaW1wYWN0IG9mIHxucGlzfCBieSBhcHBseWluZyBsaW5lYXIgcmVncmVzc2lvbiB0byBkaWZmKGxvZyh8Y2FzZXN8KSkuCnJlZ3Jlc3MgPC0gZnVuY3Rpb24obnBpcywgY2FzZXMpIHsKICBkX2wgPSBkaWZmKGxvZyhjYXNlcykpCiAgbnBpMSA8LSBucGlfcHJlZChucGlzLCAxKQogIG5waTIgPC0gbnBpX3ByZWQobnBpcywgMikKICBkID0gZGF0YS5mcmFtZSh5ID0gZF9sLAogICAgICAgICAgICAgICAgIG5waTEgPSBucGkxLAogICAgICAgICAgICAgICAgIG5waTIgPSBucGkyKQogIHIubW9kZWwgPC0gbG0oeSB+IG5waTEgKyBucGkyLCBkYXRhID0gZCkKICByLm1vZGVsJGNvZWZmaWNpZW50cwp9CgojIER1bXAgb3V0IHNvbWUgaW5mbyBvbiB0aGUgY29lZmZpY2llbnRzLgpyZW5kZXJSYXRlcyA8LSBmdW5jdGlvbihjLCBwZXJpb2QsIHRpdGxlKSB7CiAgcmF0ZV9jID0gZXhwKGMgKiBwZXJpb2QpCiAgZCA9IGRhdGEuZnJhbWUoYywgcmF0ZV9jKQogIHByaW50KGthYmxlKGQsIGNvbC5uYW1lcz1jKCJsb2cocikiLCBwYXN0ZSgiciAiLCBwZXJpb2QsICIgZGF5cyIpKSwKICAgICAgICAgICAgICBjYXB0aW9uPXBhc3RlKCJSZWdyZXNzaW9uIHVzaW5nICIsIHRpdGxlKSwgdmFsaWduPSd0JykpCn0KCiMgQXBwbHlpbmcgcmVncmVzc2lvbiBhbmQgcHJpbnQgdGhlIHJlc3VsdHMgYW5kIGNvbXBhcmlzb25zLgpyZWdyZXNzQW5kUmVuZGVyIDwtIGZ1bmN0aW9uKG5waXMsIGNhc2VzLCBwZXJpb2QsIHRpdGxlKSB7CiAgYyA8LSByZWdyZXNzKG5waXMsIGNhc2VzKQogIHJlbmRlclJhdGVzKGMsIHBlcmlvZCwgdGl0bGUpCiAgYwp9CgojIEFwcGx5IHRoZSBtZXRob2RvbG9neSB0byB8bnBpc3wgd2l0aCBhIGJhc2UgZ3Jvd3RoIHJhdGUgb2YgfHIwfC4KIyBTY2FsZSBhbGwgcmF0ZXMgdG8gYmUgcGVyIHxwZXJpb2R8LgojIFJldHVybnMgYSBsaXN0IG9mIGNvZWZmaWNpZW50IHNldHMgZnJvbSB0aGUgcmVncmVzc2lvbnMgaXQgcmFuLgojIEl0IHdpbGwgc2tpcCBkYWlseS1jYXNlIHJlZ3Jlc3Npb24gaWYgYW55IE5QSSBoYXMgYSBmYWN0b3Igb2YgMC4KZG9PbmUgPC0gZnVuY3Rpb24obnBpcywgcjAsIHBlcmlvZCkgewogIHJhdGVzIDwtIGNyZWF0ZUdyb3d0aFJhdGVzKHIwLCBucGlzLCBwZXJpb2QpCiAgZGFpbHkgPSBjcmVhdGVDYXNlcyhyYXRlcykKICBjdW11bGF0aXZlID0gY3Vtc3VtKGRhaWx5KQoKICBwbG90KGRhaWx5LCB5bGFiPSJkYWlseSBjYXNlcyIpCiAgcGxvdChjdW11bGF0aXZlLCB5bGFiPSJjdW11bGF0aXZlIGNhc2VzIikKCiAgZF9sX2RhaWx5IDwtIGRpZmYobG9nKGRhaWx5KSkKICBkX2xfY3VtdWxhdGl2ZSA8LSBkaWZmKGxvZyhjdW11bGF0aXZlKSkKCiAgcGxvdChkX2xfZGFpbHkseWxhYj0iZGVsdGEobG9nKGRhaWx5IGNhc2VzKSkiKQogIHBsb3QoZF9sX2N1bXVsYXRpdmUsIHlsYWI9ImRlbHRhKGxvZyhjdW11bGF0aXZlIGNhc2VzKSkiKQoKICBjcyA9IGxpc3QoKQogIGlmIChtaW4oc2FwcGx5KG5waXMsIGZ1bmN0aW9uKG5waSkge25waUBmYWN0b3J9KSkgPiAwKSB7CiAgICBjMSA9IHJlZ3Jlc3NBbmRSZW5kZXIobnBpcywgZGFpbHksIHBlcmlvZCwgImRhaWx5IikKICAgIGNzID0gYXBwZW5kKGNzLCBjMSkKICB9IGVsc2UgewogICAgcHJpbnQoIlNraXBwaW5nIGRhaWx5IGR1ZSB0byAwLWZhY3RvciIpCiAgfQogIGMyID0gcmVncmVzc0FuZFJlbmRlcihucGlzLCBjdW11bGF0aXZlLCBwZXJpb2QsICJjdW11bGF0aXZlIikKICBjcyA9IGFwcGVuZChjcywgYzIpCn0KYGBgCgojIFR3byBlcXVhbGx5IGVmZmVjdGl2ZSBOUElzCgpDb25zaWRlciBhbiBlcGlkZW1pYyB3aXRoIGdyb3d0aCByYXRlIG9mIDIuNSBwZXIgNy1kYXlzLgpBcHBseSB0d28gTlBJcywKZWFjaCBvZiB3aGljaCBjdXQgdGhlIDctZGF5IGdyb3d0aCByYXRlIGJ5IDUwJSwKb25lIHJ1bm5pbmcgZnJvbSBkYXlzIDExLTMwCmFuZCB0aGUgb3RoZXIgZnJvbSBkYXlzIDIxLTMwLgpUaGUgZmluYWwgMTAgZGF5cyBoYXZlIGJvdGggTlBJcyBpbiBwbGFjZSBmb3IgYSBjdXQgb2YgNzUlLgoKVXNpbmcgZGFpbHkgY2FzZXMsCmxpbmVhciByZWdyZXNzaW9uIGFzc2lnbnMgdGhlIGV4cGVjdGVkIGNvZWZmaWNpZW50cwpmcm9tIHdoaWNoIHdlIGNhbiByZWNvdmVyIHRoZSA3LWRheSBncm93dGggcmF0ZS4KClVzaW5nIGN1bXVsYXRpdmUgY2FzZXMsCmxpbmVhciByZWdyZXNzaW9uIGFzc2lnbnMgYSBtdWNoIGxvd2VyIGNvZWZmaWNpZW50CnRvIHRoZSBmaXJzdCBOUEkuCgpgYGB7cn0KbnBpcyA8LSBjKE5QSSgxMSwgMzAsIDAuNSksIE5QSSgyMSwgMzAsIDAuNSkpCmNzID0gZG9PbmUobnBpcywgMi41LCA3KQpgYGAKIyBUd28gdmVyeSB1bmVxdWFsIE5QSXMgYnV0IHRoZSBzYW1lIG91dGNvbWUKCkFub3RoZXIgZXBpZGVtaWMgd2hlcmUgdGhlIGZpcnN0IE5QSQpyZWR1Y2VzIGdyb3d0aCBieSAxJQphbmQgdGhlIHNlY29uZCBOUEkgcmVkdWNlcyBpdCBieSA5OSUKYnV0IHN0aWxsIGFwcGVhcnMgd2Vha2VyIGluIHRoZSBjdW11bGF0aXZlIHJlZ3Jlc3Npb24KCmBgYHtyfQpucGlzIDwtIGMoTlBJKDExLCAzMCwgLjk5KSwgTlBJKDIxLCAzMCwgLjAxKSkKY3MgPSBkb09uZShucGlzLCAyLjUsIDcpCmBgYAojIEFuIE5QSSB0aGF0IG1ha2VzIHRoaW5ncyB3b3JzZSBiZWF0cyB0aGUgTlBJIHRoYXQgZW5kcyB0aGUgZXBpZGVtaWMhCgpGaW5hbGx5IHRoZSBhYnN1cmRpdHksCndoZXJlIHRoZSBmaXJzdCBOUEkKaW5jcmVhc2VzIGdyb3d0aCBieSA1JQphbmQgdGhlIHNlY29uZCBOUEkgc3RvcHMgZ3Jvd3RoIGVudGlyZWx5CmJ1dCBzdGlsbCBhcHBlYXJzIHdlYWtlciBmcm9tIHRoZSByZWdyZXNzaW9uIQoKYGBge3J9Cm5waXMgPC0gYyhOUEkoMTEsIDMwLCAxLjA1KSwgTlBJKDIxLCAzMCwgMCkpCmNzID0gZG9PbmUobnBpcywgMi41LCA3KQpgYGAK