Skip to contents

geoGraph: using spherical grids to walk through the geographic space

In this vignette, we go through the basic tasks that can be achieved using geoGraph. A short overview of the functionality of the package is summarized the package’s manpage, accessible via:

?geoGraph

geoGraph aims at implementing graph approaches for geographic data. In geoGraph, a given geographic area is modeled by a fine regular grid, where each vertex has a set of spatial coordinates and a set of attributes, which can be for instance habitat descriptors, or the presence/abundance of a given species. ‘traveling’ within the geographic area can then be easily modeled as moving between connected vertices. The cost of moving from one vertex to another can be defined according to attribute values, which allows for instance to define friction routes based on habitat.

geoGraph harnesses the full power of graph algorithms implemented in R by the graph and RBGL (R Boost Graph Library) packages. In particular, RBGL is an interface between R and the comprehensive Boost Graph Library in C++, which provides fast and efficient implementations of a wide range of graph algorithms. Once we have defined frictions for an entire geographic area, we can easily, for instance, find the least costs path from one location to another, or find the most parsimonious way of connecting a set of locations.

Interfacing spatial data and graphs can be a complicated task. The purpose of geoGraph is to provide tools to achieve and simplify this ‘preliminary’ step. This is achieved by defining new classes of objects which are essentially geo-referenced graphs with node attributes (gGraph objects), and interfaced spatial data (gData objects). In this vignette, we show how to install geoGraph, construct and handle gGraph/gData objects, and illustrate some basic features of graph algorithms.

First steps

Installing the package

All the following instructions should be entered from a new R session to avoid errors due to installing attached packages.

You can install geoGraph from GitHub with:

install.packages("pak")
pak::pak("EvolEcolGroup/geograph")

Once installed, the package can be loaded using:

If you have an error regarding missing packages, you may need to install manually the packages graph and RBGL from Bioconductor:

install.packages("BiocManager")
BiocManager::install(c("graph", "RBGL"))

And then attempt to reinstall geoGraph from GitHub.

Data representation

Data representation refers to the way a given type of data is handled by a computer program. Two types of objects are used in geoGraph: gGraph, and gData objects. Both objects are defined as formal (S4) classes and often have methods for similar generic function (e.g. getNodes is defined for both objects). Essentially, gGraph objects contain underlying layers of information, including a spatial grid and possibly node attributes. gData are sets of locations (like sampled sites, for instance) which have been interfaced to a gGraph object, to allow further manipulations such as finding paths on the grid between pairs of locations.

gGraph objects

The definition of the formal class gGraph can be obtained using:

getClass("gGraph")
## Class "gGraph" [package "geoGraph"]
## 
## Slots:
##                                                   
## Name:      coords nodes.attr       meta      graph
## Class:     matrix data.frame       list   graphNEL

and a new empty object can be obtained using the constructor:

new("gGraph")
## 
## === gGraph object ===
## 
## @coords: spatial coordinates of 0 nodes
##      lon lat
## 
## @nodes.attr: 0 nodes attributes
## data frame with 0 columns and 0 rows
## 
## @meta: list of meta information with 0 items
## 
## @graph:
## A graphNEL graph with undirected edges
## Number of Nodes = 0 
## Number of Edges = 0

The documentation ?gGraph explains the basics about the object’s content. In a nutshell, these objects are spatial grids with nodes and segments connecting neighboring nodes, and additional information on the nodes or on the graph itself. coords is a matrix of longitudes and latitudes of the nodes. nodes.attr is a data.frame storing attributes of the nodes, such as habitat descriptors; each row corresponds to a node of the grid, while each column corresponds to a variable. meta is a list containing miscellaneous information about the graph itself. There is no constraint applying to the components of the list, but some typical components such as $costs or $colors will be recognized by certain functions. For instance, you can specify plotting rules for representing a given node attribute by a given color by defining a component $colors. Similarly, you can associate costs to a given node attribute by defining a component $costs. An example of this can be found in already existing gGraph objects. For instance, worldgraph.10k is a graph of the world with approximately 10,000 nodes, and only on-land connectivity i.e. no traveling on the seas.

