Part 1: Retail Centres

Welcome

This is part 1 of the 2 part course from CDRC on using the UK Retail Centres dataset to create retail catchments. The video in this part introduces the Retail Centres data set, and the practical session shows you how to work with the Retail Centres dataset in R. If you are completely new to RStudio, please check out our Short Course on Using R as a GIS.

After completing this material, you will:

The following video will provide an introduction to Retail Centres.



Setup

First we want to set up the libraries that we are going to be using for this practical - most of them should be familiar (e.g. dplyr, sf, tmap). However, the hereR library will likely be new - we are going to use this later, so install it for now and we will come back to it later.

library(sf)
library(dplyr)
library(tmap)
#install.packages("data.table")
library(data.table)
library(hereR)

Make sure you have set your working directory to wherever you have stored the CDRC-Retail-Catchment-Training folder we have provided:

#setwd("CDRC-Retail-Catchment-Training")

1. Data (& Preprocessing)

The first dataset we are going to be using is a geopackage containing the updated Retail Centre boundaries for the UK, which is within your data packs for this practical. We have given you a subset for the Liverpool City Region (LCR), containing the top 20 Retail Centres - determined as the 20 major comparison destinations - within the LCR.

All datasets you need for this practical are contained within the Data subfolder within the CDRC-Retail-Catchment-Training folder we provided.

## Read in the Retail Centre Boundaries 
rc <- st_read("Data/LCR_Retail_Centres_2018.gpkg")
## Reading layer `LiverpoolCR_Training2' from data source `C:\Users\nick\Dropbox\Work\2021-27-cdrc-les-dolega-retail-catchments-training\CDRC-Retail-Catchment-Training\Data\LCR_Retail_Centres_2018.gpkg' using driver `GPKG'
## Simple feature collection with 20 features and 5 fields
## geometry type:  POLYGON
## dimension:      XY
## bbox:           xmin: 321011 ymin: 381441.1 xmax: 352342.2 ymax: 418208.7
## projected CRS:  OSGB 1936 / British National Grid

Let’s take a look at the Retail Centre data:

## Print the first ten retail centres
rc
## Simple feature collection with 20 features and 5 fields
## geometry type:  POLYGON
## dimension:      XY
## bbox:           xmin: 321011 ymin: 381441.1 xmax: 352342.2 ymax: 418208.7
## projected CRS:  OSGB 1936 / British National Grid
## First 10 features:
##          rcID n.units n.comp.units                            rcName RetailPark
## 1  RC_EW_2713     189           44                          Wallasey          N
## 2  RC_EW_2720      76           25                            Formby          N
## 3  RC_EW_2723      75           28                          Waterloo          N
## 4  RC_EW_2746     140           27                     Allerton Road          N
## 5  RC_EW_2751     957          257                    Liverpool City          N
## 6  RC_EW_2756     308           92                        Birkenhead          N
## 7  RC_EW_2778      44           23 The Croft Retail and Leisure Park          Y
## 8  RC_EW_2788     166           36                          Old Swan          N
## 9  RC_EW_2808      48           34          New Mersey Shopping Park          Y
## 10 RC_EW_2809      63           22        Belle Vale Shopping Centre          Y
##                              geom
## 1  POLYGON ((330413.8 391857.4...
## 2  POLYGON ((329908.9 407152.4...
## 3  POLYGON ((332200.5 398543.7...
## 4  POLYGON ((339115.6 388386, ...
## 5  POLYGON ((334054.8 390283.9...
## 6  POLYGON ((331231.6 388547.5...
## 7  POLYGON ((334962.3 382860.3...
## 8  POLYGON ((339431.3 390996.1...
## 9  POLYGON ((341778.5 383950.6...
## 10 POLYGON ((343094.1 388350.9...

So for each Retail Centre (polygon):

Let’s map the Retail Centre polygons. Throughout this practical we will be using the R package tmap for all exploratory mapping - for more info on tmap visit: https://github.com/mtennekes/tmap.

In the next chunk of code we first setup the plotting mode that we want to use throughout this practical (tmap_mode). The default option is "plot" which will produce any of your plots over a plain background. By setting tmap_mode to "view" your layers are plotted automatically over an interactive Leaflet basemap.

Then we plot the Retail Centres by calling the rc object in the tm_shape() command, before specifying tm_fill() as the data is polygons not points or lines. Finally, we call tm_text() to label each of the polygons, specifying that we want the labels to be drawn from the rcName column - containing the names of the Retail Centres.

