# Vrai-Prix — Pipeline de données (documentation de continuité) > Rédigé le 2026-08-18. **Important : contrairement aux autres apps Ka, Vrai-Prix > n'a AUCUN connecteur runtime.** Toute la donnée est produite par un pipeline > **batch qui tourne sur le laptop** (`~/Desktop/qc_house_eval/` et > `~/Desktop/house/`), puis livrée au nœud M3U96a sous forme d'un unique > fichier SQLite (`data/vraiprix.db`, ~1,5 Go). Ce document existe pour que > n'importe quelle session future puisse comprendre, auditer et rejouer le > pipeline même sans accès à l'historique de conversation. ## Vue d'ensemble ``` [MAMH — rôles d'évaluation 2021-2026] [api.qub.ca — transactions] Données Québec (FGDB/GPKG, licence scraping quadtree, token ouverte, téléchargement MANUEL) Bearer recapturé via Safari │ │ │ extract_roles.py │ ~/Desktop/house/scrape_province.py ▼ ▼ unités normalisées ◄──── merge_transactions.py ──── transactions brutes (fusion spatiale kNN, 2 passes, 99,88 %) │ ▼ hedonic.py — LightGBM, 4 modèles hédoniques (estimation de valeur) │ ▼ build_vrai_prix_db.py — assemble data/vraiprix.db (units, transactions, │ market_index, FTS5 units_fts, leads) ▼ build_stats.py — agrégats / index de marché │ ▼ scp vraiprix.db (~1,5 Go) → M3U96a:~/apps/vrai-prix/data/ → app Next.js ``` ## 1. Sources amont ### 1.1 Rôles d'évaluation foncière (MAMH) - **Quoi** : rôles d'évaluation municipale du Québec, millésime **2021-2026** (MAMH — ministère des Affaires municipales et de l'Habitation). - **Où** : [Données Québec](https://www.donneesquebec.ca/) — jeu « Rôles d'évaluation foncière ». **Licence ouverte** (réutilisation permise avec attribution — Données Québec / MAMH). - **Format** : **FGDB** (Esri File Geodatabase) et/ou **GPKG** (GeoPackage), **téléchargés manuellement** (pas d'API d'ingestion automatique — les fichiers sont volumineux et versionnés par millésime). - **Contenu utile** : chaque unité d'évaluation (adresse, matricule, usage, géométrie/GPS, valeurs au rôle terrain/bâtiment, caractéristiques physiques — frontage, superficie, année de construction, nb logements…). - **Stockage laptop** : sous `~/Desktop/qc_house_eval/` (données brutes + scripts dans `scripts/`). ### 1.2 Transactions immobilières (api.qub.ca) - **Quoi** : transactions de vente résidentielles (prix réel, date d'acte), utilisées pour entraîner les modèles hédoniques et calculer l'indice de marché. - **Comment** : **scraping par quadtree** de l'API `api.qub.ca` avec `~/Desktop/house/scrape_province.py` (sur le **laptop**) : découpage récursif du territoire en tuiles jusqu'à passer sous le plafond de résultats par requête, couverture provinciale complète. - **Auth** : jeton **Bearer** requis, **recapturé manuellement via Safari** (ouvrir le site qub.ca, inspecteur web → copier l'en-tête Authorization d'une requête à `api.qub.ca`, le coller dans le script). Le jeton expire : c'est l'étape manuelle fragile du pipeline (voir « Risques »). ## 2. Étapes du pipeline (laptop — `~/Desktop/qc_house_eval/scripts/`) Ordre d'exécution (chaque étape lit la sortie de la précédente) : | # | Script | Rôle | |---|---|---| | 0 | `~/Desktop/house/scrape_province.py` | scraping quadtree des transactions api.qub.ca (token Bearer manuel) — préalable, peut tourner indépendamment | | 1 | `extract_roles.py` | extraction/normalisation des unités d'évaluation depuis les FGDB/GPKG MAMH (usages résidentiels, champs canoniques, GPS) | | 2 | `merge_transactions.py` | **fusion spatiale kNN en 2 passes** des transactions sur les unités du rôle — taux d'appariement **99,88 %** | | 3 | `hedonic.py` | entraînement **LightGBM — 4 modèles hédoniques** (par grande famille de propriété) → valeur estimée « vrai prix » par unité | | 4 | `build_vrai_prix_db.py` | assemblage du SQLite final `vraiprix.db` : tables `units`, `transactions`, `market_index`, index plein-texte FTS5 (`units_fts*`), table applicative `leads` | | 5 | `build_stats.py` | agrégats statistiques / indice de marché (alimente `market_index` et la page /stats) | ## 3. Livraison au nœud Le nœud **ne fait que servir** la base — l'app Next.js (M3U96a, ce repo) lit `data/vraiprix.db` en lecture seule. ```bash # depuis le laptop, après un build complet : scp ~/Desktop/qc_house_eval/…/vraiprix.db M3U96a:apps/vrai-prix/data/vraiprix.db ssh M3U96a 'pm2 restart vrai-prix' # recharger l'app après remplacement de la BD ``` - Taille du livrable : **~1,5 Go** (dernier fichier livré : 2026-08-08). - La BD est en WAL (`-shm`/`-wal` présents sur le nœud) ; remplacer le `.db` quand l'app est arrêtée ou juste avant un `pm2 restart` pour éviter tout mélange de journaux. ## 4. Volumétrie (build livré, vérifiée live sur le nœud 2026-08-18) | Table | Lignes | Note | |---|---|---| | `units` | **3 747 008** | unités d'évaluation résidentielles, province entière | | `transactions` | **745 119** | ventes appariées (kNN 2 passes, 99,88 %) | | `market_index` | **268** | lignes d'indice de marché (build_stats.py) | | `units_fts` (+`_data`/`_idx`/`_config`/`_docsize`) | — | index FTS5 de recherche d'adresses | | `leads` | — | table applicative (demandes des visiteurs), remplie par l'app, PAS par le pipeline | - **Dernier build : 2026-08-08** (mtime du fichier sur le nœud : 8 août 18:22). ## 5. Complétude des champs (build 2026-08-08) | Champ | Complétude | |---|---| | GPS (lat/lng des unités) | **100 %** | | Frontage | **83 %** | | Année de construction | **79 %** | Les champs manquants sont des trous du rôle MAMH lui-même (certaines municipalités ne publient pas toutes les caractéristiques) — les modèles hédoniques les traitent comme valeurs manquantes natives LightGBM. ## 6. Risques & limites (à connaître avant tout rafraîchissement) 1. **Token qub.ca manuel** : le Bearer d'api.qub.ca doit être recapturé à la main via Safari à chaque campagne de scraping ; s'il expire en cours de quadtree, relancer `scrape_province.py` (il est repriseable par tuiles). Aucun renouvellement automatique — point de fragilité n° 1. 2. **Pipeline NON reproductible sur le nœud** : les données brutes (FGDB/GPKG MAMH, dumps qub.ca), les scripts et les environnements Python (GDAL/ pyogrio pour lire les FGDB, LightGBM) ne vivent que sur le **laptop** (`~/Desktop/qc_house_eval/`, `~/Desktop/house/`). Si le laptop est perdu, le pipeline doit être reconstruit ; seul le livrable `vraiprix.db` est sur le nœud (et le repo git ne contient PAS la BD ni les données brutes). → Recommandation : archiver `qc_house_eval/scripts/` + `house/` (au moins les scripts) sur le NAS ou dans un repo gitsrv dédié. 3. **Rôle 2027 à venir** : le millésime MAMH suivant (rôles 2024-2029 / publication « 2027 ») exigera de rejouer tout le pipeline : re-télécharger les FGDB/GPKG, revalider les schémas (les noms de couches/champs bougent entre millésimes), ré-entraîner les 4 modèles. 4. **Décalage de fraîcheur** : la BD servie date du dernier build complet (2026-08-08). Le **pipeline batch** (rôles + modèles hédoniques) reste sans mise à jour incrémentale — c'est voulu (livraison atomique d'un fichier). **Depuis 2026-08-22, les ventes récentes sont toutefois rafraîchies en continu directement sur le nœud** par le connecteur incrémental `scripts/ingest-jdm.mjs` (voir section 9) : la table `transactions` avance donc au fil de l'eau, alors que les `units`/`est_*`/`market_index` restent figées jusqu'au prochain build complet. ## 7. Procédure de rafraîchissement pas à pas Sur le **laptop** : 1. **Transactions** — recapturer le token : Safari → qub.ca (section immobilier) → inspecteur web → requête vers `api.qub.ca` → copier le header `Authorization: Bearer …` → le mettre dans `~/Desktop/house/scrape_province.py` → lancer le scraping quadtree (long ; repriseable). Vérifier le volume obtenu vs ~745 k. 2. **Rôles** — si nouveau millésime MAMH : télécharger manuellement les FGDB/GPKG depuis Données Québec dans `~/Desktop/qc_house_eval/`. 3. `cd ~/Desktop/qc_house_eval/scripts/` puis, dans l'ordre : `python3 extract_roles.py` → `python3 merge_transactions.py` (contrôler le taux d'appariement, attendu ≥ 99,8 %) → `python3 hedonic.py` (4 modèles ; contrôler les métriques de validation avant de continuer) → `python3 build_vrai_prix_db.py` → `python3 build_stats.py`. 4. **Contrôles qualité** sur le `vraiprix.db` produit : `SELECT COUNT(*) FROM units;` (~3,7 M), `SELECT COUNT(*) FROM transactions;` (~745 k+), `SELECT COUNT(*) FROM market_index;`, complétude GPS = 100 %, spot-check de quelques adresses connues dans `units_fts`. 5. **Livraison** : `scp` du fichier vers `M3U96a:apps/vrai-prix/data/vraiprix.db` puis `ssh M3U96a 'pm2 restart vrai-prix'`. 6. **Vérification en prod** : ouvrir le site (vrai-prix), chercher une adresse, vérifier /stats, puis noter la date de build ici (section 4) et committer la mise à jour de ce document **sur le nœud** (remote-first : `git add docs/PIPELINE-DONNEES.md && git commit && git push origin main`). ## 8. Attribution Les données du rôle d'évaluation proviennent du **MAMH via Données Québec** (licence ouverte — attribution requise). Les transactions servent au calcul de modèles et d'indices agrégés ; les pages publiques n'exposent pas la source brute qub.ca. ## 9. Connecteur incrémental « nouvelles ventes » (sur le nœud) — 2026-08-22 Contrairement au reste du pipeline (batch, laptop), ce connecteur tourne **directement sur le nœud** et **ajoute les ventes récentes dans `data/vraiprix.db` sans reconstruire la base**. Il alimente la même source que le widget « Transactions immobilières » du Journal de Montréal (https://www.journaldemontreal.com/argent/immobilier/transactions-immobilieres), c.-à-d. l'API `api.qub.ca/real-estate-service` — la même que `scrape_province.py`, donc les `id` sont compatibles et le dédoublonnage est naturel. ### Fichiers - `scripts/qub-token.mjs` — obtient un Bearer QUB (id-token Cognito). Se connecte au compte Québecor via **Scrapfly** (login `connect.qub.ca` protégé par Akamai + reCAPTCHA v3 → réessais sur sessions neuves), puis lit le jeton dans `GET /api/checklogin` (champ `userToken`). Jeton valide 1 h. - `scripts/ingest-jdm.mjs` — le connecteur : liste les ~1478 secteurs de `/v1/locations/all`, interroge `/v1/map` par secteur (résultats triés du plus récent au plus ancien, plafond 500 → **quadtree** si saturé), mappe vers le schéma `transactions`, **joint spatialement à `units`** (id_provinc, valeur_role) et `INSERT OR IGNORE` (dédup par `id`). Charge automatiquement `.env.local`. ### Mapping API → colonnes `transactions` `id`→id · `date`→date · `amount`→amount · `address.street/city`→street/city · `geometries[0].coordinates`→lng,lat · `propertyType`→property_type · `ar.yearBuilt`→year_built · `ar.floorArea`→floor_area · `ar.buildingType`→ building_type · `ar.parcelArea`→land_area · unité appariée→id_provinc,valeur_role. ### Utilisation ```bash cd ~/apps/vrai-prix node scripts/ingest-jdm.mjs # défaut : ventes depuis max(date)-45j node scripts/ingest-jdm.mjs --since=2026-07-15 # date plancher explicite node scripts/ingest-jdm.mjs --region="Montréal" # un secteur (sous-chaîne) node scripts/ingest-jdm.mjs --dry-run # ne rien écrire (audit) ``` L'app lit la BD en WAL : les nouvelles lignes sont visibles **sans redémarrage**. ### Planification Exécution quotidienne via **pm2** (08:00, nœud) : `pm2 start scripts/ingest-jdm.mjs --name vrai-prix-ingest --cwd ~/apps/vrai-prix --no-autorestart --cron-restart="0 8 * * *" --interpreter node` (puis `pm2 save`). Logs : `pm2 logs vrai-prix-ingest`. ### Secrets (dans `.env.local`, non versionné) `SCRAPFLY_KEY`, `QUB_EMAIL`, `QUB_PASSWORD`, `QUB_SESSION` (préfixe de session). ### Limites - Login QUB reCAPTCHA v3 : réussite probabiliste (boucle de 6 essais sur sessions neuves ; ~1 essai suffit en général). - N'alimente QUE `transactions`. Les estimations hédoniques (`est_*`, `p10/p90`) et `market_index` d'une vente toute neuve restent celles du dernier build complet tant que le pipeline batch n'a pas été rejoué (voir toutefois §10 : ré-estimation partielle possible sur le nœud). - Historique de premier comblement : gap 2026-07-27 → 2026-08-12 rempli province-wide le 2026-08-22 (+4189 ventes, appariement units 100 %). ## 10. Ré-estimation hédonique sur le nœud (`scripts/hedonic-retrain.py`) — 2026-08-22 Grâce aux ventes fraîches du connecteur (§9), on dispose d'un jeu de test « jamais vu » par le modèle laptop (ventes > 2026-07-27, build du 2026-08-08). `scripts/hedonic-retrain.py` (sklearn HistGradientBoostingRegressor ≈ LightGBM, cible log-prix, features rôle + encodages locaux par unité de voisinage + tendance temporelle, pondération de récence) permet d'évaluer et de corriger le modèle en place sans le pipeline laptop : - `--eval` : bench équitable laptop vs challenger sur les ventes jamais vues (n=4849, aucune écriture). - `--apply` : ré-entraîne sur tout l'historique et applique la stratégie composite validée, avec sauvegarde intégrale dans `units_est_prev`. ### Verdict du bench 2026-08-22 (MdAPE sur ventes jamais vues) | Segment | laptop | challenger nœud | |---|---|---| | unifamilial (n=4420) | **10,3 %** | 12,8-13,0 % | | terrain (n=139) | 62,2 % | **36-37 %** | | autre (n=150) | 79,3 % | **52-54 %** | → Le modèle laptop reste MEILLEUR sur l'unifamilial (conservé) ; le challenger est appliqué UNIQUEMENT sur `terrain` + `autre` (926 432 unités, 25 % du parc). ### Autres corrections appliquées le 2026-08-22 - **Recalibrage P10/P90** du modèle laptop (couverture observée 18,5 %/16,8 % hors bornes vs cible 10/10) : élargissement `p' = est×(p/est)^α`, α_lo=1,754 / α_hi=1,255 → couverture après : **10,1 % / 9,8 %**. Les fourchettes affichées sont plus larges mais honnêtes. - **Clamp p10 ≤ est ≤ p90** partout (38 826 violations dans le build laptop, 0 après). ### Rollback `units_est_prev` contient les (est_2026, p10, p90) d'avant intervention : ```sql UPDATE units SET est_2026=(SELECT est_2026 FROM units_est_prev p WHERE p.id_provinc=units.id_provinc), p10=(SELECT p10 FROM units_est_prev p WHERE p.id_provinc=units.id_provinc), p90=(SELECT p90 FROM units_est_prev p WHERE p.id_provinc=units.id_provinc); ``` NB : le prochain build complet laptop écrasera tout — reporter au besoin ces améliorations dans `hedonic.py` (recalibrage quantiles + segments terrain/autre).