Using networks to predict Covid-19 cases in R -Part 1
Tags: Predictions, Network, Covid 19, Data Science, R

Motivation

In order to see why this approach might work we will take a look at Leicester, which had a spike in Covid cases in June leading to the UKs first local lockdown. We are going to visualise the spread of Covid in the area by creating maps using the {ggplot2} and {sf} package, and animate these using the {gganimate} package.

We need to download the case data (which we can do directly from the URL), and get it into the right format. The initial table is wide (with one row per MSOA, and one column per week) and some of the weeks have no data, so we take advantage of the relatively new {tidyr} and {dplyr} functions where , pivot_longer and across to clean this data.

n.b. Since publishing this story, the data and where it can be accessed has changed. The new URL is here. We use this new data in part 2, and you can access the new code on github.



msoa <- readr::read_csv(url, col_types = cols()) %>%

# Drop columns that are all NA

select(where(~!all(is.na(.)))) %>%

# Pivot longer

pivot_longer(dplyr::starts_with("wk_"),

names_to = "week",

values_to = "cases",

names_prefix = "wk_") %>%

# Turn week to numeric

mutate(week = as.integer(week)) %>%

# Turn NAs into 0s

mutate(across(c(last_7_days, cases),

.fns = ~ifelse(is.na(.), 0, .))) url https://c19downloads.azureedge.net/downloads/msoa_data/MSOAs_latest.csv' msoa %# Drop columns that are all NAselect(where(~!all(is.na(.)))) %>%# Pivot longerpivot_longer(dplyr::starts_with("wk_"),names_to = "week",values_to = "cases",names_prefix = "wk_") %>%# Turn week to numericmutate(week = as.integer(week)) %>%# Turn NAs into 0smutate(across(c(last_7_days, cases),.fns = ~ifelse(is.na(.), 0, .)))

The resulting table looks like this (I cut the first few columns out to simplify it). The first two columns tell us the Local Authority District (LAD) which is a larger area than MSOA. The next two columns tell us the MSOA. The final two columns tell us the week, and how many positive cases there were in that week. The “last_7_days” column tells us the number of positive cases between the last full week and today.

The first thing we should do is check that the Leicester spike really does appear in the data, which we can do by plotting a simple line graph.

msoa %>%

group_by(lad19_nm, week) %>%

summarise(cases = sum(cases))%>%

filter(lad19_nm == "Leicester") %>%

ggplot(aes(x = week, y = cases)) +

geom_line(size = 1) +

theme_minimal() +

ggtitle("Time series gaph of cases for Leicester")

(Image created by Author)

We see that there is a large spike in the data, peaking at 500 cases in week 25.

The next step is to see how this is distributed amongst the MSOAs. We could do this as another line graph (and in fact, we do in the github code) but that won’t give us the same insight as if we plot it on a map.

To plot a map, we have to download the shape file of MSOAs in the UK, which you can do here. After saving and unzipping the whole folder, we can load it in the following way:

msoa_sf <- sf::st_read("data/maps/Middle_Layer_Super_Output_Areas__December_2011__Boundaries_EW_BFE.shp")

Plotting the MSOAs of Leicester is as simple as restricting the shape file down to just those that are in Leicester (we do this by inner joining), and then adding a geom_sf() to a ggplot. To plot the case data we restrict to a particular week (let’s choose the peak of week 25) and join on the cases data too. We can now fill the MSOA areas based on how many positive cases they had in week 25.

msoa_data_wk25 <- msoa %>%

filter(lad19_nm == "Leicester",

week == 25) # We see that covid cases are clustered in several MSOAs

msoa_sf %>%

inner_join(msoa_data_wk25 %>% distinct(msoa11_cd, cases),

by = c("MSOA11CD" = "msoa11_cd")) %>%

ggplot()+

geom_sf(aes(geometry = geometry, fill = cases)) +

# Everything past this point is just formatting:

scale_fill_viridis_c() +

theme_void() +

ggtitle(label = "MSOA map for Leicester",

subtitle = "The colour shows Covid cases during week 25")

(Image created by Author)

This is where we get the first indication that networks will be useful. We see that Neighbouring MSOAs have cases similar to neighbouring areas. This makes sense. If you have an outbreak in one place you expect it to spread to neighbouring places.

The one downside of visualising data like this, is it’s hard to see how the cases change over time. That’s where {gganimate} comes in. {gganimate} is a great package that let’s you animate your graphs based on a particular variable. It allows us to create the above map for every week, and then combines it together into a gif for us.

# gganimate

# Need to install transformr for this to work

p <- msoa_sf %>%

inner_join(msoa %>% filter(lad19_nm == "Leicester"),

by = c("MSOA11CD" = "msoa11_cd")) %>%

ggplot(aes(group = week)) +

geom_sf(aes(fill = cases)) +

scale_fill_viridis_c() +

transition_time(week) +

labs(title = paste0("New covid cases for MSOAs within Leicester"),

subtitle = "Week = {frame_time}") +

theme_void() # We define this to help us pick the right number of frames

num_weeks <- n_distinct(msoa$week) animate(p, nframes = num_weeks, fps = 1, end_pause = 4)

We see that up to transition_time() , this code looks really similar to our last graph. The only difference is we don’t filter the data to a specific week, and we include a group = week in the aes() for the plot. transition_time() is how we instruct it to use week as the variable to animate over.

The final bit of code just helps us specify a good number of frames so each week gets the same number. The result is below.

Again, the importance of neighbouring areas shines through. High case numbers spread from one area to neighbouring areas. Also, just before the peak in numbers for a single MSOA (which happens in week 27) we see the neighbouring areas all have high case numbers too. Would network analysis have allowed us to predict that this one particular area would have such a spike in cases in week 27? There’s only one way to find out.