SPB Git forge

spb/wp20_uqo

Public
1commits 1branches 0releases
2.8 MBsize
maindefault branch
yesterdaylast push
TeX 59% Python 40.8%
7.3 KB · 125 lines python
Raw Blame History
1"""Unit tests — run with `.venv/bin/python -m pytest -q`. No network, no API."""2import json3import sys4from pathlib import Path56import numpy as np7import pandas as pd8import pytest910ROOT = Path(__file__).resolve().parents[1]11sys.path.insert(0, str(ROOT))1213from src.extraction.textprep import build_text, mask_money  # noqa: E40214from src.feature_schema.schema import IMAGE_SCHEMA, TEXT_SCHEMA  # noqa: E40215from src.extraction.aggregate import _wvote, _wmedian_ord, aggregate_images, GRADE_ORD  # noqa: E40216from src.evaluation.metrics import cohen_kappa, agreement_table, krippendorff_alpha_nominal  # noqa: E40217from src.cost_model import cost  # noqa: E402181920# ---------------------------------------------------------------- masking21def test_mask_money_removes_amounts_and_assessment_sentences():22    s = "Belle maison. Évaluation municipale : 429 000 $. Prix demandé 575 000$. Terrain de 11 500 pi²."23    m = mask_money(s)24    assert "429" not in m and "575" not in m25    assert "11 500 pi²" in m  # non-monetary numbers survive262728def test_build_text_masks_features_and_details():29    row = {"description": "Maison rénovée, toiture 2019.",30           "features": json.dumps(["Style : Bungalow", "Évaluation municipale : 300 000 $", "Taxes municipales : 2 500 $"]),31           "details": json.dumps({"Revêtement": "Brique", "Évaluation municipale (terrain)": "100 000 $", "Taxes scolaires": "300 $",32                                  "pieces": [{"nom": "Cuisine", "niveau": "RDC", "dimensions": "10x12", "revetement": "Céramique"}]})}33    text, st = build_text(row)34    assert "300 000" not in text and "100 000" not in text and "2 500" not in text35    assert "Brique" in text and "Céramique" in text and "toiture 2019" in text36    assert st["n_detail_masked_keys"] == 2373839# ---------------------------------------------------------------- schema compliance (API constraints)40@pytest.mark.parametrize("schema", [IMAGE_SCHEMA, TEXT_SCHEMA])41def test_schema_has_no_unsupported_keywords_and_all_required(schema):42    txt = json.dumps(schema)43    for kw in ("minimum", "maximum", "maxItems", "maxLength", "minItems"):44        assert f'"{kw}"' not in txt45    assert set(schema["required"]) == set(schema["properties"].keys())46    assert schema["additionalProperties"] is False474849# ---------------------------------------------------------------- aggregation rules50def test_weighted_vote_ignores_noninformative():51    v, s = _wvote(["brick", "not_visible", "vinyl", "brick"], [0.9, 0.9, 0.5, 0.6])52    assert v == "brick" and 0.7 < s < 0.853    assert _wvote(["not_visible", "not_applicable"], [1, 1]) == (None, 0.0)545556def test_weighted_median_ordinal():57    assert _wmedian_ord(["basic", "luxury", "standard"], [1, 1, 1], GRADE_ORD) == 258    assert _wmedian_ord(["not_visible"], [1], GRADE_ORD) is None596061def test_aggregate_images_basic_rules():62    rows = []63    def img(i, scene, **kw):64        base = {"tier": "haiku", "uid": "x:1", "image_index": i, "ok": True, "scene": scene, "photo_quality": 4,65                "staging_or_rendering": "none_apparent", "building_type_visual": "not_applicable", "storeys_visible": "not_applicable",66                "cladding": ["not_applicable"], "roof_form": "not_applicable", "roof_covering": "not_applicable",67                "garage_visible": "not_applicable", "exterior_condition": 0, "kitchen_grade": "not_applicable",68                "kitchen_counters": "not_applicable", "kitchen_cabinets": "not_applicable", "bathroom_grade": "not_applicable",69                "flooring_main": "not_applicable", "basement_finish": "not_applicable", "interior_condition": 0,70                "interior_finish_quality": 0, "renovation_evidence": "none_apparent", "visible_defects": ["none_visible"],71                "confidence": 0.8, "evidence": ""}72        base.update(kw); return base73    rows.append(img(0, "exterior_front", building_type_visual="detached", storeys_visible="2", cladding=["brick", "vinyl"],74                    garage_visible="attached", exterior_condition=4))75    rows.append(img(1, "exterior_rear_or_side", building_type_visual="detached", storeys_visible="2", cladding=["vinyl"],76                    garage_visible="none_visible", exterior_condition=3, visible_defects=["peeling_paint_or_finish"]))77    rows.append(img(2, "kitchen", kitchen_grade="upgraded", kitchen_counters="quartz_granite_or_stone", interior_condition=5,78                    interior_finish_quality=4, renovation_evidence="extensive", flooring_main="hardwood"))79    rows.append(img(3, "bathroom", bathroom_grade="standard", interior_condition=4, interior_finish_quality=3))80    df = pd.DataFrame(rows)81    a = aggregate_images(df).iloc[0]82    assert a["building_type_visual"] == "detached" and a["storeys_visible"] == "2"83    assert a["cladding_primary"] == "vinyl"  # 2 votes (0.8+0.8) vs brick (0.8)84    assert a["garage_visible"] == "attached"  # positive vote beats none_visible85    assert a["kitchen_grade"] == "upgraded" and a["bathroom_grade"] == "standard"86    assert a["renovation_evidence"] == "extensive" and a["defects"] == ["peeling_paint_or_finish"]87    assert abs(a["exterior_condition"] - 3.5) < 1e-9 and abs(a["interior_condition"] - 4.5) < 1e-988    assert a["grade_composite"] in ("standard", "upgraded")89    # truncation to the first 2 images removes all interior evidence90    a2 = aggregate_images(df, K=2).iloc[0]91    assert a2["kitchen_grade"] == "not_visible" and a2["grade_composite"] is None929394# ---------------------------------------------------------------- metrics95def test_kappa_perfect_and_chance():96    a = pd.Series(["x", "y", "x", "y"]); b = pd.Series(["x", "y", "x", "y"])97    assert abs(cohen_kappa(a, b) - 1.0) < 1e-998    c = pd.Series(["x", "x", "y", "y"]); d = pd.Series(["x", "y", "x", "y"])99    assert abs(cohen_kappa(c, d)) < 1e-9100    t = agreement_table(pd.Series(["x", "not_visible", "y"]), pd.Series(["x", "x", "x"]))101    assert t["n_compared"] == 2 and abs(t["coverage"] - 2 / 3) < 1e-9 and abs(t["accuracy"] - 0.5) < 1e-9102103104def test_krippendorff_alpha_perfect():105    lab = np.array([["a", "b", "a"], ["a", "b", "a"]], dtype=object)106    assert abs(krippendorff_alpha_nominal(lab) - 1.0) < 1e-9107108109# ---------------------------------------------------------------- cost identity110def test_cost_identity_and_monotonicity():111    df = pd.DataFrame({"floor_area_m2": [120.0, 120.0], "storeys": ["1", "1"], "attached": ["detached", "detached"],112                       "grade": ["standard", "luxury"], "condition": [3.0, 3.0], "renovation": ["none_apparent", "none_apparent"],113                       "basement": ["unfinished", "unfinished"], "garage": ["none", "none"], "pool": ["none", "none"],114                       "age": [20, 20], "cma": ["montreal", "montreal"], "roll_land_value": [100000.0, 100000.0],115                       "roll_market_date": ["2024-07-01", "2024-07-01"], "roll_building_value": [250000.0, 250000.0]})116    out = cost.cost_indication(df)117    assert np.allclose(out["V_cost"], out["L"] + out["improvements_depreciated"])118    assert np.allclose(out["improvements_depreciated"], (out["RCN"] + out["S"]) * (1 - out["D"]))119    assert out["RCN"].iloc[1] > out["RCN"].iloc[0]  # luxury > standard120    assert 0 < out["D"].iloc[0] < 0.85121    # depreciation: better condition → lower D; longer life → lower D122    df2 = df.copy(); df2["condition"] = 5.0123    assert cost.depreciation(df2).iloc[0] < out["D"].iloc[0]124    assert cost.depreciation(df, {"economic_life": 75}).iloc[0] < out["D"].iloc[0]125