# ----------------------------------------------------------------------------- # Lou-Ka — Agrégateur de logements à louer (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # hiver.py : « Vie quotidienne en hiver » — indicateur 0-100 pensé pour le # Québec : peut-on vivre son quotidien À PIED quand il fait -20 °C # et que les trottoirs sont enneigés? # # Calcul déterministe et explicable à partir des données DÉJÀ collectées # (poi_cache — plus proche commodité par catégorie, OpenStreetMap) : # épicerie, pharmacie, arrêt de bus/métro, dépanneur. Chaque sous-score est # une décroissance linéaire de la distance de marche (vol d'oiseau × 1,3). # Pas de données de pente/exposition disponibles -> non pris en compte, et la # méthodologie l'assume. Sans données POI : aucun score (jamais inventé). # ----------------------------------------------------------------------------- from __future__ import annotations import json from . import db VERSION = "hiver-1.0" DETOUR = 1.3 # vol d'oiseau -> distance de marche (même facteur que POI) VITESSE_M_MIN = 80.0 # 4,8 km/h # (catégorie, libellé, poids, distance pleine note (m), distance note nulle (m)) CRITERES = [ ("epicerie", "Épicerie", 0.35, 300, 1500), ("pharmacie", "Pharmacie", 0.20, 300, 1500), ("bus", "Arrêt de bus", 0.25, 150, 900), ("metro", "Métro", 0.05, 400, 1800), ("depanneur", "Dépanneur", 0.15, 200, 1000), ] CLASSES = [(80, "très pratique"), (60, "pratique"), (40, "exigeant")] CLASSE_MIN = "difficile" def _sous_score(dist_m: float, plein: float, nul: float) -> float: d = dist_m * DETOUR if d <= plein: return 100.0 if d >= nul: return 0.0 return 100.0 * (nul - d) / (nul - plein) def score(lat: float | None, lng: float | None, con=None) -> dict | None: """Score hiver de l'immeuble (via poi_cache) — None si données absentes.""" if lat is None or lng is None: return None own = con is None if own: con = db.connect() try: key = f"{round(lat, 4)},{round(lng, 4)}" row = con.execute("SELECT pois FROM poi_cache WHERE coord_key=?", (key,)).fetchone() if row is None: return None try: pois = {p["cat"]: p for p in json.loads(row["pois"] or "[]")} except (ValueError, TypeError): return None if not pois: return None total = 0.0 poids_total = 0.0 detail = [] for cat, label, poids, plein, nul in CRITERES: p = pois.get(cat) if p is None: # métro absent = normal hors Montréal : critère simplement omis if cat == "metro": continue s = 0.0 item = {"critere": label, "score": 0, "distance_m": None, "note": "aucun repéré à distance de marche"} else: s = _sous_score(p["dist_m"], plein, nul) item = {"critere": label, "score": round(s), "distance_m": p["dist_m"], "minutes": max(1, round(p["dist_m"] * DETOUR / VITESSE_M_MIN)), "nom": p.get("name")} total += s * poids poids_total += poids detail.append(item) if poids_total == 0: return None note = round(total / poids_total) classe = CLASSE_MIN for seuil, nom in CLASSES: if note >= seuil: classe = nom break return { "score": note, "classe": classe, "detail": detail, "statut": "calculated", "version": VERSION, "methode": ("commodités essentielles à distance de marche " "(épicerie, pharmacie, bus/métro, dépanneur — " "OpenStreetMap), décroissance linéaire de la distance " "de marche estimée (vol d'oiseau × 1,3). Pente et " "exposition non disponibles : non prises en compte."), } finally: if own: con.close()