# Framework de juste valeur (fair value) — Lou-Ka / Groupe-KA **Module** : `louka/fairvalue.py` · **Version modèle** : `fv-logement-1.0` · **Créé** : 2026-08-18 Estime en continu la **juste valeur locative** de chaque annonce publiée et la classe **sous le marché / dans le marché / au-dessus du marché**. Conçu comme le moteur générique « estimation de juste valeur » du Groupe-KA : toutes les spécificités logement vivent dans `CONFIG` (voir « Décliner sur immo-ka / auto-ka » plus bas). ## 1. Données d'entrée - **Jeu de référence** : toutes les annonces `active=1 AND published=1 AND dup_of IS NULL` au prix mensuel plausible (175–20 000 $). Les annonces en **quarantaine qualité** (`quality.py`) sont exclues — le framework repose sur les données enrichies et nettoyées des connecteurs. - Exclusions supplémentaires : prix affichés **non mensuels** (« /semaine », « par nuit » — regex `_NON_MENSUEL`) ; elles ne polluent ni les médianes ni les verdicts. - **Chambres/colocations** : segment dédié `bed="ch"` (détection `unit_type="Chambre"` ou regex `_CHAMBRE` sur le titre) — une chambre n'est jamais comparée au loyer d'un logement entier. - Rafraîchi **à chaque cycle de synchronisation** (`ingest.watch`, après géocodage, quality et dedup) et à la demande : `python run.py fairvalue`. Calcul complet : ~15 s pour ~47 000 annonces (pur Python + grille spatiale). ## 2. Méthode en trois niveaux ### N1 — Référence par segment Segments en échelle de repli, avec volume minimal `min_segment=8` : `ville|quartier|chambres` → `ville|chambres` → `chambres` (province) → global. Statistiques robustes : **médiane** + **p25/p75** (jamais la moyenne). Groupes de chambres : `0` (studio), `1`, `2`, `3`, `4+`, `ch` (chambre/coloc). ### N2 — Ajustements par caractéristiques Coefficients multiplicatifs **appris des données** (aucun coefficient inventé) : médiane de `prix/p50(segment)` des annonces AVEC la caractéristique ÷ celle des annonces SANS (minimum `coef_min_pairs=60` de chaque côté, bornés à `coef_bounds=(0.85, 1.35)`). **Centrage obligatoire** : le p50 du segment contient déjà le mélange avec/sans, donc l'ajustement appliqué est `(c si présent, sinon 1) / (part×c + (1−part))` — sans ce centrage, toutes les estimations gonflent et tout paraît « sous-évalué » (bug observé lors de la première passe : 54 % de « sous »). Caractéristique au statut inconnu = effet moyen (neutre). Superficie : `(aire/aire_type_segment)^0.35`, borné. Coefficients appris le 2026-08-18 : stationnement ×1,27 · lave-vaisselle ×1,13 · électricité incluse ×1,12 · meublé ×1,11 · chauffage inclus ×1,10 · balcon ×1,08 · climatisation ×1,07 · ascenseur ×1,06 · piscine ×1,06. ### N3 — Comparables kNN géographiques Pour les annonces géolocalisées : grille spatiale ~2 km, voisines du **même groupe de chambres** à < 3 km, pondération gaussienne `exp(−(d/800 m)²)` avec bonus ×1,3 si même type d'unité, k=12, minimum 4 comparables. Estimateur : **médiane pondérée** des loyers (la moyenne serait tirée par l'asymétrie). ### Combinaison et garde-fous - `fv = 0,6×kNN + 0,4×(segment ajusté)` quand le kNN existe, sinon segment ajusté. - **Couloir de sécurité** : `fv` bornée à `[0,70×p25 ; 1,30×p75]` du segment. - **Fourchette** : dispersion relative du segment appliquée à `fv` ; élargie si les deux méthodes divergent de > 25 % (et la confiance est abaissée). - **Confiance** : `fort` (≥ 8 comparables, segment fin, méthodes concordantes), `moyen` (≥ 4 comparables ou segment ville ≥ 15 annonces), `faible` sinon. ## 3. Classification et règles de prudence Écart relatif `d = (prix − fv)/fv` : **sous** si `d ≤ −8 %`, **sur** si `d ≥ +8 %`, **marche** entre les deux. **Pas de verdict** (pastille absente, estimation « indicative ») si : - confiance `faible` (peu de comparables / segment rare) ; - prix ambigu (`price_from` : « à partir de » = prix plancher) ; - écart hors du plausible (`verdict_dev_bounds = (−50 %, +100 %)`) — donnée probablement corrompue (coquille de prix, périodicité mal lue). ## 4. Stockage, historisation, traçabilité - `fairvalue` (1 rangée/annonce) : fv, fourchette, écart, verdict, confiance, **méthode utilisée**, nb de comparables, **segment de référence**, **version du modèle**, horodatage — chaque estimation est explicable. - `fv_segments` (append-only) : p25/p50/p75 + volume de chaque segment (niveaux 1-2) à chaque calcul → évolution des loyers de référence dans le temps. - Cache miniatures/geo non touchés. `fairvalue.explain(uid)` reconstruit la distribution du segment pour la fiche (histogramme 24 classes, bornes p1-p99). ## 5. Exposition - **Site** : pastille sur les cartes (`FairValueBadge`, icône + libellé + %, lisible daltoniens), bloc « Analyse de prix Lou-Ka » sur la fiche (`PriceAnalysis` : fv, fourchette, écart, confiance, mini-histogramme avec marqueurs « Ce loyer » / « Juste valeur »), filtre **« ▼ Sous le marché »** (tri meilleures affaires d'abord : `?deal=sous&sort=deal`), page de transparence `/juste-valeur`. - **API Lou-Ka** : champs `fv, fv_low, fv_high, fv_deviation, fv_verdict, fv_confidence` dans `/api/listings` (+ filtre `deal=`, tri `sort=deal`), pins GeoJSON, `/api/fairvalue/{uid}` (analyse détaillée + histogramme). - **API-KA** : `/api/v1/louka/fairvalue/{uid}` (proxy live pour l'app iOS KA) ; les snapshots quotidiens `louka_data` embarquent automatiquement les champs fv. - **Stats** : KPI « Annonces sous le marché », donut de répartition, tableau « Écart au marché par ville » (`statsdash.py`) — repris dans le PDF Groupe-KA. ## 6. Calibration et recalibrage - Seuils initiaux ±8 % : distribution obtenue ≈ 24 % sous / 39 % dans / 24 % sur / 13 % sans verdict (2026-08-18, ~47 k annonces). - **Procédure de recalibrage** (périodique, trimestrielle suggérée) : 1. `SELECT fv_verdict, durée de présence` — comparer le temps en ligne (`last_seen − first_seen` des annonces désactivées) par verdict : les « sous » doivent partir significativement plus vite que les « sur ». 2. Ajuster `seuil_sous/seuil_sur` pour maximiser cette séparation. 3. Re-vérifier le centrage : `AVG(deviation)` sur le parc doit rester ≈ 0 (±2 %) ; sinon revoir les coefficients (biais N2) ou le poids kNN. 4. Bumper `MODEL_VERSION` à tout changement (traçabilité des estimations). - Tests de non-régression : `tests/test_fairvalue.py` (synthétiques, hors-ligne). ## 7. Décliner sur immo-ka (vente) et auto-ka (véhicules) Le moteur est générique : segments en échelle + coefficients centrés + kNN + couloir + confiance. Pour décliner : 1. Copier `fairvalue.py`, remplacer `_load()` (requête du jeu de référence) et `CONFIG` : - **immo-ka** : segment = `ville|quartier|type de propriété|chambres`, valeur = prix demandé, caractéristiques = garage, sous-sol fini, piscine, année, terrain ; ajustement superficie avec élasticité plus forte (~0,6) ; kNN rayon 2 km. Bornes de plausibilité : 50 k–10 M $. - **auto-ka** : segment = `marque|modèle|année±1`, « géographie » remplacée par la distance en kilométrage (kNN sur km au lieu de lat/lng), caractéristiques = transmission, motorisation, groupe d'options ; dépréciation = équivalent de l'ajustement superficie. 2. Garder tels quels : `_pct`, centrage des coefficients, couloir, médiane pondérée kNN, règles de prudence, historisation, `MODEL_VERSION`. 3. Reprendre l'UI : `FairValueBadge`/`PriceAnalysis` sont paramétrés par les mêmes champs API (`fv_*`). ## 8. Limites connues - Les « bonnes affaires » à −40/−50 % contiennent encore quelques loyers à la chambre non détectés (annonces ambiguës « chambre dans grand 4½ » sans mots-clés) — bornées par `verdict_dev_bounds`. - Les alertes « bonne affaire » sur critères sauvegardés nécessitent une infrastructure de notification (courriel/push) absente de Lou-Ka à ce jour ; l'URL `/?deal=sous` est partageable/enregistrable en attendant. - Segments ruraux minces → repli `ville|chambres` provincial, confiance `faible`, pas de pastille (comportement voulu : rien d'inventé).