How to draw a variety of maps with folium in python?

This blog talks about how to draw a map with python module "folium", like how to display all locations with points or with cluster, how to paint areas with different colors, how to add labels or po...

A map can clearly present information in terms of geography. Recently I learnt how to realize geovisualization with folium module in Python. In this blog, I will talk about how to draw a different types of map like with the following points:

  • Datasets
  • Display all locations with points on the map
  • Display all locations with clusters
  • Paint areas with different colors
  • Add labels on the map
  • Add polygon borders
  • Show changes in terms of timing with heatmap
  • Show changes in terms of timing with choropleth

Datasets

Before drawing maps, I’ll talk about the datasets which are used for this blog. I downloaded Airbnb Paris’ datasets “listings” and “neighbourhoods”.

import pandas as pd
import geopandas as gpd

listing_df = pd.read_csv('listings.csv', parse_dates=['last_review'])
nbh_geo_df = gpd.read_file('neighbourhoods.geojson', driver='GeoJSON')
listing_df = listing_df[['id', 'room_type', 'neighbourhood',
                         'latitude', 'longitude', 'price']]
nbh_geo_df = nbh_geo_df[['neighbourhood', 'geometry']]

Datasets

from shapely.geometry import Point, shape

locs_geometry = [Point(xy) for xy in zip(listing_df.longitude,
                                         listing_df.latitude)]
crs = {'init': 'epsg:4326'}
# Coordinate Reference Systems, "epsg:4326" is a common projection of WGS84 Latitude/Longitude
locs_gdf = gpd.GeoDataFrame(listing_df, crs=crs, geometry=locs_geometry)

In order to put points on the map, we need to convert each coordinate to geopoint, same for the dataframe.

Display all locations with points on the map

Display all locations with points on the map

import folium
locs_map = folium.Map(location=[48.856614, 2.3522219],
                      zoom_start=13, tiles='cartodbpositron')

feature_ea = folium.FeatureGroup(name='Entire home/apt')
feature_pr = folium.FeatureGroup(name='Private room')
feature_sr = folium.FeatureGroup(name='Shared room')

for i, v in locs_gdf.iterrows():
    popup = """
    Location id : <b>%s</b><br>
    Room type : <b>%s</b><br>
    Neighbourhood : <b>%s</b><br>
    Price : <b>%d</b><br>
    """ % (v['id'], v['room_type'], v['neighbourhood'], v['price'])
    
    if v['room_type'] == 'Entire home/apt':
        folium.CircleMarker(location=[v['latitude'], v['longitude']],
                            radius=1,
                            tooltip=popup,
                            color='#FFBA00',
                            fill_color='#FFBA00',
                            fill=True).add_to(feature_ea)
    elif v['room_type'] == 'Private room':
        folium.CircleMarker(location=[v['latitude'], v['longitude']],
                            radius=1,
                            tooltip=popup,
                            color='#087FBF',
                            fill_color='#087FBF',
                            fill=True).add_to(feature_pr)
    elif v['room_type'] == 'Shared room':
        folium.CircleMarker(location=[v['latitude'], v['longitude']],
                            radius=1,
                            tooltip=popup,
                            color='#FF0700',
                            fill_color='#FF0700',
                            fill=True).add_to(feature_sr)

feature_ea.add_to(locs_map)
feature_pr.add_to(locs_map)
feature_sr.add_to(locs_map)
folium.LayerControl(collapsed=False).add_to(locs_map)

There are numerous marker types, I apply CircleMarker with a popup(input text or visualization for object displayed when clicking) and tooltip(display a text when hovering over the object), apply LayerControl to filter different types of locations.

Display all locations with clusters

Display all locations with clusters

from folium import plugins

cluster_map = folium.Map(location=[48.856614, 2.3522219],
                         zoom_start=13, tiles='cartodbpositron')