## Setup plotting
tmap_mode("view")

## Map Retail Centre Polygons for LCR
tm_shape(rc) +
  tm_fill(col = "orange") +
  tm_text("rcName", size = 0.75)

For the purpose of this analysis we want to extract the centroids of the Retail Centres to work with and construct catchments from.

A centroid is the central point of the polygon.

It is common practice when constructing catchments to do so with point data, so in this sense centroids are very helpful. The sf package has a neat st_centroid() function for extracting centroids from polygons.

## Extract centroids
rc_cent <- st_centroid(rc)

Let’s map the Retail Centre Centroids, noting we are using points to show the retail centres, rather than polygons:

## Map the centroids - note: tm_dots() is used as the object rc_cent contains point data (Retail Centre centroids)
tm_shape(rc_cent) +
  tm_dots(col = "orange") +
  tm_text("rcName", size = 0.75)

2. Creating a Retail Hierarchy

In this section we are going to build a three-tier hierarchy of Retail Centres. The hierarchy will be based on two things: the total number of comparison units in the centre (n.comp.units), and whether or not the Retail Centre is considered a major retail park.

In this next chunk of code we use the mutate function and case_when statements to generate a new column called 'hierarchy' where the Retail Centres are assigned as being ‘primary’, ‘secondary’ or ‘tertiary’ based on the number of comparison units in a centre (n.comp.units) and the value for Retail Park. Finally, we sort the dataset by the hierarchy column, using the arrange() function.

The conditions are set as follows:

Notice how we are using & and | in the case_when statements, with & meaning AND and | meaning OR.

For example, in the first assignment (tertiary), for a centre to be tertiary it has to have less than 50 comparison units AND not be a Retail Park (“N”). In the second example, we assign secondary centres as either those with comparison unit count between 50 and 99 OR those that are Retail Parks (“Y”). Notice how I have used the | symbol to account for the OR condition in the assignment of secondary centres.

## Use mutate and case_when to create the new column - notice how ~ assigns the new values based on the condition
rc_cent_nopipes <-  mutate(rc_cent, 
   hierarchy = dplyr::case_when(n.comp.units < 50 & RetailPark == "N" ~ "tertiary",
                              (n.comp.units >= 50 & n.comp.units < 100) | RetailPark == "Y" ~ "secondary",
                                n.comp.units >= 100 ~ "primary")) 

We then want to extract only certain columns from the data, which we can achieve using the select() function from dplyr:

## Use select to extract the id, n.units, n.comp.units and hierarchy columns
rc_cent_nopipes <- select(rc_cent_nopipes, rcID, rcName, n.units, n.comp.units, RetailPark, hierarchy)

Finally, we want to sort the data by the hierarchy column

## Sort by hierarchy
rc_cent_nopipes <- arrange(rc_cent_nopipes, hierarchy)

However, there is an alternative to writing two separate chunks of code to create the hierarchy, extract the columns we want and then sort the data by hierarchy. You may have seen pipes before - %>% - as they are commonly used to integrate various dplyr functions together. They work by taking the output from one function (e.g. the creation of the hierarchy column with mutate) and feed it into the first argument of the next function (e.g. selection of columns with select). We’ll show you how you can do it below:

## Use pipes to create the new hierarchy column and then select the other columns we are interested in
rc_cent <- rc_cent %>% 
  mutate(hierarchy = dplyr::case_when(n.comp.units < 50 & RetailPark == "N" ~ "tertiary",
             (n.comp.units >= 50 & n.comp.units < 100) | RetailPark == "Y" ~ "secondary",
              n.comp.units >= 100 ~ "primary")) %>%
  select(rcID, rcName, n.units, n.comp.units, RetailPark, hierarchy) %>%
  arrange(hierarchy)

Notice how when piping you don’t need to tell the mutate() and select() functions what object you want to perform that operation on, as it is piping directly from rc_cent. For clarity, let’s compare the output with and without pipes:

