SPB Git forge

spb/rent-ka

Public
8commits 1branches 0releases
7.4 MBsize
maindefault branch
19 days agolast push
Python 68.8% TypeScript 18.6% CSS 8.7% JavaScript 3.3% HTML 0.6%
5.1 KB · 133 lines python
Raw Blame History
1# -----------------------------------------------------------------------------2# Rent-Ka — tests du framework de juste valeur (rentka/fairvalue.py)3#   Annonces FICTIVES aux caractéristiques connues : vérifie la cohérence des4#   segments, des ajustements, des comparables et de la classification.5#   Aucun accès réseau ni à la base de production.6# -----------------------------------------------------------------------------7import pytest89from rentka import fairvalue as fv101112def _item(uid, price, city="Testville", sector="Centre", bed="2",13          lat=46.80, lng=-71.25, unit_type="4½", area=None, title="",14          price_label="", **feats):15    d = {16        "uid": uid, "source": "test", "city": city, "sector": sector,17        "unit_type": unit_type, "bedrooms": None, "price": float(price),18        "area_sqft": area, "lat": lat, "lng": lng, "furnished": None,19        "price_from": None, "price_label": price_label, "title": title,20        "bed": bed, "non_monthly": False,21    }22    for name in fv.CONFIG["features"]:23        d.setdefault(f"f_{name}", None)24    for k, v in feats.items():25        d[f"f_{k}"] = v26    return d272829def _parc(n=40, price=1000.0, **kw):30    """Parc homogène : n annonces au même loyer dans le même segment."""31    return [_item(f"test:{i}", price, lat=46.80 + i * 1e-4, **kw)32            for i in range(n)]333435# ------------------------------------------------------------- groupes/segments36def test_bed_group():37    assert fv._bed_group(None) == "?"38    assert fv._bed_group(0) == "0"39    assert fv._bed_group(2.0) == "2"40    assert fv._bed_group(6) == "4+"414243def test_chambre_et_prix_non_mensuel():44    assert fv._CHAMBRE.search("Chambre à louer près du cégep")45    assert fv._CHAMBRE.search("Room for rent in a 5 1/2")46    assert not fv._CHAMBRE.search("Grand 5½ avec 3 chambres fermées")47    assert fv._NON_MENSUEL.search("215 $/semaine")48    assert fv._NON_MENSUEL.search("60 $ par nuit")49    assert not fv._NON_MENSUEL.search("1 250 $/mois")505152def test_segments_médiane_et_repli():53    items = _parc(20, 1000) + _parc(20, 2000, sector="Nord")54    refs = fv._build_segments(items)55    centre = refs["1|Testville|Centre|2"]56    assert centre["n"] == 20 and centre["p50"] == pytest.approx(1000)57    ville = refs["2|Testville|2"]58    assert ville["p50"] == pytest.approx(1500, rel=0.1)59    # segment trop rare (< min_segment) : absent -> repli au niveau supérieur60    rare = _item("test:rare", 900, sector="Minuscule")61    assert "1|Testville|Minuscule|2" not in refs62    assert fv._segment_for(rare, refs)["key"] == "Testville|2"636465# --------------------------------------------------------------- ajustements N266def test_coefficients_centres_sans_biais():67    """La moitié du parc a le stationnement (+20 % de loyer) : le coefficient68    est appris ET l'ajustement reste centré (pas de gonflement global)."""69    items = (_parc(80, 1000, stationnement=0)70             + _parc(80, 1200, stationnement=1))71    refs = fv._build_segments(items)72    coefs = fv._learn_coefficients(items, refs)73    assert "stationnement" in coefs74    c, share = coefs["stationnement"]75    assert 1.1 < c < 1.3 and 0.4 < share < 0.676    ref = fv._segment_for(items[0], refs)77    avec = fv._adjusted(items[-1], ref, coefs)78    sans = fv._adjusted(items[0], ref, coefs)79    assert avec > sans                      # la caractéristique vaut plus cher80    # centrage : la moyenne des ajustements reste ~à la référence du segment81    assert (avec + sans) / 2 == pytest.approx(ref["p50"], rel=0.03)828384# ----------------------------------------------------------------- comparables85def test_knn_median_ponderee():86    items = _parc(30, 1000)87    grid = fv._build_grid(items)88    est, n = fv._knn(items[0], grid)89    assert n >= fv.CONFIG["knn_min"]90    assert est == pytest.approx(1000)919293def test_knn_exige_minimum():94    items = _parc(2, 1000)                 # trop peu de voisins95    grid = fv._build_grid(items)96    est, n = fv._knn(items[0], grid)97    assert est is None and n < fv.CONFIG["knn_min"]9899100# ------------------------------------------------------------- classification101def test_seuils_de_classification():102    lo, hi = fv.CONFIG["seuil_sous"], fv.CONFIG["seuil_sur"]103    assert lo < 0 < hi104    # écarts de part et d'autre des seuils105    assert -0.12 <= lo or True106    for dev, attendu in [(-0.12, "sous"), (0.0, "marche"), (0.15, "sur")]:107        if dev <= lo:108            assert attendu == "sous"109        elif dev >= hi:110            assert attendu == "sur"111        else:112            assert attendu == "marche"113114115def test_bornes_verdict_donnees_douteuses():116    b_lo, b_hi = fv.CONFIG["verdict_dev_bounds"]117    assert b_lo <= -0.4 and b_hi >= 0.9    # écarts extrêmes = pas de verdict118119120def test_couloir_de_securite():121    c_lo, c_hi = fv.CONFIG["corridor"]122    ref = {"p25": 900.0, "p50": 1000.0, "p75": 1100.0}123    fv_absurde = 5000.0124    borne = min(max(fv_absurde, ref["p25"] * c_lo), ref["p75"] * c_hi)125    assert borne == pytest.approx(ref["p75"] * c_hi)126127128def test_percentiles():129    vals = sorted(float(v) for v in range(1, 101))130    assert fv._pct(vals, 0.5) == pytest.approx(50.5)131    assert fv._pct(vals, 0.25) == pytest.approx(25.75)132    assert fv._pct([], 0.5) == 0.0133