geopandas
¶%matplotlib inline
import matplotlib.pyplot as plt
import geopandas as gpd
import pysal as ps
from pysal.contrib.viz import mapping as maps
In this lab, we will learn how to load, manipulate and visualize spatial data. In some senses, spatial data have become so pervasive that nowadays, they are usually included simply as "one more column" in a table. However, spatial is special and there are few aspects in which geographic data differ from standard numerical tables. In this session, we will extend the skills developed in the previous one about non-spatial data, and combine them. In the process, we will discover that, although with some particularities, dealing with spatial data in Python largely resembles dealing with non-spatial data. For example, in this lab you will learn to make slick maps like this one with just a few commands:
Or interactive ones like this one where the smallest areas of Liverpool are highlighted in red:
# Please note this will not display on your machine as it depends on an external file
# A rendered version can be found on the HTML or PDF versions of this notebook
from IPython.display import IFrame
IFrame("figs/lab03_liverpool_smallest.html", width=560, height=315)
To learn these concepts, we will be playing again with the geography of Liverpool. In particular we will use Census geographies (Available as part of the Census Data pack used before, see link) and Ordnance Survey geospatial data, available to download also from the CDRC data store (link). To make the rest of the notebook easier to follow, let us set the paths to the main two folders here. We will call the path to the Liverpool Census pack lcp_dir
, and that to the OS geodata los_dir
:
lcp_dir = '../../../../data/Liverpool/'
los_dir = '../../../../data/E08000012_OS/'
The easiest way to get from a file to a quick visualization of the data is by loading it as a GeoDataFrame
and calling the plot
command. The main library employed for all of this is geopandas
which is a geospatial extension of the pandas
library, already introduced before. geopandas
supports exactly the same functionality that pandas
does (in fact since it is built on top of it, so most of the underlying machinery is pure pandas
), plus a wide range of spatial counterparts that make manipulation and general "munging" of spatial data as easy as non-spatial tables.
In two lines of code, we will obtain a graphical representation of the spatial data contained in a file that can be in many formats; actually, since it uses the same drivers under the hood, you can load pretty much the same kind of vector files that QGIS permits. Let us start by plotting single layers in a crude but quick form, and we will build style and sophistication into our plots later on.
Let us begin with the most common type of spatial data in the social science: polygons. For example, we can load the geography of LSOAs in Liverpool with the following lines of code:
lsoas_link = lcp_dir + 'shapefiles/Liverpool_lsoa11.shp'
lsoas = gpd.read_file(lsoas_link)
Now lsoas
is a GeoDataFrame
. Very similar to a traditional, non-spatial DataFrame
, but with an additional column called geometry
:
lsoas.head()
This allows us to quickly produce a plot by executing the following line:
lsoas.plot()
This might not be the most aesthetically pleasant visual representation of the LSOAs geography, but it is hard to argue it is not quick to produce. We will work on styling and customizing spatial plots later on.
Pro-tip: if you call a single row of the geometry
column, it'll return a small plot ith the shape:
lsoas.loc[0, 'geometry']
Displaying lines is as straight-forward as polygons. To load railway tunnels in Liverpool and name the rows after the id
column (or to "index" them):
rwy_tun = gpd.read_file(los_dir + 'RailwayTunnel.shp')
rwy_tun = rwy_tun.set_index('id')
rwy_tun.info()
Note how, similarly to the polygon case, if we pick the "geometry"
column of a table with lines, a single row will display the geometry as well:
rwy_tun.loc['0ACD196C313D4F8DE050A00A568A6F6F', 'geometry']
Note how we have also indexed the table on the id
column.
A quick plot is similarly generated by (mind that because there are over 18,000 segments, this may take a little bit):
rwy_tun.plot()
Again, this is not the prettiest way to display the roads maybe, and you might want to change a few parameters such as colors, etc. All of this is possible, as we will see below, but this gives us an easy check of what lines look like.
[Optional exercise]
Obtain the graphical representation of the line with id
= 0ACD196C32214F8DE050A00A568A6F6F
.
Finally, points follow a similar structure. If we want to represent named places in Liverpool:
namp = gpd.read_file(los_dir + 'NamedPlace.shp')
namp.head()
And the plot is produced by running:
namp.plot()
It is possible to tweak several aspects of a plot to customize if to particular needs. In this section, we will explore some of the basic elements that will allow us to obtain more compelling maps.
NOTE: some of these variations are very straightforward while others are more intricate and require tinkering with the internal parts of a plot. They are not necessarily organized by increasing level of complexity.
The intensity of color of a polygon can be easily changed through the alpha
attribute in plot. This is specified as a value betwee zero and one, where the former is entirely transparent while the latter is the fully opaque (maximum intensity):
lsoas.plot(alpha=0.1)
lsoas.plot(alpha=1)
Although in some cases, the axes can be useful to obtain context, most of the times maps look and feel better without them. Removing the axes involves wrapping the plot into a figure, which takes a few more lines of aparently useless code but that, in time, it will allow you to tweak the map further and to create much more flexible designs:
f, ax = plt.subplots(1)
ax = lsoas.plot(axes=ax)
ax.set_axis_off()
plt.show()
Let us stop for a second a study each of the previous lines:
f
with one axis named ax
by using the command plt.subplots
(part of the library matplotlib
, which we have imported at the top of the notebook). Note how the method is returning two elements and we can assign each of them to objects with different name (f
and ax
) by simply listing them at the front of the line, separated by commas.ax
. This method returns the axis with the geographies in them, so we make sure to store it on an object with the same name, ax
.plt.show()
.Adding a title is a simple extra line, if we are creating the plot within a figure, as we just did. To include text on top of the figure:
f, ax = plt.subplots(1)
ax = lsoas.plot(axes=ax)
f.suptitle('Liverpool LSOAs')
plt.show()
The size of the plot is changed equally easily in this context. The only difference is that it is specified when we create the figure with the argument figsize
. The first number represents the width, the X axis, and the second corresponds with the height, the Y axis.
f, ax = plt.subplots(1, figsize=(12, 12))
ax = lsoas.plot(axes=ax)
plt.show()
You will notice that the ability to change the size of the figure is very powerful as it makes possible to obtain many different sizes and shapes for plots. However, this also may introduce some distortions in the way the shapes are represented. For example, a very wide figure can make the viewer think that polygons are in reality more "stretched out" than they are in reality:
f, ax = plt.subplots(1, figsize=(12, 4))
ax = lsoas.plot(axes=ax)
plt.show()