sdg11
Compute UN Sustainable Development Goal 11 urban indicators from open data, following the published UN-Habitat / WHO methodology.
sdg11 is a small, dependency-light toolkit for city governments, NGOs, voluntary local
review (VLR) teams and researchers who need transparent, reproducible SDG 11 numbers from data
they already have: population points or grids, transit stops, street centrelines, park
polygons, built-up areas, air-quality grids and household surveys.
- Pure Python + NumPy. SciPy is used when installed (faster connected components and nearest neighbours) with a pure-NumPy fallback that gives identical results.
- Every indicator returns an
IndicatorResultwith the value, its components, the method version, parameters, citations and a short how to report note. sdg11.report(...)bundles results into JSON and a Markdown table.- Command line:
sdg11 degurba grid.tif,sdg11 report config.toml,sdg11 demo DIR.
Not an official UN product.
sdg11is an independent open-source implementation of publicly documented methods. It is not produced, endorsed or validated by the United Nations, UN-Habitat, WHO or the European Commission. Official SDG values are reported by national statistical systems; check results against the current metadata before official use.
Installation
pip install sdg11 # NumPy only
pip install "sdg11[fast]" # + SciPy (faster labelling and nearest-neighbour search)
pip install "sdg11[geo]" # + SciPy, rasterio (GeoTIFF I/O) and geopandas
Python 3.9 or newer.
Indicators
| Code | Indicator (paraphrased from the official metadata) | Function | Value |
|---|---|---|---|
| 11.1.1 | Proportion of the urban population living in slums, informal settlements or inadequate housing. A slum household lacks one or more of: improved water, improved sanitation, sufficient living area (no more than 3 persons per habitable room), durable housing, secure tenure. Inadequate housing adds affordability. [1] | slum_proportion |
% of population |
| 11.2.1 | Proportion of the population with convenient access to public transport: living within 500 m walking distance of a low-capacity stop (bus, tram...) or 1 000 m of a high-capacity station (rail, metro, ferry), by sex, age and disability. [2] | public_transport_access |
% of population |
| 11.3.1 | Ratio of land consumption rate to population growth rate (LCRPGR): LCR = ln(Urb_t2/Urb_t1)/y, PGR = ln(Pop_t2/Pop_t1)/y, LCRPGR = LCR/PGR, with built-up area per capita as a companion metric. [3] |
land_consumption, land_consumption_from_grids |
ratio |
| 11.6.2 | Annual mean levels of fine particulate matter (PM2.5 and PM10) in cities, population weighted: sum(P_i C_i) / sum(P_i), compared with the 2021 WHO guideline and interim targets. [4, 5] |
population_weighted_concentration |
ug/m3 |
| 11.7.1 | Average share of the built-up area of cities that is open space for public use (streets + open public spaces), and the share of the population within 400 m walking distance of an open public space. [6] | open_space_share, average_open_space_share, open_space_access |
% |
| DEGURBA | Degree of Urbanisation, level 1 grid classification: urban centres, urban clusters and rural grid cells from a 1 km2 population grid. [7, 8, 9] | classify_degurba |
class grid |
Degree of Urbanisation (level 1) as implemented
Following chapter 2 of Applying the Degree of Urbanisation (2021 manual) [7]:
| Class | Cell criterion | Contiguity | Cluster criterion | Post-processing |
|---|---|---|---|---|
| Urban centre (3) | density >= 1 500 inh/km2, or built-up share >= 50 % (optional) | 4-connectivity (no diagonals) | population >= 50 000 | majority rule (a cell with >= 5 of its 8 neighbours in the centre joins it, iterated until stable) and filling of enclosed gaps < 15 km2 |
| Urban cluster (2) | density >= 300 inh/km2 | 8-connectivity (diagonals included) | population >= 5 000 | urban centres are part of their cluster but keep class 3 |
| Rural grid cell (1) | all other valid cells | |||
| No data (0) | NaN or negative population | never added to a centre; a hole containing no-data cells is not filled |
All thresholds are keyword arguments, so national variants and sensitivity tests are one call away. The classification of spatial units (cities, towns and semi-dense areas, rural areas) and level 2 are not implemented yet.
Quick start
All coordinates must be in a projected CRS in metres (UTM, a national grid...). WGS84
GeoJSON can be converted with sdg11.project_lonlat or the lonlat_origin config key.
11.2.1 public transport (hand-checkable)
import sdg11
stops = [[0, 0], [10_000, 0]]
modes = ["bus", "metro"] # low / high capacity
homes = [[499, 0], [-501, 0], [10_000, 999], [10_000, -1_001]]
people = [1, 2, 4, 8]
r = sdg11.public_transport_access(homes, people, stops, modes)
print(r) # SDG 11.2.1 ...: 33.33 % (1 + 4 of 15 residents)
print(r.components["population_served_by_high_capacity"]) # 4.0
Walking distances along a street network:
net = sdg11.WalkNetwork.from_lines([[[0, 0], [0, 300], [400, 300], [400, 0]]])
r = sdg11.public_transport_access([[400, 0]], [10], [[0, 0]], ["bus"], network=net)
r.value # 0.0 -> 400 m as the crow flies, but 1 000 m along the street
Disaggregate with groups={"female": [...], "age_65_plus": [...], "disability": [...]} and
filter stops by service frequency with stop_departures_per_hour= and
min_departures_per_hour=3 (a 20-minute headway).
11.3.1 land consumption
r = sdg11.land_consumption(
urban_area_t1_km2=100, urban_area_t2_km2=121,
population_t1=1_000_000, population_t2=1_100_000,
year_t1=2015, year_t2=2025,
)
r.value # 2.0 (ln 1.21 = 2 ln 1.1)
r.components["built_up_area_per_capita_t2_m2"] # 110.0
r.components["interpretation"] # 'Built-up area grew faster than population: ...'
land_consumption_from_grids(built_up_t1, built_up_t2, pop_t1, pop_t2, 2015, 2025, cell_area_km2=0.01, extent=urban_centre_mask) does the same from boolean masks or 0-1
built-up shares (e.g. GHS-BUILT), keeping a fixed city extent for both dates.
11.6.2 air quality
r = sdg11.population_weighted_concentration([10, 20, 40], [1, 1, 2], pollutant="PM2.5")
r.value # 27.5 ug/m3
r.components["who_2021_exceedance"]["IT-1"]["share_above_pct"] # 50.0
Concentrations can be a model grid aligned with the population grid, or monitor annual means
assigned with assign_nearest_station(...). annual_mean(daily, expected_count=365) applies
the usual 75 % data-completeness check.
11.7.1 open public space
city_a = sdg11.open_space_share(built_up_area_km2=10, open_public_space_area_km2=1.5,
street_area_km2=2.5, city="A") # 40 %
city_b = sdg11.open_space_share(30, 3.0, 3.0, city="B") # 20 %
sdg11.average_open_space_share([city_a, city_b]).value # 30.0
park = [[[0, 0], [100, 0], [100, 100], [0, 100], [0, 0]]] # [exterior, holes...]
sdg11.open_space_access([[500, 50], [501, 50]], [1, 1], [park]).value # 50.0 (400 m vs 401 m)
11.1.1 housing
households = {
"persons": [4, 2, 6, 3, 5],
"rooms": [2, 1, 1, 3, 2], # > 3 persons per room = overcrowded
"improved_water": [1, 1, 0, 1, 1],
"improved_sanitation": ["yes", "yes", "no", "yes", "no"],
"durable_housing": [1, 1, 1, 1, 1],
"secure_tenure": [1, 1, 1, 0, 1],
}
sdg11.slum_proportion(households).value # 70.0 (6 + 3 + 5 of 20 people)
Flags mean has the condition. Survey weights (weight=), an urban filter (urban=) and the
affordability criterion (housing_cost=, income=, 30 % threshold) are supported; missing
deprivation columns are reported as not assessed (the result is then a lower bound).
Degree of Urbanisation
import numpy as np
grid = np.zeros((7, 7)); grid[2:5, 2:5] = 6000 # 9 km2, 54 000 people
res = sdg11.classify_degurba(grid, cell_area_km2=1.0)
res.classes # 3 inside the block, 1 elsewhere
print(res.summary_table())
Batch report
rep = sdg11.report([r, city_a, res], title="SDG 11 - My City", city="My City", year=2025)
print(rep.to_markdown())
rep.save(json_path="sdg11.json", markdown_path="sdg11.md")
Command line
sdg11 demo ./demo # write a synthetic city dataset + config.toml
sdg11 report ./demo/config.toml --json report.json --markdown report.md
sdg11 degurba population_1km.tif --out degurba.tif --json degurba.json
sdg11 citations
sdg11 degurba reads GeoTIFF (with the geo extra), ESRI ASCII grids (.asc), .npy and
.csv. The cell area is taken from the raster (projected CRS in metres, e.g. Mollweide for
GHS-POP) or set with --cell-area-km2.
Configuration file
title = "SDG 11 - Example City"
city = "Example City"
year = 2025
# lonlat_origin = [28.97, 41.01] # project WGS84 GeoJSON inputs to local metres
[degurba]
population_grid = "population_1km.tif"
["11.1.1"]
households = "households.csv" # column names as in slum_proportion(); override e.g. water = "h2o"
["11.2.1"]
population = "population_points.geojson" # Point features
population_field = "pop"
groups = ["female", "male"] # optional properties for disaggregation
stops = "stops.geojson"
capacity_field = "mode" # "low"/"high" or bus, tram, metro, rail, ferry...
network = "streets.geojson" # optional LineStrings; Euclidean otherwise
network_max_segment_m = 50
departures_field = "departures_per_hour" # optional
min_departures_per_hour = 3 # optional
["11.3.1"]
built_up_area_t1_km2 = 82.0 # or built_up_grid_t1 / built_up_grid_t2 + cell_area_km2
built_up_area_t2_km2 = 96.5
population_t1 = 640000 # numbers or grid paths
population_t2 = 700000
year_t1 = 2015
year_t2 = 2025
["11.6.2"]
pollutant = "PM2.5"
concentration_grid = "pm25.tif" # aligned with population_grid
population_grid = "population_100m.tif"
# or: population = "...geojson", stations = "...geojson", value_field = "annual_mean"
["11.7.1"]
built_up_area_km2 = 96.5
street_area_km2 = 18.0 # or streets = "street_polygons.geojson"
open_spaces = "open_spaces.geojson" # Polygons; area used when open_public_space_area_km2 is absent
population = "population_points.geojson" # enables the 400 m access measure
population_field = "pop"
network = "streets.geojson"
Relative paths are resolved against the configuration file. Run sdg11 demo DIR for a
complete working example.
Limitations
- Inputs define quality. The indicators are only as good as the population, stop, street, open-space and built-up data supplied; OpenStreetMap completeness varies widely.
- City extent. UN-Habitat recommends computing city indicators over the urban centre (or
urban extent) delineated with the Degree of Urbanisation.
sdg11computes over whatever points or cells you pass; useclassify_degurbato define the extent first. - Euclidean fallback overstates access compared with network walking distances; the method used is recorded in every result and a warning is raised.
- Network snapping is to the nearest network node; use
max_segment_mto densify long street segments. Crossing lines without a shared vertex are not connected. - Projected coordinates are assumed (metres).
project_lonlatis a local equirectangular projection, adequate within roughly 20 km of the origin. - Open space for public use requires knowing which spaces are publicly accessible;
sdg11does not judge accessibility, ownership or quality of spaces. - 11.1.1 relies on household-survey definitions of "improved" water and sanitation, durable housing and secure tenure, which differ between surveys; the five deprivations are used as coded in your data.
- DEGURBA level 2 and the classification of spatial units are not yet implemented. The thresholds are defined for 1 km2 cells; other resolutions trigger a warning.
- Methodologies are revised; check the current metadata on the UN SDG metadata repository.
References
- UN-Habitat. SDG indicator metadata 11.1.1. https://unstats.un.org/sdgs/metadata/files/Metadata-11-01-01.pdf
- UN-Habitat. SDG indicator metadata 11.2.1. https://unstats.un.org/sdgs/metadata/files/Metadata-11-02-01.pdf
- UN-Habitat. SDG indicator metadata 11.3.1. https://unstats.un.org/sdgs/metadata/files/Metadata-11-03-01.pdf
- World Health Organization. SDG indicator metadata 11.6.2. https://unstats.un.org/sdgs/metadata/files/Metadata-11-06-02.pdf
- World Health Organization (2021). WHO global air quality guidelines: particulate matter (PM2.5 and PM10), ozone, nitrogen dioxide, sulfur dioxide and carbon monoxide. Geneva.
- UN-Habitat. SDG indicator metadata 11.7.1. https://unstats.un.org/sdgs/metadata/files/Metadata-11-07-01.pdf
- European Commission, FAO, ILO, OECD, UN-Habitat and World Bank (2021). Applying the Degree of Urbanisation: A methodological manual to define cities, towns and rural areas for international comparisons. 2021 edition. Publications Office of the European Union, Luxembourg.
- Dijkstra, L. et al. (2021). Applying the Degree of Urbanisation to the globe: A new harmonised definition reveals a different picture of global urbanisation. Journal of Urban Economics 125, 103312. https://doi.org/10.1016/j.jue.2020.103312
- United Nations Statistical Commission (2020). Endorsement of the Degree of Urbanisation at its 51st session.
sdg11 citations prints the same list; every result carries its own citations in
result.metadata["citations"].
Development
pip install -e ".[dev,geo]"
python -m pytest --cov=sdg11
python -m ruff check . && python -m ruff format --check src tests
python -m mypy src
Citing
See CITATION.cff.
License
MIT (c) Yusuf Eminoğlu
Metadata
Release files for sdg11 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| sdg11-0.1.0.tar.gz | 64.5 kB | With provenance | Details for sdg11-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| sdg11-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for sdg11-0.1.0-py3-none-any.whl |
Total release size: 117.2 kB
Release files / sdg11-0.1.0.tar.gz
| Download URL | sdg11-0.1.0.tar.gz |
|---|---|
| Size | 64.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f687c2708fa640a1b8143e63743f4448ef08ada9b1646f6e791d5b5a5e4790a3
|
|
BLAKE2b-256 checksum How to use checksums |
00bd8c416aaa180884ff5ca3daa6cd3c1e2bda933e9c8b0a7a1b29f033ec15be
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logRelease files / sdg11-0.1.0-py3-none-any.whl
| Download URL | sdg11-0.1.0-py3-none-any.whl |
|---|---|
| Size | 52.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
40e4aa33c1ae30a5f139ad128ec290db3009460c587cb87030384323043f983b
|
|
BLAKE2b-256 checksum How to use checksums |
e67b159c846db99edd81d76c8514a2f388ad604d12b7eba4c0a27140fbb16147
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.16
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logSecurity for sdg11 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
Both files in this release have been uploaded with provenance metadata.
Attestation for sdg11-0.1.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.