## Without pipes
rc_cent_nopipes
## Simple feature collection with 20 features and 6 fields
## geometry type:  POINT
## dimension:      XY
## bbox:           xmin: 321296 ymin: 381776.6 xmax: 351923.6 ymax: 417406.3
## projected CRS:  OSGB 1936 / British National Grid
## First 10 features:
##          rcID                            rcName n.units n.comp.units RetailPark
## 1  RC_EW_2751                    Liverpool City     957          257          N
## 2  RC_EW_2848                         Southport     633          159          N
## 3  RC_EW_2756                        Birkenhead     308           92          N
## 4  RC_EW_2778 The Croft Retail and Leisure Park      44           23          Y
## 5  RC_EW_2808          New Mersey Shopping Park      48           34          Y
## 6  RC_EW_2809        Belle Vale Shopping Centre      63           22          Y
## 7  RC_EW_2869  Aintree Retail and Business Park      37           23          Y
## 8  RC_EW_3077                            Widnes     189           59          N
## 9  RC_EW_3101             Ravenhead Retail Park      44           23          Y
## 10 RC_EW_3102                         St Helens     277           50          N
##    hierarchy                      geom
## 1    primary POINT (334655.1 390278.6)
## 2    primary POINT (333318.1 417406.3)
## 3  secondary POINT (331913.4 388629.2)
## 4  secondary POINT (334986.5 383054.5)
## 5  secondary POINT (341618.4 384113.4)
## 6  secondary POINT (342970.5 388474.7)
## 7  secondary POINT (337000.9 398682.6)
## 8  secondary POINT (351923.6 385615.9)
## 9  secondary   POINT (351550.8 394628)
## 10 secondary   POINT (351012.1 395384)
## With pipes
rc_cent
## Simple feature collection with 20 features and 6 fields
## geometry type:  POINT
## dimension:      XY
## bbox:           xmin: 321296 ymin: 381776.6 xmax: 351923.6 ymax: 417406.3
## projected CRS:  OSGB 1936 / British National Grid
## First 10 features:
##          rcID                            rcName n.units n.comp.units RetailPark
## 1  RC_EW_2751                    Liverpool City     957          257          N
## 2  RC_EW_2848                         Southport     633          159          N
## 3  RC_EW_2756                        Birkenhead     308           92          N
## 4  RC_EW_2778 The Croft Retail and Leisure Park      44           23          Y
## 5  RC_EW_2808          New Mersey Shopping Park      48           34          Y
## 6  RC_EW_2809        Belle Vale Shopping Centre      63           22          Y
## 7  RC_EW_2869  Aintree Retail and Business Park      37           23          Y
## 8  RC_EW_3077                            Widnes     189           59          N
## 9  RC_EW_3101             Ravenhead Retail Park      44           23          Y
## 10 RC_EW_3102                         St Helens     277           50          N
##    hierarchy                      geom
## 1    primary POINT (334655.1 390278.6)
## 2    primary POINT (333318.1 417406.3)
## 3  secondary POINT (331913.4 388629.2)
## 4  secondary POINT (334986.5 383054.5)
## 5  secondary POINT (341618.4 384113.4)
## 6  secondary POINT (342970.5 388474.7)
## 7  secondary POINT (337000.9 398682.6)
## 8  secondary POINT (351923.6 385615.9)
## 9  secondary   POINT (351550.8 394628)
## 10 secondary   POINT (351012.1 395384)

The two outputs are identical. Whether you choose to use pipes is up to you - they can really speed up data transformation, but at the same time make it harder to untangle some broken code. For more information and tutorials on piping and how to use it with different dplyr functions, please visit: https://seananderson.ca/2014/09/13/dplyr-intro/

From the rc_cent object you can see that each Retail Centre has now been assigned a hierarchical category (hierarchy), which we will use throughout the rest of the practical in determining catchments for the centres, that differ based on the hierarchical position of the centres.

You can also use the View() command to view the rc_cent data in a new tab if you want to:

## View rc_cent
View(rc_cent)

3. Catchments (1) - Fixed-Ring Buffers

Fixed-Ring Buffers are the simplest catchments, and involve drawing a circular buffer around a store (or retail centre) based on a fixed value of distance, which accounts for the expected distances consumers are willing to travel. Fixed-Ring buffers can be easily delineated in R using the st_buffer() function, where you give the function a simple features point object and distance (dependent on projection), and it returns a buffer for each individual point.

Our data (rc_cent) uses the British National Grid coordinate system, so any buffer distance will be in metres.

## Extract a 1500m buffer for each Retail Centre
buffer1.5km <- st_buffer(rc_cent, 1500)

Let’s map these to see what they look like. Note: I am setting alpha to 0.3 so I can see overlapping buffers clearly.

