This Practical is LONG but it will take you from not knowing anything about R to making freaking cool interactive maps in one practial. As you can imagine, this will be a steep learning curve.
I will give you all the code you need, it’s your job to read through the text very carefully and try to understand what bits of code are doing as you go.
There will be bits of code you don’t fully understand. Don’t worry, the key is to revisit later and try to work out what is going on then. Learning R is a long and iterative process and this is just the start…
R is both a programming language and software environment, originally designed for statistical computing and graphics. R’s great strength is that it is Open Source, can be used on any computer operating system and free for anyone to use and contribute to. Because of this, it is rapidly becoming the statistical language of choice for many academics and has a huge user community with people constantly contributing new packages to carry out all manner of statistical, graphical and importantly for us, geographical, tasks.
If you want to learn more about R and indeed download the latest version for your own use, then visit the R project pages: http://www.r-project.org/
The Wikipedia page for those who want to know a little of the history of R can be found here: http://en.wikipedia.org/wiki/R_(programming_language)
There is an almost endless supply of good R tutorials on the web. If you get stuck or want to learn even more R (and why would you not want to?!), I’d recommend trying some of the following R Tutorial websites:
http://www.statmethods.net/index.html
http://www.cyclismo.org/tutorial/R/index.html
If you want to really be up to date with the state of the art in R, then https://bookdown.org/ is a fantastic resource. It features free books by some of the pre-eminent names in the R scene - I would urge you to go and take a look.
With almost every problem you encounter with R, someone else will have had the same problem before you and posted it on a forum – someone will then post a solution below.
My usual route is to google the problem and I’ll then be directed to a post, usually on StackOverflow, StackExchange or CrossValidated. I’ve rarely not found a solution to a problem this way.
Health warning…!
Beware of posting questions on these forums yourself – contributors to these forums (especially the R ones!), whilst almost always extremely knowledgeable about R, have a bit of a reputation for being insert familiar pejorative term for less-than-polite-human-being here! As you might expect, people who have enough time to become total experts in R, have little time to work on their social skills!! Fortunately though, some other poor chump has usually taken that hit for you and you can still find a useful answer to your problem.
If you are specifically more interested in the spatial side of R, then Alex Singleton and Chris Brunsdon at the Universities of Liverpool and Maynooth also have a number of very useful R Spatial Tutorials – http://rpubs.com/alexsingleton/ & http://rpubs.com/chrisbrunsdon/
Robin Lovelace in Leeds is also frequently at the bleeding edge of developments in R spatial stuff, so keep an eye on his website too: http://robinlovelace.net/
These websites are also very very good: https://pakillo.github.io/R-GIS-tutorial/ And http://www.maths.lancs.ac.uk/~rowlings/Teaching/UseR2012/cheatsheet.html
OK, on to the practical…
When you download and install R, you get the R Graphical User Interface (GUI) as standard (below). This is fine and some purists prefer using the clean, unfussy command-line original, but it has some limitations such as no graphical way to view data tables or keep track of what is in your working directory (there are a number of others too).
The standard R GUI
Fortunately there are a number of software environments that have been developed for R to make it a little more user-friendly; the best of these by a long way is RStudio. RStudio can be downloaded for free from https://www.rstudio.com/
The RStudio GUI
If you are some kind of masochist, you are welcome to use the bundled R GUI for all of your work. If pain is not your thing, then for this practical (and future practicals) I will assume that you are using RStudio.
\(~\)
1+5
## [1] 6
or
4*5^2
## [1] 100
\(~\)
As you can see R performs these calculations instantly and prints the results in the console. This is useful for quick calculations but less useful for writing scripts requiring multiple operations or saving these for future use.
To save your scripts, you should create a new R Script file. Do this now: Select File > New File > R Script.
The R Script should open up on the top-left of your GUI. From now on type everything in this R script file and save it
getwd()
## [1] "C:/Users/Adam/Dropbox/R/wk3"
To run this line, hold *Ctrl (Cmd on a Mac) and press the Return(↲) key (if you are in the standard R installation, you would run your script with Ctrl R). You should now see your current working directory appear in the console.
Because of the new project we have already set up, this working directory should be correct, but if for any reason we wanted to change the working directory, we would use the setwd() function. For example, we wanted to change our directory to the documents folder on the C drive, we could run (don’t do this now):
setwd("C:/Documents")
When we are sure we are working in the correct working directory, we can save our script by clicking on the save icon on the script tab. Save your script as something like “wk3_part1” and you will see it appear in your files window on the right hand side. As you build up a document of R code, you should get into the habit of saving your script periodically in case of an unexpected software crash.
We can now begin to write a script without the need to run each line every time we press enter. In the script editor type:
A <- 1
B <- 2
C <- A+B
C
## [1] 3
Select (highlight) the three lines and run all three lines with Ctrl Return(↲). You will notice the lines appear in the console (the other window). If you type C and press enter in the console (C and then ctrl return in the script window) you should have the number 3 appear. From now on I recommend you type all the commands below in the script first and then run them. Copying and pasting from this document won’t necessarily work.
You will also notice that in RStudio, values A, B and C will appear in your workspace window (top right). These variables are stored in memory for future use. Try giving A and B different values and see what happens. What about if you use lower case letters?
You have just demonstrated one of the powerful aspects of R, which is that it is an object oriented programming language. A, B and C are all objects that have been assigned a value with the <- symbol (you can also use the = sign, but it operates slightly differently to <- in R, plus the arrow assignment has become standard over the years. Use alt - to type it automatically). This principle underlies the whole language and enables users to create ever more complex objects as they progress through their analysis. If you type:
ls()
## [1] "A" "B" "C"
R will produce a list of objects that are currently active.
rm(A)
will remove the object A from the workspace (do ls() again to check this or look in your workspace window).
rm() and ls() are known as functions. Functions are the other fundamental aspect to the R language. Functions can be thought of as single or multiple calculations that you apply to objects. They generally take the form of:function(object, parameter1, parameter2, parameter3...)
For example, one common function is the plot() function for displaying data as a graphical output. Add these lines to your script and run them as before and you can see some plot() outputs:
#create some datasets, first a vector of 1-100 and 101-200
Data1 <- c(1:100)
Data2 <- c(101:200)
#Plot the data
plot(Data1, Data2, col="red")
#just for fun, create some more, this time some normally distributed
#vectors of 100 numbers
Data3 <- rnorm(100, mean = 53, sd=34)
Data4 <- rnorm(100, mean = 64, sd=14)
#plot
plot(Data3, Data4, col="blue")
# symbol. This signifies that whatever comes after it on that line is a comment. Comments are ignored by the R console and they allow you to annotate your code so that you know what it is doing. It is good programming practice to comment your code extensively so that you can keep track of what your scripts are for.c() concatenates a string of numbers together into a vector. 1:100 means produce the integers between and including 1:100, the plot() function plots the two data objects and includes a parameter to change the colour of the points. To understand what a function does, you can consult the R Help system. Simply type a question mark and then the function name; for example:?plot
\(~\)
\(~\)
df <- data.frame(Data1, Data2)
plot(df, col="green")
#show the first 10 and then last 10 rows of data in df...
head(df)
## Data1 Data2
## 1 1 101
## 2 2 102
## 3 3 103
## 4 4 104
## 5 5 105
## 6 6 106
tail(df)
## Data1 Data2
## 95 95 195
## 96 96 196
## 97 97 197
## 98 98 198
## 99 99 199
## 100 100 200
You can also view elements of your data frame in RStudio by simply clicking on it in the top-right Environment window
\(~\)
\(~\)
[], with the form:data.frame[row,column]
Rows are always referenced first, before the comma, columns second, after the comma.
df[1:10, 1]
df[5:15,]
df[c(2,3,6),2]
df[,1]
colnames() function:colnames(df)<- c("column1", "column2")
To select or refer to these columns directly by name, we can either use the $ operator, which takes the form data.frame$columnName, e.g.
df$column1
## [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
## [18] 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
## [35] 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
## [52] 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
## [69] 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
## [86] 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
or we can use the double square bracket operator [[]], and refer to our column by name using quotes e.g.
df[["column1"]]
## [1] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
## [18] 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
## [35] 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
## [52] 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68
## [69] 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
## [86] 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
This again is useful if you have a lot of columns and you wish to efficiently extract one of them.
We are now going to use our London Data file from week 1 again. R does have packages (we will come on to what these are later) that will let you read excel files straight into the program, but it is easier to work with a comma separated values (.csv) file.
\(~\)
One of the most tedious things a spatial analyst / data scientist has to do is clean their data so it doesn’t cause problems for the software later. In the past, we would have needed to do this by hand - these days, we can use software to do much of this for us.
I will now give you two options to arrive at a nice cleaned dataset. If you have issues with software packages etc, you might still need to via the old skool route, however, the new skool route will be much more satisfying if it works!
Open your LondonData.xls file from week 1 in Excel, and save as LondonData.csv into your wk3/RProject folder.
Open your new .csv file in Excel. There might be some non-numeric values inside numeric columns which will cause problems in your analysis. These need to be removed before proceeding any further. To remove these, you can use the replace function in Excel. In the home tab under ‘Editing’ open up the find and replace dialogue box and enter the following into the find box:
#VALUE! n/a
Leave the replace box empty each time and click Replace All to remove these from your file, before saving the file again.
\(~\)
read.csv() function:LondonDataOSK<- read.csv("LondonData.csv")
If you look at the read.csv() help file - ?read.csv - you will see that we can actually include many more parameters when reading in a .csv file. For example, we could read in the same file as follows:
# by default in R, the file path should be defined with / but on a #windows file system it is defined with \. Using \\ instead allows R #to read the path correctly – alternatively, just use /
LondonData<- read.csv("N:\\GIS\\wk3\\RProject1\\LondonData.csv", header = TRUE, sep = ",")
This would specify the exact path; that the first row of the file contains header information; and the values in the file are separated with commas (not ; or : as can be the case sometimes). \(~\) \(~\)
To clean our data as we read it in, we are going to use a package (more about packages later - for now, just think about it as a lovely gift from the R gods) called readr which comes bundled as part of the tidyverse package. If you want to find out more about the Tidyverse (and you really should) then you should start here: https://www.tidyverse.org/ First install the package:
install.packages("tidyverse")
Now we can use the readr package which comes bundled as part of the tidyverse to read in some data (directly from the web this time - read.csv can do this too) and clean text characters out from the numeric columns before they cause problems:
library(tidyverse)
#wang the data in straight from the web using read_csv, skipping over the 'n/a' entries as you go...
LondonData <- read_csv("https://files.datapress.com/london/dataset/ward-profiles-and-atlas/2015-09-24T14:21:24/ward-profiles-excel-version.csv", na = "n/a")
\(~\)
class() function:class(LondonData)
## [1] "tbl_df" "tbl" "data.frame"
# or, if you have your old skool data from step 24 above
class(LondonDataOSK)
## [1] "data.frame"
We can also use the class function within another two functions (cbind() and lapply()) to check that our data has been read in correctly and that, for example, numeric data haven’t been read in as text or other variables. Run the following line of code:
datatypelist <- data.frame(cbind(lapply(LondonData,class)))
You should see that all columns that should be numbers are read in as numeric. Try reading in LondonData again, but this time without excluding the ‘n/a’ values in the file, e.g.
LondonData <- read_csv("https://files.datapress.com/london/dataset/ward-profiles-and-atlas/2015-09-24T14:21:24/ward-profiles-excel-version.csv")
Now run the datatypelist function again - you should see that some of the columns (those the n/a values in) have been read in as something other than numeric. This is why we need to exclude them. Isn’t readr great for helping us avoid reading in our numeric data as text!
LondonData <- edit(LondonData)
summary(df)
## column1 column2
## Min. : 1.00 Min. :101.0
## 1st Qu.: 25.75 1st Qu.:125.8
## Median : 50.50 Median :150.5
## Mean : 50.50 Mean :150.5
## 3rd Qu.: 75.25 3rd Qu.:175.2
## Max. :100.00 Max. :200.0
names(LondonData)
## [1] "Ward name"
## [2] "Old code"
## [3] "New code"
## [4] "Population - 2015"
## [5] "Children aged 0-15 - 2015"
## [6] "Working-age (16-64) - 2015"
## [7] "Older people aged 65+ - 2015"
## [8] "% All Children aged 0-15 - 2015"
## [9] "% All Working-age (16-64) - 2015"
## [10] "% All Older people aged 65+ - 2015"
## [11] "Mean Age - 2013"
## [12] "Median Age - 2013"
## [13] "Area - Square Kilometres"
## [14] "Population density (persons per sq km) - 2013"
## [15] "% BAME - 2011"
## [16] "% Not Born in UK - 2011"
## [17] "% English is First Language of no one in household - 2011"
## [18] "General Fertility Rate - 2013"
## [19] "Male life expectancy -2009-13"
## [20] "Female life expectancy -2009-13"
## [21] "% children in reception year who are obese - 2011/12 to 2013/14"
## [22] "% children in year 6 who are obese- 2011/12 to 2013/14"
## [23] "Rate of All Ambulance Incidents per 1,000 population - 2014"
## [24] "Rates of ambulance call outs for alcohol related illness - 2014"
## [25] "Number Killed or Seriously Injured on the roads - 2014"
## [26] "In employment (16-64) - 2011"
## [27] "Employment rate (16-64) - 2011"
## [28] "Number of jobs in area - 2013"
## [29] "Employment per head of resident WA population - 2013"
## [30] "Rate of new registrations of migrant workers - 2011/12"
## [31] "Median House Price (<U+00A3>) - 2014"
## [32] "Number of properties sold - 2014"
## [33] "Median Household income estimate (2012/13)"
## [34] "Number of Household spaces - 2011"
## [35] "% detached houses - 2011"
## [36] "% semi-detached houses - 2011"
## [37] "% terraced houses - 2011"
## [38] "% Flat, maisonette or apartment - 2011"
## [39] "% Households Owned - 2011"
## [40] "% Households Social Rented - 2011"
## [41] "% Households Private Rented - 2011"
## [42] "% dwellings in council tax bands A or B - 2015"
## [43] "% dwellings in council tax bands C, D or E - 2015"
## [44] "% dwellings in council tax bands F, G or H - 2015"
## [45] "Claimant rate of key out-of-work benefits (working age client group) (2014)"
## [46] "Claimant Rate of Housing Benefit (2015)"
## [47] "Claimant Rate of Employment Support Allowance - 2014"
## [48] "Rate of JobSeekers Allowance (JSA) Claimants - 2015"
## [49] "% dependent children (0-18) in out-of-work households - 2014"
## [50] "% of households with no adults in employment with dependent children - 2011"
## [51] "% of lone parents not in employment - 2011"
## [52] "(ID2010) - Rank of average score (within London) - 2010"
## [53] "(ID2010) % of LSOAs in worst 50% nationally - 2010"
## [54] "Average GCSE capped point scores - 2014"
## [55] "Unauthorised Absence in All Schools (%) - 2013"
## [56] "% with no qualifications - 2011"
## [57] "% with Level 4 qualifications and above - 2011"
## [58] "A-Level Average Point Score Per Student - 2013/14"
## [59] "A-Level Average Point Score Per Entry; 2013/14"
## [60] "Crime rate - 2014/15"
## [61] "Violence against the person rate - 2014/15"
## [62] "Deliberate Fires per 1,000 population - 2014"
## [63] "% area that is open space - 2014"
## [64] "Cars per household - 2011"
## [65] "Average Public Transport Accessibility score - 2014"
## [66] "% travel by bicycle to work - 2011"
## [67] "Turnout at Mayoral election - 2012"
Now we have some data read into R, we need to select a small subset to work on. The first thing we will do is select just the London Boroughs to work with. If you recall, the Borough data is at the bottom of the file.
We could select just the rows we need by explicitly specifying the range of rows we need:
LondonBoroughs<-LondonData[626:658,]
There is also a subset() function in R. You could look that up and see whether you could create a subset with that. Or, we could try a cool ‘data sciency’ way of pulling out the rows we want with the knowledge that the codes for London Boroughs start with E09 (the wards in the rest of the file start with E05).
Knowing this, we can use the grep() function which can use regular expressions to match patterns in text. Let’s try it!
LondonData <- data.frame(LondonData)
LondonBoroughs <- LondonData[grep("^E09",LondonData[,3]),]
head(LondonBoroughs)
AWWMAHGAWD!!! Pretty cool hey?
What that function is saying is “grep (get) me all of the rows from the London Data data frame where the text in column 3 starts with (^) E09”
You will notice that you will have two rows at the top for the City of London. This is becuase it features twice in the data set. That’s fine, we can just drop this row from our dataset:
LondonBoroughs <- LondonBoroughs[2:34,]
We can also select just a few columns from the dataset if we wish:
LondonBoroughs<-LondonBoroughs[,c(1,19,20,21)]
c() function is also used here – this is the ‘combine’ function - another very useful function in R which allows arguments (in this case, column reference numbers) into a single valuenames() function (there are various other functions for renaming columns - for example colnames() if you want to rename multiple columns:#rename the column 1 in LondonBoroughs
names(LondonBoroughs)[1] <- c("Borough Name")
plot(LondonBoroughs$Male.life.expectancy..2009.13, LondonBoroughs$X..children.in.reception.year.who.are.obese...2011.12.to.2013.14)
Now, of course, because this is R, we can pimp this graph using something a bit more fancy than the base graphics functions:
#install.packages("plotly")
library(plotly)
plot_ly(LondonBoroughs, x = ~Male.life.expectancy..2009.13, y = ~X..children.in.reception.year.who.are.obese...2011.12.to.2013.14, text = ~LondonBoroughs$`Borough Name`, type = "scatter", mode = "markers")
This next part of the practical applies the same principles introduced above to the much more complex problem of handling spatial data within R. In this workshop we will produce a gallery of maps using many of the plotting tools available in R. The resulting maps will not be that meaningful- the focus here is on sound visualisation with R and not sound analysis (I know one is useless without the other!). Good quality spatial analysis will come in the rest of the module.
Whilst the instructions are step by step you are encouraged to start deviating from them (trying different colours for example) to get a better understanding of what we are doing.
In this section we’ll require even more specialist packages, so I should probably spend some more time explaining what packages actually are! Packages are bits of code that extend R beyond the basic statistical functionality it was originally designed for. For spatial data, they enable R to process spatial data formats, carry out analysis tasks and create some of the maps that follow.
Bascially, without packages, R would be very limited. With packages, you can do virtually anything! One of the issues you will come across is that packages are being continually developed and updated and unless you keep your version of R updated and your packages updated, there may be some functions and options not available to you. This can be a problem, particularly with University installations which (at best) may only get updated once a year. Therefore, apologies in advance if things don’t work as intended!
\(~\)
maptools – either find and install it using the RStudio GUI or do the following:install.packages("maptools")
There are a few other packages we’ll need to get to grips with. Some, like ggplot2 (one of the most influential R packages ever) are part of the tidyverse package we came across earlier. Others we will need to install for the first time.
#install.packages(c("classint", "OpenStreetMap", "tmap"))
# - might also need these ones("RColorBrewer", "Sp", "rgeos", "tmap", "tmaptools", "sf", "downloader", "rgdal", "geojsonio")
Now that the packages have been installed you will not have to repeat the above steps again (when you use your account in these cluster rooms). Open a new script and save it to your working directory as “wk3_maps.r”. As before, type each of the lines of code into this window and then select and use the ctrl return keys to run them. Be sure to save your script often.
The first task is to load the packages we have just installed. note, you might have some issues with the OpenStreetMap package if your installation of java on your computer doesn’t match your installation of R – e.g. if you have installed the 64bit version of R, you also need the 64bit version of java (same with the 32bit versions) - you may also need to install the package Rcpp separately and try again
#Load Packages (ignore any error messages about being built under a #different R version):
library(maptools)
library(RColorBrewer)
library(classInt)
library(OpenStreetMap)
library(sp)
library(rgeos)
library(tmap)
library(tmaptools)
library(sf)
library(rgdal)
library(geojsonio)
R has a very well developed ecosystem of packages for working with Spatial Data. Early pioneers like Roger Bivand and Edzer Pebesma along with various colleagues were instrumental in writing packages to interface with some powerful open source libraries for working with spatial data, such as GDAL and GEOS. These were accessed via the rgdal and rgeos packages. The maptools package by Roger Bivand, amongst other things, allowed Shapefiles to be read into R. The sp package (along with spdep) by Edzer Pebesma was very important for defining a series of classes and methods for spatial data natively in R which then allowed others to write software to work with these formats. Other packages like raster advanced the analysis of gridded spatial data, while packages like classintand RColorbrewer facilitated the binning of data and colouring of choropleth maps.
Whilst these packages were extremely important for advancing spatial data analysis in R, they were not always the most straighforward to use - making a map in R could take quite a lot of effort and they were static and visually basic. However, more recently new packages have arrived to change this. Now leaflet enables R to interface with the leaflet javascript library for online, dynamic maps. ggplot2 which was developed by Hadley Wickam and colleagues radically changed the way that people thought about and created graphical objects in R, including maps, and introduced a graphical style which has been the envy of other software to the extent that there are now libraries in Python which copy the ggplot2 style!
Building on all of these, the new tmap (Thematic Map) package has changed the game completely and now enables us to read, write and manipulate spatial data and produce visually impressive and interactive maps, very easily. In parallel, the sf (Simple Features) package is helping us re-think the way that spatial data can be stored and manipulated. It’s exciting times for geographic information / spatial data science!
Choropleth maps are thematic maps which colour areas according to some phenomenon. In our case, we are going to fill some irregular polygons (the London Boroughs) with a colour that corresponds to a particular attribute.
As with all plots in R, there are multiple ways we can do this. The basic plot() function requires no data preparation but additional effort in colour selection/ adding the map key etc. qplot() and ggplot() (installed in the ggplot2 package) require some additional steps to format the spatial data but select colours and add keys etc automatically. Here, we are going to make use of the new tmap package which makes making maps very easy indeed.
EW <- geojson_read("http://geoportal.statistics.gov.uk/datasets/8edafbe3276d4b56aec60991cbddda50_2.geojson", what = "sp")
#pull out london using grep and the regex wildcard for'start of the string' (^) to to look for the bit of the district code that relates to London (E09) from the 'lad15cd' column in the data slot of our spatial polygons dataframe
LondonMap <- EW[grep("^E09",EW@data$lad15cd),]
#plot it using the base plot function
qtm(LondonMap)
#read the shapefile into a simple features object
BoroughMapSF <- read_shape("BoundaryData/england_lad_2011Polygon.shp", as.sf = TRUE)
BoroughMapSP <- read_shape("BoundaryData/england_lad_2011Polygon.shp")
#plot it very quickly usking qtm (quick thematic map) to check it has been read in correctly
qtm(BoroughMapSF)
library(methods)
#check the class of BoroughMapSF
class(BoroughMapSF)
## [1] "sf" "data.frame"
#And check the class of BoroughMapSP
class(BoroughMapSP)
## [1] "SpatialPolygonsDataFrame"
## attr(,"package")
## [1] "sp"
#now convert the SP object into an SF object...
newSF <- st_as_sf(BoroughMapSP)
#and try the other way around SF to SP...
newSP <- as(newSF, "Spatial")
#simples!
OK, enough messing around, show us the maps!!
#join the data to the @data slot in the SP data frame
BoroughMapSP@data <- data.frame(BoroughMapSP@data,LondonData[match(BoroughMapSP@data[,"code"],LondonData[,"New.code"]),])
#check it's joined.
#head(BoroughMapSP@data)
BoroughDataMap <- append_data(BoroughMapSF,LondonData, key.shp = "code", key.data = "New.code", ignore.duplicates = TRUE)
## Data contains duplicated keys: E09000001
## Over coverage: 626 out of 659 data records were not appended. Run over_coverage() to get the corresponding data row numbers and key values.
If you want to learn a bit more about the sorts of things you can do with tmap, then there are 2 vignettes that you can access here: https://cran.r-project.org/web/packages/tmap/ - I suggest you refer to these to see the various things you can do using tmap. Here’s a quick sample though:
qtmlibrary(tmap)
library(tmaptools)
qtm(BoroughDataMap, fill = "Rate.of.JobSeekers.Allowance..JSA..Claimants...2015")
tm_shape(BoroughDataMap) +
tm_polygons(c("Average.Public.Transport.Accessibility.score...2014", "Violence.against.the.person.rate...2014.15"),
style=c("jenks", "pretty"),
palette=list("YlOrBr", "Purples"),
auto.palette.mapping=FALSE,
title=c("Average Public Transport Accessibility", "Violence Against the Person Rate"))
You will notice that to choose the colour of the maps, I put in some codes. These are the names of colour ramps from the RColourBrewer package which comes bundled with tmap. RColorBrewer uses colour palettes available from the colorbrewer2 website (http://colorbrewer2.org/) which is in turn based on the work of Cynthia Brewer and colleagues at Penn State University (http://www.personal.psu.edu/cab38/ColorBrewer/ColorBrewer_updates.html). Cynthia brewer has carried out large amount of academic research into determining the best colour palettes for GIS applications and so we will defer to her expertise here.
If you want to look at the range of colour palettes available, as we;; as going to the ColorBrewer website, you can use the a little shiny app which comes bundled with tmaptools
#You might need to install the shinyjs paclage for this to work
install.packages("shinyjs")
library(shinyjs)
#it's possible to explicitly tell R which package to get the function from with the :: operator...
tmaptools::palette_explorer()
tmap_mode("view")
## tmap mode set to interactive viewing
tm_shape(BoroughDataMap) +
tm_polygons("X..children.in.year.6.who.are.obese..2011.12.to.2013.14",
style="jenks",
palette="PuRd",
auto.palette.mapping=FALSE,
title="Truffle Shuffle Intensity")
There are loads of options for creating maps with tmap - read the vignettes that have been provided by the developers of the package and see if you can adapt the maps you have just made - or even make some alternative maps using built in data.
https://cran.r-project.org/web/packages/tmap/vignettes/tmap-nutshell.html https://cran.r-project.org/web/packages/tmap/vignettes/tmap-modes.html
You should also read the reference manual on the package homepage: https://cran.r-project.org/web/packages/tmap/
Once you have finished playing around, move onto the next section.
So as you have seen, it is possible to make very nice thematic maps with tmap. However, there are other options. The ggplot2package is a very powerful graphics package that allows us to a huge range of sophisticated plots, including maps.
The latest development version of ggplot2 has support for simple features objects with the new geom_sf class (http://ggplot2.tidyverse.org/reference/ggsf.html), however the current release doesn’t quite have it yet, so I’ll show you how to create maps in ggplot the old way and then when the new relase comes out we can all go nuts with the new functionality!
The main ggplot2 page can be found here - http://ggplot2.org/ - with the help files all documented here - http://ggplot2.tidyverse.org/reference/
If you have not already done so, install and library the ggplot2 and rgeos packages (they should be installed automatically as part of tidyverse and tmap packages, but occasionally they need to be installed separately).
We will now use the fortify() function to decompose the BoroughMapSP SpatialPolygonsDataFrame object into a standard dataframe containing every vertex in the map.
Borough_geom<-fortify(BoroughMapSP, region="code")
merge() function (this performs a data join in a similar way to the match() function we saw earlier). To find out how this function works look at ?merge.Borough_geom <- merge(Borough_geom, LondonData, by.x="id", by.y="New.code")
Have a look at the Borough_geom object to see its contents. You should see a large data frame containing the latitude and longitude (they are actually eastings and northings as the data is in British National Grid format) coordinates alongside the attribute information associated with each London Borough. If you type print(Borough_geom) you will just how many coordinate pairs are required!
Now there are two main ways in which you can construct a plot in ggplot2: qplot() and ggplot(). qplot is short for ‘Quick plot’ and can be good for producing quick charts and maps, but is perhaps less good for constructing complex layered plots. ggplot() is better for building up a plot layer by layer, e.g. ggplot()+layer1+layer2, and so this is what we will use here.
The important elements of any ggplot layer are the aesthetic mappings – aes(x,y, …) – which tell ggplot where to place the plot objects. We can imagine a map just like a graph with all features mapping to an x and y axis. All geometry (geom_) types in ggplot feature some kind of aesthetic mapping and these can either be declared at the plot level, e.g.
ggplot(data.frame, aes(x=x, y=y))
or, more flexibly at the level of the individual geom_ layer, e.g.
geom_polygon(aes(x=x, y=y), data.frame)
geom_polygon() function in ggplot2:#note, your variable for median house price might be called something different, so check first by using names(Borough_geom)!
layer1<-geom_polygon(aes(x=long, y=lat, fill=Median.House.Price...U.00A3.....2014, group=group), data=Borough_geom)
ggplot() and the layer, plus a call to coord_equal() to set the correct aspect ratio of the map:ggplot()+layer1+coord_equal()
palette1<-scale_fill_continuous(low="white", high="orange", "Price(£)")
layer2<-geom_path(aes(x=long, y=lat, group=group),data=Borough_geom, colour="#bdbdbd")
labels<-labs(list(title="Average House Price 2014",x="Easting", y="Northing"))
ggplot()+layer1+layer2+palette1+labels+coord_equal()
Now this map is perfectly acceptable, but what if we wanted to add some additional information to it – perhaps information that was referenced in a different coordinates system, such as global latitude and longitude? In order to do this, we would need to re-project the data – something which can be mathematically challenging, but fortunately something which the R package sp can deal with quite easily.
First we need to check that our SpatialPolygonsDataFrame has a coordinate reference system:
print(BoroughMapSP)
## class : SpatialPolygonsDataFrame
## features : 33
## extent : 503568.1, 561957.4, 155850.8, 200933.9 (xmin, xmax, ymin, ymax)
## coord. ref. : +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs
## variables : 71
## names : code, name, altname, label, Ward.name, Old.code, New.code, Population...2015, Children.aged.0.15...2015, Working.age..16.64....2015, Older.people.aged.65....2015, X..All.Children.aged.0.15...2015, X..All.Working.age..16.64....2015, X..All.Older.people.aged.65....2015, Mean.Age...2013, ...
## min values : E09000001, Barking and Dagenham, NA, E09000001, Barking and Dagenham, 00AA, E09000001, 8100, 650, 6250, 1250, 8.0, 62.6, 5.7, 31.1, ...
## max values : E09000033, Westminster, NA, E09000033, Westminster, 00BK, E09000033, 393200, 82900, 257850, 56550, 26.1, 76.9, 18.4, 41.3, ...
To find the proj4strings for a whole range of different geographic projections, use the search facility at http://spatialreference.org/. If your boundary data doesn’t have a spatial reference system, you can read it in with one like this:
# read borough map in and explicitly set projection to British National Grid using the EPSG string code 27700
BoroughMapSP <- read_shape("BoundaryData/england_lad_2011Polygon.shp", current.projection = 27700)
#create a variable for the EPSG code to reference the proj4string (EPSG codes are shorter and easier to remember than the full strings!) and store it in a variable...
UKBNG <- "+init=epsg:27700"
#now set the proj4string for your BoroughMap object
proj4string(BoroughMapSP) <- CRS(UKBNG)
## Warning in `proj4string<-`(`*tmp*`, value = <S4 object of class structure("CRS", package = "sp")>): A new CRS was assigned to an object with an existing CRS:
## +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 +units=m +no_defs
## without reprojecting.
## For reprojection, use function spTransform
print(BoroughMapSP) # check for new CRS
## class : SpatialPolygonsDataFrame
## features : 33
## extent : 503568.1, 561957.4, 155850.8, 200933.9 (xmin, xmax, ymin, ymax)
## coord. ref. : +init=epsg:27700 +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +datum=OSGB36 +units=m +no_defs +ellps=airy +towgs84=446.448,-125.157,542.060,0.1502,0.2470,0.8421,-20.4894
## variables : 71
## names : code, name, altname, label, Ward.name, Old.code, New.code, Population...2015, Children.aged.0.15...2015, Working.age..16.64....2015, Older.people.aged.65....2015, X..All.Children.aged.0.15...2015, X..All.Working.age..16.64....2015, X..All.Older.people.aged.65....2015, Mean.Age...2013, ...
## min values : E09000001, Barking and Dagenham, NA, E09000001, Barking and Dagenham, 00AA, E09000001, 8100, 650, 6250, 1250, 8.0, 62.6, 5.7, 31.1, ...
## max values : E09000033, Westminster, NA, E09000033, Westminster, 00BK, E09000033, 393200, 82900, 257850, 56550, 26.1, 76.9, 18.4, 41.3, ...
spTransform() function:BoroughMapSPWGS84 <-spTransform(BoroughMapSP, CRS("+proj=longlat +datum=WGS84"))
print(BoroughMapSPWGS84)
## class : SpatialPolygonsDataFrame
## features : 33
## extent : -0.5103767, 0.3340146, 51.28676, 51.69187 (xmin, xmax, ymin, ymax)
## coord. ref. : +proj=longlat +datum=WGS84 +ellps=WGS84 +towgs84=0,0,0
## variables : 71
## names : code, name, altname, label, Ward.name, Old.code, New.code, Population...2015, Children.aged.0.15...2015, Working.age..16.64....2015, Older.people.aged.65....2015, X..All.Children.aged.0.15...2015, X..All.Working.age..16.64....2015, X..All.Older.people.aged.65....2015, Mean.Age...2013, ...
## min values : E09000001, Barking and Dagenham, NA, E09000001, Barking and Dagenham, 00AA, E09000001, 8100, 650, 6250, 1250, 8.0, 62.6, 5.7, 31.1, ...
## max values : E09000033, Westminster, NA, E09000033, Westminster, 00BK, E09000033, 393200, 82900, 257850, 56550, 26.1, 76.9, 18.4, 41.3, ...
#transform it back again:
BoroughMapSPBNG <-spTransform(BoroughMapSP, CRS(UKBNG))
print(BoroughMapSPBNG)
## class : SpatialPolygonsDataFrame
## features : 33
## extent : 503568.1, 561957.4, 155850.8, 200933.9 (xmin, xmax, ymin, ymax)
## coord. ref. : +init=epsg:27700 +proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +datum=OSGB36 +units=m +no_defs +ellps=airy +towgs84=446.448,-125.157,542.060,0.1502,0.2470,0.8421,-20.4894
## variables : 71
## names : code, name, altname, label, Ward.name, Old.code, New.code, Population...2015, Children.aged.0.15...2015, Working.age..16.64....2015, Older.people.aged.65....2015, X..All.Children.aged.0.15...2015, X..All.Working.age..16.64....2015, X..All.Older.people.aged.65....2015, Mean.Age...2013, ...
## min values : E09000001, Barking and Dagenham, NA, E09000001, Barking and Dagenham, 00AA, E09000001, 8100, 650, 6250, 1250, 8.0, 62.6, 5.7, 31.1, ...
## max values : E09000033, Westminster, NA, E09000033, Westminster, 00BK, E09000033, 393200, 82900, 257850, 56550, 26.1, 76.9, 18.4, 41.3, ...
#You may want to create a similar variable for WGS84
latlong <- "+init=epsg:4326"
fortify() the shapefile, merge() it with the data as before and generate some new layers with it.Borough_geomWGS84<-fortify(BoroughMapSPWGS84, region="code")
Borough_geomWGS84 <- merge(Borough_geomWGS84, LondonData, by.x="id", by.y="New.code")
layer1WGS<-geom_polygon(aes(x=long, y=lat, fill=Median.House.Price...U.00A3.....2014, group=group), data=Borough_geomWGS84)
OpenStreetMap package, but we need to know the spatial extent of the tile we are downloading. This can be obtained directly from the @bbox slot in the SpatialPolygonsDataFrame object BoroughMapSP@bbox - if you want to understand these slots, try ?SpatialPolygonsDataFrame-classBoroughMapSPWGS84@bbox
## min max
## x -0.5103767 0.3340146
## y 51.2867601 51.6918741
openmap() function of the OpenStreetMap package to obtain a backdrop tile of the appropriate extent:#put in the coordinates for upper-left (max y, min x) and lower-right (min y and max x) from the bbox
basemap<-openmap(c(51.6918741,-0.5103767),c(51.2867601,0.3340146), zoom=NULL,"stamen-toner")
#we can also make sure that our basemap is projected correctly as the CRS might be a bit odd - use openproj to fix this
basemapWGS84 <- openproj(basemap, projection = latlong)
basemapBNG <- openproj(basemap, projection="+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +datum=OSGB36 +units=m +no_defs")
OpenStreetMap uses the autoplot() function in ggplot2 to draw the tile, and we can then add the other layers to the map as we would with a standard ggplot layered plot:ggplot()+layer1WGS
autoplot(basemapWGS84)+layer1WGS
layer1<-geom_polygon(aes(x=long, y=lat, fill=Median.House.Price...U.00A3.....2014, group=group),alpha=0.7, data=Borough_geom)
autoplot(basemapBNG)+layer1+palette1+labels+coord_equal()
You’ll see above that when Plotting in latitude and longitude, your map may look a little distorted. You will also notice that to fix this, you might need to re-project everything (basemap and layers) back into British National Grid. Re-projecting your basemap is simple with the openproj() function in OpenStreetMap along with the correct proj4 string:
You will also need to make sure that your fortified shapefile in ggplot is also in BNG. Once you have this, you should be able to layer everything up in ggplot to produce something like the map above.
Note, to generate this map, I selected one of the many basemap types inside openmap() also made use for the alpha property in layer1 aesthetics (http://docs.ggplot2.org/current/aes_colour_fill_alpha.html), and removed the background using plot.background=element_blank() inside the set theme() function in ggplot2.
One of the nice features of ggplot2 is its faceting function which allows you to lay out subsets of your data in different panels of a grid.
See if you can create a faceted grid of London maps like the one shown below. To do this, you will need to go through several stages:
You will need to subset your Borough_geom data frame so that it only contains data on the same scale (you have a number of columns that show percentages, for example) – you can show facets on different scales, but we will leave this for the time being.
You will also need to make sure your subset includes a few columns of key ID information data at the beginning – e.g. ID, long, lat, piece, group and name.
You will then need to use the reshape2 package to reformat your data using the melt() function, with an argument to specify the columns where your ID data ends (all columns afterwards containing data you want to plot). A call something like:
borough_melt<-melt(borough_pct_geom,id=1:9)
object+facet_wrap(~variable)
So we created an interactive map with tmap earlier and that was pretty cool, but if we want to do even more mind-blowing stuff later on in the course, we will need to get our heads around how to do interactive maps using leaflet.
Leaflet is a Java Script library for producing interactive web maps, and some enterprising coders over at RStudio have produced a package for allowing you to create your own Leaflet maps. All of the documentation for the R Leaflet package can be found here: https://rstudio.github.io/leaflet/
library(leaflet)
library(sf)
library(sp)
library(magrittr)
##
## Attaching package: 'magrittr'
## The following object is masked from 'package:purrr':
##
## set_names
## The following object is masked from 'package:tidyr':
##
## extract
library(classInt)
Before we create a leaflet map, we need to specify what the breaks in our data are going to be…
colours<- brewer.pal(5, "Blues")
breaks<-classIntervals(BoroughDataMap$Claimant.Rate.of.Housing.Benefit..2015., n=5, style="jenks")
graphics::plot(breaks, pal=colours)
So we have now created a breaks object which uses the jenks natural breaks algorithm to divide up our variable into 5 classes. You will notice that breaks is a list of 2 objects. We want only the brks bit which contains the values for the breaks
summary(breaks)
## Length Class Mode
## var 33 -none- numeric
## brks 6 -none- numeric
breaks <- breaks$brks
Now we can create out leaflet interactive map.
Here you will see that I am using different syntax to that which has gone before. The %>% (pipe) operator is part of the magrittr package - https://cran.r-project.org/web/packages/magrittr/vignettes/magrittr.html. magrittr is an entirely new way of thinking about R syntax. Read up on it if you wish, but for now it is useful to think of the pipe operator as simply meaning “then”. Do this THEN do this THEN do that.
#create a new sp object from the earlier sf object with all of our data in THEN Transform it to WGS84 THEN convert it to SP.
BoroughDataMapSP <- BoroughDataMap %>%
st_transform(crs = 4326) %>%
as("Spatial")
#create a colour palette using colorBin colour mapping
pal <- colorBin(palette = "YlOrRd",
domain = BoroughDataMapSP$Claimant.Rate.of.Housing.Benefit..2015.,
#create bins using the breaks object from earlier
bins = breaks)
# now, add some polygons colour them using your colour palette, #overlay the, on top of a nice backdrop and add a legend. Note the #use of the magrittr pipe operator (%>%) – check the documentation #to understand how this is working…
leaflet(BoroughDataMapSP) %>%
addPolygons(stroke = FALSE,
fillOpacity = 0.5,
smoothFactor = 0.5,
color = ~pal(Claimant.Rate.of.Housing.Benefit..2015.),
popup = ~name
) %>%
addProviderTiles("CartoDB.DarkMatter") %>%
addLegend("bottomright",
pal= pal,
values = ~Claimant.Rate.of.Housing.Benefit..2015.,
title = "Housing Benefit Claimant Rate",
labFormat = labelFormat(prefix = "Per 1,000 people "),
opacity = 1
)