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%
8.0 KB · 190 lines tsx
Raw Blame History
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