## Map the buffers
tm_shape(buffer1.5km) + ## Plot the buffers
  tm_fill(col = "orange", alpha = 0.3) +
  tm_shape(rc_cent) + ## Overlay the centroids
  tm_dots(col = "orange") +
  tm_text("rcName", size = 0.75)

However, it is more informative to have buffers that vary in size depending on the hierarchical position of the Retail Centres. In this next chunk of code we have created a small function to do this. Functions are really helpful tools that enable you to apply a series of methods in sequence and return the output. This is particularly helpful when you need to repeat steps over and over again, as it removes the need for typing out the same lines of code multiple times. For those new to functions, watch this short video.

The function we have created delineates a 5000m catchment for Retail Centres classed as primary. It has three steps:

To use the function you need to run this next chunk of code to save the function to your environment, and then you can apply the function in a subsequent line of code to your dataset, to extract the catchments.

## Run this chunk to save the function to your environment
get_primary_buffer <- function(centroids) {
  
  ## Extracts centroids of 'primary' Retail Centres
  rc_primary <- filter(centroids, hierarchy == "primary")
  
  ## Constructs a 5000m buffer
  primary_buffer <- st_buffer(rc_primary, dist = 5000)
  
  ## Return the 5000m buffer
  return(primary_buffer)
}

So now that we have the get_primary_buffer function saved under ‘Functions’ in my environment pane, we can run it on the dataset to extract primary catchments.

## Get primary buffers for LCR Retail Centres
pbuffer <- get_primary_buffer(rc_cent)

Let’s plot these:

## Plot the primary buffers and all the centroids, to check only primary centres have catchments
tm_shape(pbuffer) +
  tm_fill(col = "orange", alpha = 0.3) +
  tm_shape(rc_cent) +
  tm_dots(col = "orange") +
  tm_text("rcName", size = 0.75)

Notice how we have buffers for only the Liverpool City and Southport Retail Centres - as these are the only primary centres in this dataset.

However, if we wanted to build buffers that vary in size depending on the three different hierarchy categories, we would need to build a more complex function. The function below (get_buffer) does the same as the get_primary_buffer function, but builds catchments for primary, secondary and tertiary centres that differ in size. We have also included some additional arguments (primary_dist, secondary_dist, tertiary_dist) that enable you to modify the size of the buffer for each type of centre.

Note: An additional step is included in the get_buffer function - rbind() is used to row bind the primary, secondary and tertiary buffers together into one object that is returned.

So, run the next chunk to save the get_buffer function to your environment:

## Run this chunk to save the function to your environment
get_buffer <- function(centroids, primary_dist = 5000, secondary_dist = 3000, tertiary_dist = 1500) {
  
  ## Split up the Retail Centres based on hierarchy
  rc_primary <- filter(centroids, hierarchy == "primary")
  rc_secondary <- filter(centroids, hierarchy == "secondary")
  rc_tertiary <- filter(centroids, hierarchy == "tertiary")
  
  ## Run the buffer for the different Retail Centre hierarchies separately
  primary_buffer <- st_buffer(rc_primary, dist = primary_dist)
  secondary_buffer <- st_buffer(rc_secondary, dist = secondary_dist)
  tertiary_buffer <- st_buffer(rc_tertiary, dist = tertiary_dist)
  
  ## Join together
  buffer <- rbind(primary_buffer, secondary_buffer, tertiary_buffer)
  return(buffer) ## Return
}

Now run the function using the default buffer distances, these we feel are a good representation of the distances most people would be willing to travel to access the different types of centres:

## Run the function
hbuffer <- get_buffer(rc_cent, primary_dist = 5000, secondary_dist = 3000, tertiary_dist = 1500)

Excellent! Now map these to see what they look like:

tm_shape(hbuffer)+ ## Plot the varying fixed-ring buffers
  tm_fill(col = "hierarchy", palette = c("yellow", "orange", "red"), alpha = 0.5) + # Setting col to 'hierarchy' tells tmap to generate a different colour buffer for each value in the hierarchy column
  tm_shape(rc_cent) + ## Overlay the centroids
  tm_dots(col = "orange", alpha = 0.75) +
  tm_text("rcName", size = 0.75)

4. Catchments (2) - Drive-Time Catchments

