159 lines
5.5 KiB
Python
159 lines
5.5 KiB
Python
|
|
"""Enrich supermarket postcodes with PDOK geocoding + CBS demografie."""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import time
|
||
|
|
from typing import Any, Optional
|
||
|
|
|
||
|
|
from app.connectors import cbs, pdok
|
||
|
|
from app.db import execute, execute_returning, fetch_all, fetch_one, json_param
|
||
|
|
|
||
|
|
|
||
|
|
def _postcodes_to_enrich(limit: int = 100, offset: int = 0) -> list[str]:
|
||
|
|
rows = fetch_all(
|
||
|
|
"""
|
||
|
|
SELECT DISTINCT s.postcode
|
||
|
|
FROM supermarkets s
|
||
|
|
LEFT JOIN area_analysis a ON a.postcode = s.postcode
|
||
|
|
WHERE s.postcode IS NOT NULL
|
||
|
|
AND s.postcode <> '0000AA'
|
||
|
|
AND a.id IS NULL
|
||
|
|
ORDER BY s.postcode
|
||
|
|
LIMIT %s OFFSET %s
|
||
|
|
""",
|
||
|
|
(limit, offset),
|
||
|
|
)
|
||
|
|
return [r["postcode"] for r in rows]
|
||
|
|
|
||
|
|
|
||
|
|
def enrich_postcode(postcode: str) -> dict[str, Any]:
|
||
|
|
pc = pdok.normalize_postcode(postcode)
|
||
|
|
pdok_data = pdok.lookup_postcode(pc)
|
||
|
|
if not pdok_data:
|
||
|
|
return {"postcode": pc, "status": "pdok_not_found"}
|
||
|
|
|
||
|
|
gm_code = pdok_data.get("municipality_code")
|
||
|
|
cbs_data = cbs.fetch_gemeente_stats(gm_code) if gm_code else None
|
||
|
|
if not cbs_data:
|
||
|
|
return {"postcode": pc, "status": "cbs_not_found", "pdok": pdok_data}
|
||
|
|
|
||
|
|
religious = cbs_data.get("religious_composition") or {}
|
||
|
|
row = execute_returning(
|
||
|
|
"""
|
||
|
|
INSERT INTO area_analysis (
|
||
|
|
postcode, city, population, households, avg_household_size,
|
||
|
|
avg_income, median_income, education_level, ethnic_composition,
|
||
|
|
religious_composition, unemployment_rate, housing_type, car_ownership,
|
||
|
|
data_source, last_updated
|
||
|
|
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,NOW())
|
||
|
|
ON CONFLICT (postcode) DO UPDATE SET
|
||
|
|
city = EXCLUDED.city,
|
||
|
|
population = EXCLUDED.population,
|
||
|
|
households = EXCLUDED.households,
|
||
|
|
avg_household_size = EXCLUDED.avg_household_size,
|
||
|
|
avg_income = EXCLUDED.avg_income,
|
||
|
|
median_income = EXCLUDED.median_income,
|
||
|
|
education_level = EXCLUDED.education_level,
|
||
|
|
ethnic_composition = EXCLUDED.ethnic_composition,
|
||
|
|
religious_composition = EXCLUDED.religious_composition,
|
||
|
|
housing_type = EXCLUDED.housing_type,
|
||
|
|
car_ownership = EXCLUDED.car_ownership,
|
||
|
|
data_source = EXCLUDED.data_source,
|
||
|
|
last_updated = NOW()
|
||
|
|
RETURNING id, postcode
|
||
|
|
""",
|
||
|
|
(
|
||
|
|
pc,
|
||
|
|
pdok_data.get("city") or cbs_data.get("city"),
|
||
|
|
cbs_data.get("population"),
|
||
|
|
cbs_data.get("households"),
|
||
|
|
cbs_data.get("avg_household_size"),
|
||
|
|
cbs_data.get("avg_income"),
|
||
|
|
cbs_data.get("median_income"),
|
||
|
|
json_param(cbs_data.get("education_level")),
|
||
|
|
json_param(cbs_data.get("ethnic_composition")),
|
||
|
|
json_param(religious),
|
||
|
|
cbs_data.get("unemployment_rate"),
|
||
|
|
json_param(cbs_data.get("housing_type")),
|
||
|
|
cbs_data.get("car_ownership"),
|
||
|
|
f"cbs+pdok ({cbs_data.get('data_granularity', 'gemeente')})",
|
||
|
|
),
|
||
|
|
)
|
||
|
|
|
||
|
|
execute(
|
||
|
|
"""
|
||
|
|
UPDATE supermarkets SET
|
||
|
|
city = COALESCE(NULLIF(city, 'Onbekend'), %s),
|
||
|
|
province = COALESCE(province, %s),
|
||
|
|
latitude = COALESCE(latitude, %s),
|
||
|
|
longitude = COALESCE(longitude, %s),
|
||
|
|
last_updated = NOW()
|
||
|
|
WHERE postcode = %s AND (
|
||
|
|
city = 'Onbekend' OR province IS NULL OR latitude IS NULL OR longitude IS NULL
|
||
|
|
)
|
||
|
|
""",
|
||
|
|
(
|
||
|
|
pdok_data.get("city"),
|
||
|
|
pdok_data.get("province"),
|
||
|
|
pdok_data.get("latitude"),
|
||
|
|
pdok_data.get("longitude"),
|
||
|
|
pc,
|
||
|
|
),
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"postcode": pc,
|
||
|
|
"status": "ok",
|
||
|
|
"area_id": row["id"] if row else None,
|
||
|
|
"municipality": pdok_data.get("municipality"),
|
||
|
|
"population": cbs_data.get("population"),
|
||
|
|
"muslim_proxy_pct": religious.get("muslim_proxy_pct"),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def enrich_batch(limit: int = 50, offset: int = 0, delay_sec: float = 0.15) -> dict[str, Any]:
|
||
|
|
postcodes = _postcodes_to_enrich(limit, offset)
|
||
|
|
results: list[dict[str, Any]] = []
|
||
|
|
ok = failed = 0
|
||
|
|
for pc in postcodes:
|
||
|
|
try:
|
||
|
|
result = enrich_postcode(pc)
|
||
|
|
results.append(result)
|
||
|
|
if result.get("status") == "ok":
|
||
|
|
ok += 1
|
||
|
|
else:
|
||
|
|
failed += 1
|
||
|
|
except Exception as exc: # noqa: BLE001
|
||
|
|
results.append({"postcode": pc, "status": "error", "error": str(exc)})
|
||
|
|
failed += 1
|
||
|
|
time.sleep(delay_sec)
|
||
|
|
|
||
|
|
remaining = fetch_one(
|
||
|
|
"""
|
||
|
|
SELECT COUNT(DISTINCT s.postcode) AS n
|
||
|
|
FROM supermarkets s
|
||
|
|
LEFT JOIN area_analysis a ON a.postcode = s.postcode
|
||
|
|
WHERE s.postcode <> '0000AA' AND a.id IS NULL
|
||
|
|
"""
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"processed": len(postcodes),
|
||
|
|
"ok": ok,
|
||
|
|
"failed": failed,
|
||
|
|
"remaining": int((remaining or {}).get("n") or 0),
|
||
|
|
"results": results[:20],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def enrichment_status() -> dict[str, Any]:
|
||
|
|
total_pc = fetch_one(
|
||
|
|
"SELECT COUNT(DISTINCT postcode) AS n FROM supermarkets WHERE postcode <> '0000AA'"
|
||
|
|
)
|
||
|
|
enriched = fetch_one("SELECT COUNT(*) AS n FROM area_analysis")
|
||
|
|
providers = fetch_all(
|
||
|
|
"SELECT name, provider_type, last_fetch_at, last_status, is_active FROM data_providers ORDER BY name"
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"unique_postcodes": int((total_pc or {}).get("n") or 0),
|
||
|
|
"enriched_postcodes": int((enriched or {}).get("n") or 0),
|
||
|
|
"data_providers": [dict(p) for p in providers],
|
||
|
|
}
|