This commit is contained in:
Andras Schmelczer 2026-07-12 15:03:33 +01:00
parent 982e0cc89c
commit cfaf58dfba
44 changed files with 793 additions and 201 deletions

View file

@ -14,6 +14,7 @@ from pipeline.transform.merge import (
_canonical_postcode_expr,
_best_listing_match,
_coalesce_direct_epc_columns,
_epc_council_by_postcode,
_fill_property_level_no_defaults,
_join_area_side_tables,
_finalize_listings,
@ -117,6 +118,86 @@ def test_tree_density_is_area_level_and_survives_the_split() -> None:
assert TREE_DENSITY_FEATURE not in properties_df.columns
def test_epc_council_by_postcode_aggregates_social_tenure_shares() -> None:
# 4 dwellings in one postcode, 2 ever council (was_council_house "Yes"). One
# of the two is STILL social (latest_tenure_status "Rented (social)"); the
# other has been sold off (latest cert owner-occupied) and is ex-council.
# % Council housing = 2/4 = 50.0; % Ex-council = 1/4 = 25.0.
wide = pl.LazyFrame(
{
"postcode": ["AB1 2CD", "AB1 2CD", "AB1 2CD", "AB1 2CD"],
"was_council_house": ["Yes", "Yes", "No", "No"],
"latest_tenure_status": [
"Rented (social)", # still social -> not ex-council
"Owner-occupied", # sold off -> ex-council
"Owner-occupied",
None, # never council; a null latest tenure is harmless here
],
}
)
result = _epc_council_by_postcode(wide).collect()
assert result.to_dicts() == [
{"postcode": "AB1 2CD", "% Council housing": 50.0, "% Ex-council": 25.0}
]
def test_epc_council_by_postcode_null_latest_tenure_counts_as_ex_council() -> None:
# An ever-council dwelling whose latest certificate has no recognised tenure
# (latest_tenure_status null) is treated as no-longer-social, so it counts
# toward % Ex-council (null currently_social -> False, per the spec).
wide = pl.LazyFrame(
{
"postcode": ["AB1 2CD", "AB1 2CD"],
"was_council_house": ["Yes", "No"],
"latest_tenure_status": [None, None],
}
)
result = _epc_council_by_postcode(wide).collect()
assert result.to_dicts() == [
{"postcode": "AB1 2CD", "% Council housing": 50.0, "% Ex-council": 50.0}
]
def test_epc_council_columns_are_area_level_and_survive_the_split() -> None:
# The EPC council shares are postcode-level AREA columns: they must route to
# the postcode output and NOT appear in the property output. The per-property
# "Former council house" flag is unaffected and stays property-level.
assert "% Council housing" in _AREA_COLUMNS
assert "% Ex-council" in _AREA_COLUMNS
df = pl.DataFrame(
{
"Postcode": ["AA1 1AA"],
"Last known price": [250_000],
"Former council house": ["Yes"],
}
)
postcode_features = pl.DataFrame(
{
"Postcode": ["AA1 1AA", "BB1 1BB"],
"lat": [51.0, 52.0],
"lon": [-0.1, -0.2],
"ctry25cd": ["E92000001", "E92000001"],
"% Council housing": [50.0, 0.0],
"% Ex-council": [25.0, 0.0],
}
)
postcode_df, properties_df = _split_normal_outputs(
df, postcode_features, expected_postcode_count=2
)
assert postcode_df["% Council housing"].to_list() == [50.0, 0.0]
assert postcode_df["% Ex-council"].to_list() == [25.0, 0.0]
assert "% Council housing" not in properties_df.columns
assert "% Ex-council" not in properties_df.columns
assert "Former council house" in properties_df.columns
def test_crime_columns_are_average_annual_counts() -> None:
# Crime is the average annual recorded incident count (incidents/yr) over
# 7-year and 2-year windows; the old per-1,000 "(per 1k/yr, …)" rate columns
@ -955,7 +1036,7 @@ def test_match_direct_epc_matches_by_uprn_across_postcodes() -> None:
def test_match_direct_epc_ignores_nonunique_building_uprn() -> None:
# A parent/building UPRN that resolves to several distinct (address,
# postcode) flats cannot act as a 1:1 exact-match key it would mis-link
# postcode) flats cannot act as a 1:1 exact-match key: it would mis-link
# the listing to one arbitrary flat. The listing must fall through to the
# street-address matcher, which resolves the specific flat.
matches = _match_direct_epc(
@ -1490,7 +1571,7 @@ def test_match_listing_properties_uprn_wins_dedup_tie() -> None:
def test_match_direct_epc_does_not_match_other_outcode_without_uprn() -> None:
# Matching is by postcode/UPRN/street — never by coordinate proximity — and
# Matching is by postcode/UPRN/street (never by coordinate proximity), and
# the street fallback is outcode-scoped, so a same-street EPC in a different
# OUTCODE with no shared UPRN is skipped.
matches = _match_direct_epc(
@ -1909,7 +1990,7 @@ def test_finalize_listings_dedupes_fanned_out_listing_rows() -> None:
"Leasehold/Freehold": ["Leasehold", "Leasehold"],
"Last known price": [500_000, 480_000],
"Tree canopy density percentile": [42.0, 42.0],
# Same listing URL on both collapsed rows the fan-out to fix.
# Same listing URL on both collapsed rows: the fan-out to fix.
"_actual_listing_url": ["url0", "url0"],
"_actual_asking_price": [600_000, 600_000],
"_actual_asking_price_per_sqm": [5_000, 5_000],