Introduction to cartography with Python

Mapping is an excellent way of disseminating knowledge about data, even to audiences unfamiliar with statistics. This chapter looks at the challenge of mapping and how you can use Python to build maps.

Visualisation
Exercice
Author

Lino Galiana

Published

2025-12-09

If you want to try the examples in this tutorial:
View on GitHub Onyxia Onyxia Open In Colab
  • Understand the key challenges of cartography when visualizing spatial data
  • Use Python to create maps based on core principles of geographic representation
  • Work through hands-on exercises to explore practical cartographic techniques
  • Apply tools and methods from the spatial data section to build effective map-based visualizations
  • Follow the pedagogical progression to learn how to translate spatial information into clear, informative maps

1 Introduction

Cartography is one of the oldest forms of graphical representation of information. Historically confined to military and administrative uses or navigation-related information synthesis, cartography has, at least since the 19th century, become one of the preferred ways to represent information. It was during this period that the color-shaded map, known as the choropleth map, began to emerge as a standard way to visualize geographic data.

According to Chen et al. (2008), the first representation of this type was proposed by Charles Dupin in 1826 Figure 1.1 to illustrate literacy levels across France. The rise of choropleth maps is closely linked to the organization of power through unitary political entities. For instance, world maps often use color shades to represent nations, while national maps use administrative divisions (regions, departments, municipalities, as well as states or Länder).

Figure 1.1: The first choropleth map by Dupin (1826)

The emergence of choropleth maps during the 19th century marks an important shift in cartography, transitioning from military use to political application. No longer limited to depicting physical terrain, maps began to represent socioeconomic realities within well-defined administrative boundaries.

With the proliferation of geolocated data and the increasing use of data-driven decision-making, it has become crucial for data scientists to quickly create maps. This chapter, complementing the one on spatial data, offers exercises to explore the key principles of data visualization through cartography using Python.

NoteNote

Creating high-quality maps requires time but also thoughtful decision-making. Like any graphical representation, it is essential to consider the message being conveyed and the most appropriate means of representation.
Cartographic semiology, a scientific discipline focusing on the messages conveyed by maps, provides guidelines to prevent misleading representations—whether intentional or accidental.

Some of these principles are outlined in this cartographic semiology guide from Insee. They are also summarized in this guide.

This presentation by Nicolas Lambert, using numerous examples, explores key principles of cartographic dataviz.

This chapter will first introduce some basic functionalities of Geopandas for creating static maps. To provide context to the presented information, we will use official geographic boundaries produced by IGN. We will then explore maps with enhanced contextualization and multiple levels of information, illustrating the benefits of using interactive libraries based on JavaScript, such as Folium.

1.1 Data Used

Throughout this chapter, we will use several datasets to illustrate different types of maps:

  • Population counts;
  • Departmental boundaries of metropolitan France;
  • Municipal boundaries of Finistère;
  • Forest cover in the Landes department;
  • The location of Vélib’ stations;

1.2 Prerequisite installations

Before getting started, a few packages need to be installed:

# Sur colab
1!pip install pandas fiona shapely pyproj rtree geopandas
1
Ces librairies sont utiles pour l’analyse géospatiale (cf. chapitre dédié)

We will primarily need Pandas and GeoPandas for this chapter.

import pandas as pd
import geopandas as gpd

2 First Maps to Understand the Spatial Coverage of Your Data

We will use cartiflette, which simplifies the retrieval of administrative basemaps from IGN. This package is an interministerial project designed to provide a simple Python interface for obtaining official IGN boundaries.

First, we will retrieve the departmental boundaries:

from cartiflette import carti_download

departements = carti_download(
    values="France",
    crs=4326,
    borders="DEPARTEMENT",
    vectorfile_format="geojson",
    filter_by="FRANCE_ENTIERE_DROM_RAPPROCHES",
    source="EXPRESS-COG-CARTO-TERRITOIRE",
    year=2022,
)

These data bring the DROM closer to mainland France, as explained in one of the cartiflette tutorials and as Exercise 1 will allow us to verify.

Exercise 1 aims to ensure that we have correctly retrieved the desired boundaries by simply visualizing them. This should be the first reflex of any geodata scientist.

