mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Address PR review (dancafc): - introduce UprnMatch NamedTuple (datatypes/address_match.py) for the (uprn, address, lexiscore, certificate_number) return, replacing the bare 4-tuple in get_uprn_from_epc_df / get_uprn_from_historic_epc / HistoricEpcResolver.resolve_uprn. Tuple-compatible, so unpacking is unchanged. - rename get_uprn_with_epc_df -> get_uprn_from_epc_df (+ callers). - type resolve_group_ambiguity via a GroupDecision NamedTuple and trim its docstring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
93 lines
2.8 KiB
Python
93 lines
2.8 KiB
Python
from collections import defaultdict
|
|
from typing import NamedTuple, Optional
|
|
|
|
import pandas as pd
|
|
|
|
from backend.utils.addressMatch import AddressMatch
|
|
|
|
|
|
class GroupDecision(NamedTuple):
|
|
"""One row's outcome after cross-row ambiguity resolution (ADR-0057)."""
|
|
|
|
uprn: Optional[str]
|
|
status: str # "matched" | "ambiguous_duplicate" | "unmatched"
|
|
|
|
|
|
def resolve_group_ambiguity(
|
|
matches: list[tuple[Optional[str], str]],
|
|
) -> list[GroupDecision]:
|
|
"""Resolve cross-row UPRN ambiguity within one postcode group (ADR-0057).
|
|
|
|
``matches`` is ``(uprn, normalised_address)`` per row. A UPRN that is the
|
|
best match for two rows with *different* normalised addresses is withheld
|
|
on both (a coarse EPC record absorbing several real addresses, e.g. flats in
|
|
a block); a UPRN shared only by identical addresses is a genuine re-listing
|
|
and kept. Returns a ``GroupDecision`` per row, in input order.
|
|
"""
|
|
distinct_addresses: dict[str, set[str]] = defaultdict(set)
|
|
for uprn, norm_address in matches:
|
|
if uprn:
|
|
distinct_addresses[uprn].add(norm_address)
|
|
|
|
resolved: list[GroupDecision] = []
|
|
for uprn, _norm_address in matches:
|
|
if not uprn:
|
|
resolved.append(GroupDecision(None, "unmatched"))
|
|
elif len(distinct_addresses[uprn]) > 1:
|
|
resolved.append(GroupDecision(None, "ambiguous_duplicate"))
|
|
else:
|
|
resolved.append(GroupDecision(uprn, "matched"))
|
|
return resolved
|
|
|
|
|
|
def all_uprns_match(
|
|
df: pd.DataFrame,
|
|
target_uprn: str,
|
|
column: str = "uprn",
|
|
) -> bool:
|
|
if column not in df.columns:
|
|
return False
|
|
|
|
uprns = df[column].dropna().astype(str).str.strip().unique()
|
|
|
|
if len(uprns) == 0:
|
|
return False
|
|
|
|
return len(uprns) == 1 and uprns[0] == str(target_uprn)
|
|
|
|
|
|
def rank_address_similarity(
|
|
address_list_df: pd.DataFrame,
|
|
user_address: str,
|
|
address_column: str = "address",
|
|
uprn_column: str = "uprn",
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Annotate EPC results with lexicographical similarity scores and ranks.
|
|
|
|
Returns a DataFrame sorted by descending lexiscore.
|
|
DOES NOT choose or return a UPRN.
|
|
"""
|
|
|
|
if address_column not in address_list_df.columns:
|
|
raise ValueError(f"Missing column: {address_column}")
|
|
|
|
if uprn_column not in address_list_df.columns:
|
|
raise ValueError(f"Missing column: {uprn_column}")
|
|
|
|
out = address_list_df.copy()
|
|
|
|
user_norm = AddressMatch.normalise_address(user_address)
|
|
|
|
out["lexiscore"] = out[address_column].apply(
|
|
lambda x: AddressMatch.levenshtein(user_norm, x)
|
|
)
|
|
|
|
out[uprn_column] = out[uprn_column].astype(str).str.replace(r"\.0$", "", regex=True)
|
|
|
|
out["lexirank"] = out["lexiscore"].rank(method="dense", ascending=False).astype(int)
|
|
|
|
return out.sort_values(
|
|
["lexirank", "lexiscore"],
|
|
ascending=[True, False],
|
|
)
|