worldgraph.10k@meta
## $colors
##            habitat       color
## 1              sea        blue
## 2             land       green
## 3         mountain       brown
## 4       landbridge light green
## 5 oceanic crossing  light blue
## 6  deselected land   lightgray
## 
## $costs
##            habitat cost
## 1              sea  100
## 2             land    1
## 3         mountain   10
## 4       landbridge    5
## 5 oceanic crossing   20
## 6  deselected land  100

Lastly, the graph component is a graphNEL object, which is the standard class for graphs in the graph and RBGL packages. This object contains all information on the connections between nodes, and the weights (costs) of these connections.

Four main gGraph are provided with geoGraph: rawgraph.10k, rawgraph.40k, worldgraph.10k, and worldgraph.40k. These datasets are available using the command data. The grid used in these datasets are the best geometric approximation of a regular grid for the surface of a sphere. One advantage of working with these grids is that we do not have to use a projection for geographic coordinates, which is a usual issue in regular GIS.

The difference between rawgraphs and worldgraphs is that the first are entirely connected, while in the second connections occur only on land. Numbers 10k' and40k’ indicate that the grids consist of roughly 10,000 and 40,000 nodes. For illustrative purposes, we will often use the 10k grids, since they are less heavy to handle. For most large-scale applications, the 40k versions should provide sufficient resolution. New gGraph can be constructed using the constructor (new(...)). A more detailed description of how to construct custom gGraph is provided in the vignette ‘Making custom grids in geoGraph’ (see vignette() for more information on available vignettes).

gData objects

gData objects store sets of locations interfaced with a gGraph object. When creating a gData, each location is matched to its closest node on the gGraph grid, which makes it possible to model travel between locations along the grid — for instance, to find the shortest path between two sites through different habitat types.

Like for gGraph, the content of the formal class gData can be obtained using:

getClass("gData")
## Class "gData" [package "geoGraph"]
## 
## Slots:
##                                                       
## Name:       coords    nodes.id        data gGraph.name
## Class:      matrix   character         ANY   character

and a new empty object can be obtained using the constructor:

new("gData")
## 
## === gData object ===
## 
## @coords: spatial coordinates of 0 nodes
##      lon lat
## 
## @nodes.id: 0 nodes identifiers
## character(0)
## 
## @data: data
## NULL
## 
## Associated gGraph:

As before, the description of the content of these objects can be found in the documentation (?gData). coords is a matrix of xy (longitude/latitude) coordinates in which each row is a location. nodes.id is a vector of characters giving the name of the vertices matching the locations; this is defined automatically when creating a new gData, or using the function closestNode. data is a slot storing data associated to the locations; it can be any type of object, but a data.frame should cover most requirements for storing data. Note that this object should be subsettable (i.e. the [ operator should be defined), so that data can be subsetted when subsetting the gData object. Lastly, the slot gGraph.name contains the name of the gGraph object to which the gData has been interfaced.

In the next sections, we illustrate how we can build and use gData objects from a set of locations.

Getting started with geoGraph

Importing geographic data

GeoGraphic data consist of a set of locations, possibly accompanied by additional information. For instance, one may want to study the migrations among a set of biological populations with known geographic coordinates. In geoGraph, geographic data are stored in gData objects. These objects match locations to the closest nodes on a grid (a gGraph object), and store additional data if needed.

As a toy example, let us consider four locations: Bordeaux in France, Berlin in Germany, Baku in Azerbaijan, and Timbuktu in Mali. Since we will be working with a crude grid (10,000 nodes), locations need not be exact.
We enter the longitudes and latitudes (in this order, that is, xy coordinates) of these cities in decimal degrees, as well as approximate population sizes:

bordeaux <- c(-1, 45)
berlin <- c(13, 52)
baku <- c(44, 40)
timbuktu <- c(-3, 16)

cities.dat <- rbind.data.frame(bordeaux, berlin, baku, timbuktu)
colnames(cities.dat) <- c("lon", "lat")
row.names(cities.dat) <- c("Bordeaux", "Berlin", "Baku", "Timbuktu")
cities.dat$pop <- c(250000, 3500000, 2000000, 50000)
cities.dat
##          lon lat     pop
## Bordeaux  -1  45  250000
## Berlin    13  52 3500000
## Baku      44  40 2000000
## Timbuktu  -3  16   50000

We load a gGraph object which contains the grid that will support the data:

plot(worldgraph.10k)
## Spherical geometry (s2) switched off

## Spherical geometry (s2) switched on

In this figure, each node is represented with a color depending on the habitat type, either ‘sea’ (blue) or ‘land’ (green). We are going to interface the cities data with this grid; to do so, we create a gData object using new (see ?gData object):

cities <- new("gData", coords = cities.dat[, 1:2], data = cities.dat[, 3, drop = FALSE], gGraph.name = "worldgraph.10k")
cities
## 
## === gData object ===
## 
## @coords: spatial coordinates of 4 nodes
##   lon lat
## 1  -1  45
## 2  13  52
## 3  44  40
## ...
## 
## @nodes.id: 4 nodes identifiers
##      1      2      3 
## "5774" "7696" "2629" 
## ...
## 
## @data: 4 data
##              pop
## Bordeaux  250000
## Berlin   3500000
## Baku     2000000
## ...
## 
## Associated gGraph: worldgraph.10k
plot(cities, type = "both", reset = TRUE)
## Spherical geometry (s2) switched off
## Spherical geometry (s2) switched on
plotEdges(worldgraph.10k)

This figure illustrates the matching of original locations (black crosses) to nodes of the grid (red circles). As we can see, an issue occurred for Bordeaux, which has been assigned to a node in the sea (in blue). Locations can be re-assigned to nodes with restrictions for some node attribute values using closestNode; for instance, here we constrain matching nodes to have an habitat value (defined as node attribute in worldgraph.10k) equaling land (green points):

cities <- closestNode(cities, attr.name = "habitat", attr.value = "land")
plot(cities, type = "both", reset = TRUE)
## Spherical geometry (s2) switched off
## Spherical geometry (s2) switched on
plotEdges(worldgraph.10k)

Now, all cities have been assigned to a land node of the grid. Content of cities can be accessed via various accessors (see ?gData). For instance, we can retrieve original locations, assigned nodes, and stored data using:

getCoords(cities)
##      lon lat
## 5775  -1  45
## 7696  13  52
## 2629  44  40
## 3221  -3  16
getNodes(cities)
##   5774   7696   2629   3221 
## "5775" "7696" "2629" "3221"
getData(cities)
##              pop
## Bordeaux  250000
## Berlin   3500000
## Baku     2000000
## Timbuktu   50000

More interestingly, we can now retrieve all the geographic information contained in the underlying grid (i.e. gGraph object) as node attributes:

getNodesAttr(cities)
##      habitat
## 5775    land
## 7696    land
## 2629    land
## 3221    land

In this example, the information stored in worldgraph.10k is rather crude: habitat only distinguishes the land from the sea.

Finding least-cost paths

One of the most useful applications of geoGraph is the research of least-cost paths between couples of locations. This can be achieved using the functions dijkstraFrom and dijkstraBetween on a gData object which contains all the locations of interest. These functions return least-cost paths with the format gPath. dijkstraFrom compute the paths from a given node of the grid to all locations of the gData, while dijkstraBetween computes the paths between pairs of locations of the gData. Below, we illustrate the use of dijkstraBetween to find the least-cost paths between all pairs of cities in our example.

First, we check that all populations are connected on the grid using isConnected:

isConnected(cities)
## [1] TRUE

If not all locations are connected, connectivityPlot can help diagnose the problem by displaying connected components in different colors, and is available for both gGraph and gData objects. For instance:

connectivityPlot(worldgraph.10k, edges = TRUE, seed = 1, reset = TRUE)
## Spherical geometry (s2) switched off

## Spherical geometry (s2) switched on

Since all locations in cities are connected, we can proceed further.

We can now compute least-cost paths between all pairs of cities using dijkstraBetween:

cities.paths <- dijkstraBetween(cities)
cities.paths
## 
## === gPath object ===
## 
##  number of paths: 6 
## 
##  available paths (id_origin:id_destination): 5775:7696 5775:2629 5775:3221 ...
## 
## each path, accessible with [[]] has elements 'length', 'path_detail' and 'length_detail'
## x and y coordinates of all nodes are stored as an attribute 'xy'; see ?gPath for details
plot(cities, reset = TRUE)
## Spherical geometry (s2) switched off
## Spherical geometry (s2) switched on
plot(cities.paths)

In this graph, each path is plotted with a different color, but several paths overlap in several places. We can clearly see that all paths go through the Caucasus mountains, which is the only land connection between Europe and Africa in this grid. Depending on the application, this may be a problem, since the Strait of Gibraltar is a more likely route for traveling between these two continents. In the next section, we show how to change local properties of a gGraph to allow for a connection at the Strait of Gibraltar.

Changing local properties of a gGraph

In the current version of worldgraph.10k, there is no connection at the Strait of Gibraltar as the grid is designed to only allow for land to land connectivity. We can visually check this by zooming in on the area of interest and plotting the edges of the grid:

geo.zoomin(c(-10, 2, 32, 40))
## Spherical geometry (s2) switched off
## Spherical geometry (s2) switched on
plotEdges(worldgraph.10k)

We can change this by adding a connection between the two continents, for instance by adding a connection between the two nodes of the grid which are closest to the Strait of Gibraltar. Adding and removing edges from the grid of a gGraph can be achieved by geo.add.edges and geo.remove.edges, respectively. These functions are interactive, and require the user to select individual nodes or a rectangular area in which edges are added or removed. See ?geo.add.edges for more information on these functions. For instance, we can add the above-mentioned connection between Europe and Africa by selecting the two nodes of the grid closest to the Strait of Gibraltar, and adding a connection between them. For this we just run the ‘geo.add.edges’ function, and then click on the two nodes of the grid that we want to connect (in this case, one node in Europe and one node in Africa). After adding the edge, we can save the new graph as a new object (note that we have to save the changes using <-):

newGraph <- worldgraph.10k
plot(newGraph)
newGraph <- geo.add.edges(newGraph)
img
img

We can then assign the new graph using the setGraph function, so that the new graph will be used for path computations. Now when we compute least-cost paths between cities, we can see that the path between Bordeaux and Timbuktu goes through the Strait of Gibraltar instead of the Caucasus mountains:

cities <- setGraph(cities, "newGraph")
cities.paths <- dijkstraBetween(cities)
plot(cities, reset = TRUE)
## Spherical geometry (s2) switched off
## Spherical geometry (s2) switched on
plot(cities.paths)

Example application using the Human Genome Diversity Panel

Here we show an application of the above-described methods to a real dataset, the Human Genome Diversity Panel (HGDP) dataset, which contains genetic diversity information for 51 human populations worldwide (see ?hgdp for more information on this dataset).

hgdp
## 
## === gData object ===
## 
## @coords: spatial coordinates of 51 nodes
##   lon lat
## 1  -3  59
## 2  39  44
## 3  40  61
## ...
## 
## @nodes.id: 51 nodes identifiers
##   28179   11012   22532 
## "26898" "11652" "22532" 
## ...
## 
## @data: 51 data
##   Population Region Label  n Latitude Longitude Genetic.Div
## 1   Orcadian EUROPE     1 15       59        -3      0.7259
## 2     Adygei EUROPE     2 17       44        39      0.7298
## 3    Russian EUROPE     3 25       61        40      0.7320
## ...
## 
## Associated gGraph: worldgraph.40k
plot(hgdp, reset = TRUE)
## Spherical geometry (s2) switched off
## Spherical geometry (s2) switched on

Populations of the dataset are shown by red circles, while the underlying grid (worldgraph.40k) is represented with colors depending on habitat (blue: sea; green: land; pink: coasts). Population genetics predicts that genetic diversity within populations should decay as populations are located further away from the geographic origin of the species. Here, we verify this relationship for a theoretical origin in Addis Ababa, Ethiopia. We shall seek all paths through landmasses to the HGDP populations.

First, we check again that all populations are connected on the grid using isConnected:

## [1] TRUE

Since all locations in hgdp are connected, we can proceed further. We have to set the costs of edges in the gGraph grid. To do so, we can choose between i) strictly uniform costs (using dropCosts) ii) distance-based costs – roughly uniform – (using setDistCosts) or iii) attribute-driven costs (using setCosts).

We shall first illustrate the strictly uniform costs. After setting a gGraph with uniform costs, we use dijkstraFrom to find the shortest paths between Addis Ababa and the populations of hgdp:

myGraph <- dropCosts(worldgraph.40k)
hgdp <- setGraph(hgdp, "myGraph")
addis <- cbind(38, 9)
ori <- closestNode(myGraph, addis)
paths <- dijkstraFrom(hgdp, ori)

The object paths contains the identified paths, which are stored as a list with class gPath (see ?gPath). Paths can be plotted easily:

addis <- as.vector(addis)
plot(myGraph, col = NA, reset = TRUE)
## Spherical geometry (s2) switched off
## Spherical geometry (s2) switched on
plot(paths)
points(addis[1], addis[2], pch = "x", cex = 2)
text(addis[1] + 35, addis[2], "Addis Ababa", cex = .8, font = 2)
points(hgdp, col.nodes = "black")

In this graph, each path is plotted with a different color, but several paths overlap in several places. We can extract the distances from the origin using gPath2dist, and then examine the relationship between genetic diversity within populations (stored in hgdp) and the distance from the origin:

div <- getData(hgdp)$"Genetic.Div"
dgeo.unif <- gPath2dist(paths)
plot(div ~ dgeo.unif, xlab = "GeoGraphic distance (arbitrary units)", ylab = "Genetic diversity")
lm.unif <- lm(div ~ dgeo.unif)
abline(lm.unif, col = "red")
summary(lm.unif)
## 
## Call:
## lm(formula = div ~ dgeo.unif)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.07323 -0.00659  0.00075  0.01027  0.05454 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  7.70e-01   4.64e-03   165.8   <2e-16 ***
## dgeo.unif   -8.39e-04   5.36e-05   -15.7   <2e-16 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.0187 on 49 degrees of freedom
## Multiple R-squared:  0.833,  Adjusted R-squared:  0.83 
## F-statistic:  245 on 1 and 49 DF,  p-value: <2e-16
title("Genetic diversity vs geographic distance \n uniform costs ")

Alternatively, we can use costs based on habitat. As a toy example, we will consider that coasts are four times more favorable for dispersal than the rest of the landmasses. For this we simply use the getCosts function to access the cost-rule data frame and change the cost associated with coastal cells. After defining the new cost rules, we set costs using this data frame with setCosts, and then compute and plot the corresponding shortest paths:

cost.rules <- getCosts(myGraph, res.type = "rules")
cost.rules$cost[cost.rules$habitat == "coast"] <- 0.25

myGraph <- setCosts(myGraph, attr.name = "habitat", cost.rules = cost.rules)
paths.2 <- dijkstraFrom(hgdp, ori)
plot(myGraph, col = NA, reset = TRUE)
## Spherical geometry (s2) switched off
## Spherical geometry (s2) switched on
plot(paths.2)
points(addis[1], addis[2], pch = "x", cex = 2)
text(addis[1] + 35, addis[2], "Addis Ababa", cex = .8, font = 2)
points(hgdp, col.nodes = "black")

The new paths are slightly different from the previous ones. We can examine the new relationship with genetic distance:

dgeo.hab <- gPath2dist(paths.2)
plot(div ~ dgeo.hab, xlab = "GeoGraphic distance (arbitrary units)", ylab = "Genetic diversity")
lm.hab <- lm(div ~ dgeo.hab)
abline(lm.hab, col = "red")
summary(lm.hab)
## 
## Call:
## lm(formula = div ~ dgeo.hab)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -0.11182 -0.00993  0.00141  0.01277  0.06413 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  0.770102   0.007282  105.75  < 2e-16 ***
## dgeo.hab    -0.001420   0.000147   -9.69  5.7e-13 ***
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.0268 on 49 degrees of freedom
## Multiple R-squared:  0.657,  Adjusted R-squared:  0.65 
## F-statistic: 93.9 on 1 and 49 DF,  p-value: 5.67e-13
title("Genetic diversity vs geographic distance \n habitat costs ")

Now of course depending on the application, we may want to use different grid resolutions, and/or more complex habitat information to define costs of traveling through different habitats. This information could for example be incorporated from GIS shapefiles or raster data. Further we maybe also want to edit and customize the gGraph in more detail. This is illustrated in the vignettes ‘Making custom grids’ and ‘Edit graphs’ (see vignette() for more information on available vignettes).