Creating a Choropleth Map of Bike Counts in Paris Using Folium

Li-Hsiang Hsu
7 min readSep 29, 2023

--

Folium is a Python library that makes it easy to create interactive maps. We can use Folium to generate a choropleth map showcasing the relative bike traffic importance in every district of Paris, along with the location of each bike counter and its average hourly count. Here’s a basic demonstration of how to do this.

A choropleth map of bike counts in Paris

Cycling in Paris

Cycling in Paris has witnessed a significant transformation in recent years, reflecting the city’s commitment to promoting sustainable and eco-friendly transportation solutions. Paris has undertaken ambitious initiatives to encourage cycling as a means of reducing carbon emissions, air pollution, and improving the overall quality of life and wellness for its residents and visitors.

One of the most noteworthy efforts in this regard is the extensive development of dedicated cycle paths and bike-friendly infrastructure throughout the city. These cycle paths, often separated from motorized traffic, provide safe and convenient routes for cyclists, making it easier for people to choose bicycles as a mode of transport. The expansion of the cycling network has not only improved mobility but has also contributed to reducing traffic congestion and enhancing road safety.

Bike counters

Alongside with its efforts to deploy bike paths, Paris has strategically installed bike counters throughout the city.

These counters capture essential information about bicycle traffic, offering a comprehensive overview of how cyclists navigate the city’s streets and districts and providing valuable insights into the city’s evolving transportation landscape, aligning with its ambitious efforts to promote soft and sustainable mobility.

To gain a comprehensive overview of these bike counters, I’ve crafted a choropleth map with Folium. This map visually depicts the relative significance of bike traffic in every Paris district, concurrently pinpointing the location of each bike counter and providing details about its average hourly count. I’ve utilized markers and circle markers on the map to represent the address and average hourly count of each counter.

Below, you’ll find a step-by-step guide on how to create this informative map.

Create the map

First of all, import libraries we will use to create the map.

#Import libraries
import pandas as pd
import geopandas as gpd
import json
from shapely.geometry import Point
import folium
from folium.plugins import MarkerCluster, MousePosition
from folium.features import GeoJson

Secondly import files directly from the site of Paris Open Data or download and store them on your local terminal and create two dataframes. Two datasets are used to create the map:

  • A bike count dataset accessible on the site of Paris Open Data:
  • A district dataset containing the geographical information of each district in Paris, accessible on the site of Paris Open Data:

Here’s the code:

#Import files
#bike counter data
df = pd.read_csv('comptage-velo-donnees-compteurs.csv', sep=';')
#District data in Paris
districts = gpd.read_file('arrondissements.geojson')

Alternally, import files directly from the site of Paris Open Data:


#bike counter data
url_bike = 'https://opendata.paris.fr/api/explore/v2.1/catalog/datasets/comptage-velo-donnees-compteurs/exports/csv?lang=fr&timezone=Europe%2FParis&use_labels=true&delimiter=%3B'
df = pd.read_csv(url_bike, sep=';')
#district data
url_districts = 'https://opendata.paris.fr/api/explore/v2.1/catalog/datasets/arrondissements/exports/geojson?lang=fr&timezone=Europe%2FBerlin'
districts = gpd.read_file(url_districts)

Thirdly, work on the columns to have two formally coherent datasets. You might want to jump directly to the next step if you don’t think coherence and uniformity are really crucial.

#Working on bike counter data (df) colonnes
df.columns = df.columns.astype(str).str.replace(" ", "_")

df = df.drop(columns = ['Identifiant_du_compteur',
'Identifiant_du_site_de_comptage',
'Nom_du_site_de_comptage',
"Date_d'installation_du_site_de_comptage",
'Lien_vers_photo_du_site_de_comptage',
'Identifiant_technique_compteur',
'ID_Photos',
'test_lien_vers_photos_du_site_de_comptage_',
'id_photo_1',
'url_sites',
'type_dimage',
'mois_annee_comptage',
'Date_et_heure_de_comptage'])
# Rename columns
df = df.rename(columns={'Nom_du_compteur': 'Address',
'Comptage_horaire': 'Count',
'Coordonnées_géographiques': 'Coords'})

#Working on districts data by removing some columns and and renaming retained columns
districts = districts.drop(columns = ['n_sq_co', 'l_aroff', 'c_arinsee', 'n_sq_ar', 'surface', 'perimetre', 'l_ar'])
districts = districts.rename(columns={'c_ar': 'District'})

The fourth step in this tutorial is pivotal for managing geographical data. To effectively handle this information, we employ GeoDataFrame, a powerful tool for geographic data manipulation. To do so, we follow these key steps:

  1. Create ‘Latitude’ and ‘Longitude’ Columns: First, we generate ‘Latitude’ and ‘Longitude’ columns by extracting location information from the ‘Coords’ column in our bike count dataframe. These new columns store the latitude and longitude coordinates of each bike counter, making the data geospatially accessible.
  2. Replace ‘Coords’ Column: We then eliminate the ‘Coords’ column from our dataframe and create a new ‘Coords’ column. To accomplish this, we utilize the geopandas.points_from_xy function, specially designed for establishing a geometry column in a GeoDataFrame. This function directly converts ‘Longitude’ and ‘Latitude’ columns into a GeoSeries of Point geometries.
  3. Set ‘Coords’ as Geometry: Next, we designate the ‘Coords’ column as the geometry column, resulting in the creation of the GeoDataFrame df_geo. This step is crucial for enabling spatial operations and analyses on our geospatial data.
  4. Perform Spatial Join: The final part of this process involves a spatial join between two GeoDataFrames: df_geo and districts. This operation allows us to combine and analyze geospatial data from both datasets, unlocking valuable insights and correlations.

