Skip to main content

Mapping Points with Folium

Continuing my previous work on exploring Arlington's Bikeometer data, I have decided to look at all of Arlington's counters in this article. My goal is to retrieve all of the counters available via the Bike Arlington web services API and then map them.

In order to map these points in Python, I will use the Folium module. The Folium module provides a way to feed data in Python into a Leaflet.js map. Leaflet maps are interactive, attractive, and can be directly inserted into webpages. Folium provides many options to customize these maps, and I will explore several of these options in this article.

First, I'll load the Python modules that will be needed for this project. These include Pandas, Requests, XML, Numpy, and Folium.

In [1]:
import pandas as pd
import requests
from xml.etree import ElementTree
import numpy as np
import folium

The url below accesses all the counters provided by the Bike Arlington web services API. Then I save the xml data to a local file called xml_getallcounters.xml.

In [2]:
GetAllCountersUrl = "http://webservices.commuterpage.com/counters.cfc?wsdl&method=GetAllCounters"
In [3]:
xmlfile = open('xml_getallcounters.xml', 'w')
xmldata = requests.get(GetAllCountersUrl)
xmlfile.write(xmldata.text)
xmlfile.close()

xml_data = 'xml_getallcounters.xml'

The next step is to parse that xml data. It can be somewhat tricky to parse xml data, so I usually test out the commands to pull in each data column first on one record before applying the code to all the data records.

In [4]:
tree = ElementTree.parse(xml_data)
counter = tree.find('counter')
name = counter.find('name')
name.text
Out[4]:
'110 Trail'
In [5]:
counter.find('latitude').text
Out[5]:
'38.885315'
In [6]:
counter.find('longitude').text
Out[6]:
'-77.065022'
In [7]:
counter.find('region/name').text
Out[7]:
'Arlington'

From the code above, it looks like I can pull in the counter name, latitude, longitude, and region. I'm also adding in the ID number for future reference.

In [8]:
id = []
name = []
latitude = []
longitude = []
region = []


for c in tree.findall('counter'):
    id.append(c.attrib['id'])
    name.append(c.find('name').text)
    latitude.append(c.find('latitude').text)
    longitude.append(c.find('longitude').text)
    region.append(c.find('region/name').text)

df_counters = pd.DataFrame(
    {'ID' : id,
     'Name' : name,
     'latitude' : latitude,
     'longitude' : longitude,
     'region' : region
    })
df_counters.head()
Out[8]:
ID Name latitude longitude region
0 33 110 Trail 38.885315 -77.065022 Arlington
1 30 14th Street Bridge 38.874260 -77.044610 Arlington
2 43 15th Street NW 38.907470 -77.034610 DC
3 32 Arlington Mill Trail 38.845610 -77.096046 Arlington
4 24 Ballston Connector 38.882950 -77.121235 Arlington

From the dataframe, df_counters, I want to create a map of each of their locations based on the latitude and longitude coordinates. Ideally, each of the counters will also be labeled with their name. To do this, I first create a list of latitude and longitude coordinate pairs.

In [9]:
locations = df_counters[['latitude', 'longitude']]
locationlist = locations.values.tolist()
len(locationlist)
locationlist[7]
Out[9]:
['38.943070', '-77.115660']

I checked the length of the list to see if I grabbed all the counters from the dataframe. Then I also check the latitude and longitude of a few random individual records to make sure everything is working well. Now that the data is all prepped, it time to make some maps with Folium.

The first thing to do is to create a map object centered around the area of interest. I also set them zoom level to 12 in order to see the individual points. I then added in each of the points using a for loop on the locationlist. For each location, I also create a popup tooltip to provide the name of each counter.

In [10]:
map = folium.Map(location=[38.9, -77.05], zoom_start=12)
for point in range(0, len(locationlist)):
    folium.Marker(locationlist[point], popup=df_counters['Name'][point]).add_to(map)
map
Out[10]:

