# ----------------------------------------------------------------------------- # Rent-Ka — Rental listings aggregator (Canada, outside Québec) # Author: Simon-Pierre Boucher — contact@spboucher.ai # test_kascores.py : barème des KA Scores — décroissance, bornes, labels, # non-aberration spatiale (deux immeubles voisins → scores proches). # ----------------------------------------------------------------------------- import json import math import pytest from rentka import kascores from rentka.kascores import ( Grid, TileIndex, _decroissance, label, score_calme, score_global, score_services, score_transit, score_walk, ) def test_decroissance(): assert _decroissance(100, 400, 1600) == 1.0 assert _decroissance(400, 400, 1600) == 1.0 assert _decroissance(1600, 400, 1600) == 0.0 assert _decroissance(2500, 400, 1600) == 0.0 assert 0.49 < _decroissance(1000, 400, 1600) < 0.51 def test_labels(): assert label(92) == "Exceptionnel" assert label(71) == "Excellent" assert label(60) == "Très bon" assert label(45) == "Moyen" assert label(10) == "Faible" assert label(None) is None def _env_urbain(lat=45.52, lng=-73.58): """Micro-quartier synthétique : tout à ~200 m.""" d = 0.002 # ~200 m pois = {cat: [[lat + d, lng]] for cat, *_ in kascores.WALK_BAREME} pois["bus"] = [[lat, lng + d]] pois["metro"] = [[lat - d, lng]] pois["hopital"] = [[lat + 2 * d, lng]] return {"lines": {}, "points": {}, "pois": pois} def test_walk_urbain_vs_desert(): idx = TileIndex(_env_urbain()) s, det = score_walk(idx, 45.52, -73.58) assert s is not None and s > 85 vide = TileIndex({"lines": {}, "points": {}, "pois": {}}) s2, det2 = score_walk(vide, 45.52, -73.58) assert s2 is None and "raison" in det2 # honnêteté : pas de 0 trompeur def test_transit_non_desservi_et_blend(): vide = TileIndex({"lines": {}, "points": {}, "pois": {}}) s, det = score_transit(vide, 45.52, -73.58, None) assert s is None and "raison" in det # « Non desservi », pas 0 idx = TileIndex(_env_urbain()) proche, _ = score_transit(idx, 45.52, -73.58, 90.0) loin, _ = score_transit(idx, 45.52, -73.58, 10.0) assert proche is not None and loin is not None and proche > loin def test_calme_autoroute_penalise(): lat, lng = 45.52, -73.58 calme_env = {"lines": {}, "points": {}, "pois": {}} bruyant_env = { "lines": {"autoroute": [[[lat + 0.0005, lng - 0.01], [lat + 0.0005, lng + 0.01]]]}, "points": {}, "pois": {}, } s_calme, _ = score_calme(TileIndex(calme_env), lat, lng) s_bruyant, d = score_calme(TileIndex(bruyant_env), lat, lng) assert s_calme - s_bruyant > 25 assert any("autoroute" in x["source"] for x in d["sources_bruit"]) def test_services_rendement_decroissant(): lat, lng = 45.52, -73.58 def env(n): return {"lines": {}, "points": {}, "pois": { "epicerie": [[lat + 0.001 * i, lng] for i in range(1, n + 1)]}} s3, _ = score_services(TileIndex(env(3)), lat, lng) s6, _ = score_services(TileIndex(env(6)), lat, lng) s12, _ = score_services(TileIndex(env(12)), lat, lng) assert s3 < s6 < s12 assert (s6 - s3) > (s12 - s6) # passer de 3→6 vaut plus que 6→12... x2 def test_global_renormalise_sans_transit(): s = score_global({"walk": 80, "transit": None, "bike": 60, "calme": 70, "services": 50}) assert s is not None and 60 < s < 75 assert score_global({"walk": 80, "transit": None, "bike": None, "calme": None, "services": None}) is None def test_coherence_spatiale_sur_le_parc(): """Deux immeubles à < 200 m → scores globaux proches (médiane < 8 pts).""" from rentka import db con = db.connect() rows = con.execute( "SELECT lat, lng, global AS g FROM kascores" " WHERE global IS NOT NULL ORDER BY coord_key LIMIT 3000").fetchall() con.close() if len(rows) < 200: pytest.skip("pas assez de scores calculés") g = Grid() vals = {} for r in rows: g.add(r["lat"], r["lng"]) vals[(round(r["lat"], 5), round(r["lng"], 5))] = r["g"] diffs = [] for r in rows[:800]: for d, la, ln in g.near(r["lat"], r["lng"], 200): if d < 1: continue v = vals.get((round(la, 5), round(ln, 5))) if v is not None: diffs.append(abs(v - r["g"])) assert diffs, "aucune paire voisine trouvée" diffs.sort() mediane = diffs[len(diffs) // 2] assert mediane < 8, f"médiane des écarts voisins : {mediane}"