By following these steps, we will be able to effectively manage geographical data, making it accessible for geospatial analysis and visualization.

#Create "Longitude" and "Lagititude" columns from the 'Coords' column
df['Latitude'] = df['Coords'].apply(lambda x: x.split(',')[0]).astype(float)
df['Longitude'] = df['Coords'].apply(lambda x: x.split(',')[1]).astype(float)
#Remove 'Coords' column
df = df.drop(columns='Coords')

#Create a new 'Coords' column from df['Longitude'], df['Latitude'] and set it as geometry column for GeoDataframe
df['Coords'] = gpd.points_from_xy(df.Longitude, df.Latitude)

# Convert df to GeoDataframe by setting 'Coords' column as geometry
df_geo = gpd.GeoDataFrame(df, geometry='Coords', crs=districts.crs)

# Joint df_geo with districts by using spatial joint tool provided by GeoPandas
df_geo = gpd.tools.sjoin(df_geo, districts, how='inner', op='intersects', lsuffix ='Coords', rsuffix = 'geometry')

Another approach to creating a list of Shapely objects in the ‘Coords’ column is to use the zip() function. This method involves combining the ‘Latitude’ and ‘Longitude’ columns, resulting in a new column where each row holds a tuple of (latitude, longitude) pairs. To transform these tuples into Shapely Point objects, we can make use of the .apply() function in conjunction with Point. This allows for the conversion of each tuple into a Shapely Point object and sets the ‘Coords’ column as the geometry column for the GeoDataFrame we intend to create.

#Another methode to create the 'Coords' column
df['Coords'] = list(zip(df['Longitude'], df['Latitude']))
df['Coords'] = df['Coords'].apply(Point)

In the fifth step, we create a base map and a choropleth map by binding the bike count data to the polygon information provided by the districts dataset.

#Create the base map
paris_m = folium.Map(location=[48.856578, 2.351828],
zoom_start=12, min_zoom=10, max_zoom=15,
control_scale=True) #Show a scale on the bottom of the map.

#Compute the mean hourly count for every district in Paris
dist_mean = df_geo.groupby("District", as_index = False)["Count"].mean()

# Create the choropleth map and add it to the base map
choropleth = folium.Choropleth(geo_data=districts,
key_on="feature.properties.District",
data=dist_mean,
columns=["District", "Count"],
fill_color="BuPu", # or any other color scheme
highlight=True,
legend_name="Average hourly count by district",
name="District choropleth").add_to(paris_m)

This is the base map and choropleth map we created:

Subsequently, we form a FeatureGroup that encompasses both circle and marker map subdivisions.

#create a featuregroup including circle and marker subgroups
fg = folium.FeatureGroup(name="Counters and hourly count average")
paris_m.add_child(fg)
circle_fg = folium.plugins.FeatureGroupSubGroup(fg, "Average hourly count", show=False)
paris_m.add_child(circle_fg)
marker_fg = folium.plugins.FeatureGroupSubGroup(fg, "Counter address", show=False)
paris_m.add_child(marker_fg)

Leveraging the geographical data associated with each counter, we adorn the map with markers. Within this context, we compute the mean hourly count for every individual counter, visually conveyed through circle markers. Notably, the radius of these markers corresponds to the significance of traffic flow at each respective location.

#Get the mean hourly count for every counter
df_address = df_geo.groupby(['Address', 'Longitude', 'Latitude'], as_index=False)['Count'].mean()

# Create a marker and circle marker map and add it to a FeatureGroup
for index, row in df_address.iterrows():
size = row['Count'] / 10 #Get circle size proportional to hourly count
count_data = round(row['Count'])
counter_address = row['Address']
circle = folium.CircleMarker([row['Latitude'], row['Longitude']],
radius=size,
tooltip=f'Average hourly count: {count_data}',
popup=f'Counter address: {counter_address}',
fill=True, fill_color='purple', fill_opacity=0.7,
highlight=True,).add_to(circle_fg)
marker = folium.Marker(location=[row['Latitude'], row['Longitude']],
color='blue',
tooltip=f'Counter address: {counter_address}',
popup=f'Average hourly count: {count_data}',
icon=folium.Icon(color="darkblue"),
highlight=True).add_to(marker_fg)

The map with the markers and circle markers should look like this:

Finally, we add a LayerControl to the map to toggle between choropleth map and markers and circle markers map and save it as a html page and show the map.

# Add Layer Control to the map to toggle between Choropleth and Markers and Circles
folium.LayerControl(collapsed=False, autoZIndex=True).add_to(paris_m)

# Save the map to show in a webbrowser
paris_m.save('paris_map.html')

# Display the map
paris_m

Now our choropleth map with bike count and district information in Paris is done.

The code is to be found on my GitHub page:

--

--

Li-Hsiang Hsu

Passionate Data Scientist. Dedicated to multidisciplinary approaches to study human and artificial intelligence. Interested in art, perception and emotion.