How to Classify Degree of Urbanisation with GHSL in Python

On this page

Problem statement

"Urban" means something different in every country's statistics, which makes international comparisons of urban population meaningless. The Degree of Urbanisation, endorsed by the UN Statistical Commission in 2020, fixes that with one rule applied to 1 km population grid cells everywhere: cities are dense, populous clusters of cells, towns and suburbs are smaller or looser ones, and the rest is rural. The GHSL publishes the classification ready-made as GHS-SMOD, for every epoch from 1975 to 2030.

This page reads GHS-SMOD and GHS-POP for the Netherlands at 1 km, merges the two tiles the country straddles, counts population by settlement class and by the three degrees, extracts the urban centres as named clusters, and compares 1975 with 2020.

Quick answer

Merge the tiles, mask with the boundary in the grid's CRS, and sum population by SMOD class:

smod, tf = clip(merged("SMOD", 2020))
pop, _ = clip(merged("POP", 2020))
ok = ~smod.mask & ~pop.mask
by_class = pd.Series(pop.data[ok]).groupby(smod.data[ok]).sum()
share = (by_class / by_class.sum()).rename(index=CLASSES)
print(f"population on {ok.sum():,} 1 km cells: {by_class.sum():,.0f}")
print(share.sort_values(ascending=False).map("{:.1%}".format).to_string())
population on 35,012 1 km cells: 17,369,596
urban centre                49.0%
dense urban cluster         20.4%
suburban or peri-urban      12.8%
low density rural            9.6%
rural cluster                4.7%
semi-dense urban cluster     2.2%
very low density rural       1.4%
water                        0.0%

The 35,012 cells hold 17,369,596 people. Almost half, 49.0%, live in urban centres and another fifth in dense urban clusters; the very low density rural cells, the most common class by area, hold 1.4%.

Map of the Netherlands on 1 km cells coloured by GHS-SMOD 2020 class: dark red urban centres cover the Randstad, with one large centre joining Rotterdam and Den Haag, and the main cities elsewhere; red dense urban clusters and orange suburban cells surround them and mark towns; most of the land is pale green low and very low density rural cells. Legend: urban centres 49.0% of the population, dense urban clusters 20.4%, suburban 12.8%, low density rural 9.6%. Map of the Netherlands on 1 km cells coloured by GHS-SMOD 2020 class: dark red urban centres cover the Randstad, with one large centre joining Rotterdam and Den Haag, and the main cities elsewhere; red dense urban clusters and orange suburban cells surround them and mark towns; most of the land is pale green low and very low density rural cells. Legend: urban centres 49.0% of the population, dense urban clusters 20.4%, suburban 12.8%, low density rural 9.6%.
Degree of Urbanisation, 2020.

Read the classes, collapse to three degrees, cluster centres

1. Read the classes

SMOD stores a two-digit code per cell: the first digit is the degree (3 city, 2 town or suburb, 1 rural), the second the settlement type within it:

cells = pd.Series(smod.data[ok]).value_counts().sort_index()
for code, n in cells.items():
    print(f"{code}  {CLASSES[code]:<28} {n:>6,} cells")
with rasterio.open(merged("SMOD", 2020)) as src:
    print(f"grid: {src.res[0]:.0f} m cells, {src.crs.to_string()[:30]}..., nodata {src.nodata}")
10  water                           153 cells
11  very low density rural       12,855 cells
12  low density rural            13,619 cells
13  rural cluster                 1,116 cells
21  suburban or peri-urban        3,135 cells
22  semi-dense urban cluster        224 cells
23  dense urban cluster           1,382 cells
30  urban centre                  2,528 cells
grid: 1000 m cells, ESRI:54009..., nodata -200.0

Low and very low density rural cells cover 26,474 of the 35,012 km²; urban centres cover 2,528. The grid is 1 km in World Mollweide (ESRI:54009) with -200 as nodata.

2. Collapse to the three degrees