marker_cluster = plugins.MarkerCluster().add_to(cluster_map)
for i, v in locs_gdf.iterrows():
    popup = """
    Location id : <b>%s</b><br>
    Room type : <b>%s</b><br>
    Neighbourhood : <b>%s</b><br>
    Price : <b>%d</b><br>
    """ % (v['id'], v['room_type'], v['neighbourhood'], v['price'])
    
    if v['room_type'] == 'Entire home/apt':
        folium.CircleMarker(location=[v['latitude'], v['longitude']],
                            radius=3,
                            tooltip=popup,
                            color='#FFBA00',
                            fill_color='#FFBA00',
                            fill=True).add_to(marker_cluster)
    elif v['room_type'] == 'Private room':
        folium.CircleMarker(location=[v['latitude'], v['longitude']],
                            radius=3,
                            tooltip=popup,
                            color='#087FBF',
                            fill_color='#087FBF',
                            fill=True).add_to(marker_cluster)
    elif v['room_type'] == 'Shared room':
        folium.CircleMarker(location=[v['latitude'], v['longitude']],
                            radius=3,
                            tooltip=popup,
                            color='#FF0700',
                            fill_color='#FF0700',
                            fill=True).add_to(marker_cluster)

Before drawing CircleMarker on the map, I add plugins.MarkerCluster() on the map for clustering coordinates.

Paint areas with different colors

Paint areas with different colors

nbh_count_df = listing_df.groupby('neighbourhood')['id'].nunique().reset_index()
nbh_count_df.rename(columns={'id':'nb'}, inplace=True)
nbh_geo_count_df = pd.merge(nbh_geo_df, nbh_count_df, on='neighbourhood')
nbh_geo_count_df['QP'] = nbh_geo_count_df['nb'] / nbh_geo_count_df['nb'].sum()
nbh_geo_count_df['QP_str'] = nbh_geo_count_df['QP'].apply(lambda x : str(round(x*100, 1)) + '%')

from branca.colormap import linear
nbh_count_colormap = linear.YlGnBu_09.scale(min(nbh_count_df['nb']),
                                            max(nbh_count_df['nb']))

nbh_locs_map = folium.Map(location=[48.856614, 2.3522219],
                          zoom_start = 12, tiles='cartodbpositron')

style_function = lambda x: {
    'fillColor': nbh_count_colormap(x['properties']['nb']),
    'color': 'black',
    'weight': 1.5,
    'fillOpacity': 0.7
}

folium.GeoJson(
    nbh_geo_count_df,
    style_function=style_function,
    tooltip=folium.GeoJsonTooltip(
        fields=['neighbourhood', 'nb', 'QP_str'],
        aliases=['Neighbourhood', 'Location amount', 'Quote-part'],
        localize=True
    )
).add_to(nbh_locs_map)

nbh_count_colormap.add_to(nbh_locs_map)
nbh_count_colormap.caption = 'Airbnb location amount'
nbh_count_colormap.add_to(nbh_locs_map)

To paint areas in terms of locations’ average price, we need to calculate the values firstly. Then use branca.colormap.linear to set colormap, insert the colormap into style_function, plot a GeoJSON overlay on the base map with folium.GeoJson, then we can draw the map.

Add labels on the map

Add labels on the map

nbh_locs_label_map = folium.Map(location=[48.856614, 2.3522219],
                                zoom_start = 12, tiles='cartodbpositron')

style_function = lambda x: {
    'fillColor': nbh_count_colormap(x['properties']['nb']),
    'color': 'black',
    'weight': 1,
    'fillOpacity': 0.7
}

folium.GeoJson(
    nbh_geo_count_df,
    style_function=style_function,
    tooltip=folium.GeoJsonTooltip(
        fields=['neighbourhood', 'nb', 'QP_str'],
        aliases=['Neighbourhood', 'Location amount', 'Quote-part'],
        localize=True
    )
).add_to(nbh_locs_label_map)

folium.map.CustomPane('labels').add_to(nbh_locs_label_map)
folium.TileLayer('CartoDBPositronOnlyLabels',
                 pane='labels').add_to(nbh_locs_label_map)

Do you notice that? The labels are usually covered by painted colors. In this case, I used folium.TileLayer to add CartoDBPositronOnlyLabels on the map, so that we add another labels’ layer.

Add polygon borders

Add polygon borders

dept_geo = gpd.read_file('departements.geojson', driver='GeoJSON')
departments = {'75', '77', '78', '91', '92', '93', '94', '95'}
dept_geo = dept_geo[dept_geo['code'].isin(departments)]

polygon_map = folium.Map(location=[48.716614, 2.3522219],
                         zoom_start = 9, tiles='cartodbpositron')

style_function = lambda x: {
    'fillColor': nbh_count_colormap(x['properties']['nb']),
    'color': 'black',
    'weight': 1.5,
    'fillOpacity': 0.7
}

