Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1// -----------------------------------------------------------------------------2// Lou-Ka — Agrégateur de logements à louer (province de Québec)3// Auteur : Simon-Pierre Boucher — contact@spboucher.ai4// pages/KaScores.tsx : « Comment sont calculés les KA Scores ? »5// Méthodologie publique (transparence obligatoire du barème), couverture6// du parc en direct, et réglage des priorités personnelles (les curseurs7// repondèrent le KA Score global affiché sur les fiches).8// -----------------------------------------------------------------------------9import { useEffect, useMemo, useState } from "react";10import {11 KA_DEFAULT_WEIGHTS, KaScoresStats, fetchKaScoresStats, kaWeights,12} from "../api";13import { KaScoreCircle } from "../components/KaScoreBadge";1415const SCORES: { key: string; nom: string; mesure: string; methode: string }[] = [16 {17 key: "walk", nom: "KA Walk Score", mesure: "Tout faire à pied.",18 methode: "Distance de marche estimée au plus proche de chaque besoin "19 + "quotidien (épicerie, pharmacie, parc, café, école, clinique, garderie, "20 + "dépanneur, gym, bibliothèque), pondérée par son importance — une "21 + "épicerie pèse trois fois plus qu'un gym. Pleine note en deçà d'un "22 + "seuil par catégorie (ex. épicerie ≤ 400 m), décroissance linéaire "23 + "ensuite. Petit bonus quand le choix existe (3 épiceries ou cafés à "24 + "moins de 800 m).",25 },26 {27 key: "transit", nom: "KA Transit Score", mesure: "La desserte en transport collectif.",28 methode: "Deux volets : la distance de marche au plus proche arrêt de bus "29 + "ou station de métro (OpenStreetMap), et la qualité de desserte du "30 + "secteur mesurée par Statistique Canada (base des mesures de "31 + "proximité 2021, percentile canadien). Un territoire sans arrêt à "32 + "distance de marche affiche « Non desservi », pas un 0 brut.",33 },34 {35 key: "bike", nom: "KA Bike Score", mesure: "Le vélo au quotidien.",36 methode: "Kilomètres de voies cyclables (pistes et bandes OSM) à moins "37 + "de 1 km, plus l'accessibilité des besoins quotidiens à distance de "38 + "vélo. Le dénivelé n'est pas encore pris en compte (v1).",39 },40 {41 key: "calme", nom: "KA Calme Score", mesure: "La tranquillité estimée du secteur.",42 methode: "Pénalités décroissantes selon la distance aux sources de bruit "43 + "cartographiées : autoroutes, artères principales, voies ferrées, "44 + "aéroports et héliports, zones industrielles, concentration de bars. "45 + "Bonus pour un parc tout proche. C'est une estimation basée sur "46 + "l'environnement, PAS une mesure sonore.",47 },48 {49 key: "services", nom: "KA Services Score", mesure: "La richesse globale des services.",50 methode: "Comptage pondéré des points d'intérêt par famille — commerces "51 + "(1 km), santé (3 km), éducation (3 km), loisirs (1 km) — avec "52 + "rendement décroissant : passer de 0 à 3 épiceries compte beaucoup "53 + "plus que de 12 à 15.",54 },55];5657const NOMS_COURTS: Record<string, string> = {58 walk: "Marche", transit: "Transport", bike: "Vélo",59 calme: "Calme", services: "Services",60};6162export default function KaScoresPage() {63 const [stats, setStats] = useState<KaScoresStats | null>(null);64 const [weights, setWeights] = useState<Record<string, number>>(() => kaWeights());6566 useEffect(() => {67 fetchKaScoresStats().then(setStats).catch(() => {});68 document.title = "KA Scores — méthodologie | Lou-Ka";69 }, []);7071 const somme = useMemo(72 () => Object.values(weights).reduce((a, b) => a + b, 0), [weights]);7374 const setW = (k: string, v: number) => {75 const next = { ...weights, [k]: v };76 setWeights(next);77 try { localStorage.setItem("louka_ks_poids", JSON.stringify(next)); } catch { /* privé */ }78 };79 const reset = () => {80 setWeights(KA_DEFAULT_WEIGHTS);81 try { localStorage.removeItem("louka_ks_poids"); } catch { /* privé */ }82 };8384 return (85 <div className="container page-doc">86 <section className="hero hero-doc">87 <span className="kicker">Transparence</span>88 <h1>Comment sont calculés les <span className="hl">KA Scores</span> ?</h1>89 <p className="lede">90 Cinq scores maison de 0 à 100 évaluent l'emplacement de chaque91 logement : marche, transport collectif, vélo, calme et services.92 Voici exactement comment — méthode, sources et limites.93 </p>94 {stats && (95 <div className="stat-row">96 <span className="stat-chip">97 <b>{stats.couverture_pct.toLocaleString("fr-CA")} %</b> du parc scoré98 ({stats.avec_score.toLocaleString("fr-CA")} annonces)99 </span>100 {stats.moyennes.global?.moyenne != null && (101 <span className="stat-chip">102 score global moyen <b>{stats.moyennes.global.moyenne}</b>103 </span>104 )}105 <span className="stat-chip">barème <b>{stats.version}</b></span>106 </div>107 )}108 </section>109110 <div className="ka-demo-circles" aria-hidden="true">111 <KaScoreCircle score={92} nom="Marche" />112 <KaScoreCircle score={74} nom="Transport" />113 <KaScoreCircle score={61} nom="Vélo" />114 <KaScoreCircle score={45} nom="Calme" />115 <KaScoreCircle score={83} nom="Services" />116 </div>117118 {SCORES.map((s) => (119 <section className="f-bloc doc-bloc" key={s.key}>120 <h2>{s.nom}</h2>121 <p><b>Ce qu'il mesure :</b> {s.mesure}</p>122 <p>{s.methode}</p>123 {stats?.moyennes[s.key]?.moyenne != null && (124 <p className="fine">125 Moyenne du parc : {stats.moyennes[s.key]!.moyenne} —{" "}126 {stats.moyennes[s.key]!.n.toLocaleString("fr-CA")} immeubles évalués.127 </p>128 )}129 </section>130 ))}131132 <section className="f-bloc doc-bloc" id="priorites">133 <h2>Vos priorités, votre score</h2>134 <p>135 Le KA Score global affiché partout est une moyenne pondérée des cinq136 scores. Réglez ici ce qui compte pour <em>vous</em> — les fiches137 afficheront votre score personnalisé à côté du score standard.138 Ces réglages restent sur votre appareil.139 </p>140 <div className="ka-poids">141 {Object.keys(KA_DEFAULT_WEIGHTS).map((k) => (142 <label key={k} className="ka-poids-row">143 <span>{NOMS_COURTS[k]}</span>144 <input145 type="range" min={0} max={50} step={5}146 value={Math.round((weights[k] ?? 0) * 100)}147 onChange={(e) => setW(k, Number(e.target.value) / 100)}148 aria-label={`Importance de ${NOMS_COURTS[k]}`}149 />150 <b>{Math.round(((weights[k] ?? 0) / (somme || 1)) * 100)} %</b>151 </label>152 ))}153 </div>154 <button className="btn btn-ghost" onClick={reset}>155 Revenir aux pondérations standard156 </button>157 </section>158159 <section className="f-bloc doc-bloc">160 <h2>Sources, honnêteté et limites</h2>161 <ul className="doc-liste">162 <li>163 <b>Sources :</b> points d'intérêt, routes, rails et voies cyclables164 © contributeurs <a href="https://www.openstreetmap.org/copyright"165 target="_blank" rel="noreferrer">OpenStreetMap</a> (ODbL) ;166 desserte en transport : Base de données des mesures de proximité,167 Statistique Canada (2021).168 </li>169 <li>170 <b>Distances :</b> à vol d'oiseau multipliées par 1,3 (facteur171 réseau usuel) — pas un routage piéton exact.172 </li>173 <li>174 <b>Données insuffisantes :</b> un secteur mal cartographié (zone175 rurale) affiche « Données insuffisantes » plutôt qu'un score176 trompeur ; un territoire sans transport collectif affiche177 « Non desservi ».178 </li>179 <li>180 <b>Cohérence :</b> les scores sont calculés par immeuble ; deux181 logements du même immeuble partagent exactement les mêmes scores.182 </li>183 <li>184 <b>Versionnage :</b> le barème est versionné ; tout changement de185 méthode recalcule l'ensemble du parc et la date de calcul est186 affichée sur chaque fiche.187 </li>188 </ul>189 </section>190 </div>191 );192}193