Small fixes
This commit is contained in:
parent
54fbcb1ea6
commit
083f8a982e
24 changed files with 1505 additions and 79 deletions
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue