If you work with geospatial data in Python, you most likely are familiar with the fantastic GeoPandas library. GeoPandas leverages the power of Maplotlib to enable users to make maps of their data. However, until recently, it has not been easy to add basemaps to these maps. Basemaps are the contextual map data, like Google Maps, on top of which geospatial data are often displayed.
The new Python library contextily, which stands for contextual map tiles, now makes it possible and relatively straightforward to add map tile basemaps to Geopandas maps or to save map tiles as geospatial raster files. Below we walk through a few common workflows for doing this.
We start by importing the libraries we will use.
%matplotlib inline
import pandas as pd
import geopandas as gpd
import contextily as cx
import matplotlib.pyplot as plt
This assumes we have already installed in our Python environment the following libraries
- pandas
- geopandas (and all dependancies)
- matplotlib
- contextily
- descartes
Installing geopandas can be tricky - so please read the installation instructions on the GeoPandas website!
READ DATA INTO A GEOPANDAS GEODATAFRAME
Let's fetch the census places data to map. Census places include cities and other populated places. Here we fetch the 2019 cartographic boundary (cb_) file of California (06) places. We read the data in from this URL to a GeoPandas GeoDataFrame named places.
ca_places_url = "https://www2.census.gov/geo/tiger/GENZ2019/shp/cb_2019_06_place_500k.zip"
places = gpd.read_file(ca_places_url)
Now, we can use the geodataframe plot method to make a quick map.
places.plot();
We can see the outlines of CA cities in the plotted map. Now, let's take a look at the data. It looks just like a pandas dataframe, and it is. So all pandas dataframe methods can be used with this GeoDataFrame. What is special is that GeoPandas adds methods and attributes specific to geospatial vector data. These apply to the geometry column.
places.head()
STATEFP |
PLACEFP |
PLACENS |
AFFGEOID |
GEOID |
NAME |
LSAD |
ALAND |
AWATER |
geometry |
|
0 |
06 |
36490 |
02410102 |
1600000US0636490 |
0636490 |
Industry |
25 |
30529397 |
723181 |
POLYGON ((-118.05750 34.01640, -118.05603 34.0... |
1 |
06 |
40130 |
02411620 |
1600000US0640130 |
0640130 |
Lancaster |
25 |
244187339 |
681671 |
POLYGON ((-118.32517 34.75176, -118.32073 34.7... |
2 |
06 |
75000 |
02411987 |
1600000US0675000 |
0675000 |
Stockton |
25 |
161025631 |
7985703 |
POLYGON ((-121.41881 38.04418, -121.41801 38.0... |
3 |
06 |
43000 |
02410866 |
1600000US0643000 |
0643000 |
Long Beach |
25 |
131302222 |
75937543 |
MULTIPOLYGON (((-118.12890 33.75801, -118.1273... |
4 |
06 |
78106 |
02412042 |
1600000US0678106 |
0678106 |
Tehama |
25 |
2057210 |
0 |
POLYGON ((-122.13364 40.02417, -122.13295 40.0... |
Like a pandas datafrmae, we can subset the geodataframe by selecting a row or rows by place name. Let's select the city of Berkeley, CA and map it.
berkeley = places[places['NAME']=='Berkeley']
berkeley.plot();
USE CONTEXTILY TO ADD A BASEMAP
Above we can see the map of the boundary of the city of Berkeley, CA. The axis labels display the longitude and latitude coordinates for the bounding extent of the city.
Let's use contextily in it's most simple form to add a basemap to provide the geographic context for Berkeley.
ax = berkeley.to_crs('EPSG:3857').plot(figsize=(9, 9))
cx.add_basemap(ax)
There are a few important things to note about the above code.
- We use matplotlib to define the plot canvas as ax.
- We then add the contextily basemap to the map with the code cx.add_basemap(ax)
Additionally, we dynamically transform the coordinate reference system, or CRS, of the Berkeley geodataframe from geographic lat/lon coordinates to web mercator using the method to_crs('EPSG:3857')Web mercator is the default CRS used by all web map tilesets. It is referenced by the code EPSG:3857 where EPSG stands for the initials of the organization that created these codes (the European Petroleum Survey Group).
Let's clean up the map by adding some code to change the symbology of the Berkeley city boundary. This will highlight the value of adding a basemap.
First, let's map the boundary without a fill color.
berkeley.plot(edgecolor="red", facecolor="none");
Let's build on those symbology options and add the contextily basemap.
ax = berkeley.to_crs('EPSG:3857').plot(edgecolor="red",
facecolor="none",
linewidth=2,
figsize=(9, 9)
)
cx.add_basemap(ax)
MAPPING POINT DATA
Let's expand on this example by mapping a point dataset of BART station locations.
First we fetch these data from a D-Lab web mapping tutorial.
bart_url = 'https://raw.githubusercontent.com/dlab-berkeley/Geospatial-Fundamentals-in-R-with-sf/master/data/bart.csv'
bart = pd.read_csv(bart_url)
Take a look at the data:
bart.head()
X |
Y |
STATION |
OPERATOR |
DIST |
CO |
|
0 |
-122.283348 |
37.874061 |
NORTH BERKELEY |
BART |
4 |
ALA |
1 |
-122.268250 |
37.869689 |
DOWNTOWN BERKELEY |
BART |
4 |
ALA |
2 |
-122.270119 |
37.853207 |
ASHBY |
BART |
4 |
ALA |
3 |
-122.251777 |
37.844510 |
ROCKRIDGE |
BART |
4 |
ALA |
4 |
-122.267120 |
37.828705 |
MACARTHUR |
BART |
4 |
ALA |
CONVERTING POINT DATA IN A DATAFRAME TO GEOSPATIAL DATA IN A GEODATAFRAME
Because these data are in a CSV file we read them into a Pandas DataFrame.
In order to map these data we need to convert these data to a GeoPandas GeoDataFame. To do this, we need to specify:
- the data, here the geodataframe bart
- the coordinate data, here the columns bart['X'] and bart['Y']
- the CRS of the bart coordinate data, here EPSG:4326
The CRS code 'EPSG:4326' stands for the World Geodetic System of 1984, or WGS84. This is the most commonly used CRS for geographic (lat/lon) coordinate data.
#Convert the DataFrame to a GeoDataFrame.
bart_gdf = gpd.GeoDataFrame(bart, geometry=gpd.points_from_xy(bart['X'], bart['Y']), crs='EPSG:4326')
# and take a look
bart_gdf.plot();
Now that we have the BART data in a geodataframe we can use the same commands as we did above to map it with a contextily basemap.
ax = bart_gdf.to_crs('EPSG:3857').plot(figsize=(9, 9))
cx.add_basemap(ax)
We have the full range of matplotlib style options to enhance the map, a few of which are shown in the example below.
ax = bart_gdf.to_crs('EPSG:3857').plot(color="red",
edgecolor="black",
markersize=50,
figsize=(9, 9))
ax.set_title('Bay Area Bart Stations')
cx.add_basemap(ax)
CHANGING THE BASEMAP
By default contextiley returns maptiles from the OpenStreetmap Mapnik basemap. However, there are other available tilesets from different providers. These tilesets are stored in the contextily cx.providers dictionary.
That's a large dictionary and you can view it. Alternatively, and more simply, you can access the list of the providers in this dictionary using the command cs.providers.keys.
# change basemap - can be one of these
# first see available provider names
cx.providers.keys()
dict_keys(['OpenStreetMap', 'OpenSeaMap', 'OpenPtMap', 'OpenTopoMap', 'OpenRailwayMap', 'OpenFireMap', 'SafeCast', 'Thunderforest', 'OpenMapSurfer', 'Hydda', 'MapBox', 'Stamen', 'Esri', 'OpenWeatherMap', 'HERE', 'FreeMapSK', 'MtbMap', 'CartoDB', 'HikeBike', 'BasemapAT', 'nlmaps', 'NASAGIBS', 'NLS', 'JusticeMap', 'Wikimedia', 'GeoportailFrance', 'OneMapSG'])
Once you have the list of providers, you can find the names of their specific tilesets.
Below, we retrieve the list of the tilesets available from the provider CartoDB.
# Get names of tile sets for a specific provider
cx.providers.CartoDB.keys()
dict_keys(['Positron', 'PositronNoLabels', 'PositronOnlyLabels', 'DarkMatter', 'DarkMatterNoLabels', 'DarkMatterOnlyLabels', 'Voyager', 'VoyagerNoLabels', 'VoyagerOnlyLabels', 'VoyagerLabelsUnder'])
Now we can specify a different tileset using the source argument to the add_basemap method.
# Change the basemap provider and tileset
ax = bart_gdf.to_crs('EPSG:3857').plot(figsize=(9, 9))
cx.add_basemap(ax, source=cx.providers.CartoDB.Positron)
LEARNING MORE
Above, we prove a very short introduction to the excellent contextily library. You can find more detailed information on the contextily homepage, available at: https://github.com/geopandas/contextily. We especially encourage you to check out the notebook examples provided in the contextily github repo. Also, a big shoutout to D-Lab friend Chris Holdgraf in the Division of Computing, Data Science and Society who has been a contributer to this Python library.