In HCDC data, date of reporting and date of admission is not NA, but onset date can have NA values.
Attack rate
Attack rate is the proportion of new daily cases relative to the remaining susceptible pool. In a closed population:
If the daily case count is increasing, the daily attack rate must also increase: because the numerator (daily cases) is growing while the denominator (susceptible pool) is dropping
The daily attack rate peaks later than the daily case count: because even when new cases begin to drop, the susceptible pool decreases at an even faster rate, causing the ratio to continue rising for a lag period
Day 20 (The Peak):
Susceptible pool at start of day: 10,000
New cases today: 1,000
Attack rate: 1,000 / 10,000 = 10%
Day 21 (Cases start to drop):
Susceptible pool at start of day: 9,000 (because 10,000 - 1,000)
Because \(S'(t)\) is negative, \(-S'(t)I(t) > 0\), therefore \(I'(t) > 0\).
\(A'(t) = \frac{\beta}{N} I'(t)\), this proves that \(A'(t) > 0\) when incidence peaks, meaning attack rate is still increasing.
When incidence peaks, attack rate is still increasing, so before incidence peaks (incidence is increasing), attack rate is guaranteed to increase.
In an open population model, when the birth rate or migration is very high (higher than the rate of depletion), the denominator increase faster and can make the attack rate drops.
Code
# Define the SIR equationssir_equations <-function(time, state, parameters) {with(as.list(c(state, parameters)), { dS <--beta * S * I / N dI <- beta * S * I / N - gamma * I dR <- gamma * Ilist(c(dS, dI, dR)) })}# Set parameters (R0 = 4)N <-100000initial_state <-c(S = N -1, I =1, R =0)parameters <-c(beta =0.4, gamma =0.1) times <-seq(0, 150, by =1)# Simulate and calculate metricssimulation <-ode(y = initial_state, times = times, func = sir_equations, parms = parameters) |>as.data.frame() |>as_tibble() |>mutate(daily_cases =as.numeric(lag(S, default =first(S)) - S),daily_attack_rate =as.numeric(daily_cases /lag(S, default =first(S))) ) |>mutate(daily_attack_rate =if_else(is.na(daily_attack_rate), 0, daily_attack_rate))# Plot 1: Absolute scale (Numerator vs Denominator)plot1 <- simulation |>ggplot(aes(x = time)) +geom_line(aes(y = S, colour ="Susceptible (denominator)"), linewidth =1.2) +geom_col(aes(y = daily_cases, fill ="Daily cases (numerator)"), alpha =0.8) +scale_y_continuous(labels = comma) +scale_colour_manual(values =c("Susceptible (denominator)"="#2C3E50")) +# Dark Slatescale_fill_manual(values =c("Daily cases (numerator)"="#2980B9")) +# Clear Bluelabs(title ="Daily cases vs remaining susceptible pool",x =NULL, # Remove x-axis label on top plot to reduce cluttery ="Number of individuals" ) +theme_minimal() +theme(legend.position ="top", legend.title =element_blank(),plot.title =element_text(face ="bold") )# Plot 2: Observing the peak lag (Cases vs Attack rate)scale_ar <-max(simulation$daily_cases) /max(simulation$daily_attack_rate)plot2 <- simulation |>ggplot(aes(x = time)) +geom_col(aes(y = daily_cases, fill ="Daily cases"), alpha =0.4) +geom_line(aes(y = daily_attack_rate * scale_ar, colour ="Daily attack rate"), linewidth =1.2) +scale_y_continuous(name ="Daily new cases",labels = comma,sec.axis =sec_axis(~ . / scale_ar, name ="Daily attack rate", labels = percent) ) +scale_colour_manual(values =c("Daily attack rate"="#C0392B")) +# Strong Redscale_fill_manual(values =c("Daily cases"="#2980B9")) +# Clear Bluelabs(title ="Lag effect: incidence peak vs attack rate peak",x ="Time (days)" ) +theme_minimal() +theme(legend.position ="top", legend.title =element_blank(),plot.title =element_text(face ="bold"),axis.title.y.left =element_text(colour ="#2980B9", face ="bold", margin =margin(r =10)),axis.title.y.right =element_text(colour ="#C0392B", face ="bold", margin =margin(l =10)) )# Combine plots using patchworkplot1 / plot2