fix: clarify population evolution semantics
This commit is contained in:
@@ -209,8 +209,8 @@ class VectorFeatureService:
|
||||
intersection_area = func.ST_Area(
|
||||
func.ST_Transform(func.ST_Intersection(VectorFeature.geometry, envelope), 31370)
|
||||
)
|
||||
value_expression = numeric_value * intersection_area / func.nullif(source_area, 0.0)
|
||||
is_estimate = True
|
||||
coverage_ratio = intersection_area / func.nullif(source_area, 0.0)
|
||||
value_expression = numeric_value * coverage_ratio
|
||||
aggregate_value = (
|
||||
db.query(func.coalesce(func.sum(value_expression), 0.0))
|
||||
.filter(*selection_filter)
|
||||
@@ -218,6 +218,16 @@ class VectorFeatureService:
|
||||
.scalar()
|
||||
)
|
||||
metric_value = float(aggregate_value or 0.0)
|
||||
if method == "area_weighted_sum":
|
||||
partial_feature_count = (
|
||||
db.query(func.count(VectorFeature.id))
|
||||
.filter(*selection_filter)
|
||||
.filter(coverage_ratio < 0.999999)
|
||||
.scalar()
|
||||
)
|
||||
is_estimate = bool(partial_feature_count)
|
||||
if not is_estimate and config.get("warning_only_when_estimate", True):
|
||||
warning = None
|
||||
elif method != "feature_count":
|
||||
raise AppError(
|
||||
code="INVALID_SELECTION_AGGREGATION",
|
||||
|
||||
@@ -38,6 +38,14 @@ class ScalarSession:
|
||||
return ScalarQuery(self.value)
|
||||
|
||||
|
||||
class SequenceScalarSession:
|
||||
def __init__(self, values: list[float]):
|
||||
self.values = iter(values)
|
||||
|
||||
def query(self, *args): # noqa: ANN002, ARG002
|
||||
return ScalarQuery(next(self.values))
|
||||
|
||||
|
||||
class VersionQuery:
|
||||
def __init__(self, latest: DatasetVersion | None):
|
||||
self.latest = latest
|
||||
@@ -183,6 +191,40 @@ def test_selection_area_aggregation_returns_hectares_without_loading_all_feature
|
||||
assert result["feature_count"] == 40
|
||||
|
||||
|
||||
def test_population_area_weighting_is_exact_for_full_features_and_estimated_for_partial_features() -> None:
|
||||
dataset = temporal_dataset(project_id=uuid4(), observed_year=2025, metric_method="area_weighted_sum")
|
||||
dataset.source_metadata["selection_aggregation"].update(
|
||||
{
|
||||
"property": "population_total",
|
||||
"label": "Inwoners",
|
||||
"unit": "inwoners",
|
||||
"warning": "Partial-sector estimate",
|
||||
"warning_only_when_estimate": True,
|
||||
}
|
||||
)
|
||||
bbox = {"min_x": 5.0, "min_y": 51.1, "max_x": 5.2, "max_y": 51.3, "crs": "EPSG:4326"}
|
||||
|
||||
full = VectorFeatureService.summarize_features_by_bbox(
|
||||
SequenceScalarSession([38_675.0, 0]),
|
||||
dataset=dataset,
|
||||
bbox=bbox,
|
||||
total_feature_count=49,
|
||||
)
|
||||
partial = VectorFeatureService.summarize_features_by_bbox(
|
||||
SequenceScalarSession([1_250.5, 2]),
|
||||
dataset=dataset,
|
||||
bbox=bbox,
|
||||
total_feature_count=3,
|
||||
)
|
||||
|
||||
assert full["metric_value"] == 38_675.0
|
||||
assert full["is_estimate"] is False
|
||||
assert full["warning"] is None
|
||||
assert partial["metric_value"] == 1_250.5
|
||||
assert partial["is_estimate"] is True
|
||||
assert partial["warning"] == "Partial-sector estimate"
|
||||
|
||||
|
||||
def test_temporal_compare_returns_delta_and_canonical_change_payload(monkeypatch) -> None:
|
||||
project_id = uuid4()
|
||||
earlier = temporal_dataset(project_id=project_id, observed_year=2021)
|
||||
@@ -244,6 +286,7 @@ def test_temporal_frontend_and_official_operator_contracts_exist() -> None:
|
||||
assert "Vergelijk periode" in workspace
|
||||
assert "/temporal/compare" in temporal_api
|
||||
assert "Statbel" in population and "area_weighted_sum" in population
|
||||
assert '"identity_stable": False' in population
|
||||
assert "HistLandgebruik" in landuse and "intersection_area" in landuse
|
||||
assert "<wfs:GetFeature" in landuse and "session.post(" in landuse
|
||||
assert 'lambda value: value.startswith("weg"), "weg*"' in landuse
|
||||
|
||||
@@ -7803,3 +7803,5 @@ Live deployment correction:
|
||||
- Vector upload persistence is now atomic across Dataset, DatasetVersion and VectorFeature rows, with storage cleanup on rollback. This prevents the failed-indexing orphan state observed during the live import.
|
||||
- Tower could read the historical WFS capabilities but its gateway rejected XML `FILTER` content embedded in a GET query. The operator now sends the same read-only WFS 2.0 GetFeature request as XML POST; a live two-feature forest page returned successfully before redeployment.
|
||||
- The 1778 edition classifies roads as `weg`, while later editions can use road subclasses. The exact server-side road filter now uses `weg*`, covering the documented class family without admitting unrelated themes.
|
||||
- Live population comparison returned 37,015 inhabitants for 2021 and 38,675 for 2025 (+1,660 / +4.48%). Full covered sectors now report exact source totals; only cut sectors are labelled area-weighted estimates.
|
||||
- Population sector identity is explicitly unstable across annual geometry editions, so source boundary/code changes are not presented as added/removed population objects.
|
||||
|
||||
@@ -42,6 +42,10 @@ Complete sectors use their published population total. A rectangle that cuts
|
||||
through a sector uses an explicitly labelled area-weighted estimate; the
|
||||
source does not justify a more precise intra-sector distribution.
|
||||
|
||||
Statistical-sector codes and boundaries can change between editions. GeoIntel
|
||||
therefore compares population metrics but does not present sector additions,
|
||||
removals or boundary changes as population-object lineage.
|
||||
|
||||
### Mol historical land use
|
||||
|
||||
`scripts/provision_mol_historical_landuse.py` uses the official Digitaal
|
||||
|
||||
@@ -225,14 +225,14 @@ def upload_snapshot(
|
||||
"nis_code": MUNICIPALITY_NIS_CODE,
|
||||
"attribution": ATTRIBUTION,
|
||||
"license": "CC BY 4.0",
|
||||
"identity_stable": True,
|
||||
"comparison_property": "population_total",
|
||||
"identity_stable": False,
|
||||
"identity_limitation": "Statistical-sector codes and boundaries can change between annual editions.",
|
||||
"selection_aggregation": {
|
||||
"method": "area_weighted_sum",
|
||||
"property": "population_total",
|
||||
"label": "Inwoners",
|
||||
"unit": "inwoners",
|
||||
"is_estimate": True,
|
||||
"warning_only_when_estimate": True,
|
||||
"warning": "Bevolking binnen een gedeeltelijke statistische sector is oppervlaktegewogen en blijft een schatting.",
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user