lgtm
This commit is contained in:
parent
982e0cc89c
commit
cfaf58dfba
44 changed files with 793 additions and 201 deletions
|
|
@ -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],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue