feat: ValoPlex — moteur d'évaluation spécialisé plex du Québec
393 867 plex (2+ logements), modèle hédonique en ratio prix/rôle avec calibration conforme Mondrian par gabarit, ajustement par porte, pro forma d'investisseur complet (hypothèque canadienne, projection 5 ans, point mort, droits de mutation), rapports PDF. Identité orange #ff9f45. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
68 changed files +16,147 −0
added
.gitignore
+17 −0
@@ -0,0 +1,17 @@ | ||
| 1 | +# dépendances / build | |
| 2 | +app/node_modules/ | |
| 3 | +app/.next/ | |
| 4 | +*.tsbuildinfo | |
| 5 | +app/next-env.d.ts | |
| 6 | + | |
| 7 | +# données — jamais versionnées (base SQLite, modèles, estimations) | |
| 8 | +app/data/ | |
| 9 | +data/ | |
| 10 | +models/ | |
| 11 | + | |
| 12 | +# divers | |
| 13 | +.DS_Store | |
| 14 | +*.log | |
| 15 | +app/test-results/ | |
| 16 | +app/playwright-report/ | |
| 17 | +__pycache__/ | |
added
README.md
+108 −0
@@ -0,0 +1,108 @@ | ||
| 1 | +# ValoPlex | |
| 2 | + | |
| 3 | +> **La vraie valeur de votre plex, porte par porte.** | |
| 4 | +> Le moteur d'évaluation spécialisé plex du Québec — 393 867 immeubles de 2 logements et plus, 1 733 744 portes, un modèle hédonique en ratio à l'épreuve des échelles. | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | +**En production : [www.valoplex.com](https://www.valoplex.com)** · petit frère spécialisé de [Vrai-Prix](https://www.vrai-prix.com) | |
| 17 | + | |
| 18 | +--- | |
| 19 | + | |
| 20 | +## Pourquoi un moteur spécialisé plex ? | |
| 21 | + | |
| 22 | +Un plex est un **immeuble à revenus** : sa valeur se pense par porte, par revenu, par TGA — pas comme une unifamiliale. ValoPlex couvre **tous** les immeubles résidentiels multi-logements du Québec (CUBF 1000, 2 logements et plus — du duplex à la tour) avec un modèle et une interface dédiés à cette logique. | |
| 23 | + | |
| 24 | +| Fonctionnalité | Description | | |
| 25 | +|---|---| | |
| 26 | +| 🚪 Économie par porte | Valeur/porte, rôle/porte, aire/porte, terrain/porte + benchmark vs marché local | | |
| 27 | +| 🧮 Pro forma investisseur | Loyer implicite, état des résultats complet, hypothèque canadienne, DSCR, cashflow/porte, projection 5 ans, point mort, sensibilité aux taux — interactif | | |
| 28 | +| 💸 Frais « tout le kit » | Droits de mutation (barème QC + barème bonifié de Montréal), notaire, inspection, liquidités totales | | |
| 29 | +| 🏢 Frise des portes | Une porte SVG dessinée par logement (signature) + badge DUPLEX/TRIPLEX/…/MULTI N PORTES | | |
| 30 | +| 📍 Adresses officielles | Toutes les inscriptions d'adresses de l'unité (table des adresses du rôle) | | |
| 31 | +| ⚖️ Ajustement portes | 4ᵉ ajustement des comparables : 50 % du prix/porte, plafonné ±30 % | | |
| 32 | +| 🗺️ Plan des comparables | Carte plan d'arpenteur SVG (azimut + distance réels), état vide honnête si aucun plex comparable | | |
| 33 | +| 📄 Rapports PDF | Standard 3 p. (avec pro forma), professionnel bancaire 6 p., parc, statistique provincial | | |
| 34 | +| 📉 Statistiques | Les plex du Québec valent **400,6 G$** — répartition par gabarit et par municipalité | | |
| 35 | + | |
| 36 | +## Le modèle : hédonique en ratio, calibré par gabarit | |
| 37 | + | |
| 38 | +Trois idées font la différence : | |
| 39 | + | |
| 40 | +1. **Cible en ratio `log(prix / valeur au rôle)`** — pas le prix. Un modèle en prix brut, dominé par la masse des duplex, écrase les grands immeubles (nous avons mesuré une tour au rôle de 705 M$ prédite… 5,6 M$). En ratio, le duplex de 400 k$ et la tour de 700 M$ sont évalués avec la même justesse relative. Le modèle connaît en plus **l'ancienneté du rôle** (mois depuis la date des conditions du marché du dénominateur). | |
| 41 | +2. **Rétrécissement hors domaine** : au-delà de 12 portes (données de ventes rares), l'estimation glisse vers l'ancre empirique de sa bande de gabarit (poids du modèle : 55 % → 20 %). | |
| 42 | +3. **Calibration conforme Mondrian** : les fourchettes P10–P90 sont calibrées **par bande de portes** sur un ensemble de calibration dédié — couverture vérifiée de **80,0 %** (pas promise : mesurée). | |
| 43 | + | |
| 44 | +### Validation (norme IAAO, 13 617 ventes de test jamais vues) | |
| 45 | + | |
| 46 | +| Segment | MdAPE | Ratio médian | | |
| 47 | +|---|---|---| | |
| 48 | +| Tous les plex | **13,8 %** | 0,976 | | |
| 49 | +| Duplex (2 portes) | 13,9 % | 0,970 | | |
| 50 | +| Triplex (3 portes) | **12,6 %** | 0,983 | | |
| 51 | +| 4–5 portes | 14,1 % | 0,971 | | |
| 52 | +| 6–12 portes | 17,9 % | 0,955 | | |
| 53 | +| Test temporel 2026 (9 172 ventes) | 13,8 % | 0,972 | | |
| 54 | +| Couverture de la fourchette (calibrée) | **79,9 ≈ 80 %** | — | | |
| 55 | + | |
| 56 | +## Le pro forma (`app/src/lib/proforma.ts` — 20 tests) | |
| 57 | + | |
| 58 | +Approche revenu **inversée** : quelle structure de revenus la valeur estimée suppose-t-elle ? | |
| 59 | + | |
| 60 | +- Loyer implicite en forme fermée : `RB = (V·TGA + F) / (1 − vacance − %variables)` | |
| 61 | +- Dépenses détaillées : taxes municipales et scolaire (dérivées du rôle), assurances, entretien, gestion, déneigement, énergie des communs, réserve | |
| 62 | +- Hypothèque **canadienne** (composition semestrielle), cashflow/porte/mois, DSCR, cash-on-cash | |
| 63 | +- Capital remboursé an 1, **projection 5 ans** (équité, gain total, multiple sur liquidités) | |
| 64 | +- **Loyer de point mort** et **taux de point mort** (bissection), sensibilité taux ±1 % | |
| 65 | +- Droits de mutation : barème provincial + barème bonifié de Montréal (jusqu'à 4 %) | |
| 66 | + | |
| 67 | +Cinq curseurs (taux, mise de fonds, amortissement, TGA, appréciation) — tout recalcule en direct. | |
| 68 | + | |
| 69 | +## Structure du dépôt | |
| 70 | + | |
| 71 | +``` | |
| 72 | +valoplex/ | |
| 73 | +├── app/ # application Next.js 16 (TypeScript strict, Tailwind 4) | |
| 74 | +│ ├── src/lib/ # engine.ts (comparables), proforma.ts, report*.ts (pdfkit) | |
| 75 | +│ ├── src/components/ # RadarMap, MetricViz, Proforma, DoorsRow… | |
| 76 | +│ └── src/app/ # pages + API routes | |
| 77 | +└── scripts/ # pipeline données/modèle (Python) | |
| 78 | + ├── hedonic_plex.py # entraînement (ratio + Mondrian) et prédictions 2021-2026 | |
| 79 | + └── build_valoplex_db.py # base SQLite + stats provinciales | |
| 80 | +``` | |
| 81 | + | |
| 82 | +## Démarrage | |
| 83 | + | |
| 84 | +```bash | |
| 85 | +cd app && npm install | |
| 86 | +# placer la base dans app/data/valoplex.db (non versionnée) | |
| 87 | +npm run dev | |
| 88 | +npm run test # 33 tests (moteur + finance) | |
| 89 | +``` | |
| 90 | + | |
| 91 | +Pipeline (nécessite les rôles MAMH et les transactions enrichies, non versionnés) : | |
| 92 | + | |
| 93 | +```bash | |
| 94 | +python3 scripts/hedonic_plex.py train && python3 scripts/hedonic_plex.py predict | |
| 95 | +python3 scripts/build_valoplex_db.py | |
| 96 | +``` | |
| 97 | + | |
| 98 | +> **Note données** : base SQLite (165 Mo), modèles entraînés et parquets d'estimation non versionnés. Données sources : [rôles d'évaluation foncière du Québec](https://www.donneesquebec.ca/recherche/dataset/roles-d-evaluation-fonciere-du-quebec) (MAMH, licence ouverte). | |
| 99 | + | |
| 100 | +### Design | |
| 101 | + | |
| 102 | +Identité « éditorial sharp » : papier grainé `#f5f3ee`, encre `#141814`, **accent orange vif `#ff9f45`**, ocre brûlé `#b25f16` — sœur chromatique de Vrai-Prix (rouge). | |
| 103 | + | |
| 104 | +## Auteur | |
| 105 | + | |
| 106 | +**Simon-Pierre Boucher** — [contact@spboucher.ai](mailto:contact@spboucher.ai) | |
| 107 | + | |
| 108 | +*Estimations statistiques à titre indicatif — ne remplacent pas une évaluation par un évaluateur agréé (OEAQ) ni un conseil financier. Voir [conditions](https://www.valoplex.com/conditions) et [méthodologie](https://www.valoplex.com/methodologie).* | |
added
app/.gitignore
+41 −0
@@ -0,0 +1,41 @@ | ||
| 1 | +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | |
| 2 | + | |
| 3 | +# dependencies | |
| 4 | +/node_modules | |
| 5 | +/.pnp | |
| 6 | +.pnp.* | |
| 7 | +.yarn/* | |
| 8 | +!.yarn/patches | |
| 9 | +!.yarn/plugins | |
| 10 | +!.yarn/releases | |
| 11 | +!.yarn/versions | |
| 12 | + | |
| 13 | +# testing | |
| 14 | +/coverage | |
| 15 | + | |
| 16 | +# next.js | |
| 17 | +/.next/ | |
| 18 | +/out/ | |
| 19 | + | |
| 20 | +# production | |
| 21 | +/build | |
| 22 | + | |
| 23 | +# misc | |
| 24 | +.DS_Store | |
| 25 | +*.pem | |
| 26 | + | |
| 27 | +# debug | |
| 28 | +npm-debug.log* | |
| 29 | +yarn-debug.log* | |
| 30 | +yarn-error.log* | |
| 31 | +.pnpm-debug.log* | |
| 32 | + | |
| 33 | +# env files (can opt-in for committing if needed) | |
| 34 | +.env* | |
| 35 | + | |
| 36 | +# vercel | |
| 37 | +.vercel | |
| 38 | + | |
| 39 | +# typescript | |
| 40 | +*.tsbuildinfo | |
| 41 | +next-env.d.ts | |
added
app/AGENTS.md
+9 −0
@@ -0,0 +1,9 @@ | ||
| 1 | +<!-- BEGIN:nextjs-agent-rules --> | |
| 2 | + | |
| 3 | +# This is NOT the Next.js you know | |
| 4 | + | |
| 5 | +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. | |
| 6 | + | |
| 7 | +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. | |
| 8 | + | |
| 9 | +<!-- END:nextjs-agent-rules --> | |
added
app/CLAUDE.md
+1 −0
@@ -0,0 +1 @@ | ||
| 1 | +@AGENTS.md | |
added
app/README.md
+3 −0
@@ -0,0 +1,3 @@ | ||
| 1 | +# ValoPlex — application | |
| 2 | + | |
| 3 | +Voir le [README principal](../README.md) à la racine du dépôt. | |
added
app/assets/fonts/Inter-Regular.ttf
+0 −0
Binary file not shown.
added
app/assets/fonts/JetBrainsMono-Bold.ttf
+0 −0
Binary file not shown.
added
app/assets/fonts/JetBrainsMono-Regular.ttf
+0 −0
Binary file not shown.
added
app/assets/fonts/SpaceGrotesk-Bold.ttf
+0 −0
Binary file not shown.
added
app/assets/fonts/SpaceGrotesk-Medium.ttf
+0 −0
Binary file not shown.
added
app/eslint.config.mjs
+18 −0
@@ -0,0 +1,18 @@ | ||
| 1 | +import { defineConfig, globalIgnores } from "eslint/config"; | |
| 2 | +import nextVitals from "eslint-config-next/core-web-vitals"; | |
| 3 | +import nextTs from "eslint-config-next/typescript"; | |
| 4 | + | |
| 5 | +const eslintConfig = defineConfig([ | |
| 6 | + ...nextVitals, | |
| 7 | + ...nextTs, | |
| 8 | + // Override default ignores of eslint-config-next. | |
| 9 | + globalIgnores([ | |
| 10 | + // Default ignores of eslint-config-next: | |
| 11 | + ".next/**", | |
| 12 | + "out/**", | |
| 13 | + "build/**", | |
| 14 | + "next-env.d.ts", | |
| 15 | + ]), | |
| 16 | +]); | |
| 17 | + | |
| 18 | +export default eslintConfig; | |
added
app/next.config.ts
+8 −0
@@ -0,0 +1,8 @@ | ||
| 1 | +// Auteur : Simon-Pierre Boucher — contact@spboucher.ai | |
| 2 | +import type { NextConfig } from "next"; | |
| 3 | + | |
| 4 | +const nextConfig: NextConfig = { | |
| 5 | + serverExternalPackages: ["better-sqlite3", "pdfkit"], | |
| 6 | +}; | |
| 7 | + | |
| 8 | +export default nextConfig; | |
added
app/package-lock.json
+8233 −0
Large diff (8,233 lines) is not loaded by default to keep the page fast.Load diff
added
app/package.json
+33 −0
Large diff (33 lines) is not loaded by default to keep the page fast.Load diff
added
app/postcss.config.mjs
+7 −0
Large diff (7 lines) is not loaded by default to keep the page fast.Load diff
added
app/public/file.svg
+1 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
app/public/globe.svg
+1 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
app/public/next.svg
+1 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
app/public/vercel.svg
+1 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
app/public/window.svg
+1 −0
Large diff (2 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/api/estimate/route.ts
+42 −0
Large diff (42 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/api/lead/route.ts
+17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/api/portfolio/route.ts
+14 −0
Large diff (14 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/api/report/portfolio/route.ts
+25 −0
Large diff (25 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/api/report/pro/route.ts
+28 −0
Large diff (28 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/api/report/route.ts
+26 −0
Large diff (26 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/api/report/stats/route.ts
+17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/api/search/route.ts
+20 −0
Large diff (20 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/conditions/page.tsx
+119 −0
Large diff (119 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/confidentialite/page.tsx
+129 −0
Large diff (129 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/estimation/[id]/page.tsx
+17 −0
Large diff (17 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/estimation/manuelle/page.tsx
+36 −0
Large diff (36 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/favicon.ico
+0 −0
Binary file not shown.
added
app/src/app/globals.css
+234 −0
Large diff (234 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/layout.tsx
+94 −0
Large diff (94 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/methodologie/page.tsx
+259 −0
Large diff (259 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/page.tsx
+100 −0
Large diff (100 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/parc/page.tsx
+358 −0
Large diff (358 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/app/stats/page.tsx
+13 −0
Large diff (13 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/CookieConsent.tsx
+226 −0
Large diff (226 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/EstimateForm.tsx
+95 −0
Large diff (95 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/FooterNav.tsx
+34 −0
Large diff (34 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/HeaderNav.tsx
+153 −0
Large diff (153 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/LangContext.tsx
+46 −0
Large diff (46 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/LeadForm.tsx
+56 −0
Large diff (56 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/LegalPage.tsx
+78 −0
Large diff (78 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/MetricViz.tsx
+470 −0
Large diff (470 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/Proforma.tsx
+212 −0
Large diff (212 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/RadarMap.tsx
+252 −0
Large diff (252 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/ResultView.tsx
+595 −0
Large diff (595 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/SearchBox.tsx
+95 −0
Large diff (95 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/components/StatsView.tsx
+282 −0
Large diff (282 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/lib/db.ts
+144 −0
Large diff (144 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/lib/engine.test.ts
+155 −0
Large diff (155 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/lib/engine.ts
+312 −0
Large diff (312 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/lib/estimator.ts
+304 −0
Large diff (304 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/lib/i18n.ts
+117 −0
Large diff (117 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/lib/proforma.test.ts
+123 −0
Large diff (123 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/lib/proforma.ts
+290 −0
Large diff (290 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/lib/report-portfolio.ts
+308 −0
Large diff (308 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/lib/report-pro.ts
+448 −0
Large diff (448 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/lib/report-stats.ts
+206 −0
Large diff (206 lines) is not loaded by default to keep the page fast.Load diff
added
app/src/lib/report.ts
+454 −0
Large diff (454 lines) is not loaded by default to keep the page fast.Load diff
added
app/tsconfig.json
+34 −0
Large diff (34 lines) is not loaded by default to keep the page fast.Load diff
added
app/vitest.config.ts
+9 −0
Large diff (9 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/build_valoplex_db.py
+255 −0
Large diff (255 lines) is not loaded by default to keep the page fast.Load diff
added
scripts/hedonic_plex.py
+363 −0
Large diff (364 lines) is not loaded by default to keep the page fast.Load diff