Metadata-Version: 2.4
Name: sdg11
Version: 0.1.0
Summary: UN SDG 11 urban indicators (11.1.1, 11.2.1, 11.3.1, 11.6.2, 11.7.1) and the Degree of Urbanisation from open data, following the UN-Habitat metadata.
Author-email: Yusuf Eminoğlu <yusufeminoglu@gmail.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/YusufEminoglu/sdg11_sdk
Project-URL: Repository, https://github.com/YusufEminoglu/sdg11_sdk
Project-URL: Issues, https://github.com/YusufEminoglu/sdg11_sdk/issues
Project-URL: Changelog, https://github.com/YusufEminoglu/sdg11_sdk/blob/main/CHANGELOG.md
Keywords: sdg,sdg11,sustainable-development-goals,un-habitat,urban-indicators,degree-of-urbanisation,degurba,public-transport-access,land-consumption,lcrpgr,open-space,air-quality,pm2.5,slums,gis,urban-planning
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Operating System :: OS Independent
Classifier: Topic :: Scientific/Engineering :: GIS
Classifier: Topic :: Sociology
Classifier: Intended Audience :: Science/Research
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy>=1.20.0
Requires-Dist: tomli>=1.1.0; python_version < "3.11"
Provides-Extra: fast
Requires-Dist: scipy>=1.7.0; extra == "fast"
Provides-Extra: geo
Requires-Dist: scipy>=1.7.0; extra == "geo"
Requires-Dist: rasterio>=1.3.0; extra == "geo"
Requires-Dist: geopandas>=0.12.0; extra == "geo"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Dynamic: license-file

# sdg11

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

[![PyPI](https://img.shields.io/pypi/v/sdg11.svg)](https://pypi.org/project/sdg11/)
[![Python](https://img.shields.io/pypi/pyversions/sdg11.svg)](https://pypi.org/project/sdg11/)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/YusufEminoglu/sdg11_sdk/blob/main/LICENSE)

`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

```bash
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)

```python
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:

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```python
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

```bash
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

```toml
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](https://unstats.un.org/sdgs/metadata/).

## 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

```bash
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](https://github.com/YusufEminoglu/sdg11_sdk/blob/main/CITATION.cff).

## License

MIT (c) Yusuf Eminoğlu
