Non contiguous cartogram in Python

logo of a chart:Cartogram

Cartograms are a kind a map that changes region size and/or shape according to a numerical variable. It's for example used to highlight population density differences between countries.

This post explains how to create a non-contiguous cartogram, which is a kind of cartogram that preserves shape but modify sizes. We'll go over a concrete example to illustrate difference in population density in Asia.

About cartograms

To give you a visual idea, here is the cartogram we will step-by-step create in this post:

preview final chart

Libraries & Data

For creating this chart, we will need to load the following libraries:

# matplotlib tools
import matplotlib.pyplot as plt
from matplotlib.font_manager import FontProperties

# map libraries
import geopandas as gpd
import geoplot as gplt
import geoplot.crs as gcrs

# colors
from pypalettes import load_cmap

# annotations
from highlight_text import fig_text, ax_text

# data manipulation
import pandas as pd

# increase resolution
plt.rcParams['figure.dpi'] = 300
plt.rcParams['savefig.dpi'] = 300

Dataset

Let's start by loading shape data:

world = gpd.read_file('https://raw.githubusercontent.com/holtzy/The-Python-Graph-Gallery/master/static/data/all_world.geojson')
world.head()
name geometry
0 Fiji MULTIPOLYGON (((180.00000 -16.06713, 180.00000...
1 Tanzania POLYGON ((33.90371 -0.95000, 34.07262 -1.05982...
2 W. Sahara POLYGON ((-8.66559 27.65643, -8.66512 27.58948...
3 Canada MULTIPOLYGON (((-122.84000 49.00000, -122.9742...
4 United States of America MULTIPOLYGON (((-122.84000 49.00000, -120.0000...

Then we load data about the Asian population and surfaces

# get asian population dataset
url = 'https://raw.githubusercontent.com/holtzy/The-Python-Graph-Gallery/master/static/data/asia.csv'
asia = pd.read_csv(url)
asia.head()
Country Total Population Surface Area (sq. km)
0 Russia 1.444444e+08 17098250.0
1 China 1.425671e+09 9600013.0
2 India 1.428628e+09 3287259.0
3 Kazakhstan 1.960663e+07 2724902.0
4 Saudi Arabia 3.694702e+07 2149690.0

Once we have our 2 datasets, we can merge them and create pop_norm_surface column as a measure of population density:

# merge the datasets together
data = world.merge(asia, how='right', left_on='name', right_on='Country')

# filter the data
data = data[['Country', 'geometry', 'Total Population', 'Surface Area (sq. km)']]
data = data[~data['Country'].isin(['Russia', 'Bangladesh', 'Lebanon'])]
data.dropna(inplace=True)
data['pop_norm_surface'] = data['Total Population'] / data['Surface Area (sq. km)']

# display first rows
data.columns = ['Country', 'geometry', 'pop', 'surfaces', 'pop_norm_surface']
data.head()
Country geometry pop surfaces pop_norm_surface
1 China MULTIPOLYGON (((109.47521 18.19770, 108.65521 ... 1.425671e+09 9600013.0 148.507231
2 India POLYGON ((97.32711 28.26158, 97.40256 27.88254... 1.428628e+09 3287259.0 434.595407
3 Kazakhstan POLYGON ((87.35997 49.21498, 86.59878 48.54918... 1.960663e+07 2724902.0 7.195354
4 Saudi Arabia POLYGON ((34.95604 29.35655, 36.06894 29.19749... 3.694702e+07 2149690.0 17.187141
5 Indonesia MULTIPOLYGON (((141.00021 -2.60015, 141.01706 ... 2.775341e+08 1916862.0 144.785656

Simple map of Asia

Let's start by a creating a simple version of our chart:

  • create a figure and axe using the figure() and add_subplot() functions
  • create the cartogram with the cartogram() function from geoplot. We specify that we want the size and the color of each country to be mapped with the pop_norm_surface column of our dataset (aka density population)
  • create the background map with the popyplot() function from geoplot

And that's it!

fig = plt.figure(figsize=(12, 7))
ax = fig.add_subplot(111, projection=gcrs.PlateCarree())

gplt.cartogram(
   data, projection=gcrs.PlateCarree(),
   scale='pop_norm_surface', hue='pop_norm_surface', limits=(0,1),
   ax=ax
)
gplt.polyplot(data, ax=ax)
plt.show()

Custom colors

Now we can add a bit of customization:

  • load a color map using pypalettes
  • use the set_facecolor() function to change the background color of the graph
  • change the color of the background map
  • reduce the linewidth argument from 1 to 0.1
# colors
cmap = load_cmap("Antennarius_multiocellatus", type='continuous', reverse=True)
background_color = '#edf2f4'
text_color = '#14213d'
map_color = 'white'

fig = plt.figure(figsize=(12, 7))
ax = fig.add_subplot(111, projection=gcrs.PlateCarree())
fig.set_facecolor(background_color)
ax.set_facecolor(background_color)

gplt.cartogram(
   data, projection=gcrs.PlateCarree(), cmap=cmap,
   scale='pop_norm_surface', hue='pop_norm_surface', limits=(0,1),
   ax=ax
)
gplt.polyplot(data, facecolor=map_color, edgecolor='black', linewidth=0.1, ax=ax)
plt.show()