Méthodologie — Modèle hédonique de prix des maisons (Québec)
Pipeline : Realtor.ca + DuProprio → nettoyage → feature engineering (règles + LLM + vision) → régression hédonique log-linéaire à effets fixes.
1. Origine des données
| Source | Fichier | Nb enreg. | Nature |
|---|---|---|---|
| Realtor.ca | dataset_realtor-property-crawler_...json (161 Mo) |
27 302 | Annonces MLS via crawler Realtor (Apify) |
| DuProprio | dataset_duproprio-property-crawler_...json (87 Mo) |
7 693 | Annonces vente sans intermédiaire (Apify) |
- Collecte : scrapers Apify, extraction le 2026-07-03. Chaque enregistrement est un objet JSON structuré.
- Champs communs :
source,url,listingId,title,price/priceNumber,currency,address{full,street,city,province,postalCode,...},property{...},features[],description,images[],scrapedAt. - Spécificités :
- Realtor :
property.buildingType,storeys,yearBuilt,parkingSpaces,rooms[](dimensions par pièce), pas de coordonnées GPS, images sans caption utile. - DuProprio :
property.type,levels,coordinates{lat,lon}(7 691 présentes), images avec captions (Kitchen, etc.), pas derooms.
- Realtor :
- Instantané transversal (cross-section) : un seul point dans le temps → pas de dimension temporelle, donc pas d'effet fixe temporel nécessaire.
Identifiant unique construit : uid = source + ":" + listingId (évite toute collision entre les deux espaces d'ID).
2. Construction du dataset combiné « maisons »
- Fusion des deux tableaux JSON (schéma harmonisé).
- Définition de « maison » (choix utilisateur : unifamiliales + townhouses) :
- Realtor :
buildingType ∈ {House, Row / Townhouse} - DuProprio :
type ∈ {Home, Bungalow, Semi Detached, Intergenerational, Country Home, Cottage, Villa, Townhouse} - Exclus : condos, plex (duplex→quintuplex), terrains/lots, fermes, commercial, mobile homes.
- Realtor :
- Filtre prix présent :
priceNumbernon nul et > 0.
→ 19 273 maisons avec prix (15 470 Realtor + 3 803 DuProprio).
3. Nettoyage des aberrations
Les prix scrappés contiennent des erreurs de parsing (min 899 $, max 368 G$). Règles appliquées :
| Variable | Règle | Justification |
|---|---|---|
| Prix | garder 50 k$ – 15 M$ | plage plausible d'une maison au QC ; retire loyers mal parsés et concaténations |
| Superficie habitable | 300 – 20 000 ft² (sinon → manquant) | valeurs physiquement possibles |
| Terrain | 200 – 5 000 000 ft² (sinon → manquant) | idem |
| Chambres / sdb | 0–15 / 0–12 (sinon → manquant) | plafonds réalistes |
| Prix/pied² | garder P0,5 – P99,5 (≈ 73–1 276 $/ft²) sur les biens à surface réelle | retire les erreurs résiduelles de parsing prix ou surface |
→ 19 115 maisons conservées (158 retirées).
Principe : on borne plutôt que de supprimer quand une seule variable est aberrante (on met la variable à manquant et on l'impute), pour ne pas perdre l'observation entière.
4. Construction des variables
4.1 Variable cible
ln_price = ln(priceNumber) — transformation logarithmique (rend la distribution symétrique, linéarise les effets multiplicatifs, coefficients ≈ % d'impact sur le prix).
4.2 Variables structurelles (règles déterministes)
- Surface & terrain : parsing d'unités hétérogènes → tout en pieds carrés.
"1535 sqft","1,333.2 ft²","11 561.52 ft²"(séparateurs de milliers variés),"961.6 m2"(m² × 10,7639).
- Reconstruction de surface via les pièces (Realtor) : dimensions
"23 ft ,4 in x 13 ft ,8 in"→ pieds-décimaux → aire ; somme sur toutes les pièces. Calibration sur le recouvrement (ratio médian surface réelle / somme pièces ≈ 0,98) → sert à comblersquareFeetmanquant.- Constat : les annonces à pièces détaillées ≈ celles qui ont déjà
squareFeet→ gain de couverture faible, mais les comptes/types de pièces restent informatifs.
- Constat : les annonces à pièces détaillées ≈ celles qui ont déjà
- Autres :
beds,baths,parking(Realtor),storeys/levels,age = 2026 − yearBuilt(+age²pour la courbure), drapeauxhas_*marquant les valeurs imputées.
4.3 Aménités (règles par mots-clés bilingues)
17 dummies détectés dans features[] + description + title (FR/EN) : piscine, bord de l'eau, garage, foyer, climatisation/thermopompe, sous-sol fini, cul-de-sac, golf, ski, proximité parc/école/autoroute/transport, absence de voisin arrière.
4.4 Détail des pièces (Realtor, rooms[])
Nombre de pièces, nombre de niveaux, chambre principale, sous-sol, bureau, salle familiale, atelier, cellier à vin, solarium, mezzanine, bibliothèque, buanderie.
4.5 Signaux de listing
Nombre de photos (n_images, écrêté à 60), présence de visite virtuelle — proxies de gamme/qualité de la mise en marché.
4.6 Variables géographiques (effets fixes)
- Ville nettoyée (Realtor : on retire le quartier entre parenthèses ; minuscules ; normalisation). Villes avec ≥ 20 annonces → dummy propre ; les rares →
__other__. ≈ 194 villes en effets fixes. - Type de propriété harmonisé, regroupé si < 30 annonces.
- Source (Realtor/DuProprio) → dummy captant les différences systématiques entre plateformes.
4.7 Imputation
Valeurs manquantes après nettoyage : imputées par médiane par type de propriété (puis médiane globale), avec un drapeau has_* pour que le modèle distingue « valeur observée » de « valeur imputée ».
5. Mécanismes IA pour le feature engineering
Deux couches IA extraient des variables impossibles à obtenir par règles — l'une du texte, l'autre des images.
5.1 Extraction de specs depuis la description — GPT-4o-mini (OpenAI)
- But : transformer la description libre en 20 attributs structurés (état : à rénover→neuf, clé en main, cuisine/sdb rénovée, revêtements de sol, comptoirs quartz/granit, chauffage, vue sur l'eau, revenus locatifs, grand terrain, secteur tranquille, + mots-clés de vente).
- Mécanisme : appel Chat Completions avec
response_format = json_schemastrict (schéma imposé, énumérations fermées) → sortie JSON valide garantie, pas de parsing fragile.temperature = 0(déterminisme). Système : « extracteur factuel, uniquement d'après le texte ; absence → false/inconnu/[] ». - Ingénierie : ~600 tokens in / ~110 out par annonce ; parallèle (24 threads), résumable (cache
specs.jsonlclé = uid), retry exponentiel sur 429/5xx. - Coût/temps : ~$3 / ~19 k annonces.
5.2 Attributs visuels de façade — Claude Haiku 4.5 (Anthropic, vision)
- But : lire ce que le texte ne dit pas — état visuel réel, attrait extérieur (curb appeal), niveau de rénovation, matériaux, aménagement paysager, cadre (urbain/banlieue/rural/bord d'eau/boisé), indice de luxe, qualité de la photo.
- Sélection d'image : 1 photo/annonce = la façade. Realtor → 1re photo. DuProprio → photo dont la caption est l'adresse (pas une pièce), via exclusion de mots-clés de pièces.
- Mécanisme : Messages API, image passée par URL (
source.type = url, pas de téléchargement),output_config.format = json_schema(12 attributs, énumérations fermées). Système : « évaluateur objectif ; scores fondés sur critères concrets (surfaces, matériaux, aménagement), pas sur un goût personnel ; si non-extérieur → is_exterior=false ». - Ingénierie : ~2 470 tokens in / ~100 out par image ; parallèle (16 threads), résumable (cache
facade_specs.jsonl), retry sur 429/5xx/529. - Coût/temps : ~$57 / ~19 k images.
- Pourquoi Haiku : modèle vision le moins cher et rapide, largement suffisant pour de la classification d'attributs concrets.
5.3 Pourquoi le JSON structuré strict est central
Le schéma imposé (types + enum + champs requis + additionalProperties:false) fait valider la sortie au niveau de l'API : le modèle est forcé de renvoyer exactement les champs attendus, dans le bon domaine de valeurs. On obtient des variables directement tabulables, reproductibles, sans post-traitement de texte libre.
5.4 Intégration au dataset
Les sorties LLM (clé uid) sont converties en variables numériques : booléens → 0/1, condition → score ordinal 0-5, revêtements → dummies (bois franc, quartz/granit), vue → dummy eau, etc., avec un drapeau has_gpt / is_exterior pour marquer la disponibilité.
6. Modèle économétrique
6.1 Spécification — hédonique log-linéaire à effets fixes
$$\ln(P_i) = \alpha + \beta,\ln(\text{surface}_i) + \gamma,\ln(\text{terrain}i) + \sum_k \delta_k X{ki} + \sum_c \lambda_c \mathbb{1}[\text{ville}_i=c] + \varepsilon_i$$
- Log-log sur surface et terrain → coefficients = élasticités (% prix par % surface).
- Log-niveau sur le reste → un coefficient $b$ ≈ $(e^b-1)\times100$ % d'impact sur le prix.
- Effets fixes ville ($\lambda_c$) : absorbent tout le différentiel de localisation (le plus gros déterminant du prix). Chaque coefficient = prime/décote de la ville vs la ville de référence.
- Effets fixes type de propriété et source.
6.2 Double spécification (couverture vs richesse)
Realtor ne rapporte squareFeet que pour ~38 % des annonces → deux modèles, prédictions combinées :
- Modèle A (riche) : inclut
ln(surface), estimé sur les biens à surface mesurée (réelle ou reconstruite via pièces). Meilleur ajustement. - Modèle B (large) : sans surface, estimé sur toutes les maisons → sert à pricer celles sans surface (via beds/baths/terrain/pièces + specs).
- Prédiction finale : A si surface mesurée, sinon B.
6.3 Estimation & inférence
- MCO (OLS) sur
ln_price. - Erreurs-types robustes à l'hétéroscédasticité HC3 (les résidus immobiliers sont hétéroscédastiques ; HC3 est conservateur en petit/moyen échantillon).
- Correction du biais de rétrotransformation : $\hat{P} = e^{\hat{\ln P}} \times e^{\hat\sigma^2/2}$ (correction normale de Duan) — sinon $e^{\hat{\ln P}}$ sous-estime systématiquement le niveau.
6.4 Interprétation
Coefficients rapportés en % d'impact prix + significativité (p-values sur SE HC3). Effets fixes ville présentés comme primes de localisation.
7. Statistiques & diagnostics
| Indicateur | Signification |
|---|---|
| R² / R²-ajusté | part de variance de ln_price expliquée |
| Écart-type des résidus (log) | → erreur typique en % = $(e^{\sigma}-1)\times100$ |
| Erreur absolue médiane | |prix réel / prix prédit − 1| médian sur toutes les prédictions |
| % à ±15 % / ±25 % | part des prédictions dans la fourchette |
resid_pct par annonce |
surprix/sous-prix vs modèle → repère aubaines & surévaluations |
Progression observée : v2 (structurel) R²≈0,70 → v3 (+ pièces/photos) R²≈0,72 → v4 (+ specs GPT) → v5 (+ vision) attendu.
8. Limites & biais (transparence)
- Endogénéité / non-causalité : de belles photos ou des specs riches corrèlent avec des courtiers haut de gamme et des biens chers → coefficients prédictifs, pas causaux. À ne pas lire comme « ajouter une piscine ⇒ +X % ».
- Biais de sélection : uniquement des biens en vente (prix demandé, pas prix de transaction) ; les biens sans surface/pièces sont non aléatoires (annonces moins complètes).
- Imputation : les médianes par type réduisent la variance des variables imputées (atténuation possible des coefficients) — d'où les drapeaux
has_*. - Extraction LLM : erreurs/hallucinations possibles ;
temperature=0+ schéma strict + énumérations fermées les limitent, mais pas de validation humaine systématique. La vision juge sur une seule photo. - Instantané unique : pas de dynamique de marché ; primes de localisation figées au 2026-07-03.
- Couverture géographique : villes < 20 annonces regroupées en
__other__(moins de finesse spatiale là où les données sont rares).
9. Fichiers produits
| Fichier | Contenu |
|---|---|
hedonic.py, hedonic_v4.py |
pipelines reproductibles (nettoyage + modèles) |
extract_specs.py, facade_vision.py |
extracteurs LLM (texte / vision), parallèles & résumables |
specs.jsonl, facade_specs.jsonl |
sorties LLM brutes (clé uid) |
maisons_hedonic*.csv |
dataset final + prix prédit + resid_pct |
hedonic_*_summary.txt |
sorties statistiques complètes |