sdg11 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sdg11/__init__.py +73 -0
- sdg11/_result.py +117 -0
- sdg11/air_quality.py +166 -0
- sdg11/citations.py +160 -0
- sdg11/cli.py +127 -0
- sdg11/components.py +114 -0
- sdg11/config.py +258 -0
- sdg11/degurba.py +324 -0
- sdg11/geometry.py +159 -0
- sdg11/housing.py +265 -0
- sdg11/io.py +267 -0
- sdg11/land.py +192 -0
- sdg11/network.py +159 -0
- sdg11/open_space.py +227 -0
- sdg11/py.typed +0 -0
- sdg11/report.py +197 -0
- sdg11/synthetic.py +168 -0
- sdg11/transport.py +210 -0
- sdg11-0.1.0.dist-info/METADATA +332 -0
- sdg11-0.1.0.dist-info/RECORD +24 -0
- sdg11-0.1.0.dist-info/WHEEL +5 -0
- sdg11-0.1.0.dist-info/entry_points.txt +2 -0
- sdg11-0.1.0.dist-info/licenses/LICENSE +21 -0
- sdg11-0.1.0.dist-info/top_level.txt +1 -0
sdg11/__init__.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""sdg11 - UN Sustainable Development Goal 11 urban indicators from open data.
|
|
2
|
+
|
|
3
|
+
Indicators follow the published UN-Habitat / WHO metadata and the Degree of
|
|
4
|
+
Urbanisation manual. This is an independent open-source implementation, not an
|
|
5
|
+
official UN product.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__version__ = "0.1.0"
|
|
11
|
+
|
|
12
|
+
from ._result import IndicatorResult
|
|
13
|
+
from .air_quality import (
|
|
14
|
+
WHO_2021_ANNUAL,
|
|
15
|
+
annual_mean,
|
|
16
|
+
assign_nearest_station,
|
|
17
|
+
population_weighted_concentration,
|
|
18
|
+
)
|
|
19
|
+
from .citations import ALL_CITATIONS, Citation
|
|
20
|
+
from .components import label
|
|
21
|
+
from .config import run_config
|
|
22
|
+
from .degurba import (
|
|
23
|
+
RURAL,
|
|
24
|
+
URBAN_CENTRE,
|
|
25
|
+
URBAN_CLUSTER,
|
|
26
|
+
DegurbaResult,
|
|
27
|
+
classify_degurba,
|
|
28
|
+
)
|
|
29
|
+
from .geometry import project_lonlat
|
|
30
|
+
from .housing import is_overcrowded, slum_proportion
|
|
31
|
+
from .land import built_up_area_km2, land_consumption, land_consumption_from_grids
|
|
32
|
+
from .network import WalkNetwork
|
|
33
|
+
from .open_space import (
|
|
34
|
+
average_open_space_share,
|
|
35
|
+
open_space_access,
|
|
36
|
+
open_space_share,
|
|
37
|
+
polygons_area_km2,
|
|
38
|
+
)
|
|
39
|
+
from .report import Report, report
|
|
40
|
+
from .transport import capacity_class, public_transport_access
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"__version__",
|
|
44
|
+
"ALL_CITATIONS",
|
|
45
|
+
"Citation",
|
|
46
|
+
"DegurbaResult",
|
|
47
|
+
"IndicatorResult",
|
|
48
|
+
"Report",
|
|
49
|
+
"RURAL",
|
|
50
|
+
"URBAN_CENTRE",
|
|
51
|
+
"URBAN_CLUSTER",
|
|
52
|
+
"WHO_2021_ANNUAL",
|
|
53
|
+
"WalkNetwork",
|
|
54
|
+
"annual_mean",
|
|
55
|
+
"assign_nearest_station",
|
|
56
|
+
"average_open_space_share",
|
|
57
|
+
"built_up_area_km2",
|
|
58
|
+
"capacity_class",
|
|
59
|
+
"classify_degurba",
|
|
60
|
+
"is_overcrowded",
|
|
61
|
+
"label",
|
|
62
|
+
"land_consumption",
|
|
63
|
+
"land_consumption_from_grids",
|
|
64
|
+
"open_space_access",
|
|
65
|
+
"open_space_share",
|
|
66
|
+
"polygons_area_km2",
|
|
67
|
+
"population_weighted_concentration",
|
|
68
|
+
"project_lonlat",
|
|
69
|
+
"public_transport_access",
|
|
70
|
+
"report",
|
|
71
|
+
"run_config",
|
|
72
|
+
"slum_proportion",
|
|
73
|
+
]
|
sdg11/_result.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Result container shared by every indicator function."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from .citations import Citation
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _jsonable(value: Any) -> Any:
|
|
16
|
+
"""Convert numpy scalars/arrays and nested containers into JSON-safe values."""
|
|
17
|
+
if isinstance(value, Citation):
|
|
18
|
+
return value.to_dict()
|
|
19
|
+
if isinstance(value, dict):
|
|
20
|
+
return {str(k): _jsonable(v) for k, v in value.items()}
|
|
21
|
+
if isinstance(value, (list, tuple)):
|
|
22
|
+
return [_jsonable(v) for v in value]
|
|
23
|
+
if isinstance(value, np.ndarray):
|
|
24
|
+
return _jsonable(value.tolist())
|
|
25
|
+
if isinstance(value, np.generic):
|
|
26
|
+
return _jsonable(value.item())
|
|
27
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
28
|
+
return None
|
|
29
|
+
return value
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class IndicatorResult:
|
|
34
|
+
"""Outcome of one SDG 11 indicator computation.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
indicator: Official indicator code, e.g. ``"11.2.1"``.
|
|
38
|
+
name: Short human-readable indicator name.
|
|
39
|
+
value: Headline value in ``unit`` (``None`` when it is mathematically undefined,
|
|
40
|
+
e.g. a zero population growth rate in 11.3.1).
|
|
41
|
+
unit: Unit of ``value`` (``"%"``, ``"ratio"``, ``"ug/m3"``...).
|
|
42
|
+
components: Intermediate quantities and companion metrics, keyed by name.
|
|
43
|
+
metadata: Indicator code, method version, parameters and citations.
|
|
44
|
+
how_to_report: Short guidance on reporting the value.
|
|
45
|
+
disaggregation: Optional breakdown of ``value`` by population group.
|
|
46
|
+
warnings: Data-quality notes raised during the computation.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
indicator: str
|
|
50
|
+
name: str
|
|
51
|
+
value: Optional[float]
|
|
52
|
+
unit: str
|
|
53
|
+
components: Dict[str, Any] = field(default_factory=dict)
|
|
54
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
55
|
+
how_to_report: str = ""
|
|
56
|
+
disaggregation: Dict[str, Any] = field(default_factory=dict)
|
|
57
|
+
warnings: List[str] = field(default_factory=list)
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def citations(self) -> List[Citation]:
|
|
61
|
+
"""Citations attached to the method."""
|
|
62
|
+
cites = self.metadata.get("citations", [])
|
|
63
|
+
return [c for c in cites if isinstance(c, Citation)]
|
|
64
|
+
|
|
65
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
66
|
+
"""Return a JSON-serialisable dictionary."""
|
|
67
|
+
out: Dict[str, Any] = {
|
|
68
|
+
"indicator": self.indicator,
|
|
69
|
+
"name": self.name,
|
|
70
|
+
"value": self.value,
|
|
71
|
+
"unit": self.unit,
|
|
72
|
+
"components": self.components,
|
|
73
|
+
"disaggregation": self.disaggregation,
|
|
74
|
+
"metadata": self.metadata,
|
|
75
|
+
"how_to_report": self.how_to_report,
|
|
76
|
+
"warnings": self.warnings,
|
|
77
|
+
}
|
|
78
|
+
result = _jsonable(out)
|
|
79
|
+
assert isinstance(result, dict)
|
|
80
|
+
return result
|
|
81
|
+
|
|
82
|
+
def to_json(self, indent: Optional[int] = 2) -> str:
|
|
83
|
+
"""Serialise to a JSON string."""
|
|
84
|
+
return json.dumps(self.to_dict(), indent=indent, ensure_ascii=False)
|
|
85
|
+
|
|
86
|
+
def formatted_value(self, digits: int = 2) -> str:
|
|
87
|
+
"""Value rounded for tables, or ``"n/a"`` when undefined."""
|
|
88
|
+
if self.value is None or not math.isfinite(self.value):
|
|
89
|
+
return "n/a"
|
|
90
|
+
return f"{self.value:.{digits}f}"
|
|
91
|
+
|
|
92
|
+
def __str__(self) -> str:
|
|
93
|
+
unit = "" if self.unit in ("", "ratio") else f" {self.unit}"
|
|
94
|
+
return f"SDG {self.indicator} {self.name}: {self.formatted_value()}{unit}"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def build_metadata(
|
|
98
|
+
indicator: str,
|
|
99
|
+
method_version: str,
|
|
100
|
+
citations: List[Citation],
|
|
101
|
+
parameters: Optional[Dict[str, Any]] = None,
|
|
102
|
+
) -> Dict[str, Any]:
|
|
103
|
+
"""Assemble the standard metadata block used by every result."""
|
|
104
|
+
from . import __version__
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
"indicator": indicator,
|
|
108
|
+
"method_version": method_version,
|
|
109
|
+
"software": f"sdg11 {__version__}",
|
|
110
|
+
"parameters": dict(parameters or {}),
|
|
111
|
+
"citations": list(citations),
|
|
112
|
+
"disclaimer": (
|
|
113
|
+
"Computed with the open-source sdg11 package; not an official UN or "
|
|
114
|
+
"UN-Habitat product. Validate against national statistical procedures "
|
|
115
|
+
"before official reporting."
|
|
116
|
+
),
|
|
117
|
+
}
|
sdg11/air_quality.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""SDG 11.6.2 - Annual mean levels of fine particulate matter in cities (population weighted).
|
|
2
|
+
|
|
3
|
+
The custodian agency (WHO) defines the indicator as the annual mean concentration of
|
|
4
|
+
PM2.5 (or PM10) to which the urban population is exposed, weighted by population::
|
|
5
|
+
|
|
6
|
+
C_pw = sum(P_i * C_i) / sum(P_i)
|
|
7
|
+
|
|
8
|
+
over the population units ``i`` (grid cells or neighbourhoods) of the city. Exposure
|
|
9
|
+
is compared with the 2021 WHO air quality guideline levels and interim targets.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
|
|
18
|
+
from ._result import IndicatorResult, build_metadata
|
|
19
|
+
from .citations import WHO_11_6_2, WHO_AQG_2021
|
|
20
|
+
from .geometry import as_points, nearest
|
|
21
|
+
|
|
22
|
+
METHOD_VERSION = "WHO 11.6.2 metadata, population-weighted annual mean (sdg11 v1)"
|
|
23
|
+
|
|
24
|
+
#: 2021 WHO annual-mean levels in ug/m3: interim targets 1-4 and the guideline (AQG).
|
|
25
|
+
WHO_2021_ANNUAL: Dict[str, Dict[str, float]] = {
|
|
26
|
+
"PM2.5": {"IT-1": 35.0, "IT-2": 25.0, "IT-3": 15.0, "IT-4": 10.0, "AQG": 5.0},
|
|
27
|
+
"PM10": {"IT-1": 70.0, "IT-2": 50.0, "IT-3": 30.0, "IT-4": 20.0, "AQG": 15.0},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
HOW_TO_REPORT = (
|
|
31
|
+
"Report the population-weighted annual mean concentration in ug/m3 for the city "
|
|
32
|
+
"(urban centre), the pollutant (PM2.5 preferred), reference year, concentration source "
|
|
33
|
+
"(monitors, satellite-derived model or fusion) and population source. Add the share of "
|
|
34
|
+
"the population exposed above the WHO 2021 guideline and interim targets."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _pollutant_key(pollutant: str) -> str:
|
|
39
|
+
key = pollutant.strip().upper().replace("_", ".").replace(" ", "")
|
|
40
|
+
if key in ("PM2.5", "PM25"):
|
|
41
|
+
return "PM2.5"
|
|
42
|
+
if key == "PM10":
|
|
43
|
+
return "PM10"
|
|
44
|
+
raise ValueError("pollutant must be 'PM2.5' or 'PM10'.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def annual_mean(
|
|
48
|
+
values: Any, expected_count: Optional[int] = None, min_coverage: float = 0.75
|
|
49
|
+
) -> Tuple[Optional[float], float]:
|
|
50
|
+
"""Annual mean of a monitor time series with a data-completeness check.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
values: Daily (or hourly) concentrations; ``NaN`` marks missing records.
|
|
54
|
+
expected_count: Records expected in the year (e.g. 365 or 8760). Defaults to the
|
|
55
|
+
length of ``values``.
|
|
56
|
+
min_coverage: Minimum share of valid records (75 % is the usual criterion).
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
``(mean, coverage)``; ``mean`` is ``None`` when coverage is insufficient.
|
|
60
|
+
"""
|
|
61
|
+
arr = np.asarray(values, dtype=float).ravel()
|
|
62
|
+
n_expected = int(expected_count) if expected_count is not None else arr.size
|
|
63
|
+
if n_expected <= 0:
|
|
64
|
+
raise ValueError("expected_count must be positive.")
|
|
65
|
+
valid = arr[np.isfinite(arr)]
|
|
66
|
+
coverage = valid.size / n_expected
|
|
67
|
+
if coverage < min_coverage or valid.size == 0:
|
|
68
|
+
return None, coverage
|
|
69
|
+
return float(valid.mean()), coverage
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def assign_nearest_station(
|
|
73
|
+
population_xy: Any,
|
|
74
|
+
station_xy: Any,
|
|
75
|
+
station_values: Any,
|
|
76
|
+
max_distance_m: float = float("inf"),
|
|
77
|
+
) -> np.ndarray:
|
|
78
|
+
"""Give each population point the value of its nearest monitoring station.
|
|
79
|
+
|
|
80
|
+
Points farther than ``max_distance_m`` from any station get ``NaN`` (and are then
|
|
81
|
+
excluded by :func:`population_weighted_concentration`).
|
|
82
|
+
"""
|
|
83
|
+
vals = np.asarray(station_values, dtype=float).ravel()
|
|
84
|
+
if vals.shape[0] != as_points(station_xy).shape[0]:
|
|
85
|
+
raise ValueError("station_values must have one value per station.")
|
|
86
|
+
dist, idx = nearest(population_xy, station_xy)
|
|
87
|
+
out = np.full(dist.shape[0], np.nan)
|
|
88
|
+
ok = (idx >= 0) & (dist <= max_distance_m)
|
|
89
|
+
out[ok] = vals[idx[ok]]
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def population_weighted_concentration(
|
|
94
|
+
concentration: Any,
|
|
95
|
+
population: Any,
|
|
96
|
+
pollutant: str = "PM2.5",
|
|
97
|
+
year: Optional[int] = None,
|
|
98
|
+
) -> IndicatorResult:
|
|
99
|
+
"""Population-weighted annual mean PM concentration (SDG 11.6.2).
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
concentration: Annual mean concentration (ug/m3) per population unit; any shape,
|
|
103
|
+
``NaN`` for missing values.
|
|
104
|
+
population: Population per unit, same shape as ``concentration``.
|
|
105
|
+
pollutant: ``"PM2.5"`` or ``"PM10"``.
|
|
106
|
+
year: Optional reference year stored in the metadata.
|
|
107
|
+
"""
|
|
108
|
+
key = _pollutant_key(pollutant)
|
|
109
|
+
c = np.asarray(concentration, dtype=float).ravel()
|
|
110
|
+
p = np.asarray(population, dtype=float).ravel()
|
|
111
|
+
if c.shape != p.shape:
|
|
112
|
+
raise ValueError("concentration and population must have the same shape.")
|
|
113
|
+
p = np.where(np.isfinite(p) & (p > 0), p, 0.0)
|
|
114
|
+
ok = np.isfinite(c) & (c >= 0)
|
|
115
|
+
warnings: List[str] = []
|
|
116
|
+
excluded = float(p[~ok].sum())
|
|
117
|
+
total = float(p.sum())
|
|
118
|
+
if excluded > 0:
|
|
119
|
+
warnings.append(
|
|
120
|
+
f"{excluded:.0f} residents ({100.0 * excluded / total:.1f} %) live in units without "
|
|
121
|
+
"a valid concentration and were excluded."
|
|
122
|
+
)
|
|
123
|
+
covered = float(p[ok].sum())
|
|
124
|
+
value: Optional[float] = None
|
|
125
|
+
if covered > 0:
|
|
126
|
+
value = float((p[ok] * c[ok]).sum() / covered)
|
|
127
|
+
else:
|
|
128
|
+
warnings.append("No population with a valid concentration; the indicator is undefined.")
|
|
129
|
+
exceed: Dict[str, Any] = {}
|
|
130
|
+
for level, threshold in WHO_2021_ANNUAL[key].items():
|
|
131
|
+
above = ok & (c > threshold)
|
|
132
|
+
exceed[level] = {
|
|
133
|
+
"threshold_ug_m3": threshold,
|
|
134
|
+
"population_above": float(p[above].sum()),
|
|
135
|
+
"share_above_pct": 100.0 * float(p[above].sum()) / covered if covered else None,
|
|
136
|
+
}
|
|
137
|
+
components: Dict[str, Any] = {
|
|
138
|
+
"pollutant": key,
|
|
139
|
+
"population_weighted_mean_ug_m3": value,
|
|
140
|
+
"unweighted_mean_ug_m3": float(c[ok].mean()) if ok.any() else None,
|
|
141
|
+
"min_ug_m3": float(c[ok].min()) if ok.any() else None,
|
|
142
|
+
"max_ug_m3": float(c[ok].max()) if ok.any() else None,
|
|
143
|
+
"total_population": total,
|
|
144
|
+
"population_with_data": covered,
|
|
145
|
+
"n_units": int(c.size),
|
|
146
|
+
"n_units_with_data": int(ok.sum()),
|
|
147
|
+
"who_2021_exceedance": exceed,
|
|
148
|
+
"ratio_to_who_guideline": (
|
|
149
|
+
value / WHO_2021_ANNUAL[key]["AQG"] if value is not None else None
|
|
150
|
+
),
|
|
151
|
+
}
|
|
152
|
+
return IndicatorResult(
|
|
153
|
+
indicator="11.6.2",
|
|
154
|
+
name=f"Population-weighted annual mean {key}",
|
|
155
|
+
value=value,
|
|
156
|
+
unit="ug/m3",
|
|
157
|
+
components=components,
|
|
158
|
+
metadata=build_metadata(
|
|
159
|
+
"11.6.2",
|
|
160
|
+
METHOD_VERSION,
|
|
161
|
+
[WHO_11_6_2, WHO_AQG_2021],
|
|
162
|
+
{"pollutant": key, "year": year},
|
|
163
|
+
),
|
|
164
|
+
how_to_report=HOW_TO_REPORT,
|
|
165
|
+
warnings=warnings,
|
|
166
|
+
)
|
sdg11/citations.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Bibliographic references for the methods implemented in :mod:`sdg11`.
|
|
2
|
+
|
|
3
|
+
The indicator metadata documents are maintained by the custodian agencies on the
|
|
4
|
+
UN Statistics Division SDG metadata repository. They are revised from time to
|
|
5
|
+
time; always check the current version before official reporting.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import asdict, dataclass
|
|
11
|
+
from typing import Dict, Optional
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class Citation:
|
|
16
|
+
"""A single reference."""
|
|
17
|
+
|
|
18
|
+
key: str
|
|
19
|
+
authors: str
|
|
20
|
+
year: str
|
|
21
|
+
title: str
|
|
22
|
+
publisher: str
|
|
23
|
+
url: Optional[str] = None
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> Dict[str, Optional[str]]:
|
|
26
|
+
return asdict(self)
|
|
27
|
+
|
|
28
|
+
def __str__(self) -> str:
|
|
29
|
+
text = f"{self.authors} ({self.year}). {self.title}. {self.publisher}."
|
|
30
|
+
if self.url:
|
|
31
|
+
text += f" {self.url}"
|
|
32
|
+
return text
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
_METADATA = "https://unstats.un.org/sdgs/metadata/files/"
|
|
36
|
+
|
|
37
|
+
UN_HABITAT_11_1_1 = Citation(
|
|
38
|
+
key="unhabitat-11.1.1",
|
|
39
|
+
authors="UN-Habitat",
|
|
40
|
+
year="current",
|
|
41
|
+
title=(
|
|
42
|
+
"SDG indicator metadata 11.1.1: Proportion of urban population living in slums, "
|
|
43
|
+
"informal settlements or inadequate housing"
|
|
44
|
+
),
|
|
45
|
+
publisher="United Nations Statistics Division, SDG metadata repository",
|
|
46
|
+
url=_METADATA + "Metadata-11-01-01.pdf",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
UN_HABITAT_11_2_1 = Citation(
|
|
50
|
+
key="unhabitat-11.2.1",
|
|
51
|
+
authors="UN-Habitat",
|
|
52
|
+
year="current",
|
|
53
|
+
title=(
|
|
54
|
+
"SDG indicator metadata 11.2.1: Proportion of population that has convenient "
|
|
55
|
+
"access to public transport, by sex, age and persons with disabilities"
|
|
56
|
+
),
|
|
57
|
+
publisher="United Nations Statistics Division, SDG metadata repository",
|
|
58
|
+
url=_METADATA + "Metadata-11-02-01.pdf",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
UN_HABITAT_11_3_1 = Citation(
|
|
62
|
+
key="unhabitat-11.3.1",
|
|
63
|
+
authors="UN-Habitat",
|
|
64
|
+
year="current",
|
|
65
|
+
title=(
|
|
66
|
+
"SDG indicator metadata 11.3.1: Ratio of land consumption rate to population growth rate"
|
|
67
|
+
),
|
|
68
|
+
publisher="United Nations Statistics Division, SDG metadata repository",
|
|
69
|
+
url=_METADATA + "Metadata-11-03-01.pdf",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
WHO_11_6_2 = Citation(
|
|
73
|
+
key="who-11.6.2",
|
|
74
|
+
authors="World Health Organization",
|
|
75
|
+
year="current",
|
|
76
|
+
title=(
|
|
77
|
+
"SDG indicator metadata 11.6.2: Annual mean levels of fine particulate matter "
|
|
78
|
+
"(e.g. PM2.5 and PM10) in cities (population weighted)"
|
|
79
|
+
),
|
|
80
|
+
publisher="United Nations Statistics Division, SDG metadata repository",
|
|
81
|
+
url=_METADATA + "Metadata-11-06-02.pdf",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
WHO_AQG_2021 = Citation(
|
|
85
|
+
key="who-aqg-2021",
|
|
86
|
+
authors="World Health Organization",
|
|
87
|
+
year="2021",
|
|
88
|
+
title=(
|
|
89
|
+
"WHO global air quality guidelines: particulate matter (PM2.5 and PM10), ozone, "
|
|
90
|
+
"nitrogen dioxide, sulfur dioxide and carbon monoxide"
|
|
91
|
+
),
|
|
92
|
+
publisher="World Health Organization, Geneva",
|
|
93
|
+
url="https://www.who.int/publications/i/item/9789240034228",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
UN_HABITAT_11_7_1 = Citation(
|
|
97
|
+
key="unhabitat-11.7.1",
|
|
98
|
+
authors="UN-Habitat",
|
|
99
|
+
year="current",
|
|
100
|
+
title=(
|
|
101
|
+
"SDG indicator metadata 11.7.1: Average share of the built-up area of cities that "
|
|
102
|
+
"is open space for public use for all, by sex, age and persons with disabilities"
|
|
103
|
+
),
|
|
104
|
+
publisher="United Nations Statistics Division, SDG metadata repository",
|
|
105
|
+
url=_METADATA + "Metadata-11-07-01.pdf",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
DEGURBA_MANUAL_2021 = Citation(
|
|
109
|
+
key="degurba-manual-2021",
|
|
110
|
+
authors="European Commission, FAO, ILO, OECD, UN-Habitat and World Bank",
|
|
111
|
+
year="2021",
|
|
112
|
+
title=(
|
|
113
|
+
"Applying the Degree of Urbanisation: A methodological manual to define cities, "
|
|
114
|
+
"towns and rural areas for international comparisons (2021 edition)"
|
|
115
|
+
),
|
|
116
|
+
publisher="Publications Office of the European Union, Luxembourg",
|
|
117
|
+
url="https://ec.europa.eu/eurostat/web/products-manuals-and-guidelines/-/ks-02-20-499",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
DIJKSTRA_2021 = Citation(
|
|
121
|
+
key="dijkstra-2021",
|
|
122
|
+
authors="Dijkstra, L., Florczyk, A. J., Freire, S., Kemper, T., Melchiorri, M., "
|
|
123
|
+
"Pesaresi, M. and Schiavina, M.",
|
|
124
|
+
year="2021",
|
|
125
|
+
title=(
|
|
126
|
+
"Applying the Degree of Urbanisation to the globe: A new harmonised definition "
|
|
127
|
+
"reveals a different picture of global urbanisation"
|
|
128
|
+
),
|
|
129
|
+
publisher="Journal of Urban Economics 125, 103312",
|
|
130
|
+
url="https://doi.org/10.1016/j.jue.2020.103312",
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
UNSC_2020_DEGURBA = Citation(
|
|
134
|
+
key="unsc-51-2020",
|
|
135
|
+
authors="United Nations Statistical Commission",
|
|
136
|
+
year="2020",
|
|
137
|
+
title=(
|
|
138
|
+
"Report on the fifty-first session: endorsement of the Degree of Urbanisation as the "
|
|
139
|
+
"recommended "
|
|
140
|
+
"method for delineating cities, urban and rural areas for international "
|
|
141
|
+
"statistical comparisons"
|
|
142
|
+
),
|
|
143
|
+
publisher="UN Statistical Commission, 51st session",
|
|
144
|
+
url="https://unstats.un.org/unsd/statcom/51st-session/",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
ALL_CITATIONS = {
|
|
148
|
+
c.key: c
|
|
149
|
+
for c in (
|
|
150
|
+
UN_HABITAT_11_1_1,
|
|
151
|
+
UN_HABITAT_11_2_1,
|
|
152
|
+
UN_HABITAT_11_3_1,
|
|
153
|
+
WHO_11_6_2,
|
|
154
|
+
WHO_AQG_2021,
|
|
155
|
+
UN_HABITAT_11_7_1,
|
|
156
|
+
DEGURBA_MANUAL_2021,
|
|
157
|
+
DIJKSTRA_2021,
|
|
158
|
+
UNSC_2020_DEGURBA,
|
|
159
|
+
)
|
|
160
|
+
}
|
sdg11/cli.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Command-line interface: ``sdg11 degurba``, ``report``, ``demo`` and ``citations``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Optional
|
|
10
|
+
|
|
11
|
+
from . import __version__, io
|
|
12
|
+
from .citations import ALL_CITATIONS
|
|
13
|
+
from .config import run_config
|
|
14
|
+
from .degurba import classify_degurba
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _cmd_degurba(args: argparse.Namespace) -> int:
|
|
18
|
+
grid, area, info = io.read_grid(args.grid)
|
|
19
|
+
cell = args.cell_area_km2 if args.cell_area_km2 is not None else area
|
|
20
|
+
if cell is None:
|
|
21
|
+
print(
|
|
22
|
+
"Cell area unknown for this format; assuming 1 km2 (use --cell-area-km2).",
|
|
23
|
+
file=sys.stderr,
|
|
24
|
+
)
|
|
25
|
+
cell = 1.0
|
|
26
|
+
result = classify_degurba(
|
|
27
|
+
grid,
|
|
28
|
+
cell_area_km2=cell,
|
|
29
|
+
fill_gaps=not args.no_gap_fill,
|
|
30
|
+
backend=args.backend,
|
|
31
|
+
)
|
|
32
|
+
print(f"Degree of Urbanisation (level 1) for {args.grid}")
|
|
33
|
+
print(f"Grid {grid.shape[0]} x {grid.shape[1]} cells, cell area {cell:g} km2")
|
|
34
|
+
print(
|
|
35
|
+
f"Urban centres: {result.components['n_urban_centres']}, "
|
|
36
|
+
f"urban clusters: {result.components['n_urban_clusters']}"
|
|
37
|
+
)
|
|
38
|
+
print()
|
|
39
|
+
print(result.summary_table())
|
|
40
|
+
for w in result.warnings:
|
|
41
|
+
print(f"warning: {w}", file=sys.stderr)
|
|
42
|
+
if args.out:
|
|
43
|
+
io.write_grid(args.out, result.classes, info)
|
|
44
|
+
print(
|
|
45
|
+
f"\nClass grid written to {args.out} (3 urban centre, 2 urban cluster, 1 rural, 0 no data)"
|
|
46
|
+
)
|
|
47
|
+
if args.json:
|
|
48
|
+
Path(args.json).write_text(json.dumps(result.to_dict(), indent=2), encoding="utf-8")
|
|
49
|
+
print(f"Summary written to {args.json}")
|
|
50
|
+
return 0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _cmd_report(args: argparse.Namespace) -> int:
|
|
54
|
+
rep = run_config(args.config)
|
|
55
|
+
rep.save(json_path=args.json, markdown_path=args.markdown)
|
|
56
|
+
if args.json:
|
|
57
|
+
print(f"JSON report written to {args.json}", file=sys.stderr)
|
|
58
|
+
if args.markdown:
|
|
59
|
+
print(f"Markdown report written to {args.markdown}", file=sys.stderr)
|
|
60
|
+
if not args.quiet:
|
|
61
|
+
print(rep.to_markdown())
|
|
62
|
+
return 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _cmd_demo(args: argparse.Namespace) -> int:
|
|
66
|
+
from .synthetic import write_synthetic_city
|
|
67
|
+
|
|
68
|
+
path = write_synthetic_city(args.directory)
|
|
69
|
+
print(f"Synthetic example data written to {args.directory}")
|
|
70
|
+
print(f"Run: sdg11 report {path}")
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _cmd_citations(args: argparse.Namespace) -> int:
|
|
75
|
+
for c in ALL_CITATIONS.values():
|
|
76
|
+
print(f"- {c}")
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
81
|
+
parser = argparse.ArgumentParser(
|
|
82
|
+
prog="sdg11",
|
|
83
|
+
description="Compute UN SDG 11 urban indicators and the Degree of Urbanisation.",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument("--version", action="version", version=f"sdg11 {__version__}")
|
|
86
|
+
sub = parser.add_subparsers(dest="command")
|
|
87
|
+
|
|
88
|
+
p_deg = sub.add_parser("degurba", help="Classify a population grid (DEGURBA level 1).")
|
|
89
|
+
p_deg.add_argument("grid", help="Population grid: .tif (needs rasterio), .asc, .npy or .csv")
|
|
90
|
+
p_deg.add_argument("--cell-area-km2", type=float, default=None, help="Override the cell area.")
|
|
91
|
+
p_deg.add_argument("--out", help="Write the class grid (.tif, .asc, .npy or .csv).")
|
|
92
|
+
p_deg.add_argument("--json", help="Write the summary as JSON.")
|
|
93
|
+
p_deg.add_argument("--no-gap-fill", action="store_true", help="Skip urban-centre gap filling.")
|
|
94
|
+
p_deg.add_argument("--backend", choices=["auto", "scipy", "numpy"], default="auto")
|
|
95
|
+
p_deg.set_defaults(func=_cmd_degurba)
|
|
96
|
+
|
|
97
|
+
p_rep = sub.add_parser("report", help="Run every indicator in a TOML configuration.")
|
|
98
|
+
p_rep.add_argument("config", help="Path to the TOML configuration file.")
|
|
99
|
+
p_rep.add_argument("--json", help="Write the JSON report here.")
|
|
100
|
+
p_rep.add_argument("--markdown", help="Write the Markdown report here.")
|
|
101
|
+
p_rep.add_argument("--quiet", action="store_true", help="Do not print the Markdown report.")
|
|
102
|
+
p_rep.set_defaults(func=_cmd_report)
|
|
103
|
+
|
|
104
|
+
p_demo = sub.add_parser("demo", help="Write a synthetic example dataset and config.")
|
|
105
|
+
p_demo.add_argument("directory", help="Output folder (created if missing).")
|
|
106
|
+
p_demo.set_defaults(func=_cmd_demo)
|
|
107
|
+
|
|
108
|
+
p_cit = sub.add_parser("citations", help="List the methodological references.")
|
|
109
|
+
p_cit.set_defaults(func=_cmd_citations)
|
|
110
|
+
return parser
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv: Optional[List[str]] = None) -> int:
|
|
114
|
+
parser = build_parser()
|
|
115
|
+
args = parser.parse_args(argv)
|
|
116
|
+
if not getattr(args, "func", None):
|
|
117
|
+
parser.print_help()
|
|
118
|
+
return 1
|
|
119
|
+
try:
|
|
120
|
+
return int(args.func(args))
|
|
121
|
+
except (OSError, ValueError, KeyError, ImportError) as exc:
|
|
122
|
+
print(f"sdg11: error: {exc}", file=sys.stderr)
|
|
123
|
+
return 2
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__": # pragma: no cover
|
|
127
|
+
sys.exit(main())
|