Skip to main content

sdg11

Compute UN Sustainable Development Goal 11 urban indicators from open data, following the published UN-Habitat / WHO methodology.

PyPI Python License: MIT

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 IndicatorResult with 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. sdg11 is 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. sdg11 computes over whatever points or cells you pass; use classify_degurba to 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_m to densify long street segments. Crossing lines without a shared vertex are not connected.
  • Projected coordinates are assumed (metres). project_lonlat is a local equirectangular projection, adequate within roughly 20 km of the origin.
  • Open space for public use requires knowing which spaces are publicly accessible; sdg11 does 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

  1. UN-Habitat. SDG indicator metadata 11.1.1. https://unstats.un.org/sdgs/metadata/files/Metadata-11-01-01.pdf
  2. UN-Habitat. SDG indicator metadata 11.2.1. https://unstats.un.org/sdgs/metadata/files/Metadata-11-02-01.pdf
  3. UN-Habitat. SDG indicator metadata 11.3.1. https://unstats.un.org/sdgs/metadata/files/Metadata-11-03-01.pdf
  4. World Health Organization. SDG indicator metadata 11.6.2. https://unstats.un.org/sdgs/metadata/files/Metadata-11-06-02.pdf
  5. World Health Organization (2021). WHO global air quality guidelines: particulate matter (PM2.5 and PM10), ozone, nitrogen dioxide, sulfur dioxide and carbon monoxide. Geneva.
  6. UN-Habitat. SDG indicator metadata 11.7.1. https://unstats.un.org/sdgs/metadata/files/Metadata-11-07-01.pdf
  7. 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.
  8. 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
  9. 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.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sdg11 0.1.1
File Size Uploaded Provenance Actions
sdg11-0.1.1.tar.gz 64.6 kB With provenance Details for sdg11-0.1.1.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for sdg11 0.1.1
File Interpreter ABI Platform Actions
sdg11-0.1.1-py3-none-any.whl Python 3 none any Details for sdg11-0.1.1-py3-none-any.whl

Total release size: 117.2 kB

Release files / sdg11-0.1.1.tar.gz

Download URL sdg11-0.1.1.tar.gz
Size 64.6 kB
Tags Source
SHA-256 checksum
How to use checksums
f2bbcd3d574c0717586a36a5f8ef847aafbbb25444fff1e563b36f695e30afae
BLAKE2b-256 checksum
How to use checksums
c47d4cdebe8a3646fc61fca9aecdac5ce52403eb1e441f6a79b2d9f84cd4521a
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 log

Release files / sdg11-0.1.1-py3-none-any.whl

Download URL sdg11-0.1.1-py3-none-any.whl
Size 52.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8866600e414b6b1ee813dd952853276bfbacd1aee5997da79f146f58eea96811
BLAKE2b-256 checksum
How to use checksums
5ce81dd286a010d0d80d851e7aa5c0bfa3971963f83d0fcc6611b98948e6bedb
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 log

Security for sdg11 0.1.1

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.

Select from 2 files

Attestation for sdg11-0.1.1.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 log

A 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.

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page