Small fixes

This commit is contained in:
Andras Schmelczer 2026-06-14 14:52:44 +01:00
parent 54fbcb1ea6
commit 083f8a982e
24 changed files with 1505 additions and 79 deletions

View file

@ -1579,8 +1579,11 @@ def transform_gias_schools(
# Store". GEOLYTIX is authoritative for its chains, so an OSM grocery row that
# sits on top of a GEOLYTIX point AND carries that point's brand name is the
# same physical store and is dropped. Independent corner shops never carry a
# chain brand, so they are kept.
GROCERY_DEDUP_RADIUS_M = 50.0
# chain brand, so they are kept. 100m (not 50m) because OSM and GEOLYTIX
# routinely place the same branded store 50-100m apart (entrance vs centroid vs
# car park); at 50m ~166 same-brand duplicates (Iceland, Morrisons Daily, Tesco
# Express, Sainsbury's Local, ...) survived just outside the radius.
GROCERY_DEDUP_RADIUS_M = 100.0
# Brand-token aliases so an OSM name spelt differently from the GEOLYTIX brand
# still matches. GEOLYTIX's "Co-op" tokenises to "coop", but OSM frequently
@ -1591,15 +1594,34 @@ _GROCERY_TOKEN_ALIASES = {
"cooperatives": "coop",
}
# Brand tokens shorter than the 3-char floor that must still match. "M&S" strips
# to "ms" (len 2) and would otherwise tokenise to set(), so M&S never deduped and
# OSM M&S grocery rows survived as phantom duplicates of the GEOLYTIX M&S chain.
# "M&S" is the only GEOLYTIX brand whose alnum form is <3 chars, so this
# allowlist is safe (no other brand collides).
_GROCERY_SHORT_BRAND_TOKENS = {"ms"}
# OSM tags Iceland/Farmfoods stores shop/frozen_food, which CATEGORY_MAP routes
# to "Deli & Specialty". The grocery dedup drops the ones colocated with a
# GEOLYTIX twin, but genuine stores GEOLYTIX lacks would otherwise survive
# mislabelled as deli, shadowing the brand pill and falling outside the headline
# grocery metric. Relabel the survivors (matched by name) to the brand so they
# line up with the GEOLYTIX rows.
OSM_BRAND_RELABEL = {
"Iceland": "Iceland",
"Farmfoods": "Farmfoods",
}
def _significant_tokens(name: str | None) -> set[str]:
"""Lower-case alphanumeric tokens of length >= 3 from a POI name (aliased)."""
"""Lower-case alphanumeric tokens of length >= 3 from a POI name (aliased),
plus short brand tokens in _GROCERY_SHORT_BRAND_TOKENS (e.g. "ms" for M&S)."""
if not name:
return set()
tokens: set[str] = set()
for raw in str(name).lower().split():
token = "".join(ch for ch in raw if ch.isalnum())
if len(token) >= 3:
if len(token) >= 3 or token in _GROCERY_SHORT_BRAND_TOKENS:
tokens.add(_GROCERY_TOKEN_ALIASES.get(token, token))
return tokens
@ -1608,7 +1630,14 @@ def _significant_tokens(name: str | None) -> set[str]:
# gaps. Where NaPTAN already has a stop within this radius the area is covered,
# so the colocated OSM platform is dropped to avoid double-counting; OSM
# platforms with no nearby NaPTAN stop (the gaps) are kept.
BUS_STOP_DEDUP_RADIUS_M = 50.0
#
# 100 m, not 50 m: at 50 m ~24.6k OSM platforms 50-100 m from a NaPTAN stop
# survived and double-counted (16.7k at 50-75 m, 7.8k at 75-100 m). The NaPTAN
# nearest-neighbour spacing (opposite-side-of-road pairs) has a 166.7 m median
# (p25=98.7 m), so 100 m clears the 50-100 m duplicate spike while leaving a
# genuine opposite-direction stop (~166 m away) in place; 125-150 m would start
# dropping those real opposite-side stops.
BUS_STOP_DEDUP_RADIUS_M = 100.0
def osm_stops_near_naptan(
@ -1652,9 +1681,9 @@ def osm_groceries_colocated_with_geolytix(
An OSM Groceries row is a duplicate when a GEOLYTIX point lies within
``radius_m`` metres AND that point's brand tokens (its ``category``, e.g.
"Tesco", "Co-op") are all present in the OSM row's name — i.e. the same
physical branded store. Brands with no token >= 3 chars (e.g. "M&S") never
match, so they are conservatively kept rather than risk a false drop.
"Tesco", "Co-op", "M&S") are all present in the OSM row's name — i.e. the
same physical branded store. Short brands like "M&S" match via
_GROCERY_SHORT_BRAND_TOKENS; brands that still tokenise to set() are kept.
``osm_groceries`` needs columns ``id``, ``name``, ``lat``, ``lng``;
``geolytix`` needs ``category`` (the brand), ``lat``, ``lng``.
@ -1774,6 +1803,31 @@ def transform(
# pre-deduplicated.
lf = lf.unique(subset=["id", "category"], keep="first", maintain_order=True)
# Relabel OSM Iceland/Farmfoods rows that CATEGORY_MAP put in "Deli &
# Specialty" to the brand, so the genuine stores GEOLYTIX lacks (the grocery
# dedup drops the colocated twins) line up with the GEOLYTIX brand pills and
# the headline grocery metric instead of shadowing them as deli.
for _brand_name, _brand in OSM_BRAND_RELABEL.items():
_is_brand = (
(pl.col("group") == "Groceries")
& (pl.col("category") == "Deli & Specialty")
& (pl.col("name") == _brand_name)
)
lf = lf.with_columns(
pl.when(_is_brand)
.then(pl.lit(_brand))
.otherwise(pl.col("category"))
.alias("category"),
pl.when(_is_brand)
.then(pl.lit(_brand))
.otherwise(pl.col("icon_category"))
.alias("icon_category"),
pl.when(_is_brand)
.then(pl.lit("\U0001f6d2"))
.otherwise(pl.col("emoji"))
.alias("emoji"),
)
naptan_df = pl.scan_parquet(naptan_path).collect()
mask = in_england_mask(
boundary_path,