This commit is contained in:
Andras Schmelczer 2026-07-12 21:31:13 +01:00
parent ca771a7edf
commit d7f844d566
12 changed files with 198 additions and 251 deletions

View file

@ -56,6 +56,22 @@ DYNAMIC_FILTER_ALL_GROUPS = {"Public Transport", "Leisure", "Health"}
DYNAMIC_FILTER_COUNT_THRESHOLD_GROUPS = {"Groceries"}
DYNAMIC_FILTER_EXCLUDED_CATEGORIES = {"Park"}
# Combined "nearest of any rail mode" distance: the distance to the closest of a
# Rail station, Tube station, DLR station, or Tram & Metro stop. Materialized as
# its own real column so the server loads it like any other per-category
# distance. min_distance_per_postcode already supports multi-category groups, so
# this is a genuine nearest-of-the-union query (identical to the per-mode
# minimum). Distance only: a combined "within Xkm" count is not meaningful, so
# it is intentionally omitted from the count groups.
COMBINED_STATION_GROUP_KEY = "poi_any_station"
COMBINED_STATION_DISPLAY_NAME = "Any station"
COMBINED_STATION_SOURCE_CATEGORIES = [
"Rail station",
"Tube station",
"DLR station",
"Tram & Metro stop",
]
def _poi_category_slug(category: str) -> str:
ascii_text = (
@ -227,10 +243,21 @@ def main():
dynamic_counts_5km = count_pois_per_postcode(
postcodes, pois, groups=poi_category_groups, radius_km=5
)
# Distances additionally include the combined "Any station" group (nearest of
# any rail mode). It is distance-only, so it is added here but not to the
# 2km/5km count groups above.
distance_groups = {
**poi_category_groups,
COMBINED_STATION_GROUP_KEY: COMBINED_STATION_SOURCE_CATEGORIES,
}
distance_display_names = {
**poi_display_names,
COMBINED_STATION_GROUP_KEY: COMBINED_STATION_DISPLAY_NAME,
}
dynamic_distances = min_distance_per_postcode(
postcodes, pois, groups=poi_category_groups
postcodes, pois, groups=distance_groups
)
dynamic_renames = _dynamic_poi_metric_renames(poi_display_names)
dynamic_renames = _dynamic_poi_metric_renames(distance_display_names)
dynamic_counts_2km = dynamic_counts_2km.rename(
{k: v for k, v in dynamic_renames.items() if k in dynamic_counts_2km.columns}
)

View file

@ -1,6 +1,9 @@
import polars as pl
from pipeline.transform.poi_proximity import (
COMBINED_STATION_DISPLAY_NAME,
COMBINED_STATION_GROUP_KEY,
COMBINED_STATION_SOURCE_CATEGORIES,
GREENSPACE_PARK_FUNCTIONS,
POI_GROUPS_2KM,
_build_poi_category_groups,
@ -8,7 +11,7 @@ from pipeline.transform.poi_proximity import (
_greenspace_count_frame,
_groceries_categories,
)
from pipeline.utils.poi_counts import count_pois_per_postcode
from pipeline.utils.poi_counts import count_pois_per_postcode, min_distance_per_postcode
def test_groceries_2km_counts_geolytix_brand_categories() -> None:
@ -84,6 +87,42 @@ def test_dynamic_poi_groups_include_requested_categories_only() -> None:
assert "poi_school" not in groups
def test_combined_station_distance_is_nearest_of_any_rail_mode() -> None:
"""The combined "Any station" distance is the nearest of Rail/Tube/DLR/Tram,
materialized as its own column so the server loads it directly (no runtime
synthesis). A far rail station and a near DLR stop: the combined distance
must follow the DLR stop."""
postcodes = pl.DataFrame(
{"postcode": ["E14 5AB"], "lat": [51.5054], "lon": [-0.0235]}
)
pois = pl.DataFrame(
{
"category": ["Rail station", "DLR station"],
"group": ["Public Transport", "Public Transport"],
"lat": [51.6000, 51.5055],
"lng": [-0.2000, -0.0236],
}
)
distance_groups = {
"poi_rail_station": ["Rail station"],
COMBINED_STATION_GROUP_KEY: COMBINED_STATION_SOURCE_CATEGORIES,
}
result = min_distance_per_postcode(postcodes, pois, groups=distance_groups)
renames = _dynamic_poi_metric_renames(
{COMBINED_STATION_GROUP_KEY: COMBINED_STATION_DISPLAY_NAME}
)
result = result.rename({k: v for k, v in renames.items() if k in result.columns})
combined = result["Distance to nearest amenity (Any station) (km)"][0]
rail_only = result["poi_rail_station_nearest_km"][0]
# The near DLR stop is within a few hundred metres; the only rail station is
# kilometres away. The combined distance must follow the DLR stop.
assert combined < 0.2
assert rail_only > 5.0
assert combined < rail_only
def test_dynamic_poi_metric_renames_support_park_count_options() -> None:
assert _dynamic_poi_metric_renames({"parks": "Park"}) == {
"parks_nearest_km": "Distance to nearest amenity (Park) (km)",