In this section we are going to use the HERE API to delineate drive-time catchments for the Retail Centres. You need to make sure you have the hereR package installed, and then need to get an API key to use the functions within the package.

To get an API key:

## Set API key
#set_key("insert-key-here")

NOTE: This API key will only be valid for a few hours at a time. If you receive this error in this next section: "Request 'id = 1' failed Status 401." you will need to go back to the HERE website and generate a new API key - if you already have two in use, just delete these on the site and you will be able to create additional API keys.

4a. Building Simple Drive-Time Catchments

Now you’re good to go.

Drive-Time Catchments are similar to Fixed-Ring buffers in that they also apply a fixed measure (e.g. distance, time) from a store or retail centre. However, they are different in that they use digitised road networks, speed limits and transport modes to generate a (more accurate) polygon that represents the extent to which a vehicle can travel in all directions under a certain time limit or distance constraint.

The function we are going to be using for Drive-Time catchments is the isoline() function from the hereR package. You supply the function with a series of points, and can then set various parameters such as whether you want the catchment to be based on distance/time (range_type) and what transport mode you want the catchment to be based on (transport_mode).

Here is an example for the Liverpool City Retail Centre in the dataset, where we set a 15-minute catchment based on car being the transport mode. The 'range' argument is where you select how you want to determine the maximum size of the catchment; in this case it’s by time, and we set a value of 15 - i.e. 15 minutes Drive-Time.

NOTE: each value you set for range must be multiplied by 60 for the delineation to work correctly (e.g. range = (15 x 60)).

We also set transport_mode to “car” for the purposes of the practical, but there are lots of options available in the isoline documentation.

## Extract Liverpool City Retail Centre - the first in our dataset
rc_a <- rc_cent[1, ]

## Extract the 10-minute driving catchment
iso_a <- isoline(rc_a, range = (15 * 60), range_type = "time", transport_mode = "car")

Let’s map the isoline and its centroid to see what it looks like:

## Map the drive-time catchment for the first Retail Centre
tm_shape(iso_a) +
  tm_fill(col = "orange", alpha = 0.5) +
  tm_shape(rc_a) +
  tm_dots(col = "orange") +
  tm_text("rcName", size = 0.75)

To extract isolines for more than one point we need to add an additional argument to the isoline function (aggregate = FALSE), which prevents the API from combining each individual isoline into one multipolygon. In this case we are going to construct a 5-minute drive-time catchment for each centre:

## Extract the 5-minute catchment for every Retail Centre in LCR
iso <- isoline(rc_cent, range = (5 * 60), range_type = "time", transport_mode = "car", aggregate = FALSE)

Map them:

## Map the 5-minute drive-time catchments for LCR Retail Centres
tm_shape(iso) +
  tm_fill(col = "orange", alpha = 0.3) +
  tm_shape(rc_cent) +
  tm_dots(col = "orange") +
  tm_text("rcName", size = 0.75)

4b. Drive-Time Catchments at Different Times/Days

An interesting element with building drive-time catchments is to what extent variations in date/time and traffic are taken into account when building the catchment. For example, a 10-minute drive-time catchment delineated for a Sunday morning is likely to be much larger than for a Friday evening during rush-hour traffic. To demonstrate this, we are going to implement two drive-time catchments for two different days/times of the week.

To set this up, we need to use the datetime function in the isoline() function, we will do it again with the first Retail Centre in the dataset. First for a Friday in rush hour:

## First set up the date & time you are interested in - e.g. Friday 7th May - 5pm 
friday7th <- as.POSIXct("2021-05-07 17:30:00 BST", tz = "Europe/London")
## Build the catchment for Friday (rush hour)
friday_iso <- isoline(rc_a, range = (10 * 60), range_type = "time", transport_mode = "car",
                      datetime = friday7th)

Then for Sunday morning:

## First set up the date & time you are interested in - e.g. Sunday 9th - 5am
sunday9th <- as.POSIXct("2021-05-09 05:00:00 BST", tz = "Europe/London")
## Build the catchment for Friday (rush hour)
sunday_iso <- isoline(rc_a, range = (10 * 60), range_type = "time", transport_mode = "car",
                      datetime = sunday9th)

Let’s map them - we can build two maps individually saving them as p1 and p2.

## Make the Maps
p1 <- tm_shape(friday_iso) +
  tm_fill(col = "orange", alpha = 0.75) +
  tm_shape(rc_a) +
  tm_dots(col = "orange") +
  tm_text("rcName", size = 0.75)
