interactive maps with folium#
getting started#
In this practical, you’ll gain some more experience working with vector data in python using geopandas. You’ll see another way to convert text data into geospatial data given coordinates, and you’ll see how to join different datasets based on shared attributes.
You will also see how you can create interactive maps using folium
and geopandas
, such as the example shown
below:
To begin, launch Jupyter Notebooks as you have in the previous exercises, and begin to work through
the notebook (Week3/Folium.ipynb
).
Note
Below this point is the non-interactive text of the notebook. To actually run the notebook, you’ll need to follow the instructions above to open the notebook and run it on your own computer!
Jeremy Leven#
overview#
In last week’s practical, we saw how we can use cartopy
to create
static maps. In the first exercise from this week, we saw how we can use
geopandas
together with shapely
to work with vector geometries.
In this exercise, we’ll see how we can use geopandas
, together with
folium
, to easily create interactive maps.
objectives#
Use
geopandas
to convert csv data with geographic information into vector dataUse
geopandas.GeoDataFrame.explore()
to create an interactive map from vector dataUse
pandas.DataFrame.merge()
to join attribute tables based on shared field values
data provided#
In the data_files folder, you should have the following files:
Airports.csv, a csv file with information about Airport locations in Northern Ireland
NI_Wards.shp, a shapefile of electoral wards for Northern Ireland
transport_data.csv, a csv file with information about public transport for the electoral wards
Note: I will not go over the steps in detail, but if you would like to
see how I created the transport_data.csv
file, the script can be
found at data_files/aggregate_data.py. To run that script, you will
need to download the following files into the data_files folder:
09-05-2022busstop-list.geojson
, a Geographic JavaScript Object Notation (GeoJSON) file containing locations for all bus stops in Northern Ireland (valid as of May 2022), from OpenDataNItranslink-stations-ni.geojson
, a GeoJSON file containing the locations of all Bus and Rail stations in Northern Ireland, from OpenDataNI
getting started#
To get started, run the following cell to import the packages we’ll use in the exercise.
import pandas as pd
import geopandas as gpd
import folium
Next, we’ll load the Wards dataset using geopandas
, just like what
we saw in the previous exercise:
wards = gpd.read_file('data_files/NI_Wards.shp')
Remember that we can use .head()
(documentation)
to see a preview of the data:
wards.head()
To create an interactive map from the GeoDataFrame, we use
.explore()
(documentation),
which creates a folium.Map
(documentation)
object.
We’ll use the Population
column to visualize each polygon, and we’ll
set the color using the viridis colormap from matplotlib
(more
about
colormaps):
m = wards.explore('Population', cmap='viridis')
As you can see, this adds a color scale/legend to the upper right-hand corner of the map, which tells us what the colors of each polygon correspond to. You can zoom in/out to see detail, including on the OpenStreetMap base layer.
And, when you hover over each polygon, you can see additional information about it, taken directly from the attribute table. We could stop here, but in the next sections, we’ll see how we can build on this by adding additional data, customizing markers and legend information, and even saving the map to an html file that we can share with others.
converting csv data to vector data, revisited#
We’ve already seen an example of this before in week 1:
df = pd.read_csv('data_files/GPSPoints.txt')
df['geometry'] = list(zip(df['lon'], df['lat'])) # zip is an iterator, so we use list to create
# something that pandas can use.
df['geometry'] = df['geometry'].apply(Point) # using the 'apply' method of the dataframe,
# turn the coordinates column
# into points (instead of a tuple of lat, lon coordinates).
# NB: Point takes (x, y) coordinates
gdf = gpd.GeoDataFrame(df)
gdf.set_crs("EPSG:4326", inplace=True) # this sets the coordinate reference system to epsg:4326, wgs84 lat/lon
Here, we’ll see how we can use a different method to acheive the same goal.
df = pd.read_csv('data_files/Airports.csv') # read the csv data
# create a new geodataframe
airports = gpd.GeoDataFrame(df[['name', 'website']], # use the csv data, but only the name/website columns
geometry=gpd.points_from_xy(df['lon'], df['lat']), # set the geometry using points_from_xy
crs='epsg:4326') # set the CRS using a text representation of the EPSG code for WGS84 lat/lon
airports.head() # show the new geodataframe
Here, we’ve used the geometry
and crs
arguments of
geopandas.GeoDataFrame.__init__()
(documentation)
to do the same thing in a single step.
For the geometry
argument, we used gpd.points_from_xy()
(documentation)
to create the geomtry based on the latitude and longitude information
stored in the csv file.
For the crs
argument, we used the same EPSG code for WGS84
latitude/longitude as before. Now that we have the dataset loaded, we’ll
see how we can add it to an existing folium.Map
object.
adding data to an existing map#
In the
documentation
for .explore()
, you might notice the following argument:
m: mfolium.Map (default None)
Existing map instance on which to draw the plot.
Earlier, we used the default option of None
, which created a new map
object. Since we already have a map object in place, we can pass this as
an argument to add additional data to the map.
We also have a few additional arguments here - we’ll say more about what those do after the jump.
# add the airport points to the existing map
airports.explore('name',
m=m, # add the markers to the same map we just created
marker_type='marker', # use a marker for the points, instead of a circle
popup=True, # show the information as a popup when we click on the marker
legend=False, # don't show a separate legend for the point layer
)
As you can see, the default color for the Marker style is blue with a white circle in the middle. Later on, we will see how we can customize the marker to use other colors and icons.
joining tables based on attribute data#
In the previous exercise, we saw how we can use a spatial join to combine vector data based on their spatial relationship. Sometimes, though, we will need to combine data that have spatial information with data that don’t have spatial information - in that case, we’ll need to join the tables based on some shared attribute.
To see how this works, we can first load the information about public
transportation for each electoral ward using pandas
:
transport = pd.read_csv('data_files/transport_data.csv')
transport.head()
Note that this dataset doesn’t have any geospatial information, not even
latitude/longitude coordinates. But, it does have a Ward Code attribute,
which matches the Ward Code attributes from the wards
shapefile.
Because these attributes are shared between the two tables, we can use
geopandas.GeoDataFrame.merge()
(documentation)
to perform a join operation.
merged = wards.merge(transport, left_on='Ward Code', right_on='Ward Code')
merged.head()
To join the two tables, we use the left_on
and right_on
arguments of merge()
, which tells merge()
which columns to use
from the left table, wards
(what
ArcGIS
calls the “input table”), and the right table, transport
(what
ArcGIS calls the “join table”).
customizing legends and markers#
Once we have this information, we can create a different map that shows
the distance to the nearest bus/rail station (in km) for each electoral
ward in Northern Ireland, again using
geopandas.GeoDataFrame.explore()
.
This time, though, we’ll make sure to change the legend caption.
“Population” is easy enough to understand, but “Distance” probably needs
a bit more information - distance to what? In what units? We can use the
legend_kwds
argument to set our own caption:
legend_kwds={'caption': 'Distance to nearest bus/rail station in km'} # set the caption to a longer explanation
Note that the form of the legend_kwds
argument is a dict (curly
braces, {
and }
), with a single key/value pair. There are other
arguments that we can pass to the legend, but we’ll only set the
caption
for now.
We can also customize the markers for our airport locations - the default is not necessarily informative, as it’s not clear what each marker is until we hover over it/click on it.
Here, we pass a dict to the marker_kwds
argument:
'marker_kwds': {'icon': folium.Icon(color='red', icon='plane', prefix='fa')} # make the markers red with a plane icon
The only key/value pair in this dict is the icon
, which tells
folium
how to style the marker. In this case, we want it to be a
folium.Map.Icon
(documentation),
with the following arguments:
color='red'
icon='plane'
prefix='fa'
folium
has support for a number of different icon styles, including
FontAwesome and
Bootstrap
glyphicons. I’ve creatively chosen the plane
icon from FontAwesome
(prefix='fa'
) for the airports, and made them red
to stand out
from the background a bit, but feel free to make your own adjustments to
this style.
Run the cell below to create the new map:
m = merged.explore('Distance', # show the Distance column
cmap='plasma', # use the 'plasma' colormap from matplotlib
legend_kwds={'caption': 'Distance to nearest bus/rail station in km'} # set the caption to a longer explanation
)
airport_args = {
'm': m, # add the markers to the same map we just created
'marker_type': 'marker', # use a marker for the points, instead of a circle
'popup': True, # show the information as a popup when we click on the marker
'legend': False, # don't show a separate legend for the point layer
'marker_kwds': {'icon': folium.Icon(color='red', icon='plane', prefix='fa')} # make the markers red with a plane icon from FA
}
# use the airport_args with the ** unpacking operator - more on this next week!
airports.explore('name', **airport_args)
m # show the map
The last thing we might want to do is save the map to an html file, so that we can share it online:
m.save('NI_Transport.html')
additional exercises and next steps#
That wraps up the introduction to creating interactive maps using
geopandas
and folium
. If you’re looking for additional practice,
here are some suggestions to get you started:
In the
transport
dataset, there is a column calledNumBus
, which corresponds to the number of bus stops in each electoral ward. Use this, and some of the topics covered previously, to create a map that shows the number of bus stops per capita for each electoral ward, rather than the distance to the nearest bus/rail station.Download the Translink bus/train station location data from OpenNI, and add these data to the map using a custom marker that shows whether the station is a rail station (
R
), a bus station (B
), or a mixed-use (I
) station.