TipExercise 1: Representing Boundaries with GeoPandas Methods
  1. Use the plot method on the departements dataset to check the spatial extent. What projection do the displayed coordinates suggest? Verify using the crs method.
  2. Reproject the data to Lambert 93 (EPSG: 2154) and create the same map.
  3. Using appropriate matplotlib options, create a map with black boundaries, a white background, and no axes.
  4. Create the same map for the municipalities of Finistère.

The map of the departments, without modifying any options, looks like this:

The displayed coordinates suggest WGS84, which can be verified using the crs method:

<Geographic 2D CRS: EPSG:4326>
Name: WGS 84
Axis Info [ellipsoidal]:
- Lat[north]: Geodetic latitude (degree)
- Lon[east]: Geodetic longitude (degree)
Area of Use:
- name: World.
- bounds: (-180.0, -90.0, 180.0, 90.0)
Datum: World Geodetic System 1984 ensemble
- Ellipsoid: WGS 84
- Prime Meridian: Greenwich

If we convert to Lambert 93 (the official system for mainland France), we obtain a different extent, which is supposed to be more accurate for the mainland (but not for the relocated DROM, since, for example, French Guiana is actually much larger).

And of course, we can easily reproduce the failed maps from the chapter on GeoPandas, for example, if we apply a transformation designed for North America:

departements.to_crs(5070).plot()

If we create a slightly more aesthetically pleasing map, we get:

And the same for Finistère:

These maps are simple, yet they already rely on implicit knowledge. They require familiarity with the territory. When we start coloring certain departments, recognizing which ones have extreme values will require a good understanding of French geography. Likewise, while it may seem obvious, nothing in our map of Finistère explicitly states that the department is bordered by the ocean. A French reader would see this as self-evident, but a foreign reader, who may not be familiar with the details of our geography, would not necessarily know this.

To address this, we can use interactive maps that allow:

  • Displaying contextual information when hovering over or clicking on an element of the map.
  • Displaying a basemap with contextual information such as transport networks, localities, or natural boundaries.

For this, we will retain only the data corresponding to an actual spatial extent, excluding our zoom on Île-de-France and the DROM.

departements_no_duplicates = (
  departements
1  .drop_duplicates(subset = "INSEE_DEP")
)
departements_hexagone = (
  departements_no_duplicates
2  .loc[~departements['INSEE_DEP'].str.startswith("97")]
)
1
On retire le zoom sur l’Île de France
2
On ne garde que la France hexagonale

We successfully obtain the hexagon:

departements_hexagone.plot()

For the next exercise, we will need a few additional variables. First, the geometric center of France, which will help us position the center of our map.

minx, miny, maxx, maxy = departements_hexagone.total_bounds
center = [(miny + maxy) / 2, (minx + maxx) / 2]

We will also need a dictionary to provide Folium with information about our map parameters.

style_function = lambda x: {
1    'fillColor': 'white',
    'color': 'black',     
    'weight': 1.5,        
    'fillOpacity': 0.0   
}
1
In fact, this will allow for a transparent layer by combining it with the fillOpacity parameter set to 0%.

style_function is an anonymous function that will be used in the exercise.

Information that appears when hovering over an element is called a tooltip in web development terminology.

import folium
tooltip = folium.GeoJsonTooltip(
    fields=['LIBELLE_DEPARTEMENT', 'INSEE_DEP', 'POPULATION'],
    aliases=['Département:', 'Numéro:', 'Population:'],
    localize=True
)

For the next exercise, the GeoDataFrame must be in the Mercator projection. Folium requires data in this projection because it relies on navigation basemaps, which are designed for this representation. Typically, Folium is used for local visualizations where the surface distortion caused by the Mercator projection is not problematic.

For the next exercise, where we will represent France as a whole, we are slightly repurposing the library. However, since France is still relatively far from the North Pole, the distortion remains a small trade-off compared to the benefits of interactivity.

TipExercise 2: Creating a First Interactive Map with Folium
  1. Create the base layer using the center object and set zoom_start to 5.
  2. Update it using our departements_hexagone dataset and the parameters style_function and tooltip.

Here is the base layer from question 1:

Make this Notebook Trusted to load map: File -> Trust Notebook

And once formatted, this gives us the map:

Make this Notebook Trusted to load map: File -> Trust Notebook