The map above doesn't look too bad, but there are several improvements that can be made. First, the default OpenStreetMap tiles are rather busy. Second, I noticed that a few data points are not visible when the map is zoomed in at 12. It appears the API includes counters from Alexandria, DC, and Montgomery County in addition to the Arlington County sensors. Third, I also noticed that several points are hard to see since they can be very close to each other. To fix these issues, I switch the basemap tiles to CartoDB dark_matter and reduced the zoom level to 11, and I created marker clusters.

The marker clusers group points that overlap and then it labels the resulting cirlce with the number of points in that area. If you click on the circle, the map zooms to the area to show you the individual points. That's pretty cool.

In [11]:
map2 = folium.Map(location=[38.9, -77.05], tiles='CartoDB dark_matter', zoom_start=11)

marker_cluster = folium.MarkerCluster().add_to(map2)

for point in range(0, len(locationlist)):
    folium.Marker(locationlist[point], popup=df_counters['Name'][point]).add_to(marker_cluster)
map2
Out[11]:

The result map point are a bit easier to see, but I'm not sure I'm a bit fan of the dark color of the CartoDB dark_matter tiles. I think I prefer a lighter color for this map. For the next map, I decided to try out the Stamen Terrain tiles. I also decided to change the look of the point icons. I changed their color to dark blue. I also tried to add a bicycle icon. The bicycle icon was not available in version 0.2.1 of Folium, so I instead went with a pedestrian looking icon.

In [12]:
map2 = folium.Map(location=[38.9, -77.05], tiles='Stamen Terrain', zoom_start=11)

marker_cluster = folium.MarkerCluster().add_to(map2)

for point in range(0, len(locationlist)):
    folium.Marker(locationlist[point], popup=df_counters['Name'][point], icon=folium.Icon(color='darkblue', icon_color='white', icon='male', angle=0, prefix='fa')).add_to(marker_cluster)
map2
Out[12]:

The terrain map is an improvement over the dark map, but I still found the green coloring for natural areas to be distracting from the main objective of the map. I now also want to differentiate the counters by region. To do that, I decided to create a function to assign a unique color to each region.

In [13]:
def regioncolors(counter):
    if counter['region'] == 'Arlington':
        return 'green'
    elif counter['region'] == 'Alexandria':
        return 'blue'
    elif counter['region'] == 'DC':
        return 'red'
    else:
        return 'darkblue'
df_counters["color"] = df_counters.apply(regioncolors, axis=1)
df_counters.head()
Out[13]:
ID Name latitude longitude region color
0 33 110 Trail 38.885315 -77.065022 Arlington green
1 30 14th Street Bridge 38.874260 -77.044610 Arlington green
2 43 15th Street NW 38.907470 -77.034610 DC red
3 32 Arlington Mill Trail 38.845610 -77.096046 Arlington green
4 24 Ballston Connector 38.882950 -77.121235 Arlington green

I then changed the point icon color for each counter based on its region. I also switched the basemap to the lighter CartoDB positron tiles. While I was at it, I also realized it would be a really good idea to include the counter ID number on the map since you need that number when making specific API calls for counter data.

In [14]:
map3 = folium.Map(location=[38.9, -77.05], tiles='CartoDB positron', zoom_start=11)

marker_cluster = folium.MarkerCluster().add_to(map3)

for point in range(0, len(locationlist)):
    folium.Marker(locationlist[point], popup='ID:'+df_counters['ID'][point]+' '+df_counters['Name'][point], icon=folium.Icon(color=df_counters["color"][point], icon_color='white', icon='male', angle=0, prefix='fa')).add_to(marker_cluster)
map3
Out[14]:

I'm fairly please with this last map. In the future, I would like to change the symbols on the pointers based on whether the particular sensor can distiguish between bicycles and pedestrians. It is also possible to attach bar charts or other graph types to these pointers. In addition to point maps, Folium can also create choropleth maps for displaying regional statistics using color gradients.

Comments

Comments powered by Disqus
Mastodon