rgdal
Welcome to Spatial Data in R! This first set of tutorials (in three parts) is provides an introduction to the two types of spatial data you will encounter in R: vector data and raster data. By the end of this tutorial, you should have a good sense of how R thinks about spatial data, and how to import and export spatial datasets you may get from other programs or sources.
Almost all spatial vector data structures in R are based on the sp
package. Even other libraries that may seem independent are usually built on top of sp
, even if you can’t see it.
The sp
package has three main types of spatial data we’ll work with: points, lines, and polygons. There are some differences between these different types, but they are all very similar.
To help you understand what these data structures are like, in this section we’ll create some spatial data from scratch. This is probably not how you’ll work with data in the future – most of the time you just import spatial data from a source – but this exercise will help give you a good foundation and help you know how to troubleshoot problems in the future.
There are three basic steps to creating spatial data by hand:
Spatial*
objects (*
stands for Points, Lines, or Polygons)
Spatial*
object into a Spatial*DataFrame
object.Exercise 0
Discuss with your neighbor: What do we need to specify in order to define spatial vector data?
Points are the most basic geometric shape, so we begin by building a SpatialPoints
object.
A points is defined by a pair of x-y coordiantes, so we can create a set of points by (a) creating matrix of x-y points, and (b) passing them to the SpatialPoints
function to create our first SpatialPoints
object:
library(sp)
toy.coordinates <- rbind(c(1.5, 2), c(2.5, 2), c(0.5, 0.5), c(1, 0.25), c(1.5,
0), c(2, 0), c(2.5, 0), c(3, 0.25), c(3.5, 0.5))
toy.coordinates
[,1] [,2]
[1,] 1.5 2.00
[2,] 2.5 2.00
[3,] 0.5 0.50
[4,] 1.0 0.25
[5,] 1.5 0.00
[6,] 2.0 0.00
[7,] 2.5 0.00
[8,] 3.0 0.25
[9,] 3.5 0.50
my.first.points <- SpatialPoints(toy.coordinates) # ..converted into a spatial object
plot(my.first.points)
To get a summary of how R sees these points, we can ask it for summary information in a couple different ways. Here’s a summary of available commands:
summary(my.first.points)
Object of class SpatialPoints
Coordinates:
min max
coords.x1 0.5 3.5
coords.x2 0.0 2.0
Is projected: NA
proj4string : [NA]
Number of points: 9
coordinates(my.first.points)
coords.x1 coords.x2
[1,] 1.5 2.00
[2,] 2.5 2.00
[3,] 0.5 0.50
[4,] 1.0 0.25
[5,] 1.5 0.00
[6,] 2.0 0.00
[7,] 2.5 0.00
[8,] 3.0 0.25
[9,] 3.5 0.50
Unlike a simple geometric object, a SpatialPoints
object has the ability to keep track of how the coordinates of its points relate to places in the real world through an associated “Coordinate Reference System” (CRS – the combination of a geographic coordinate system and possibly a projection), which is stored using a code called a proj4string
. The proj4string is so important to a SpatialPoints
object, that it’s presented right in the summary:
summary(my.first.points)
Object of class SpatialPoints
Coordinates:
min max
coords.x1 0.5 3.5
coords.x2 0.0 2.0
Is projected: NA
proj4string : [NA]
Number of points: 9
In this case, however, while our SpatialPoints
object clearly knows what a CRS is, the Spatial object we just created does not have a projection or geographic coordinate system defined. It is ok to plot, but be aware that for many meaningful spatial operations you will need to define a CRS.
CRS objects can be created by passing the CRS()
function the code associated with a known projection. You can find the codes for most commonly used projections from www.spatialreference.org.
Note that the same CRS can often be called in many ways. For example, one of the most commonly used CRS is the WGS84 latitude-longitude projection. You can create a WGS84 lat-long projection object by either passing the reference code for the projection – CRS("+init=EPSG:4326"
) – or by directly calling all its relevant parameters – CRS("+proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs")
.
Note: Some users report this only works if EPSG
is lower-case (epsg
), so try that if you have issues!
Here’s an illustration of assigning a CRS:
is.projected(my.first.points) # see if a projection is defined.
[1] NA
# Returns `NA` if no geographic coordinate system or projection; returns
# FALSE if has geographic coordinate system but no projection.
crs.geo <- CRS("+init=EPSG:32633") # UTM 33N
proj4string(my.first.points) <- crs.geo # define projection system of our data
is.projected(my.first.points)
[1] TRUE
summary(my.first.points)
Object of class SpatialPoints
Coordinates:
min max
coords.x1 0.5 3.5
coords.x2 0.0 2.0
Is projected: TRUE
proj4string :
[+init=EPSG:32633 +proj=utm +zone=33 +datum=WGS84 +units=m +no_defs
+ellps=WGS84 +towgs84=0,0,0]
Number of points: 9
When CRS
is called with only an EPSG code, R will try to complete the CRS with the parameters looked up in the EPSG table:
crs.geo <- CRS("+init=EPSG:32633") # looks up UTM 33N
crs.geo # prints all parameters
CRS arguments:
+init=EPSG:32633 +proj=utm +zone=33 +datum=WGS84 +units=m +no_defs
+ellps=WGS84 +towgs84=0,0,0
We’ll talk more about managing projections in the next lesson.
Geometry-only objects (objects without attributes) can be subsetted similar to how vectors or lists are subsetted; we can select the first two points by
my.first.points[1:2]
class : SpatialPoints
features : 2
extent : 1.5, 2.5, 2, 2 (xmin, xmax, ymin, ymax)
coord. ref. : +init=EPSG:32633 +proj=utm +zone=33 +datum=WGS84 +units=m +no_defs +ellps=WGS84 +towgs84=0,0,0
Moving from a SpatialPoints
to a SpatialPointsDataFrame
occurs when you add a data.frame
of attributes to the points. Let’s just add an arbitrary table to the data – this will label each point with a letter and a number. Note points will merge with the data.frame
based on the order of observations.
df <- data.frame(attr1 = c("a", "b", "z", "d", "e", "q", "w", "r", "z"), attr2 = c(101:109))
df
attr1 attr2
1 a 101
2 b 102
3 z 103
4 d 104
5 e 105
6 q 106
7 w 107
8 r 108
9 z 109
my.first.spdf <- SpatialPointsDataFrame(my.first.points, df)
summary(my.first.spdf)
Object of class SpatialPointsDataFrame
Coordinates:
min max
coords.x1 0.5 3.5
coords.x2 0.0 2.0
Is projected: TRUE
proj4string :
[+init=EPSG:32633 +proj=utm +zone=33 +datum=WGS84 +units=m +no_defs
+ellps=WGS84 +towgs84=0,0,0]
Number of points: 9
Data attributes:
attr1 attr2
z :2 Min. :101
a :1 1st Qu.:103
b :1 Median :105
d :1 Mean :105
e :1 3rd Qu.:107
q :1 Max. :109
(Other):2
Now that we have attributes, we can also subset our data the same way we would subset a data.frame
. Some subsetting:
my.first.spdf[1:2, ] # row 1 and 2 only
class : SpatialPointsDataFrame
features : 2
extent : 1.5, 2.5, 2, 2 (xmin, xmax, ymin, ymax)
coord. ref. : +init=EPSG:32633 +proj=utm +zone=33 +datum=WGS84 +units=m +no_defs +ellps=WGS84 +towgs84=0,0,0
variables : 2
names : attr1, attr2
min values : a, 101
max values : b, 102
my.first.spdf[1:2, "attr1"] # row 1 and 2 only, attr1
class : SpatialPointsDataFrame
features : 2
extent : 1.5, 2.5, 2, 2 (xmin, xmax, ymin, ymax)
coord. ref. : +init=EPSG:32633 +proj=utm +zone=33 +datum=WGS84 +units=m +no_defs +ellps=WGS84 +towgs84=0,0,0
variables : 1
names : attr1
min values : a
max values : b
plot(my.first.spdf[which(my.first.spdf$attr2 > 105), ]) # select if attr2 > 5
A SpatialPointsDataFrame
object can be created directly from a data.frame
by specifying which columns contain the coordinates. This is interesting, for example if you have a spreadsheet that contains latitude, longitude and some values. You can read this into a data frame with read.table
, and then create the object from the data frame in one step by using the coordinates()
command. That automatically turns the dataframe object into a SpatialPointsDataFrame
.
R_Workshop
on your Desktop.RGIS1_Data
folder from coursework in this directory.read.csv()
to read sf_restaurant_inspections.csv
into a dataframe in R and name it sf.df
.head()
to examine the first few lines of the dataframe.class()
to examine which object class the table belongs to.sp
library (library(sp)
)Convert the table into a spatial object using the coordinates
command and passing the names of the columns in the table that correspond to longitude and latitude:
coordinates(sf.df) <- c("longitude", "latitude")
Note the reverse order! While we usually say “latitude and longitude”, since longitude corresponds to the x-coordinates on a map and latitude corresponds to the y-coodinates and R assumes coordinates to be ordered (x,y), we pass c("longitude","latitude")
!
Use class()
again to examine which object class the table belongs to now:
What to you observe?
Plot restaurants with terrible scores by subsetting on Score
and using the plot
command.
SpatialPolygons are very, very common, especially in political science (think administrative borders, electoral constituencies, etc.), so they’re important to get used to.
SpatialPolygons
from scratch.SpatialPolygons
are a little more complicated than SpatialPoints
. With SpatialPoints
, we moved directly from x-y coordinates to a SpatialPoints
object.
To get a SpatialPolygons
object, we have to build it up by (a) creating Polygon
objects, (b) combining those into Polygons
objects (note the “s” at the end), and finally (c) combining those to create SpatialPolygons
. So what are these components?
Polygon
object is a single geometric shape (e.g. a square, rectangle, etc.) defined by a single uninterrupted line around the exterior of the shape.Polygons
object consists of one or more simple geometric objects (Polygon
objects) that combine to form what you think of as a single unit of analysis (an “observation”). For example, each island in Hawaii would be a Polygon
, but Hawaii itself is the Polygons
consisting of all the individual island Polygon
objects.SpatialPolygons
object is a collection of Polygons
objects, where each Polygons
object is an “observation”. For example, if we wanted a map of US states, we would make a Polygons
object for each state, then combine them all into a single SpatialPolygons
. If you’re familiar with shapefiles, SpatialPolygons
is basically the R analogue of a shapefile
or layer
.One special note: if you want to put a hole in a polygon (e.g. drawing a donut, or if you wanted to draw South Africa and leave a hole in the middle for Lesotho) you do so by (a) creating a Polygon
object for the outline, (b) creating a second Polygon
object for the hole and passing the argument hole=TRUE
, and (c) combine the two into a Polygons
object.
Let’s try building up a SpatialPolygon
!
# create polyon objects from coordinates. Each object is a single geometric
# polygon defined by a bounding line.
house1.building <- Polygon(rbind(c(1, 1), c(2, 1), c(2, 0), c(1, 0)))
house1.roof <- Polygon(rbind(c(1, 1), c(1.5, 2), c(2, 1)))
house2.building <- Polygon(rbind(c(3, 1), c(4, 1), c(4, 0), c(3, 0)))
house2.roof <- Polygon(rbind(c(3, 1), c(3.5, 2), c(4, 1)))
house2.door <- Polygon(rbind(c(3.25, 0.75), c(3.75, 0.75), c(3.75, 0), c(3.25,
0)), hole = TRUE)
# create lists of polygon objects from polygon objects and unique ID A
# `Polygons` is like a single observation.
h1 <- Polygons(list(house1.building, house1.roof), "house1")
h2 <- Polygons(list(house2.building, house2.roof, house2.door), "house2")
# create spatial polygons object from lists A SpatialPolygons is like a
# shapefile or layer.
houses <- SpatialPolygons(list(h1, h2))
plot(houses)
As with SpatialPoints, we can associated a data.frame
with SpatialPolygons. There are two things that are important about doing this with SpatialPolygons:
data.frame
with a SpatialPolygons object, R will line up rows and polygons by matching Polygons
object names with the data.frame
row.names
.SpatialPolygonsDataFrame
’s life, the association between Polygons
and rows of your data.frame
is based on the order of rows in your data.frame
, so don’t try to change the order of the data.frame
by hand!Make attributes and plot. Note how the door – which we created with hole=TRUE
is empty!
attr <- data.frame(attr1 = 1:2, attr2 = 6:5, row.names = c("house2", "house1"))
houses.DF <- SpatialPolygonsDataFrame(houses, attr)
as.data.frame(houses.DF) # Notice the rows were re-ordered!
attr1 attr2
house1 2 5
house2 1 6
spplot(houses.DF)
As with SpatialPoints
, a SpatialPolygons
object on Earth doesn’t actually know where it until you set its Coordinate Reference System, which you can do the same way you did with the SpatialPoints
objects:
crs.geo <- CRS("+init=EPSG:4326") # geographical, datum WGS84
proj4string(houses.DF) <- crs.geo # define projection system of our data
Exercise 1.2 Answer code at end of exercise – but don’t cheat!
Add an attribute Table with each country’s GDP Per Capita (approximately $7,000 for South Africa and $1,000 for Lesotho).
Plot your SpatialPolygons using spplot()
The result should look something like this:
SpatialLines
objects are basically like SpatialPolygons
, except they’re built up using Line
objects (each a single continuous line, like each branch of a river), Lines
objects (collection of lines that form a single unit of analysis, like all the parts of a river), to SpatialLines
(collection of “observations”, like rivers).
Here’s a quick summary of the construction flow of SpatialObjects:
rgdal
Normally we do not create Spatial*
objects by hand. It is much more common to work with existing data read from external sources like shapefiles, or databases.
In order to read those into R and turn them into Spatial*
family objects we rely on the rgdal
package. It provides us direct access to the powerful GDAL library from within R.
We can read in and write out spatial data using:
readOGR()
and writeOGR()
The parameters provided for each function varies depending on the exact spatial file type you are reading. We will take the ESRI shapefile as an example. A shapefile - as you know - consists of various files, and R expects all those files to be in one directory.
When reading in a shapefile, readOGR()
expects at least the following two arguments:
datasource name (dsn) # the path to the folder that contains the files
# Note that this is a path to the folder
layer name (layer) # the file name without extension
# Note that this is not a path but just the name of the file
For example, if I have a shapefile called sf_incomes.shp
and all its associated files (like .dbf, .prj, .shx) in a directory called PH
on my desktop, and I have my working directory set to my desktop folder, my command to read this shapefile would look like this:
readOGR(dsn = "shapefiles", layer = "sf_incomes")
or in short:
readOGR("shapefiles", "sf_incomes")
Note you may run into trouble if you set your working directory to the folder holding the shapefile as ‘readOGR()’ doesn’t like it if the first argument is an empty string.
rgdal
package.R_Workshop
folder and it contains the materials you downloaded and unzipped earlier.sf_income
into an object called sf
. Make sure you understand the directory structure.summary()
, class()
, str("sf", max.level = 2)
,names(sf)
head(as.data.frame(sf))
sf[sf$MdIncHH > 40000,]
spplot(sf, "MdIncHH")
Turns out Spatial* objects are just a collection of things that you can see using the str
command:
str(my.first.spdf)
Formal class 'SpatialPointsDataFrame' [package "sp"] with 5 slots
..@ data :'data.frame': 9 obs. of 2 variables:
.. ..$ attr1: Factor w/ 8 levels "a","b","d","e",..: 1 2 8 3 4 5 7 6 8
.. ..$ attr2: int [1:9] 101 102 103 104 105 106 107 108 109
..@ coords.nrs : num(0)
..@ coords : num [1:9, 1:2] 1.5 2.5 0.5 1 1.5 2 2.5 3 3.5 2 ...
.. ..- attr(*, "dimnames")=List of 2
.. .. ..$ : NULL
.. .. ..$ : chr [1:2] "coords.x1" "coords.x2"
..@ bbox : num [1:2, 1:2] 0.5 0 3.5 2
.. ..- attr(*, "dimnames")=List of 2
.. .. ..$ : chr [1:2] "coords.x1" "coords.x2"
.. .. ..$ : chr [1:2] "min" "max"
..@ proj4string:Formal class 'CRS' [package "sp"] with 1 slot
.. .. ..@ projargs: chr "+init=EPSG:32633 +proj=utm +zone=33 +datum=WGS84 +units=m +no_defs +ellps=WGS84 +towgs84=0,0,0"
What this shows is that my.first.spdf
is actually a collection of different things – data
, coords
, bbox
, and a proj4string
. In the same way we can get many of these things with commands, we can also call them with the @
command!
bbox(my.first.spdf)
min max
coords.x1 0.5 3.5
coords.x2 0.0 2.0
my.first.spdf@bbox
min max
coords.x1 0.5 3.5
coords.x2 0.0 2.0
coordinates(my.first.spdf)
coords.x1 coords.x2
[1,] 1.5 2.00
[2,] 2.5 2.00
[3,] 0.5 0.50
[4,] 1.0 0.25
[5,] 1.5 0.00
[6,] 2.0 0.00
[7,] 2.5 0.00
[8,] 3.0 0.25
[9,] 3.5 0.50
my.first.spdf@coords
coords.x1 coords.x2
[1,] 1.5 2.00
[2,] 2.5 2.00
[3,] 0.5 0.50
[4,] 1.0 0.25
[5,] 1.5 0.00
[6,] 2.0 0.00
[7,] 2.5 0.00
[8,] 3.0 0.25
[9,] 3.5 0.50
as.data.frame(my.first.spdf)
attr1 attr2 coords.x1 coords.x2
1 a 101 1.5 2.00
2 b 102 2.5 2.00
3 z 103 0.5 0.50
4 d 104 1.0 0.25
5 e 105 1.5 0.00
6 q 106 2.0 0.00
7 w 107 2.5 0.00
8 r 108 3.0 0.25
9 z 109 3.5 0.50
my.first.spdf@data
attr1 attr2
1 a 101
2 b 102
3 z 103
4 d 104
5 e 105
6 q 106
7 w 107
8 r 108
9 z 109
In R, the items with @
at the start of the line are called “slots”. If you are used to working in another object-oriented language like Java or Python, these are analogous to attributes. You can access each of these “slots” using the @
operator, making them equivalent of some function calls. It is a general recommendation to not modify slots by directly assigning values to them unless you know very well what you do.
Answers to Exercise 1.2
sa.outline <- rbind(c(16, -29), c(33, -21), c(33, -29), c(26, -34), c(17, -35))
sa.hole <- rbind(c(29, -28), c(27, -30), c(28, -31))
lesotho <- rbind(c(29, -28), c(27, -30), c(28, -31))
# Make Polygon objects
sa.outline.poly <- Polygon(sa.outline)
sa.hole.poly <- Polygon(sa.hole, hole = TRUE)
lesotho.poly <- Polygon(lesotho)
# Make Polygons objects
sa.polys <- Polygons(list(sa.outline.poly, sa.hole.poly), "SA")
lesotho.polys <- Polygons(list(lesotho.poly), "Lesotho")
# Make spatialy Polygons
map <- SpatialPolygons(list(sa.polys, lesotho.polys))
# Check
plot(map)
# Add data
df <- data.frame(c(7000, 1000), row.names = c("SA", "Lesotho"))
mapDF <- SpatialPolygonsDataFrame(map, df)
# Add WGS 84 coordinate system
proj4string(mapDF) <- CRS("+init=EPSG:4326")
# Plot
spplot(mapDF)
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.