folium.GeoJson(
    nbh_geo_count_df,
    style_function=style_function,
    tooltip=folium.GeoJsonTooltip(
        fields=['neighbourhood', 'nb', 'QP_str'],
        aliases=['Neighbourhood', 'Location amount', 'Quote-part'],
        localize=True
    )
).add_to(polygon_map)

folium.GeoJson(
    dept_geo,
    style_function = lambda x: {
        'color': 'black',
        'weight': 2.5,
        'fillOpacity': 0
    },
    name='Departement').add_to(polygon_map)

nbh_count_colormap.add_to(polygon_map)
nbh_count_colormap.caption = 'Airbnb location amount'
nbh_count_colormap.add_to(polygon_map)

I downloaded Ile-de-France’s geojson data from here. To draw polygons’ border, the only thing that we need to do is add the polygons’ coordinates and set fillOpacity to be 0.

Show changes in terms of timing with heatmap

Changes in terms of timing with heatmap

date_index = [d.strftime('%Y-%m-%d') for d in listing_summary_history.date_of_data.unique()][::-1]

locs_history_map = folium.Map(location=[48.856614, 2.3522219],
                              zoom_start = 13, tiles='cartodbpositron')

hm = plugins.HeatMapWithTime(
    data=locs_yearly,
    index=date_index,
    auto_play=True,
    radius=4,
    max_opacity=0.3
)

hm.add_to(locs_history_map)

For presenting the changes in terms of time on a heatmap, we can apply plugins.HeatMapWithTime(). data is the points you want to plot, which is a list of list of points of the form [lat, lng] or [lat, lng, weight]. index gives the label (or timestamp) of the elements of data, should be the same length as data, or is replaced by a simple count if not specified.

Show changes in terms of timing with choropleth

Changes in terms of timing with choropleth

nbh_locsNb_history = listing_summary_history.groupby(['date_of_data',
                                                      'neighbourhood'])['id'].nunique().reset_index()
nbh_locsNb_history.rename(columns={'id':'nb'}, inplace=True)
nbh_locsNb_history.sort_values(['neighbourhood', 'date_of_data'],
                               inplace=True)
nbh_locsNb_history.reset_index(drop=True, inplace=True)

nbh_geo_sorted_df = nbh_geo_df.sort_values('neighbourhood').reset_index(drop=True)

The map above describes the price’s changes of each neighbourhood in terms of time series. Thus, before making the map, we need to calculate the average location’s price for each neighbourhood each year.

datetime_index = pd.DatetimeIndex(nbh_locsNb_history.date_of_data.unique())
dt_index_epochs = datetime_index.astype(int) // 10**9
dt_index = np.array(dt_index_epochs).astype('U10')

Then I prepared convert datetime to U10 with pandas.DatetimeIndex() and astype().

styledata = {}
s = 0
e = 44
for i, v in nbh_geo_sorted_190709.iterrows():
    df = pd.DataFrame(
        {'color': np.array(nbh_locsNb_history.nb[s:e]),
         'opacity': np.array([1] * 44)},
        index=dt_index
    )
    styledata[i] = df
    s += 44
    e += 44

max_color = max(nbh_locsNb_history['nb'])
min_color = min(nbh_locsNb_history['nb'])
max_opacity, min_opacity = 1, 1

cmap = linear.YlGnBu_09.scale(min_color, max_color)

def norm(x):
    return (x - x.min()) / (x.max() - x.min())

for i, data in styledata.items():
    data['color'] = data['color'].map(cmap)
    data['opacity'] = 1

styledict = {
    str(nbh): data.to_dict(orient='index') for nbh, data in styledata.items()
}

The last step is to define a color map in terms of neighbourhoods’ prices for each year.

from folium.plugins import TimeSliderChoropleth

nbh_locs_history_map = folium.Map(location=[48.856614, 2.3522219],
                                  zoom_start = 12, tiles='cartodbpositron')

TimeSliderChoropleth(
    data=nbh_geo_sorted_190709.to_json(),
    styledict=styledict
).add_to(nbh_locs_history_map)

We can apply TimeSliderChoropleth to draw the time series map. data should be geojson string, styledict is the dictionary where the keys are the geojson feature ids and the values are dicts of {time: style_options_dict}.

Reference