Code
library(vroom)
library(dplyr)
library(glue)
library(lubridate)
library(echarts4r)
David Beauchesne & Robert Moriarity
May 28, 2024
Part of my postdoctoral work at the University of Toronto has put me in touch with environmental health scientists during the pandemic, one of them being an old friend, Robert Moriarity. We met during our respective master’s at Concordia University in Montreal and always thought that combining our respective expertises would be highly relevant. Rob is now an assistant professor at the School of Occupational and Public Health at the Toronto Metropolitan University.
The goal of our collaboration on my end was to begin incorporating health, social and cultural elements to cumulative effects assessments, but COVID-19 was at the forefront of many of our discussions. As such, I explored some datasets describing COVID-19 outcomes that were available in Canada and a few projects were developped from there. For this particular post, we wish to present two specific datasets and explore the variations of COVID-19 through time in Canada.
The Timeline of COVID-19 in Canada initiative2 provides daily COVID-19 outcome metrics (e.g. deaths, cases, hospitalizations, and tests completed) at the national, provincial/territorial, and health region scales. Here, we used the finer scale spatial data at the health region-level for which daily number of cases and deaths are available for 99 health regions across the country. We limited the temporal window of our exploration to the onset of the pandemic (2020-01-01) to the onset of the Omicron variant (2022-03-11), as it coincides with certain Canadian provinces phasing out reporting at the level of health regions and with the advent of rapid testing, which decreased case reporting throughout the country.
Let us begin by downloading and filtering the data from the GitHub repository where they are stored.
# Get data
hr <- vroom::vroom("https://github.com/ccodwg/CovidTimelineCanada/raw/main/geo/hr.csv")
path <- "https://raw.githubusercontent.com/ccodwg/CovidTimelineCanada/main/data/hr/"
covid <- dplyr::bind_rows(
vroom::vroom(glue::glue("{path}cases_hr.csv")),
vroom::vroom(glue::glue("{path}deaths_hr.csv"))
) |>
filter(date <= as.Date("2022-03-11")) |>
filter(sub_region_1 != 9999)
# Create summary table
dat <- hr |>
group_by(region) |>
summarise(
region_num = n(),
population = sum(pop, na.rm = TRUE)
)
covid |>
group_by(name, region) |>
summarize(value_daily = sum(value_daily)) |>
tidyr::pivot_wider(names_from = name, values_from = value_daily) |>
left_join(dat, by = "region") |>
select(region, region_num, population, cases, deaths) |>
knitr::kable(
col.names = c("Province", "Health regions", "Population", "Cases", "Deaths"),
row.names = FALSE,
)
Province | Health regions | Population | Cases | Deaths |
---|---|---|---|---|
AB | 5 | 4543111 | 530478 | 4003 |
BC | 5 | 5319324 | 352038 | 2958 |
MB | 5 | 1409223 | 132724 | 1721 |
NB | 7 | 812061 | 41512 | 317 |
NL | 4 | 525972 | 29365 | 76 |
NS | 4 | 1019725 | 47123 | 210 |
NT | 1 | 45605 | 9514 | 20 |
NU | 1 | 40526 | 3265 | 5 |
ON | 34 | 15109416 | 1139241 | 12377 |
PE | 1 | 170688 | 19703 | 16 |
QC | 18 | 8695659 | 927181 | 13807 |
SK | 13 | 1204858 | 127486 | 1147 |
YT | 1 | 43789 | 3939 | 23 |
tmp <- left_join(
hr,
covid |>
group_by(name, sub_region_1) |>
summarize(outcomes = sum(value_daily)) |>
tidyr::pivot_wider(names_from = name, values_from = outcomes),
by = c("hruid" = "sub_region_1")
) |>
select(name = name_canonical, pop, cases, deaths) |>
mutate(
cases_per_100k = (cases / pop) * 100000,
deaths_per_100k = (deaths / pop) * 100000
) |>
rename(
Population = pop,
`Number of cases` = cases,
`Number of deaths` = deaths,
`Number of cases per 100 000 habitants` = cases_per_100k,
`Number of deaths per 100 000 habitants` = deaths_per_100k
)
json <- sf::st_read("https://github.com/ccodwg/CovidTimelineCanada/raw/main/geo/hr.geojson", quiet = TRUE) |>
rename(name = name_canonical) |>
geojsonio::geojson_list()
tmp |>
lapply(function(x) {
if (is.numeric(x)) {
x / max(x)
} else {
x
}
}) |>
as.data.frame() |>
tidyr::pivot_longer(-name, names_to = "Outcomes", values_to = "value") |>
group_by(Outcomes) |>
e_charts(name, timeline = TRUE) |>
e_map_register("Canada", json) |>
e_map(value, map = "Canada", roam = TRUE) |>
e_visual_map(value, color = viridis::magma(100)) |>
echarts4r::e_timeline_serie(
title = list(
list(text = "Relative number of COVID-19 cases"),
list(text = "Relative number of COVID-19 cases per 100 000 habitants"),
list(text = "Relative number of COVID-19 deaths"),
list(text = "Relative number of COVID-19 deaths per 100 000 habitants"),
list(text = "Relative population size")
)
)
Map of health regions with relative population sizes and relative COVID-19 outcomes.
Along with cumulative outcomes, we wish to look at the timing of certain types of regulatory interventions that were instated during the pandemic to curb the rates of outcomes (e.g. lockdowns and mask mandates). We used data from the Canadian Institute for Health Information detailing the timeline of federal, provincial and territorial government interventions during the COVID-19 pandemic1.
curl::curl_download(
"https://www.cihi.ca/sites/default/files/document/aoda-covid-19-intervention-timeline-in-canada-en.xlsx",
"mandates.xlsx"
)
mandates <- readxl::read_xlsx("mandates.xlsx", sheet = 3, skip = 2) |>
janitor::clean_names() |>
filter(entry_id != "End of worksheet") |>
mutate(intervention_category = case_when(
intervention_category == "\r\nDistancing" ~ "Distancing",
.default = intervention_category
)) |>
mutate(jurisdiction = case_when(
jurisdiction == "Alta." ~ "AB",
jurisdiction == "B.C." ~ "BC",
jurisdiction == "Can." ~ "Canada",
jurisdiction == "Man." ~ "MB",
jurisdiction == "N.B." ~ "NB",
jurisdiction == "N.L." ~ "NL",
jurisdiction == "N.S." ~ "NS",
jurisdiction == "N.W.T." ~ "NT",
jurisdiction == "Nun." ~ "NU",
jurisdiction == "Ont." ~ "ON",
jurisdiction == "P.E.I." ~ "PE",
jurisdiction == "Que." ~ "QC",
jurisdiction == "Sask." ~ "SK",
jurisdiction == "Y.T." ~ "YT"
)) |>
arrange(jurisdiction, start_date)
mandates |>
group_by(intervention_category, jurisdiction) |>
summarize(number = n()) |>
tidyr::pivot_wider(names_from = jurisdiction, values_from = number) |>
rename(`Intervention category` = intervention_category) |>
knitr::kable()
Intervention category | AB | BC | Canada | MB | NB | NL | NS | NT | NU | ON | PE | QC | SK | YT |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Case management | 33 | 30 | 34 | 32 | 25 | 14 | 11 | 8 | 7 | 36 | 25 | 38 | 28 | 6 |
Closures/openings | 94 | 89 | 2 | 115 | 125 | 113 | 91 | 28 | 224 | 187 | 84 | 208 | 70 | 54 |
Contextual information | 3 | 4 | NA | 3 | 4 | 3 | 4 | 4 | 4 | 4 | 4 | 4 | 4 | 4 |
Distancing | 31 | 32 | 2 | 54 | 69 | 50 | 28 | 12 | 70 | 77 | 34 | 80 | 27 | 20 |
Health services | 23 | 23 | 3 | 21 | 39 | 36 | 31 | 12 | 43 | 33 | 37 | 59 | 17 | 14 |
Health workforce | 45 | 54 | 41 | 44 | 19 | 18 | 38 | 3 | NA | 80 | 7 | 105 | 42 | 11 |
Public information | 31 | 22 | 6 | 44 | 63 | 38 | 21 | 9 | 16 | 85 | 33 | 62 | 22 | 17 |
State of emergency | 5 | 3 | 1 | 2 | 3 | 1 | 1 | 3 | 2 | 6 | 4 | 1 | 2 | 6 |
Travel | 9 | 9 | 55 | 15 | 64 | 42 | 55 | 20 | 67 | 3 | 50 | 13 | 7 | 14 |
Vaccine | 71 | 98 | 40 | 99 | 66 | 41 | 62 | 23 | 20 | 75 | 51 | 118 | 77 | 34 |
We filtered the intervention types to only consider those that have a direct influence on the behaviour of individuals, such as closures, limitations on gatherings, social distancing, mask mandates and travel restrictions. This filtering process yielded a total of 52 mandates with beginning and end dates.
# label: mandates-filtering
#| tbl-cap: Filtered regulatory mandates implemented nationally and at the provincial and territorial level during the COVID-19 pandemic in Canada.
man <- vroom("mandates.csv") |>
left_join(mandates, by = "entry_id") |>
mutate(
start_date = as_date(start_date),
action = case_when(entry_id == "MB615" ~ "Eased", .default = action)
) |>
tidyr::pivot_wider(
id_cols = c(mandate_id, jurisdiction, intervention_category, intervention_type),
names_from = action,
values_from = start_date
) |>
mutate(name = glue("{intervention_category}: {intervention_type}"))
# Table
man |>
select(-mandate_id, -name) |>
arrange(jurisdiction, intervention_category, intervention_type) |>
knitr::kable(col.names = c("Jurisdiction", "Category", "Type", "New", "Eased"))
Jurisdiction | Category | Type | New | Eased |
---|---|---|---|---|
AB | Closures/openings | Daycares | 2020-03-15 | 2020-05-14 |
AB | Closures/openings | Education | 2020-03-15 | 2020-09-01 |
AB | Distancing | Gatherings | 2020-03-27 | 2020-05-15 |
AB | Public information | Masks | 2020-12-08 | 2021-08-16 |
AB | State of emergency | State of emergency | 2020-03-17 | 2020-06-15 |
AB | State of emergency | State of emergency | 2020-11-27 | 2021-02-22 |
BC | Closures/openings | Education | 2020-03-17 | 2020-09-10 |
BC | Distancing | Gatherings | 2020-03-16 | 2022-02-16 |
BC | Public information | Masks | 2020-11-19 | 2022-03-11 |
BC | State of emergency | State of emergency | 2020-03-18 | 2021-06-30 |
Canada | Travel | Restrictions | 2020-03-21 | 2022-04-25 |
MB | Closures/openings | Daycares | 2020-03-20 | 2020-06-01 |
MB | Closures/openings | Education | 2020-03-23 | 2020-09-08 |
MB | Distancing | Gatherings | 2020-03-13 | 2020-05-22 |
MB | Public information | Masks | 2020-11-11 | 2022-03-15 |
MB | State of emergency | State of emergency | 2020-03-20 | 2021-10-21 |
NB | Closures/openings | Daycares | 2020-03-16 | 2020-05-08 |
NB | Closures/openings | Education | 2020-03-16 | 2020-09-08 |
NB | Distancing | Gatherings | 2020-03-12 | 2020-04-24 |
NB | Public information | Masks | 2020-10-09 | 2022-03-14 |
NB | State of emergency | State of emergency | 2020-03-19 | 2021-07-30 |
NL | Closures/openings | Daycares | 2020-03-16 | 2020-05-11 |
NL | Closures/openings | Education | 2020-03-17 | 2020-09-09 |
NL | Distancing | Gatherings | 2020-03-18 | 2020-04-30 |
NL | Public information | Masks | 2020-08-24 | 2021-08-10 |
NL | State of emergency | State of emergency | 2020-03-18 | NA |
NS | Closures/openings | Daycares | 2020-03-17 | 2020-06-15 |
NS | Closures/openings | Education | 2020-03-23 | 2020-09-08 |
NS | Distancing | Gatherings | 2020-03-15 | 2020-05-15 |
NS | Public information | Masks | 2020-05-22 | 2022-03-21 |
NS | State of emergency | State of emergency | 2020-03-23 | NA |
NT | Closures/openings | Education | 2020-03-16 | 2020-08-28 |
NT | Distancing | Gatherings | 2020-03-17 | 2020-06-12 |
NT | Public information | Masks | 2021-05-03 | 2022-04-01 |
NT | State of emergency | State of emergency | 2020-03-24 | 2020-07-08 |
NU | Closures/openings | Daycares | 2020-03-16 | 2020-06-01 |
NU | Closures/openings | Education | 2020-03-17 | 2020-08-10 |
NU | Distancing | Gatherings | 2020-03-20 | 2020-06-01 |
NU | Public information | Masks | 2021-06-14 | 2022-04-11 |
NU | State of emergency | State of emergency | 2020-03-18 | 2022-04-11 |
ON | Closures/openings | Daycares | 2020-03-17 | 2020-07-27 |
ON | Closures/openings | Education | 2020-03-14 | 2020-09-08 |
ON | Closures/openings | Non-essential services | 2020-03-25 | 2020-05-19 |
ON | Distancing | Gatherings | 2020-03-13 | 2020-06-12 |
ON | Public information | Masks | 2020-10-03 | 2022-03-21 |
ON | State of emergency | State of emergency | 2020-03-17 | 2020-07-24 |
ON | State of emergency | State of emergency | 2021-01-14 | 2021-02-19 |
ON | State of emergency | State of emergency | 2021-04-08 | 2021-06-09 |
PE | Closures/openings | Daycares | 2020-03-17 | 2020-05-22 |
PE | Closures/openings | Education | 2020-03-23 | 2020-09-08 |
PE | Distancing | Gatherings | 2020-04-03 | 2020-05-08 |
PE | Public information | Masks | 2020-11-20 | 2022-05-06 |
PE | State of emergency | State of emergency | 2020-04-17 | 2020-06-29 |
QC | Closures/openings | Daycares | 2020-03-16 | 2020-05-11 |
QC | Closures/openings | Education | 2020-03-16 | 2020-08-26 |
QC | Distancing | Gatherings | 2020-03-20 | 2020-05-22 |
QC | Public information | Masks | 2020-08-24 | 2022-05-14 |
QC | State of emergency | State of emergency | 2020-03-13 | NA |
SK | Closures/openings | Daycares | 2020-03-20 | 2020-04-30 |
SK | Closures/openings | Education | 2020-03-20 | 2020-09-08 |
SK | Distancing | Gatherings | 2020-03-16 | 2020-04-24 |
SK | Public information | Masks | 2020-11-06 | 2022-02-28 |
SK | State of emergency | State of emergency | 2020-03-18 | 2021-07-11 |
YT | Closures/openings | Daycares | 2020-04-17 | 2020-05-22 |
YT | Closures/openings | Education | 2020-03-18 | 2020-08-19 |
YT | Distancing | Gatherings | 2020-03-16 | 2020-04-28 |
YT | Public information | Masks | 2020-12-01 | 2022-03-18 |
YT | State of emergency | State of emergency | 2020-03-27 | 2021-08-25 |
YT | State of emergency | State of emergency | 2021-11-08 | 2022-03-17 |
Below, we explore the temporal trends in cumulative cases and deaths for each Canadian health region on a weekly basis. Within each region, we normalized cumulative weekly cases and deaths by the population size times 100 000 to obtain an estimate of the cumulative outcomes per 100 000 habitants for each health region. We present a plot for each province and territory, each line representing the temporal trends for each health region. Mandates are overlaid as vertical lines on the line plots. Red lines represent the implementation of new regulatory mandates, while green lines identifies the date when mandates were lifted.
man <- mutate(man, Eased = case_when(is.na(Eased) ~ as.Date("2030-01-01"), .default = Eased))
dat <- covid |>
mutate(
week = week(date),
year = year(date),
date = ymd(glue("{year}0101")) + weeks(week - 1)
) |>
left_join(hr[, c("hruid", "pop", "name_short")], by = c("sub_region_1" = "hruid")) |>
group_by(date, region, sub_region_1, name_short, name, pop) |>
summarize(value_weekly = sum(value_daily)) |>
ungroup() |>
mutate(outcomes_per_100000 = (value_weekly / pop) * 100000) |>
arrange(name, region, sub_region_1, date) |>
select(-pop, value_weekly)
# Chart
p <- dat |>
filter(name == "cases") |>
group_by(region) |>
group_split() |>
lapply(function(j) {
j |>
group_by(name_short) |>
e_charts(date) |>
e_title("Province / territory", j$region[1]) |>
e_line(outcomes_per_100000, smooth = TRUE) |>
e_datazoom(x_index = 0, type = "slider") |>
e_tooltip(trigger = "axis") %>%
purrr::reduce2(
.x = man$New[man$jurisdiction %in% c(j$region[1], "Canada")],
.y = man$name[man$jurisdiction %in% c(j$region[1], "Canada")],
.f = function(x, y, z) e_mark_line(x, data = list(xAxis = y, lineStyle = list(color = "red")), title = z),
.init = .
) %>%
purrr::reduce2(
.x = man$Eased[man$jurisdiction %in% c(j$region[1], "Canada")],
.y = man$name[man$jurisdiction %in% c(j$region[1], "Canada")],
.f = function(x, y, z) e_mark_line(x, data = list(xAxis = y, lineStyle = list(color = "lightgreen")), title = z),
.init = .
)
})
do.call(e_arrange, p)
# Chart
x <- dat |>
filter(name == "deaths") |>
group_by(region) |>
group_split() |>
lapply(function(j) {
j |>
group_by(name_short) |>
e_charts(date) |>
e_title("Province / territory", j$region[1]) |>
e_line(outcomes_per_100000, smooth = TRUE) |>
e_datazoom(x_index = 0, type = "slider") |>
e_tooltip(trigger = "axis") %>%
purrr::reduce2(
.x = man$New[man$jurisdiction %in% c(j$region[1], "Canada")],
.y = man$name[man$jurisdiction %in% c(j$region[1], "Canada")],
.f = function(x, y, z) e_mark_line(x, data = list(xAxis = y, lineStyle = list(color = "red")), title = z),
.init = .
) %>%
purrr::reduce2(
.x = man$Eased[man$jurisdiction %in% c(j$region[1], "Canada")],
.y = man$name[man$jurisdiction %in% c(j$region[1], "Canada")],
.f = function(x, y, z) e_mark_line(x, data = list(xAxis = y, lineStyle = list(color = "lightgreen")), title = z),
.init = .
)
})
do.call(e_arrange, x)