p2 <- tm_shape(sunday_iso) +
  tm_fill(col = "orange", alpha = 0.75) +
  tm_shape(rc_a) +
  tm_dots(col = "orange") +
  tm_text("rcName", size = 0.75)

Now that we have the two maps saved as objects, we can plot them side-by-side, using the tmap_arrange() function. The function (from the tmap package) allows small maps to be arranged in a grid layout. Below we specify that we want them to be side-by-side (ncol = 2), but we could stack them on top of each other by swapping ncol = 2 to nrow = 2.

So you can see a slightly larger catchment for 6am on a Sunday morning in comparison to rush hour on a Friday evening. It is sometimes useful therefore to specify the time/day you want in the hereR API. If you don’t see any difference, try updating the day and time to the current week.

## Plot them side-by-side
tmap_arrange(p1, p2, ncol = 2)

4c. Building Drive-Time Catchments that account for Retail Centre Hierarchy

The final approach we want to take is to use the hierarchies created in Section 2 to build drive-time catchments that vary depending on the position of the Retail Centre in the hierarchy. Similar to Section 3, I am going to use functions again to make drive-time catchments for the Retail Centres, which vary in size depending on the hierarchy of the Retail Centre.

As in Section 3 we will first show you how you can build a simplified function to extract drive-time catchments for the primary centres only.

The function below does four things:

  • Filters the centroids to get only ‘primary’ centres
  • Constructs a 15-minute drive-time catchment for the ‘primary’ centres
  • Merges on the Retail Centre data to the drive-time catchment
  • Returns 15-minute drive-time catchments for the primary centres

Note: We have included three additional arguments. range_type and transport_mode allow you to select the measure (e.g time, distance) and mode of transport (e.g. car, bike, bus) for the catchment, and dist controls the maximum size of the catchment based on the range_type you selected.

So run the next chunk of code to save the get_primary_drive_time() function to your environment:

## Function to get drive-time catchments for the primary Retail Centres
get_primary_drive_time <- function(centroids, dist = 15, range_type = "time", transport_mode = "car") {
  
  ## Filter the centroids to extract the primary centres
  rc_primary <- filter(centroids, hierarchy == "primary")
  
  ## Build the drive-time catchment
  primary_drive_time <- isoline(rc_primary, range = (dist * 60), 
                                range_type = range_type, transport_mode = transport_mode, aggregate = FALSE)
  
  ## Clean up the isoline - join on the Retail Centre information
  rc_primary <- rc_primary %>%
    as.data.frame() %>% 
    select(rcID, n.units, n.comp.units, hierarchy) %>%
    bind_cols(primary_drive_time) %>% ## Equivalent of cbind(), but for piping
    st_as_sf() ## Ensures final object is SF not dataframe
  return(rc_primary)
}

Now that we have the get_primary_drive_time function saved, we can run it on our data to build catchments for only the primary centres, note: We have set dist to 15 which means the function will return a 15-minute drive-time catchment.

## Get catchments for primary centres
primary_iso <- get_primary_drive_time(rc_cent, dist = 15, range_type = "time", transport_mode =  "car")

Now let’s map them, and overlay the centroids to check only the primary centres have catchments.

## Map the primary drive-time catchments
tm_shape(primary_iso) +
  tm_fill(col = "orange", alpha = 0.5) +
  tm_shape(rc_cent) +
  tm_dots(col = "orange") +
  tm_text("rcName", size = 0.75)

So you can see that not all the centres have catchments, as our function is constructed to only delineate catchments for centres that are ‘primary’. The next function fixes this, by repeating the steps in the previous function to extract secondary and tertiary catchments as well as the primary ones, before joining these together to extract catchments for every centre in the dataset, using rbind() again.

Note: We have also included a primary_dist, secondary_dist and tertiary_dist argument so that the sizes of the catchments for each of the three types of centre can be modified. However, these default values we believe provide an accurate representation of the driving distances for each catchment.

