Python 68.8%
TypeScript 18.6%
CSS 8.7%
JavaScript 3.3%
HTML 0.6%
1// -----------------------------------------------------------------------------2// Rent-Ka — Rental listings aggregator (Canada, outside Québec)3// Author: Simon-Pierre Boucher — contact@spboucher.ai4// pages/KaScores.tsx: "How are KA Scores calculated?"5// Public methodology (mandatory scale transparency), live coverage of the6// inventory, and personal priority settings (the sliders reweight the7// global KA Score shown on listing pages).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: "Doing everything on foot.",18 methode: "Estimated walking distance to the nearest of each daily need "19 + "(grocery, pharmacy, park, café, school, clinic, daycare, convenience "20 + "store, gym, library), weighted by importance — a grocery store weighs "21 + "three times more than a gym. Full marks below a per-category "22 + "threshold (e.g. grocery ≤ 400 m), linear decay afterwards. Small "23 + "bonus when there is choice (3 grocery stores or cafés within 800 m).",24 },25 {26 key: "transit", nom: "KA Transit Score", mesure: "Public transit service.",27 methode: "Two parts: the walking distance to the nearest bus stop or rapid "28 + "transit station (OpenStreetMap), and the area's transit service level "29 + "measured by Statistics Canada (2021 proximity measures database, "30 + "Canadian percentile). An area with no stop within walking distance "31 + "shows \"Not served\", not a raw 0.",32 },33 {34 key: "bike", nom: "KA Bike Score", mesure: "Everyday cycling.",35 methode: "Kilometres of bike lanes (OSM paths and lanes) within 1 km, plus "36 + "the reachability of daily needs at biking distance. Elevation is not "37 + "yet taken into account (v1).",38 },39 {40 key: "calme", nom: "KA Quiet Score", mesure: "The estimated quietness of the area.",41 methode: "Decaying penalties by distance to mapped noise sources: "42 + "highways, main arteries, railways, airports and heliports, industrial "43 + "zones, bar concentrations. Bonus for a park close by. This is an "44 + "environment-based estimate, NOT a sound measurement.",45 },46 {47 key: "services", nom: "KA Services Score", mesure: "The overall richness of services.",48 methode: "Weighted count of points of interest by family — shops (1 km), "49 + "health (3 km), education (3 km), leisure (1 km) — with diminishing "50 + "returns: going from 0 to 3 grocery stores counts much more than from "51 + "12 to 15.",52 },53];5455const NOMS_COURTS: Record<string, string> = {56 walk: "Walk", transit: "Transit", bike: "Bike",57 calme: "Quiet", services: "Services",58};5960export default function KaScoresPage() {61 const [stats, setStats] = useState<KaScoresStats | null>(null);62 const [weights, setWeights] = useState<Record<string, number>>(() => kaWeights());6364 useEffect(() => {65 fetchKaScoresStats().then(setStats).catch(() => {});66 document.title = "KA Scores — methodology | Rent-Ka";67 }, []);6869 const somme = useMemo(70 () => Object.values(weights).reduce((a, b) => a + b, 0), [weights]);7172 const setW = (k: string, v: number) => {73 const next = { ...weights, [k]: v };74 setWeights(next);75 try { localStorage.setItem("rentka_ks_weights", JSON.stringify(next)); } catch { /* private */ }76 };77 const reset = () => {78 setWeights(KA_DEFAULT_WEIGHTS);79 try { localStorage.removeItem("rentka_ks_weights"); } catch { /* private */ }80 };8182 return (83 <div className="container page-doc">84 <section className="hero hero-doc">85 <span className="kicker">Transparency</span>86 <h1>How are <span className="hl">KA Scores</span> calculated?</h1>87 <p className="lede">88 Five in-house scores from 0 to 100 rate each rental's location:89 walking, public transit, biking, quiet and services. Here is exactly90 how — method, sources and limits.91 </p>92 {stats && (93 <div className="stat-row">94 <span className="stat-chip">95 <b>{stats.couverture_pct.toLocaleString("en-CA")} %</b> of the inventory scored96 ({stats.avec_score.toLocaleString("en-CA")} listings)97 </span>98 {stats.moyennes.global?.moyenne != null && (99 <span className="stat-chip">100 average global score <b>{stats.moyennes.global.moyenne}</b>101 </span>102 )}103 <span className="stat-chip">scale <b>{stats.version}</b></span>104 </div>105 )}106 </section>107108 <div className="ka-demo-circles" aria-hidden="true">109 <KaScoreCircle score={92} nom="Walk" />110 <KaScoreCircle score={74} nom="Transit" />111 <KaScoreCircle score={61} nom="Bike" />112 <KaScoreCircle score={45} nom="Quiet" />113 <KaScoreCircle score={83} nom="Services" />114 </div>115116 {SCORES.map((s) => (117 <section className="f-bloc doc-bloc" key={s.key}>118 <h2>{s.nom}</h2>119 <p><b>What it measures:</b> {s.mesure}</p>120 <p>{s.methode}</p>121 {stats?.moyennes[s.key]?.moyenne != null && (122 <p className="fine">123 Inventory average: {stats.moyennes[s.key]!.moyenne} —{" "}124 {stats.moyennes[s.key]!.n.toLocaleString("en-CA")} buildings rated.125 </p>126 )}127 </section>128 ))}129130 <section className="f-bloc doc-bloc" id="priorites">131 <h2>Your priorities, your score</h2>132 <p>133 The global KA Score shown everywhere is a weighted average of the134 five scores. Set here what matters to <em>you</em> — listing pages135 will show your personalized score next to the standard one.136 These settings stay on your device.137 </p>138 <div className="ka-poids">139 {Object.keys(KA_DEFAULT_WEIGHTS).map((k) => (140 <label key={k} className="ka-poids-row">141 <span>{NOMS_COURTS[k]}</span>142 <input143 type="range" min={0} max={50} step={5}144 value={Math.round((weights[k] ?? 0) * 100)}145 onChange={(e) => setW(k, Number(e.target.value) / 100)}146 aria-label={`Importance of ${NOMS_COURTS[k]}`}147 />148 <b>{Math.round(((weights[k] ?? 0) / (somme || 1)) * 100)} %</b>149 </label>150 ))}151 </div>152 <button className="btn btn-ghost" onClick={reset}>153 Back to standard weights154 </button>155 </section>156157 <section className="f-bloc doc-bloc">158 <h2>Sources, honesty and limits</h2>159 <ul className="doc-liste">160 <li>161 <b>Sources:</b> points of interest, roads, rails and bike lanes162 © <a href="https://www.openstreetmap.org/copyright"163 target="_blank" rel="noreferrer">OpenStreetMap</a> contributors164 (ODbL); transit service: Proximity Measures Database, Statistics165 Canada (2021).166 </li>167 <li>168 <b>Distances:</b> straight-line multiplied by 1.3 (usual network169 factor) — not exact pedestrian routing.170 </li>171 <li>172 <b>Insufficient data:</b> a poorly mapped area (rural zone) shows173 "Insufficient data" rather than a misleading score; an area with174 no public transit shows "Not served".175 </li>176 <li>177 <b>Consistency:</b> scores are computed per building; two rentals178 in the same building share exactly the same scores.179 </li>180 <li>181 <b>Versioning:</b> the scale is versioned; any method change182 recomputes the whole inventory and the computation date is shown183 on every listing page.184 </li>185 </ul>186 </section>187 </div>188 );189}190