"""CBS Open Data — gemeente demografie via OData.""" from __future__ import annotations import json import re import urllib.parse import urllib.request from typing import Any, Optional CBS_BASE = "https://opendata.cbs.nl/ODataApi/OData" _GEMEENTE_CACHE: dict[str, dict[str, Any]] = {} def _int_val(raw: Any) -> Optional[int]: if raw is None: return None s = str(raw).strip().replace(".", "") if not s or s == ".": return None try: return int(s) except ValueError: return None def _float_val(raw: Any) -> Optional[float]: if raw is None: return None s = str(raw).strip() if not s or s == ".": return None try: return float(s) except ValueError: return None def _fetch_untyped(dataset: str, filter_expr: str, top: int = 1) -> list[dict[str, Any]]: params = urllib.parse.urlencode( {"$filter": filter_expr, "$top": str(top), "$format": "json"}, quote_via=urllib.parse.quote, ) url = f"{CBS_BASE}/{dataset}/UntypedDataSet?{params}" with urllib.request.urlopen(url, timeout=45) as resp: data = json.loads(resp.read().decode()) return data.get("value", []) def _normalize_gm(code: str) -> str: code = (code or "").strip().upper() if code.startswith("GM"): return code digits = re.sub(r"\D", "", code) return f"GM{digits}" if digits else code def fetch_gemeente_stats(gemeente_code: str) -> Optional[dict[str, Any]]: gm = _normalize_gm(gemeente_code) if not gm: return None if gm in _GEMEENTE_CACHE: return _GEMEENTE_CACHE[gm] pop_rows = _fetch_untyped( "03759ned", f"RegioS eq '{gm}' and Leeftijd eq '10000' and Geslacht eq 'T001038' " f"and BurgerlijkeStaat eq 'T001019' and substringof('2024',Perioden)", ) income_rows = _fetch_untyped( "86005NED", f"RegioS eq '{gm}' and substringof('2023',Perioden) and Geslacht eq 'T001038'", ) area_rows = _fetch_untyped( "84583NED", f"startswith(WijkenEnBuurten,'{gm}') and SoortRegio_2 eq 'Gemeente '", ) population = _int_val(pop_rows[0].get("BevolkingOp1Januari_1")) if pop_rows else None avg_income = None median_income = None if income_rows: avg_income = _float_val(income_rows[0].get("GemiddeldPersoonlijkInkomen_6")) median_income = _float_val(income_rows[0].get("MediaanPersoonlijkInkomen_7")) if avg_income: avg_income *= 1000 if median_income: median_income *= 1000 area = area_rows[0] if area_rows else {} pop_area = _int_val(area.get("AantalInwoners_5")) or population households = _int_val(area.get("HuishoudensTotaal_28")) niet_westers = _int_val(area.get("NietWestersTotaal_18")) marokko = _int_val(area.get("Marokko_19")) turkije = _int_val(area.get("Turkije_22")) suriname = _int_val(area.get("Suriname_21")) avg_hh_size = _float_val(area.get("GemiddeldeHuishoudensgrootte_32")) income_per_inhabitant = _float_val(area.get("GemiddeldInkomenPerInwoner_72")) if income_per_inhabitant and not avg_income: avg_income = income_per_inhabitant * 1000 muslim_proxy_pct = None niet_westers_pct = None if pop_area and pop_area > 0: if marokko is not None and turkije is not None: muslim_proxy_pct = round((marokko + turkije) / pop_area * 100, 2) if niet_westers is not None: niet_westers_pct = round(niet_westers / pop_area * 100, 2) stats = { "gemeente_code": gm, "population": pop_area, "households": households, "avg_household_size": avg_hh_size, "avg_income": avg_income, "median_income": median_income, "unemployment_rate": None, "ethnic_composition": { "niet_westers_totaal": niet_westers, "niet_westers_pct": niet_westers_pct, "marokko": marokko, "turkije": turkije, "suriname": suriname, }, "religious_composition": { "muslim_proxy_pct": muslim_proxy_pct, "note": "Indicatief: Marokko+Turkije / bevolking (CBS Kerncijfers wijken en buurten)", }, "education_level": { "laag": _int_val(area.get("OpleidingsniveauLaag_64")), "middelbaar": _int_val(area.get("OpleidingsniveauMiddelbaar_65")), "hoog": _int_val(area.get("OpleidingsniveauHoog_66")), }, "housing_type": { "koop_pct": _float_val(area.get("Koopwoningen_40")), "huur_pct": _float_val(area.get("HuurwoningenTotaal_41")), }, "car_ownership": _float_val(area.get("PersonenautoSPerHuishouden_102")), "data_granularity": "gemeente", "data_source": "cbs+pdok", } _GEMEENTE_CACHE[gm] = stats return stats