## Run this chunk to save this function to your environment
get_drive_time <- function(centroids, primary_dist = 15, secondary_dist = 10, tertiary_dist = 5, 
                           range_type = "time", transport_mode = "car") {
  
  ## Split up the Retail Centres based on hierarchy
  rc_primary <- filter(centroids, hierarchy == "primary")
  rc_secondary <- filter(centroids, hierarchy == "secondary")
  rc_tertiary <- filter(centroids, hierarchy == "tertiary")
  
  ## Delineate the isolines for the different Retail Centre hierarchies separately
  primary_drive_time <- isoline(rc_primary, range = (primary_dist * 60),
                                range_type = range_type, transport_mode = transport_mode, aggregate = FALSE)
  secondary_drive_time <- isoline(rc_secondary, range = (secondary_dist * 60),
                                  range_type = range_type, transport_mode = transport_mode, aggregate = FALSE)
  tertiary_drive_time <- isoline(rc_tertiary, range = (tertiary_dist * 60),
                                 range_type = range_type, transport_mode = transport_mode, aggregate = FALSE)
  
  ## Join the Retail Centre info onto each set of catchments
  primary <- rc_primary %>%
    as.data.frame() %>%
    select(rcID, rcName, n.units, n.comp.units, hierarchy) %>%
    bind_cols(primary_drive_time)
  secondary <- rc_secondary %>%
    as.data.frame() %>%
    select(rcID, rcName, n.units, n.comp.units, hierarchy) %>%
    bind_cols(secondary_drive_time)
  tertiary <- rc_tertiary %>%
    as.data.frame() %>%
    select(rcID, rcName, n.units, n.comp.units, hierarchy) %>%
    bind_cols(tertiary_drive_time)
  
  ## Join catchments together
  isolines <- st_as_sf(rbind(primary, secondary, tertiary))
  return(isolines)
}

Now you can run the function. We want to extract a set of drive time catchments using different values for each level of the hierarchy - 15 minutes for primary centres, 10 minutes for secondary centres and 5 minutes for the tertiary centres.

## Extract the hierarchical drive time catchments for LCR Retail Centres
iso_hierarchy <- get_drive_time(rc_cent, primary_dist = 15, secondary_dist = 10, tertiary_dist = 5, 
                                range_type = "time", transport_mode = "car")

If you start getting error messages like this: In (function (res) : Request ‘id = 7’ failed: Status 429. it means you have made too many requests to the HERE API. Wait for a few seconds, and try again. (See https://developer.here.com/documentation/places/dev_guide/topics/http-status-codes.html for details).

Great! Now you have drive-time catchments for the Retail Centres that account for the hierarchy. Let’s map them to see what they look like:

tm_shape(iso_hierarchy) + ## Plot the hierarchical drive-time catchments 
  tm_fill(col = "hierarchy", palette = c("yellow", "orange", "red"), alpha = 0.5) + ## Setting col = 'hierarchy' tells tmap to plot a different colour for each value of hierarchy
  tm_shape(rc_cent) + ## Overlay the centroids
  tm_dots(col = "orange" ) +
  tm_text("rcName", size = 0.75)

4d. Optional Exercise - Using Pipes with tmap

A neat trick if you wanted to extract the catchment for one Retail Centre of interest (e.g. Liverpool Central), would be to using a filter prior to mapping. Remember that pipes automatically feed in the output from one step as the input to the next step, so you won’t need to input anything into the tm_shape() argument if you’ve piped directly from iso_hierarchy.

## Filter to catchments of interest
iso_hierarchy %>%
  filter(rcName== "Liverpool City") %>%
  tm_shape() + ## Notice how tm_shape can be left empty as you have piped directly from the iso_hierarchy object
  tm_fill(col = "orange", alpha = 0.5) +
  tm_text("rcName", size = 0.75)

Writing Out Your Files

At this point we need to write out the Retail Centre centroids as they contain the ‘Hierarchy’ column and we will need this for Part 2 of this course.

## Write out to Data folder
# st_write(rc_cent, "Data/Part2_Retail_Centres.gpkg")

Summary

That’s it! OK so that’s Part 1 of this practical completed, by now you should have a good understanding of:


This practical was written using R and RStudio by Patrick Ballantyne (), and conceptualised by Dr. Les Dolega (). This version was created on 14 May 2021.

The latest version of the workbook is available from https://data.cdrc.ac.uk/dataset/advanced-gis-methods-training-retail-centres-and-catchment-areas and https://github.com/patrickballantyne/CDRC-Retail-Catchment-Training.

Thanks to Dr. Nick Bearman () for his assistance and advice in putting these materials together.

This work is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. To view a copy of this license, visit http://creativecommons.org/licenses/by-sa/4.0/deed.en.