#!/usr/bin/env python # coding: utf-8 # ## How to plot a map # # In this last notebook, we'll finally plot our colour-coded map of Singapore, with each road colour representing a different linguistic origin. But first, we need to merge a few files so that our original GeoJSON file that has all the linestring information is augmented with linguistic origin information. # ### Merging files with (Geo)Pandas # In[27]: get_ipython().run_line_magic('matplotlib', 'inline') import geopandas as gpd # In[28]: # These are our three files # File 1: The filtered list of roads roads_df = gpd.read_file("singapore-roads-filtered.geojson") roads_df # In[29]: # File 2: The list of corrected road names. This is a "bridge" between the name given # in the GeoJSON file and the actual name used in the classification step. bridge_df = gpd.pd.read_csv("singapore-roadnames-final-split.csv") bridge_df.drop(["Unnamed: 0", "has_malay_road_tag"], inplace=True, axis=1) bridge_df # In[30]: # And lastly, this file gives us the classification info classification_df = gpd.pd.read_csv('singapore-roadnames-final-classified.csv') classification_df.drop(["has_malay_road_tag", "Unnamed: 0", "comment"], inplace=True, axis=1) classification_df # In[31]: # First we merge the last two files. merged_df = bridge_df.merge(classification_df, how="left", on="road_name") merged_df # In[32]: # Now we do the second road of merging: final_df = roads_df.merge(merged_df, how="left", on="name") final_df # In[33]: # There are a few NaNs in here as a result of the order in which I did filtering in # the first notebook, so let's just drop them again final_df2 = final_df[final_df['classification'].notnull()] type(final_df2) # In[34]: # we lost the Geo-ness, convert back to GeoDataFrame final_df2 = gpd.GeoDataFrame(final_df2) # In[35]: # Let's save it final_df2.to_file("singapore-roads-classified.geojson", driver="GeoJSON") # ### Plotting the map # # In this notebook, I'm going to show you the easiest way I know of to plot a map like this, using GeoPandas' plotting functions and another library called [`mplleaflet`](https://github.com/jwass/mplleaflet) by Jake Wasserman. It's not on `pip` yet so you'll have to install it from git: # # `pip install git+git://github.com/jwass/mplleaflet.git` # # It also requires the library `mplexporter`: # # `pip install git+git://github.com/mpld3/mplexporter.git` # # Because there is limited control of styling with GeoPandas/mplleaflet, afterwards I'll list a few ways I know about of making prettier maps. # In[4]: import matplotlib.pyplot as plt import mplleaflet # In[5]: final_df2 = gpd.read_file("singapore-roads-classified.geojson") # In[6]: # GeoPandas' plot function can take a column and colour-code by this column ax = final_df2.plot(column='classification', colormap='Accent') # At this point, we have a matplotlib map. We could export it to PDF, but it's not a terribly good way to present our data. Let me show you a nifty way to open this very map in an interactive interface - moreover, on top of a basemap that shows extra information like roadnames. # In[7]: mplleaflet.display(fig=ax.figure, crs=final_df2.crs, tiles='cartodb_positron') # You can pan, zoom in, etc - right within this notebook, which is super cool. And it took only a couple of commands. If you want to use this map elsewhere, you can export it with the following command: just replace `display` with `show`, and supply a filepath. # In[8]: mplleaflet.show(fig=ax.figure, crs=final_df2.crs, tiles='cartodb_positron', path='sgmap.html') # Now, there's some problems with this map. We weren't controlling what colours were assigned to each road, for example. We can't control the thickness of the linestrings at various zoom levels (a Leaflet issue). Also, there's no legend. While I don't know how to solve the second two problems, there is a way to solve the first, which is to define a custom colormap in the order in which GeoPandas draws each batch of linestrings. # In[9]: # the order is this: a sorted list of values categories = list(set(final_df2['classification'].values)) categories.sort() categories # In[10]: # define the colormap with logical colours (for a Singaporean at least): # blue for British # red for Chinese # etc from matplotlib.colors import LinearSegmentedColormap cmap = LinearSegmentedColormap.from_list('my cmap', ['blue', 'red', 'gray', 'yellow', 'green', 'purple']) # In[11]: # pass the custom colormap ax2 = final_df2.plot(column='classification', colormap=cmap) # In[12]: mplleaflet.display(fig=ax2.figure, crs=final_df2.crs, tiles='cartodb_positron') # In[14]: mplleaflet.show(fig=ax2.figure, crs=final_df2.crs, tiles='cartodb_positron', path='sgmap2.html') # ## Alternatives to mplleaflet # # `mplleaflet` is awesome for exploratory data analysis, but you might want to have more control over how your map looks. For this, I recommend using one of the following: # # * QGIS (C++ but has Python bindings) # * Mapnik (C++ but has Python bindings) # * Tilemill (GUI built on top of Mapnik) # * Folium (maybe, haven't investigated fully) # # A nice feature of Tilemill is that it allows you to define your map styling using CartoCSS. For example, here's how we would define the colours: # In[ ]: [classification='Malay']{ line-color: green; } [classification='British']{ line-color: blue; } [classification='Chinese']{ line-color: red; } [classification='Indian']{ line-color: yellow; } [classification='Other']{ line-color: purple; } [classification='Generic']{ line-color: gray; } # You can also control the line width at various zoom levels: line-opacity: 0.7; [zoom>18] {line-width: 10;} [zoom=18] {line-width: 7;} [zoom=17] {line-width: 6;} [zoom=16] {line-width: 5;} [zoom=15] {line-width: 3.5;} [zoom=14] {line-width: 3;} [zoom=13] {line-width: 1.5;} [zoom<13] {line-width: 1;} # Some web map sites like CartoDB also use CartoCSS. Here's a map I made in CartoDB, nicely styled with a legend. # In[141]: from IPython.display import HTML HTML("") # That's all, folks!