Spatial networks – case study St James centre, Edinburgh (2/3)
This is part two in a series I’m writing on network analysis. The first part is here. In this section I’m going to cover allocating resources, again using the St James’ development in Edinburgh as an example. Most excitingly (for me), the end of this post covers the impact of changes in resource allocation.
Edinburgh (and surrounds) has more than one shopping centre. Many more. I’ve had a stab at narrowing these down to those that are similar to the St James centre, i.e. they’re big, (generally) covered and may have a cinema. You can see a plot of these below. As you can see the majority are concentrated around the population centre of Edinburgh.
As with the previous post I’ve used GRASS GIS for the network analysis, QGIS for cartography and R for some subsequent analysis. I’ve used the Ordnance Survey code-point open and openroads datasets for the analysis and various Ordnance Survey maps for the background.
An allocation map shows how you can split your network to be serviced by different resource centres. I like to think of it as deciding which fire station sends an engine to which road. But this can be extended to any resource with multiple locations: bank branches, libraries, schools, swimming pools. In this case we’re using shopping centres. As always the GRASS manual page contains a full walk through of how to run the analysis. I’ll repeat the steps I took below:
# connect points to network v.net roads_EH points=shopping_centres out=centres_net op=connect thresh=200 # allocate, specifying range of center cats (easier to catch all): v.net.alloc centres_net out=centres_alloc center_cats=1-100000 node_layer=2 # Create db table v.db.addtable map=centres_alloc@shopping_centres # Join allocation and centre tables v.db.join map=centres_alloc column=cat other_table=shopping_centres other_column=cat # Write to shp v.out.ogr -s input=centres_alloc output=shopping_alloc format=ESRI_Shapefile output_layer=shopping_alloc
The last step isn’t strictly necessary, as QGIS and R can connect directly to the GRASS database, but old habits die hard! We’ve now got a copy of the road network where all roads are tagged with which shopping centre they’re closest too. We can see this below:
A few things stand out for me:
- Ocean terminal is a massive centre but is closest to few people.
- Some of the postcodes closest to St James, as really far away.
- The split between Fort Kinnaird and St James is really stark just east of the A702.
If I was a councillor and I coordinated shopping centres in a car free world, I now know where I’d be lobbying for better public transport!
We can also do a similar analysis using the shortest path, as in the previous post. Instead of looking for the shortest path to a single point, we can get GRASS to calculate the distance from each postcode to its nearest shopping centre (note this is using the postcodes_EH file from the previous post):
# connect postcodes to streets as layer 2 v.net --overwrite input=roads_EH points=postcodes_EH output=roads_net1 operation=connect thresh=400 arc_layer=1 node_layer=2 # connect shops to streets as layer 3 v.net --overwrite input=roads_net1 points=shopping_centres output=roads_net2 operation=connect thresh=400 arc_layer=1 node_layer=3 # inspect the result v.category in=roads_net2 op=report # shortest paths from postcodes (points in layer 2) to nearest stations (points in layer 3) v.net.distance --overwrite in=roads_net2 out=pc_2_shops flayer=2 to_layer=3 # Join postcode and distance tables v.db.join map=postcodes_EH column=cat other_table=pc_2_shops other_column=cat # Join station and distance tables v.db.join map=postcodes_EH column=tcat other_table=shopping_centres other_column=cat subset_columns=Centre # Make a km column # Really short field name so we can output to shp v.db.addcolumn map=postcodes_EH columns="dist_al_km double precision" v.db.update map=postcodes_EH column=dist_al_km qcol="dist/1000" # Make a st james vs column # Uses results from the previous blog post v.db.addcolumn map=postcodes_EH columns="diff_km double precision" v.db.update map=postcodes_EH column=diff_km qcol="dist_km-dist_al_km" # Write to shp v.out.ogr -s input=postcodes_EH output=pc_2_shops format=ESRI_Shapefile output_layer=pc_2_shops
Again we can plot these up in QGIS (below). These are really similar results to the road allocation previously, but give us a little more detail on where the population are as each postcode is show. However, the eagle eyed of you will have noticed we pulled out the distance for each postcode in the code above and then compared it to the distance to St James alone. We can use this for considering the impact of resource allocation.
Switching to R, we can interrogate the postcode data further. Using R’s gdal library we can read in the shp file and generate some summary statistics:
|Centre||No. of postcodes closest
# Package install.packages("rgdal") library(rgdal) # Read file postcodes = readOGR("/home/user/dir/dir/network/data/pc_2_shops.shp") # How many postcodes for each centre? table(postcodes$Centre)
We can also look at the distribution of distances for each shopping centre using a box and whisker plot. As in the map we can see that Fort Kinnaird and St James are closest to the most distant postcodes, and that Ocean terminal has a small geographical catchment. The code for this plot is a the end of this post.
We can also repeat the plot from the previous blog post and look at how many postcodes are within walking and cycling distance of their nearest centre. In the previous post I showed the solid line and circle points for the St James centre. We can now compare those results to the impact of people travelling to their closest centre (below). The number of postcodes within walking distance of their nearest centre is nearly double that of St James alone, and those within cycling distance rises to nearly 50%! Code at the end of the post.
We also now have two curves on the above plot, and the area between them is the distance saved if each postcode travelled to its closest shopping centre instead of the St James.
The total distance is a whopping 123,680 km!
This impact analysis is obviously of real use in these times of reduced public services. My local council, Midlothian, is considering closing all its libraries bar one. What impact would this have on users? How would the road network around the kept library cope? Why have they just been building new libraries? It’s also analysis I really hope the DWP undertook before closing job centres across Glasgow. Hopefully the work of this post helps people investigate these impacts themselves.
# distance saved # NA value is one postcode too far to be joined to road - oops! sum(postcodes$diff_km, na.rm=T) # Boxplot png("~/dir/dir/network/figures/all-shops_distance_boxplot.png", height=600, width=800) par(cex=1.5) boxplot(dist_al_km ~ Centre, postcodes, lwd=2, range=0, main="Box and whiskers of EH postcodes to their nearest shopping centre", ylab="Distance (km)") dev.off() # Line plot # Turn into percentage instead of postcode counts x = sort(postcodes$dist_km) x = quantile(x, seq(0, 1, by=0.01)) y = sort(postcodes$dist_al_km) y = quantile(y, seq(0, 1, by=0.01)) png("~/dir/dir/network/figures/all-shops_postcode-distance.png", height=600, width=800) par(cex=1.5) plot(x, type="l", main="EH postcode: shortest road distances to EH shopping centres", xlab="Percentage of postcodes", ylab="Distance (km)", lwd=3) lines(y, lty=2, lwd=3) points(max(which(x<2)), 2, pch=19, cex=2, col="purple4") points(max(which(x<5)), 5, pch=19, cex=2, col="darkorange") points(max(which(y<2)), 2, pch=18, cex=2.5, col="purple4") points(max(which(y<5)), 5, pch=18, cex=2.5, col="darkorange") legend("topleft", c("St James", "Nearest centre", paste0(max(which(x<2)), "% postcodes within 2 km (walking) of St James"), paste0(max(which(x<5)), "% postcodes within 5 km (cycling) of St James"), paste0(max(which(y<2)), "% postcodes within 2 km (walking) of nearest centre"), paste0(max(which(y<5)), "% postcodes within 5 km (cycling) of nearest centre")), col=c("black", "black", "purple4", "darkorange", "purple4", "darkorange"), pch=c(NA, NA, 19, 19, 18, 18), lwd=c(3), lty=c(1, 2, NA, NA, NA, NA), pt.cex=c(NA, NA, 2, 2, 2.5, 2.5)) dev.off()