The first level of the classification has three classes, which is what the international statistics report:

degree = {3: "cities", 2: "towns and semi-dense areas", 1: "rural areas"}
d = by_class.groupby(by_class.index // 10).sum()
d.index = d.index.map(degree)
area = cells.groupby(cells.index // 10).sum()
area.index = area.index.map(degree)
print(pd.DataFrame({"population": d.round(), "share": (d / d.sum()).map("{:.1%}".format), "km2": area}).to_string())
                            population  share    km2
rural areas                  2727094.0  15.7%  27743
towns and semi-dense areas   6135948.0  35.3%   4741
cities                       8506554.0  49.0%   2528

At the first level, 49.0% of the population lives in cities, 35.3% in towns and semi-dense areas, and 15.7% in rural areas, which take 79% of the land, 27,743 km².

Population and land by Degree of Urbanisation, the Netherlands 2020, from GHS-SMOD and GHS-POP. the three degrees: share of population, 2020cities49.0% on 2,528 km2towns, semi-dense35.3% on 4,741 km2rural areas15.7% on 27,743 km2020%40%60%Rural areas hold one person in six on four fifths of the land spatialworkflow.io
The three degrees.

3. Urban centres as clusters

An urban centre is a contiguous group of class 30 cells. Label the groups with scipy.ndimage.label, sum each one's population, and name each after the municipalities that hold most of its people:

lab, n = ndimage.label(smod.filled(0) == 30)
pc = pd.Series(ndimage.sum(pop.filled(0), lab, range(1, n + 1)), index=range(1, n + 1))
print(f"{n} urban centres holding {pc.sum():,.0f} people")
r, c = np.nonzero(lab)
xs, ys = rasterio.transform.xy(tf, r, c)
cells_ = gpd.GeoDataFrame({"centre": lab[r, c], "people": pop.filled(0)[r, c]}, geometry=gpd.points_from_xy(xs, ys), crs=MOLL)
cells_ = cells_.to_crs(28992).sjoin(GM[["statnaam", "geometry"]])
for k, people in pc.nlargest(6).items():
    own = cells_[cells_.centre == k].groupby("statnaam").people.sum().nlargest(2)
    print(f"{' + '.join(own.index):<26} {people:>10,.0f} people, {(lab == k).sum():>3} km2, {cells_[cells_.centre == k].statnaam.nunique():>2} municipalities")
45 urban centres holding 8,506,554 people
Rotterdam + 's-Gravenhage   2,116,766 people, 668 km2, 21 municipalities
Amsterdam + Zaanstad        1,171,569 people, 288 km2, 11 municipalities
Utrecht + Nieuwegein          493,888 people, 115 km2,  6 municipalities
Eindhoven + Veldhoven         284,160 people,  96 km2,  4 municipalities
Leiden + Katwijk              266,405 people,  67 km2,  9 municipalities
Dordrecht + Zwijndrecht       252,804 people,  86 km2,  8 municipalities

The classification finds 45 urban centres holding 8,506,554 people. The largest joins Rotterdam and Den Haag with 19 other municipalities into one centre of 2.1 million on 668 km², because the 1 km cells between them are dense enough to connect; the hand-built centres in the urban extent guide keep the two apart. Amsterdam and Zaanstad form the second, with 1.2 million.

The Netherlands in 1975 against 2020

The data used on this page

GHS-SMOD R2023A, version 2.0, and GHS-POP R2023A, both at 1 km in World Mollweide for 1975 and 2020 (tiles R3_C19 and R4_C19), and the 2020 country and municipality boundaries from the PDOK CBS gebiedsindelingen web feature service.

import io
import zipfile
from pathlib import Path

import geopandas as gpd
import numpy as np
import pandas as pd
import rasterio
import requests
from rasterio.mask import mask
from rasterio.merge import merge
from scipy import ndimage

Path("data").mkdir(exist_ok=True)
GHSL = "https://jeodpp.jrc.ec.europa.eu/ftp/jrc-opendata/GHSL"
MOLL = "ESRI:54009"
CLASSES = {30: "urban centre", 23: "dense urban cluster", 22: "semi-dense urban cluster", 21: "suburban or peri-urban",
           13: "rural cluster", 12: "low density rural", 11: "very low density rural", 10: "water"}
def tile(kind, year, t):
    v = "V2_0" if kind == "SMOD" else "V1_0"
    name = f"GHS_{kind}_E{year}_GLOBE_R2023A_54009_1000"
    tif = Path("data", f"{name}_{v}_{t}.tif")
    if not tif.exists():
        url = f"{GHSL}/GHS_{kind}_GLOBE_R2023A/{name}/{v.replace('_', '-')}/tiles/{tif.stem}.zip"
        zipfile.ZipFile(io.BytesIO(requests.get(url, timeout=600).content)).extract(tif.name, "data")
    return tif
def merged(kind, year):
    out = Path("data", f"{kind}_{year}_nl_1km.tif")
    if not out.exists():
        srcs = [rasterio.open(tile(kind, year, t)) for t in ("R3_C19", "R4_C19")]
        a, transform = merge(srcs)
        profile = srcs[0].profile | {"height": a.shape[1], "width": a.shape[2], "transform": transform}
        with rasterio.open(out, "w", **profile) as dst:
            dst.write(a)
    return out
B = Path("data/landsdeel_2020.geojson")
G = Path("data/gemeente_2020.geojson")
for path, layer in ((B, "landsdeel"), (G, "gemeente")):
    if not path.exists():
        path.write_bytes(requests.get("https://service.pdok.nl/cbs/gebiedsindelingen/2020/wfs/v1_0", timeout=300, params={
            "service": "WFS", "version": "2.0.0", "request": "GetFeature", "typeName": f"gebiedsindelingen:{layer}_gegeneraliseerd",
            "outputFormat": "application/json", "srsName": "EPSG:28992"}).content)
NL, GM = gpd.read_file(B).dissolve(), gpd.read_file(G)
def clip(path):
    with rasterio.open(path) as src:
        a, transform = mask(src, NL.to_crs(src.crs).geometry, crop=True, filled=False)
    return a[0], transform
print({kind: rasterio.open(merged(kind, 2020)).shape for kind in ("SMOD", "POP")})
{'SMOD': (2000, 1000), 'POP': (2000, 1000)}

Each merged layer is 2,000 by 1,000 cells, the two 1,000 km tiles stacked.

Example 1: 1975 against 2020

The same rule applied to the 1975 epoch shows how the country's settlement pattern moved:

def degrees(year):
    s, _ = clip(merged("SMOD", year)); p, _ = clip(merged("POP", year))
    k = ~s.mask & ~p.mask
    return pd.Series(p.data[k]).groupby(s.data[k] // 10).sum().rename(index=degree)
D = pd.DataFrame({y: degrees(y) for y in (1975, 2020)})
D["change"] = D[2020] - D[1975]
print(D.round(-3).astype(int).to_string(formatters={c: "{:,}".format for c in D.columns}))
print((D[[1975, 2020]] / D[[1975, 2020]].sum()).map("{:.1%}".format).to_string())
                                1975      2020    change
rural areas                2,501,000 2,727,000   226,000
towns and semi-dense areas 5,560,000 6,136,000   576,000
cities                     5,559,000 8,507,000 2,948,000
                             1975   2020
rural areas                 18.4%  15.7%
towns and semi-dense areas  40.8%  35.3%
cities                      40.8%  49.0%

Cities gained 2,948,000 people between 1975 and 2020, towns and semi-dense areas 576,000 and rural areas 226,000. The city share rose from 40.8% to 49.0%, a mix of growth inside 1975's cities and cells that crossed the thresholds into the city class, which a table by class cannot separate; comparing the two SMOD layers cell by cell can.

Map of the Netherlands on 1 km cells showing how the Degree of Urbanisation changed between 1975 and 2020: dark red cells were cities in both years, orange cells became city, yellow cells went from rural to town, and pink cells were towns in both years; the orange additions ring the Randstad cities and fill Almere. Map of the Netherlands on 1 km cells showing how the Degree of Urbanisation changed between 1975 and 2020: dark red cells were cities in both years, orange cells became city, yellow cells went from rural to town, and pink cells were towns in both years; the orange additions ring the Randstad cities and fill Almere.
Where the classes changed.

Density per land area and cluster thresholds

The Degree of Urbanisation rules applied to 1 km population cells. the rules, on 1 km cells1urban centre (30)1,500+ per km2, cluster of 50,000+2dense urban cluster (23)1,500+ per km2, 5,000 to 50,0003semi-dense cluster (22)900+ per km2, 2,500+ people4suburban, rural cluster (21, 13)rest of 300+ clusters; 500 to 5,0005low, very low density (12, 11)50+ per km2; under 50Same thresholds everywhere, so classes compare across countries spatialworkflow.io
The rules.

The classification works on 1 km cells, with densities measured per km² of permanent land. An urban centre is a cluster of cells with at least 1,500 inhabitants per km² totalling at least 50,000 people, its edge smoothed and holes under 15 km² filled; GHSL also admits cells with a high built-up density, so that office parks and malls do not split a city. A dense urban cluster meets the same density with 5,000 to 50,000 people. Urban clusters at the first level are contiguous cells of at least 300 per km² totalling 5,000; within them, semi-dense clusters need 900 per km² and 2,500 people at least 2 km from a denser cluster, and the rest are suburban or peri-urban. Outside urban clusters, rural clusters have 300 per km² and 500 to 5,000 people, and the remaining cells are low density rural (50 or more per km²) or very low density rural.

Because the thresholds are fixed and the grid is the same everywhere, the classes mean the same thing in the Netherlands as in Nigeria, which national definitions do not. The cost is that the classes depend on the population grid's errors and on where its cell edges fall; a town split by a cell edge can drop below a threshold. For local planning, national definitions and finer grids remain better; for comparing countries or years, this is the standard.

Versions, Mollweide and two tiles

Using GHS-SMOD Degree of Urbanisation: problems and fixes. Degree of Urbanisation trapsTrapInsteadone tile onlymerge R3 and R4: NL crosses thembilinear resampling of codesnearest or mode onlyV1-0 filesV2-0: V1-0 is marked obsoletecentres named by centroidby the municipalities holding thepeopleGHS-SMOD R2023A, 1 km, World Mollweide spatialworkflow.io
Degree of Urbanisation traps.
  • Version: GHS-SMOD R2023A exists as V1-0 and V2-0; the JRC marks V1-0 obsolete, so use V2-0 and say so.
  • Mollweide only at 1 km: SMOD is published in Mollweide; reproject vector data to ESRI:54009 rather than warping the classes, which are categorical and must not be resampled bilinearly.
  • Two tiles: the Netherlands crosses the boundary between tile rows R3 and R4; merge before masking, or half the country is missing.
  • Degree of Urbanisation for areas: the method also classifies local administrative units by where their population lives; the GHSL publishes that separately.

FAQ

What is GHS-SMOD?

The GHSL settlement model layer: the Degree of Urbanisation classes for every 1 km cell of the globe, for each epoch from 1975 to 2030, computed from GHS-POP and GHS-BUILT.

What do the SMOD codes mean?

The first digit is the degree, 3 city, 2 town or semi-dense area, 1 rural; the second is the type, from 30 urban centre to 11 very low density rural, with 10 for water.

What share of the Dutch population lives in cities?

In GHS-SMOD and GHS-POP 2020, 49.0% of the Dutch population lives in the 45 urban centres, and 15.7% in rural areas.

Can I resample SMOD to another grid?

Only with nearest-neighbour or a mode filter; the values are codes, not quantities.