# ============================================================================= # QWHPI — Quebec Weekly Housing Price Index # Author : Simon-Pierre Boucher # Contact : contact@spboucher.ai # File : engine/tests/test_features_clean.py # Purpose : Unit tests for cleaning flags and feature engineering. # ============================================================================= """Cleaning + feature unit tests on synthetic rows.""" from __future__ import annotations import datetime as dt import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src")) import polars as pl from qwhpi.clean import add_flags from qwhpi.features import build_features def synthetic(n: int = 8) -> tuple[pl.DataFrame, pl.DataFrame]: tx = pl.DataFrame({ "id": [f"t{i}" for i in range(n)], "date": [dt.date(2023, 1, 9)] * n, "amount": [300000.0, 300000.0, 55000.0, 20_000_000.0, 400000.0, 500000.0, 350000.0, 360000.0], "street": ["1 rue A", "1 rue A", "2 rue B", "3 rue C", "4 rue D", "5 rue E", "6 rue F", "7 rue G"], "zipCode": ["G1A 1A1"] * n, "city": ["X"] * n, "lat": [46.8] * n, "lng": [-71.2] * n, "propertyType": ["unifamilial"] * 6 + ["condo", "indéterminé"], "yearBuilt": [1990, 1990, 1990, 1990, 2030, None, 2000, 2000], "floorArea": [120.0, 120.0, 120.0, 120.0, 5000.0, None, 80.0, 80.0], "buildingType": ["single-story"] * 6 + [None, None], "previousValue": [250000.0] * n, "totalArValue": [280000.0, 280000.0, 280000.0, 280000.0, 380000.0, 100.0, 330000.0, 330000.0], "ownerType": ["physical_person"] * n, "week": [dt.date(2023, 1, 9)] * n, "log_amount": [12.6] * n, }) geo = pl.DataFrame({ "id": [f"t{i}" for i in range(n)], "geo_code": ["23027"] * n, "municipality": ["Québec"] * n, "munic_type": ["V"] * n, "mrc_code": ["230"] * n, "mrc": ["Québec"] * n, "region_code": ["03"] * n, "region": ["Capitale-Nationale"] * n, "join_method": ["within"] * n, }) return tx, geo def test_flags(): tx, geo = synthetic() df = add_flags(tx, geo) by_id = {r["id"]: r for r in df.iter_rows(named=True)} assert by_id["t1"]["duplicate_flag"] is True # exact dup of t0 assert by_id["t0"]["duplicate_flag"] is False # first kept assert by_id["t3"]["exclude_reason"] == "price_above_10m" assert by_id["t5"]["nonmarket_ratio"] is True # ratio 5000x assert by_id["t4"]["yb_suspect"] is True # built 2030 assert by_id["t4"]["fa_suspect"] is True # 5000 m2 assert by_id["t4"]["yearBuilt"] is None # nulled, kept assert by_id["t7"]["is_undetermined_type"] is True # nothing is deleted assert df.height == tx.height def test_features_age_bins_and_imputation(): tx, geo = synthetic() feat = build_features(add_flags(tx, geo)) by_id = {r["id"]: r for r in feat.iter_rows(named=True)} assert by_id["t0"]["age_bin"] == "04_21-40" # age 33 # missing yearBuilt -> conditional-median age imputation, NO missing bin # (time-correlated missingness rule — see features.py docstring) assert by_id["t5"]["yb_missing"] is True assert by_id["t5"]["age_filled"] is not None assert by_id["t5"]["age_bin"] != "99_missing" assert by_id["t5"]["fa_missing"] is True assert by_id["t5"]["floor_area_filled"] is not None # imputed assert by_id["t0"]["loc_fine"] == "G1A"