Interactive Maps using Bokeh

Taking off from Pia's previous presentation on mapping using matplotlib last hack session, I tried to look for alternative ways to make maps in Python which are easier and more interactive.

After doing some research, it seems that good candidate for this is Bokeh, as can be seen in the Bokeh tutorials for interactive chloropleth maps.

Why use Bokeh?

After watching some PyCon 2015 videos, I can see that Bokeh is a promising data visualization tool for Python, since it's:

  • Easy to Use. Create charts and maps with few lines of code;
  • Flexible. Has 3 levels for customizing charts: bokeh.models (lowest level, for specific customization), bokeh.plotting, and bokeh.charts (highest level, for quick interactive charts;
  • Interactive. Has built-in interactive controls (e.g., pan, zoom, hover)
  • Compatible. Easily integrated into iPython notebooks, and can output html, javascript code for ease in integration with website / dashboards.

Learning from the Bokeh mapping tutorial

Looking into the Bokeh tutorial code, the data used for the chloropleth map is a built-in dataset for the Bokeh package, and has already been cleaned for use in mapping visualizations, as seen below:


In [ ]:
import bokeh
bokeh.sampledata.download()


Creating /Users/regonglao/.bokeh directory
Creating /Users/regonglao/.bokeh/data directory
Using data directory: /Users/regonglao/.bokeh/data
Downloading: CGM.csv (1589982 bytes)
   1589982 [100.00%]
Downloading: US_Counties.zip (3182088 bytes)
   3182088 [100.00%]
Unpacking: US_Counties.csv
Downloading: us_cities.json (713565 bytes)
    713565 [100.00%]
Downloading: unemployment09.csv (253301 bytes)
     32768 [ 12.94%]

In [ ]:
from bokeh.sampledata import us_counties
us_counties

The us_counties object has a .data attribute which outputs a dictionary. If we tidy the dict into a DataFrame, it can be easily seen that the the longitude and latitude coordinates are properly listed per county:


In [ ]:
import pandas as pd
counties = pd.DataFrame.from_dict(us_counties.data,  orient='index')
counties.head()

Reality Check: But what about Shapefiles?

In reality, though, geographical data isn't this clean. So how do we input shapefiles, extract and process its content, and convert into a datatype that could be used for Bokeh plotting? Let's use the Python Shapefile Library (pyshp) and some code for this!


In [ ]:
# getParts function: Return points for each shape object
# input: Shapefile
def getParts (shapeObj):
    points = []
    num_parts = len(shapeObj.parts)
    end = len(shapeObj.points) - 1
    segments = list(shapeObj.parts) + [end]

    for i in range(num_parts):
        points.append(shapeObj.points[segments[i]:segments[i+1]])

    return points

In [ ]:
# getDict function: return a dict with the location's name, list of latitudes, and list of longitudes.
# input: muni name, shapefile, column number for muni IDs
def getDict (muni_name, shapefile, num=2):

    muniDict = {muni_name: {} }
    rec = []
    shp = []
    points = []

    for i in shapefile.shapeRecords( ):
        if i.record[num] == muni_name:
            rec.append(i.record)
            shp.append(i.shape)

    for j in shp:
        for i in getParts(j):
            points.append(i)

    lat = []
    lng = []
    for i in points:
        lat.append( [j[0] for j in i] )
        lng.append( [j[1] for j in i] )

    muniDict[muni_name]['lat_list'] = lat
    muniDict[muni_name]['lng_list'] = lng

    return muniDict

Simple Philippine Interactive Map using Bokeh

Let's try creating a municipal-level map of the Philippines using Bokeh! (Data source: DSWD DROMIC shapefiles c/o David Garcia.)


In [ ]:
import numpy as np
import shapefile #from pyshp
from bokeh.plotting import figure, output_file, show

dat = shapefile.Reader("data\DSWD_PH_MC_Pop.shp") #read muni-level shapefile
munis = set([i[2] for i in dat.iterRecords()]) #get unique list of IDs

output_file("sample_map.html") #Bokeh output is an HTML file!

TOOLS="pan,wheel_zoom,box_zoom,reset,previewsave" #Bokeh built-in tools for interactive graphs
p = figure(title="Municipal-level Map", tools=TOOLS, plot_width=900, plot_height=800) #create Bokeh figure

for muni_name in munis: #plot data from shapefile
    data = getDict(muni_name, dat)
    p.patches(data[muni_name]['lat_list'], data[muni_name]['lng_list'], line_color="black")

show(p) #output Bokeh figure as sample_map.html

See how easy it was to make an interactive map in an HTML file using a few lines of code?

Now let's try to put the Bokeh interactive map inside the iPython notebook. Try playing with the map below using the interactive tools at the upper right corner of the map!


In [ ]:
from bokeh.io import output_notebook
output_notebook()
show(p)

Awesome! Now let's try to add some more data and functionality to the municipal-level map by exploring the dataset.


In [ ]:
## Data Cleaning and Processing
# extract names of municipalities from shapefile
muni_name = []
for i in dat.iterRecords():
    if type(i[5])==bytes:
        muni_name.append(i[5].decode("utf-8", "ignore"))
    else:
        muni_name.append(i[5])

muni_lati = []
muni_long = []

## extract lists of latitude and longitude values for each municipality from shapefile
muni_list = [i[4] for i in dat.iterRecords()] #get unique list of IDs
    
for muni_id in muni_list:
    data = getDict(muni_id, dat, num=4)
    muni_lati.append(data[muni_id]['lat_list'])
    muni_long.append(data[muni_id]['lng_list'])
    
muni_lon = [i[0] for i in muni_long]
muni_lat = [i[0] for i in muni_lati]

col_names = ["REG_PSGC", "REG_NAME", "PROV_PSGC", "PROV_NAME", "MC_PSGC", "MC_NAME", "BRGYS", "POP_2010", "POP_GR", "PPOP_2011",\
             "PPOP_2012", "PPOP_2013", "PPOP_2014", "POOR_2012", "NPOOR_2012"] #culled out from the XML shapefile metadata

#extract municipal-level data from shapefile
muni_data = [i for i in dat.iterRecords()]
muni_dat = pd.DataFrame(muni_data, columns=col_names)

In [156]:
#make two new columns:
# (1) muni_estpop: estimated 2012 population (by adding POOR_2012 and NPOOR_2012)
# (2) muni_pov: estimated 2012 poverty rate in percent (by dividing POOR_2012 with muni_estpop times 100)
muni_dat['muni_estpop'] = muni_dat["POOR_2012"]+muni_dat["NPOOR_2012"]
muni_dat['muni_pov'] = muni_dat["POOR_2012"]*100/muni_dat['muni_estpop']
muni_dat.describe()


Out[156]:
BRGYS POP_2010 POP_GR PPOP_2011 PPOP_2012 PPOP_2013 PPOP_2014 POOR_2012 NPOOR_2012 muni_pov muni_estpop
count 1634.000000 1634.000000 1634.000000 1634.000000 1634.000000 1634.000000 1634.000000 1634.000000 1634.000000 1630.000000 1634.000000
mean 25.720930 56502.757650 1.880673 57755.927785 59042.744186 60364.315177 61721.735618 18896.545900 13040.771726 59.365143 31937.317625
std 28.119885 118823.837796 0.719570 121839.293221 124966.630146 128210.214443 131574.708100 21830.279906 19964.411641 14.111606 39862.657189
min 1.000000 222.000000 -0.220000 228.000000 235.000000 242.000000 249.000000 0.000000 0.000000 6.564246 0.000000
25% 14.000000 19461.000000 1.460000 19897.500000 20259.250000 20591.500000 20963.750000 7900.500000 5398.250000 50.224397 14486.500000
50% 21.000000 31975.500000 1.680000 32675.000000 33206.500000 33822.500000 34452.500000 13852.000000 8747.000000 59.498341 23325.000000
75% 30.000000 55644.000000 2.180000 56762.250000 57970.500000 58999.500000 60016.000000 23406.750000 14583.750000 68.985985 37348.750000
max 897.000000 2761720.000000 5.050000 2832144.000000 2904364.000000 2978425.000000 3054375.000000 396134.000000 333964.000000 97.609525 694302.000000

In [158]:
from bokeh.palettes import Reds9
#assign colors for each municipality using Bokeh's Reds9 color palette
muni_pov = [i for i in muni_dat['muni_pov']]
muni_color = []
for pov in muni_pov:
    if np.isnan(pov)==True:
        muni_color.append(Reds9[0])
    else:
        muni_color.append(Reds9[int(pov/100*8)])

Now that our data's clean and ready to go, let's plot it using Bokeh! To make the map more interactive, let's also add a pop-up balloon containing information on the municipalities using Bokeh's built-in Hover Tool.

(Hover your mouse on the map below (and try playing with the interactive tools again in the upper right hand corner of the map) to explore the map's functionality!)


In [164]:
from bokeh.plotting import ColumnDataSource
from bokeh.models import PanTool, BoxZoomTool, HoverTool, PreviewSaveTool, ResetTool, WheelZoomTool, BoxSelectTool

TOOLS2 = [PanTool(), BoxZoomTool(), BoxSelectTool(), HoverTool(), PreviewSaveTool(), ResetTool(), WheelZoomTool()]
p2 = figure(title="Municipal-level Map of the Philippines", tools = TOOLS2, plot_width=900, plot_height=800)

source = ColumnDataSource(data=dict(
    x=muni_lat,
    y=muni_lon,
    color=muni_color,
    name=muni_name,
    rate=muni_pov,
))

hover = p2.select_one(HoverTool)
hover.point_policy = "follow_mouse"
hover.tooltips = [
    ("Municipality / City", "@name"),
    ("Poverty Rate", "@rate%"),
    ("(Latitude, Longitude)", "($x, $y)")
]

p2.patches('x', 'y', source=source,
          fill_color=muni_color, fill_alpha=0.7,
          line_color="white", line_width=0.5)

output_file("sample_map_with_hover.html", title="Municipal-level Map of the Philippines")
show(p2)